Generalized accounts
Motivation
Plain old accounts (POA) have very little flexibility when it comes to the logic controlling them. While this results in a uniform user experience and makes development easier, e.g. a wallet only needs to handle a single method of accessing accounts, it also ignores that people may have varying needs when managing their accounts.
This has led to a variety of smart contract wallets being developed, which can enforce spending limits, enable more sophisticated authorization schemes via multi-signatures and other things. The biggest drawback of this approach is the need to still own a "normal" account, in order to invoke the smart contract and pay for its execution.
Both of these issues, relying on a single private key and smart contracts not being able to pay for their own execution, will be addressed by this proposal.
Generalized accounts (GA) are thus primarily introduced to improve the UX of the system.
Approaches
We give a brief overview of some of the different approaches, that would allow for authorization logic to be more flexible.
They lie on a spectrum from the current fixed signature and nonce scheme to having full fledged smart contracts.
When it comes to modifications required for transactions, there are two ways to do it:
- change all current transactions such that instead of signatures/nonces, a more
general
auth_data
is attached - introduce a new transaction type
meta_tx
, that wraps a "normal" transaction withauth_data
for a generalized account
In either case, signature logic will need to be touched, s.t. an empty signature can be accepted.
Additionally, the more flexible authorization logic will make transaction validation costlier and therefore also increase the associated fees.
Side-effect free
A fairly non-invasive solution would be to attach a pure function to accounts. This means that all relevant authorization code and data, e.g. public keys for signature checks, have to be included directly in the function body and can not be updated. Nonce handling would still be handled implicitly, i.e. stay unchanged.
Making the authorization function optional ensures POAs to not be impacted.
The downsides of this would be the need to for special handling of the authorization code because execution would not happen in the normal VM context and thus likely end up with a lot of implementation complexity.
This approach would be a slight improvement over the status quo but still leave a lot left to be desired, such as calling other contracts, enforcing dynamic spending limits or more complex nonce handling.
Full smart contracts
The far end of the flexibility spectrum is occupied by fully fledged smart contracts, which can pay for their own execution.
Here POAs stay untouched and instead everything is handled by smart contracts.
The biggest downside is that initialising a smart contract requires a POA with funds, because currently smart contracts cannot create other smart contracts. In addition, the VM does not yet have to the ability to create all the available transaction types.
Hybrid
For the hybrid solution POAs and smart contracts stay separate, meaning that accounts can still be used with the implicit nonce and signature authorization. But accounts are extended with two new fields, allowing its owner to specify the address of a smart contract and name of a function, which will be called whenever a transaction needs to be authorised. As soon as the new authorization contract is attached to an account, it is no longer possible to author transactions with the old nonce plus signature method.
Background
POAs and smart contract states are currently stored separately.
The only state transitions accounts can undergo are change of nonce and balance, which always have to be authorised by producing a valid signature with the private key belonging to the account id.
Smart contracts on the other hand can have arbitrary state associated with them, where transitions are controlled solely by user defined logic.
All transactions can be abstracted in the following way:
Fieldname Size (bytes)
-------------- -----
| from | 32 |
-------------- -----
| data | var |
-------------- -----
| nonce | 32 |
-------------- -----
| signature | 64 |
-------------- -----
with the data
field containing the byte sequence making up the specific
transaction fields.
The signature is computed as sign(hash(serialize(from||data||nonce)))
. It
guarantees integrity of the data
and nonce
fields, while the nonce
prevents replay of old transactions.
In addition to integrity, the signature is used to prove that the issuer knows
the private key for the public key specified in the from
field, i.e. used as
authorization.
The above scheme is the only one natively supported via the POA.
If the authorization were to be abstracted as well, then we end up with:
Fieldname Size (bytes)
-------------- -----
| data | var |
-------------- -----
| auth_data | var |
-------------- -----
which roughly corresponds to the newly introduced meta_tx
, that wraps
transaction data
into a smart contract call. This call uses the supplied
auth_data
to assess whether the transaction is to be authorised or not by
the GA.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Overview
In order to convert a POA into a GA, an existing account will have to issue a transaction similar to a contract creation, which associates a smart contract with the POA. After the conversion, a new meta transaction, that wraps a »normal« transaction, will be used to interact with the GA. This meta transaction is always a smart contract call and inner transactions will only be authorised if that call returns true.
Accounts
With the introduction of generalized accounts, the structure of the entries stored in the account tree changes from:
Fieldname Size (bytes)
-------------- -----
| nonce | 32 |
-------------- -----
| balance | 32 |
-------------- -----
to
Fieldname Size (bytes)
-------------- -----
| nonce | 32 |
-------------- -----
| balance | 32 |
-------------- -----
| ga_contract | 32 |
-------------- -----
| ga_auth_fun | var |
-------------- -----
For the actual wire format please consult the serialization document.
The ga_contract
MUST be the identifier of a deployed contract or empty.
The ga_auth_fun
MUST be the identifier of a function to be called or empty.
If the ga_contract
field is not empty, then the ga_auth_fun
field MUST NOT
be empty.
If ga_contract
is not empty, then authorization MUST be done by calling the
ga_auth_fun
of the contract located at ga_contract
. The data provided to the
ga_auth_fun
is supplied via a meta_tx
. The ga_auth_fun
MUST
return true
for the authorization to succeed.
The nonce
field is irrelevant if the account is a GA.
The ga_contract
and ga_auth_fun
fields MUST NOT go from non-empty to empty,
i.e. after a POA has been converted to a GA it cannot be undone.
The ga_contract
and ga_auth_fun
fields MUST NOT change after they have been
set once.
If the ga_contract
field is empty, then authorization MUST be done via the old
verification of the signature attached to transactions. In this case, a
transaction MUST only be considered valid, if
1. a signature is attached, that has been produced by the private key that
belongs to the account id
2. the nonce
included in the transaction is the successor of the nonce
stored in the account.
Transactions
Two new transactions are introduced. The ga_attach_tx
is used to convert a POA
into a GA by attaching code to it.
Afterwards the new meta_tx
is used to interact with the GA.
ga_attach_tx
Fieldname Size (bytes)
-------------- -----
| owner_id | 32 |
-------------- -----
| nonce | 8 |
-------------- -----
| code | var |
-------------- -----
| auth_fun | 32 |
-------------- -----
| ct_version | 4 |
-------------- -----
| fee | var |
-------------- -----
| ttl | 8 |
-------------- -----
| gas | var |
-------------- -----
| gas_price | var |
-------------- -----
| call_data | var |
-------------- -----
For the actual wire format please consult the serialization document.
The semantics of this transaction are almost identical to the Create Contract Transaction.
As such, only the additional logic not covered by the Create Contract
Transaction
will be listed here.
The address for the contract, which holds code
, will be computed just like
regular contract creation.
The owner_id
is the POA, which will be converted into a GA, if the transaction
is valid. If the account at owner_id
is already a GA, the transaction MUST be
considered invalid.
The transaction MUST include a signature created by the private key belonging to
the owner_id
.
The auth_fun
is a function hash as described in the
Sophia
section.
If valid, the transaction will set the ga_contract
and ga_auth_fun
of the
owner_id
account.
From Ceres, and onwards, it is only possible to attach authorization code to a
fresh account. An account is fresh as long as it hasn't signed any
transactions - this means that GAAttachTx is only allowed with nonce = 1
.
meta_tx
Before Iris, version 1:
Fieldname Size (bytes)
-------------- -----
| ga_id | 32 |
-------------- -----
| auth_data | var |
-------------- -----
| abi_version | 2 |
-------------- -----
| fee | var |
-------------- -----
| gas | var |
-------------- -----
| gas_price | var |
-------------- -----
| ttl | 8 |
-------------- -----
| tx | var |
-------------- -----
After Iris, version 2:
Fieldname Size (bytes)
-------------- -----
| ga_id | 32 |
-------------- -----
| auth_data | var |
-------------- -----
| abi_version | 2 |
-------------- -----
| fee | var |
-------------- -----
| gas | var |
-------------- -----
| gas_price | var |
-------------- -----
| tx | var |
-------------- -----
For the actual wire format please consult the serialization document.
The semantics of this transaction are similar to the Call Contract Transaction.
A meta_tx
is always a call of the auth_fun
on the contract located at
ga_id
with the supplied auth_data
.
The auth_fun
call MUST return true for the tx
to be considered valid.
The auth_fun
is a function hash as described in the Sophia
section.
The ttl
is the maximum block height that could include the transaction. This
had changed from Iris hardfork and the innermost transaction's ttl
is being
used instead.
The tx
, or inner transaction, MUST be a well formed transaction with nonce set
to 0
. The tx
MUST NOT be a ga_attach_tx
transaction.
The authorization
function is executed in a special FATE environment where
Auth.tx_hash
and Auth.tx
are available. These can (and should!) be used to
securely authorize a transaction. Note: Auth.tx_hash
computation differs
between protocol versions, see serialization
specification for details.
Backwards compatibility
Going from POA to GA is entirely optional and both types of accounts can coexist without problems.
Discussion
Threat model/attack vectors
Special care needs to be taken when writing contracts, that handle authorization logic for a GA. If the contract does not enforce integrity checking or replay protection, then it will be vulnerable to abuse.
Example contracts
Some mechanisms, that we had in mind, as motivation for GA:
- spending limits (daily/weekly/...)
- spending limits per transaction type
- restrictions on transaction types that can be sent
- (social) recovery options
- multi-sig (e.g. phone + hardware wallet, or multiple people)
- using different signature algorithms (i.e. other than EdDSA)
GAs are further explained and an example is provided in Generalized accounts explained.