Diem Accounts#
| DIP | 11 |
| Title | Diem Accounts |
| Author | Sam Blackshear (@sblackshear), Bob Wilsion (@bob-wilson), Tim Zakian (@tzakian) |
| Status | Draft |
| Type | Informational |
| Created | 2/11/2021 |
DIP11: Diem Accounts#
Overview#
The Diem Blockchain is organized as a map from 16 byte account addresses to Move resources. Every nonempty address must contain a DiemAccount resource, and it may also contain other resources. We refer to a nonempty address as a Diem account. This DIP describes the structure of Diem accounts, their relationship to transactions, and events that are triggered when accounts send and receive funds.
Account Creation#
An account is created by publishing a DiemAccount resource under an empty address. At creation time, the account is assigned an immutable role. DIP2 specifies the account roles and the policies for account creation (e.g., which account roles have the permission to create new accounts).
The creator of an account must specify the initial authentication key (see below) for the account. The account address will be the last 16 bytes of the authentication key.
Account Deletion#
A DiemAccount resource cannot be deleted. Once published under an address, it will remain indefinitely.
Account Structure#
The DiemAccount Move resource is defined as follows.
We will now explain the structure and purpose of each field, as well as some related concepts such as balances.
Authentication Key#
An authentication key is a 32-byte value used to authenticate the sender of a transaction. An authentication key is a hash of an authentication policy. Currently, Diem supports two kinds of account authentication policies: single-signature and multi-signature.
To generate a single-signature authentication key, generate a fresh key-pair (pubkey, privkey). Diem uses the PureEdDSA scheme over the Ed25519 curve, as defined in RFC 8032. Then, derive a 32-byte authentication key authentication_key = sha3-256(pubkey | 0x00), where | denotes concatenation. 0x00 is a 1-byte signature scheme identifier. Transactions sent from an account with this authentication key should include pubkey and be signed with privkey.
Diem also supports K-of-N multi-signature authentication. In K-of-N multi-signature, there are a total of N (public key, private key) pairs, and >=K of those N signatures must be used to authenticate a transaction. To create a K-of-N multi-signature authentication key, generate N Ed25519 keypairs (pubkey_1, privkey_1), …, (pubkey_N, privkey_N) then compute authentication``_key = sha3-256(pubkey_1 | … | pubkey_N | K | 0x01). 0x01 is a 1-byte signature scheme identifier. Transactions sent from an account with this authentication key should include all of pubkey_1, ... pubkey_N and be signed with at least K of privkey_1 ... privkey_N.
Diem supports rotation of account authentication keys via mutation of the DiemAccount.authentication_key field. When the authentication key for account A is rotated, subsequent transactions sent from A should use the public/private keypair(s) that were used to derive the new authentication key.
Sequence Number#
A sequence number is an 8-byte value that is used to prevent replays of transactions sent from a particular account. A freshly created account has sequence_number 0. Each transaction sent from account A that is included in the Diem blockchain increases the sequence_number of account A by 1.
Balances#
Diem is a multi-currency blockchain. An account can store a balance in a particular CurrencyType via the Balance Move resource defined as follows
A Diem<CurrencyType> represents a coin of type CurrencyType. DIP20 defines both the Diem standard and the set of valid CurrencyType’s.
In order to send and receive Diem<CurrencyType>, an account must have a Balance<CurrencyType> resource. A transaction that sends Diem<CurrencyType> to an account that does not exist or an account without a corresponding balance resource will abort.
Balance resources can be added either at account creation time or subsequently. Only the account owner can add new Balance resources after account creation. Once a Balance resource has been added to an account, it cannot be removed--that is, the account will always be able to send and receive CurrencyType.
Withdraw Capability#
The WithdrawCapability Move resource is defined as follows:
A WithdrawCapability { addr } represents the authority to debit the Balance resource(s) published under addr. There is exactly one WithdrawCapability { addr } resource in the Diem system for each non-empty address addr.
Key Rotation Capability#
The KeyRotationCapability Move resource is defined as follows:
A KeyRotationCapability { addr } represents the authority to rotate the authentication key of addr. There is exactly one KeyRotationCapability { addr } resource in the Diem system for each non-empty address addr.
Event Handles#
An EventHandle is a 24-byte value that uniquely identifies an event stream. The last 16 bytes of an EventHandle are always the account address that the events are intended for. The first 8 bytes are derived from an address-specific counter that is incremented each time a new EventHandle is created for that address.
- The
received_eventsfield of an account at addressAholds anEventHandlethat records an event each timeAreceives funds from another account. The counter value (first 8 bytes) of this handle is 0. - Similarly
sent_eventsfield of an account at addressAholds anEventHandlethat records an event each timeAsends funds to another account. The counter value (first 8 bytes) of this handle is 1.
Freezing#
As DIP2 explains, some Diem accounts can be frozen by the DiemRoot account. A frozen account cannot send transactions or receive funds sent by other accounts.
Relationship of accounts and transactions#
There is a close relationship between accounts and transactions. The validity and behavior of a Diem transaction are largely determined by the account from which it is sent. A valid transaction must be consistent with the various resources in the sending account, and the account’s resources are modified when the transaction executes.
Transaction Validation Overview#
When entering the system, each transaction is validated and either discarded if it is malformed or added to a ranked pool of pending transactions otherwise. A discarded transaction is removed from the system without being recorded on the blockchain. A validated transaction can proceed but may still be discarded later when it executes.
The details of the validation process are defined in terms of the specific data structures used to represent transactions and the validation results, so this section first gives an overview of the Rust code for those data structures.
If a transaction is malformed, the validation result (VMValidatorResult) indicates that the transaction must be discarded. If the transaction is successfully validated, the result also includes information to rank the transaction priority.
Validation examines the content of a transaction to determine if it is well formed. The transaction content is organized into three layers: SignedTransaction, RawTransaction, and the transaction payload (TransactionPayload and WriteSetPayload):
There are several different kinds of transactions that can be stored in the transaction payload: executing a script, publishing a module, and applying a WriteSet for system maintenance or updates. The payload is stored inside a RawTransaction structure that includes the various fields that are common to all of these transactions, and the RawTransaction is signed and wrapped inside a SignedTransaction structure that includes the signature and public key.
The validation process performs a sequence of checks on a transaction. Some of these checks are implemented directly in the Diem software and others are specified in Move code and evaluated via the Move VM. Some of the checks apply to all transactions and others are specific to the type of payload. To ensure consistent error handling, the checks should be performed in the order specified here.
General Validation Checks#
The following checks are performed for any transaction, regardless of the payload:
Check if the signature in the
SignedTransactionis consistent with the public key and theRawTransactioncontent. If not, this check fails with anINVALID_SIGNATUREstatus code. Note that comparing the transaction's public key against the sender account's authorization key is done separately in Move code.Check that the
gas_currency_codein theRawTransactionis a name composed of uppercase ASCII alphanumeric characters where the first character is a letter. If not, validation will fail with anINVALID_GAS_SPECIFIERstatus code. Note that this check does not ensure that the name corresponds to a currency recognized by the Diem Framework.Normalize the
gas_unit_pricefrom theRawTransactionto the Diem (XDX) currency. If the validation is successful, the normalized gas price is returned as thescorefield of theVMValidatorResultfor use in prioritizing the transaction. The normalization is calculated using theto_xdx_exchange_ratefield of the on-chainCurrencyInfofor the specified gas currency. This can fail with a status code ofCURRENCY_INFO_DOES_NOT_EXISTif the exchange rate cannot be retrieved.Load the
RoleIdresource from the sender's account. If the validation is successful, this value is returned as thegovernance_rolefield of theVMValidatorResultso that governance transactions can be prioritized.
Gas and Size Checks#
Next, there are a series of checks related to the transaction size and gas parameters. These checks are performed for Script and Module payloads, but not for WriteSet transactions. The constraints for these checks are defined by the GasConstants structure in the DiemVMConfig module.
Check if the transaction size exceeds the limit specified by the
max_transaction_size_in_bytesfield ofGasConstants. If the transaction is too big, validation fails with anEXCEEDED_MAX_TRANSACTION_SIZEstatus code.If the
max_gas_amountfield in theRawTransactionis larger than themaximum_number_of_gas_unitsfield ofGasConstants, then validation fails with a status code ofMAX_GAS_UNITS_EXCEEDS_MAX_GAS_UNITS_BOUND.There is also a minimum gas amount based on the transaction size. The minimum charge is calculated in terms of internal gas units that are scaled up by the
gas_unit_scaling_factorfield ofGasConstantsto allow more fine grained accounting. First, theGasConstantsstructure specifies amin_transaction_gas_unitsvalue that is charged for all transactions regardless of their size. Next, if the transaction size in bytes is larger than thelarge_transaction_cutoffvalue, then the minimum gas amount is increased byintrinsic_gas_per_bytefor every byte in excess oflarge_transaction_cutoff. The resulting value is divided by thegas_unit_scaling_factorto obtain the minimum gas amount. If themax_gas_amountfor the transaction is less than this minimum requirement, validation fails with a status code ofMAX_GAS_UNITS_BELOW_MIN_TRANSACTION_GAS_UNITS.The
gas_unit_pricefrom theRawTransactionmust be within the range specified by theGasConstants. If the price is less thanmin_price_per_gas_unit, validation fails with a status code ofGAS_UNIT_PRICE_BELOW_MIN_BOUND. If the price is more thanmax_price_per_gas_unit, validation fails with a status code ofGAS_UNIT_PRICE_ABOVE_MAX_BOUND.
Prologue Function Checks#
The rest of the validation is performed in Move code, which is run using the Move VM with gas metering disabled. Each kind of transaction payload has a corresponding prologue function that is used for validation. These prologue functions are defined in the DiemAccount module:
Script: The prologue function isscript_prologue. In addition to the common checks listed below, it also calls theis_script_allowedfunction in theDiemTransactionPublishingOptionmodule with the hash of the script bytecode to check if it is on the list of allowed scripts. If not, validation fails with anUNKNOWN_SCRIPTstatus code.Module: The prologue function ismodule_prologue. In addition to the common checks listed below, it also calls theis_module_allowedfunction in theDiemTransactionPublishingOptionmodule to see if publishing is allowed for the transaction sender. If not, validation fails with aINVALID_MODULE_PUBLISHERstatus code.WriteSet: The prologue function iswriteset_prologue. In addition to the common checks listed below, it also checks that the sender is the Diem root address and thatRoles::has_diem_root_role(sender)is true. If those checks fail, the status code is set toREJECTED_WRITE_SET.
The following checks are performed by all the prologue functions:
If the transaction's
chain_idvalue does not match the expected value for the blockchain, validation fails with aBAD_CHAIN_IDstatus code.Check if the transaction sender has an account, and if not, fail with a
SENDING_ACCOUNT_DOES_NOT_EXISTstatus code.Call the
AccountFreezing::account_is_frozenfunction to check if the transaction sender's account is frozen. If so, the status code is set toSENDING_ACCOUNT_FROZEN.Check that the hash of the transaction's public key (from the
authenticatorin theSignedTransaction) matches the authentication key in the sender's account. If not, validation fails with anINVALID_AUTH_KEYstatus code.The transaction sender must be able to pay the maximum transaction fee. The maximum fee is the product of the transaction's
max_gas_amountandgas_unit_pricefields. If the maximum fee is non-zero, the coin specified by the transaction'sgas_currency_codemust have been registered as a valid gas currency (via theTransactionFeemodule), or else validation will fail with aBAD_TRANSACTION_FEE_CURRENCYstatus. If the sender's account balance for the gas currency is less than the maximum fee, validation fails with anINSUFFICIENT_BALANCE_FOR_TRANSACTION_FEEstatus code. ForWriteSettransactions, the maximum fee is treated as zero, regardless of the gas parameters specified in the transaction.Check if the transaction is expired. If the transaction's
expiration_timestamp_secsfield is greater than or equal to the current blockchain timestamp, fail with aTRANSACTION_EXPIREDstatus code.Check if the transaction's
sequence_numberis already the maximum value, such that it would overflow if the transaction was processed. If so, validation fails with aSEQUENCE_NUMBER_TOO_BIGstatus code.The transaction's
sequence_numbermust match the current sequence number in the sender's account. If the transaction sequence number is too low, validation fails with aSEQUENCE_NUMBER_TOO_OLDstatus code. If the number is too high, the behavior depends on whether it is the initial validation or the re-validation done as part of the execution phase. Multiple transactions with consecutive sequence numbers from the same account can be in flight at the same time, but they must be executed strictly in order. For that reason, a transaction sequence number higher than expected for the sender's account is accepted during the initial validation, but rejected with aSEQUENCE_NUMBER_TOO_NEWstatus code during the execution phase. Note that this check for "too new" sequence numbers must be the last check in the prologue function so that a transaction cannot get through the initial validation when it has some other fatal error.
If the prologue function fails for any other reason, which would indicate some kind of unexpected problem, validation fails with a status code of UNEXPECTED_ERROR_FROM_KNOWN_MOVE_FUNCTION.
Epilogue Functions#
After a transaction is successfully validated, it is entered into a pool of pending transactions. When the transaction is chosen to execute, it is first re-validated because the account resources may have changed. If the transaction is still valid, and if the execution also succeeds, the final step is to run a Move epilogue function defined in the DiemAccount module.
The epilogue increments the sender's sequence_number and deducts the transaction fee based on the gas price and the amount of gas consumed. The epilogue function is run with gas metering disabled.
If an error occurs when processing a transaction, all the side effects from the transaction will be discarded, but the sending account still needs to be charged the transaction fee for gas consumption. This is handled by running the epilogue function starting from the original blockchain state. Note that the epilogue function may be run twice. For example, the transaction may make a payment that drops the account balance so that the first attempt to run the epilogue fails because of insufficient funds to pay the transaction fee. After dropping the side effects of the payment, however, the second attempt to run the epilogue should always succeed, because the validation process ensures that the account balance can cover the maximum transaction fee. If the second "failure" epilogue execution somehow fails (due to an internal inconsistency in the system), execution fails with an UNEXPECTED_ERROR_FROM_KNOWN_MOVE_FUNCTION status, and the transaction is discarded.
WriteSet transactions use a special writeset_epilogue function from the DiemAccount module. The writeset_epilogue calls the standard epilogue to increment the sequence_number, emits an AdminTransactionEvent, and if the WriteSetPayload is a Direct value, it also emits a NewEpochEvent to trigger reconfiguration. For a Script value in the WriteSetPayload, it is the responsibility of the code in the script to determine whether a reconfiguration is necessary, and if so, to emit the appropriate NewEpochEvent. If the epilogue does not execute successfully, the status code is set to UNEXPECTED_ERROR_FROM_KNOWN_MOVE_FUNCTION.
Account events#
Whenever the value of one of the account’s balances is changed, specific events are emitted that describe the state change that occurred.
Whenever an account’s balance resource receives funds a ReceivedPaymentEvent event with the following structure is recorded on the received_events event handle in the account’s DiemAccount resource.
The amount field of the recorded event specifies the amount by which the account’s balance was increased, and the currency_code specifies the balance of the currency in which the funds were added. The payer field specifies the address from which the deposited funds were withdrawn, and the metadata field contains any metadata attached to funds by the payer.
When an account’s balance resource has funds withdrawn from it a SentPaymentEvent event with the following structure is recorded on the sent_events event handle in the debited account’s DiemAccount resource.
The amount and currency_code fields of the recorded event specify the amount and the currency of the withdrawal from the account’s balance. The payee field specifies where the withdrawn funds are re-deposited, and the metadata fields contains any metadata that the payee wishes to attach to the payment.
Account Event Invariants#
The SentPaymentEvent and ReceivedPaymentEvent, along with gas payments for a particular account, can be used to derive the account’s balance. To see this, let H be the event handle that received one of the above SentPaymentEvent or ReceivedPaymentEvent events, and let A be the account address under which this event handle is published. The following invariants hold for these events:
- The
currency_codebalance under A increases byamountif and only if aReceivedPaymentEvent{amount, currency_code, payer}is recorded on handle H. - If a
SentPaymentEvent{amount, currency_code, payee}is recorded on handle H, A’scurrency_codebalance decreases by the specifiedamount. - When A sends a transaction for execution with
gas_used= X,gas_unit_price = Y, andgas_currency_code = C,this will decrease A's balance in currencyCbyX * Y. This will not record an event. - Every decrease in the balance of an account has either a corresponding
SentPaymentEventor a corresponding gas charge. - In almost all cases, a
SentPaymentEvent {amount, currency code, payee}recorded on a handle H1 is coupled with aReceivedPaymentEvent {amount, currency_code, payer}recorded on a handle H2, such that H1’s address is equal topayerand H2’s address is equal topayee. This means that H1 is sending funds, while H2 is receiving those funds. The only exceptions are:- Minting: When new coins are minted to an address A, the payer address of the
ReceivedPaymentEventwill appear as the reserved address0x0. There is no correspondingSentPaymentEvent. - Preburning: A preburn transaction sent from address A will emit a
SentPaymentEventwhere both the payer and payee address are A. There is no correspondingReceivedPaymentEvent. - Cancelling a preburn sent from address A. In this case, a
ReceivedPaymentEventwill be emitted to A’s event stream where both the payer and payee address are A. There is no correspondingSentPaymentEvent.
- Minting: When new coins are minted to an address A, the payer address of the
Similarly, MintEvents and BurnEvents can be used to derive the total supply of a particular currency in the system. The following invariants hold for these events:
- A
MintEvent {amount, currency_code}is emitted if and only if the value of allcurrency_codecoins in the system increases byamount. - A
BurnEvent {amount, currency_code}is emitted if and only if the quantity ofcurrency_codein the system decreases byamount.
No events other than SentPaymentEvent, ReceivedPaymentEvent, MintEvent, BurnEvent, and transaction fees are relevant for account balance reconciliation.