- 2020-09-16: Beneficiary allowance, add message results
- 2020-09-08: Initial draft
Accepted
Currently each runtime can define its own token (or none at all) and there is no mechanism that would support transfer of consensus layer tokens into a runtime and back out.
Introducing such a mechanism would allow the consensus layer tokens to be used inside runtimes for various functions. This ADR proposes such a mechanism.
On a high level, this proposal adds support for consensus/runtime token transfers as follows:
-
Each staking account can set an allowance for beneficiaries. Each staking account can set an allowance, a maximum amount a beneficiary can withdraw from the given account. Beneficiaries are identified by their address. This is similar to approve/transferFrom calls defined by the ERC-20 Token Standard. Previously such functionality was already present but was removed in oasis-core#2021.
-
Each runtime itself has an account in the consensus layer. This account contains the balance of tokens which are managed exclusively by the runtime and do not belong to any specific regular account in the consensus layer.
It is not possible to transfer directly into a runtime account and doing so may result in funds to be locked without a way to reclaim them.
The only way to perform any operations on runtime accounts is through the use of messages emitted by the runtime during each round. These messages are subject to discrepancy detection and instruct the consensus layer what to do.
Combined, the two mechanisms enable account holders to set an allowance in the benefit of runtimes so that the runtimes can withdraw up to the allowed amount from the account holder's address.
This proposal introduces the following new address context for the runtime accounts:
oasis-core/address: runtime
Initial version for the address context is 0
. To derive the address, the
standard address derivation scheme is used, with the runtime's 32-byte
identifier used as the data
part.
This proposal introduces/updates the following consensus state in the staking module:
The general account data structure is modified to include an additional field storing the allowances as follows:
type GeneralAccount struct {
// ... existing fields omitted ...
Allowances map[Address]quantity.Quantity `json:"allowances,omitempty"`
}
This proposal adds the following new transaction methods in the staking module:
Allow enables an account holder to set an allowance for a beneficiary.
Method name:
staking.Allow
Body:
type Allow struct {
Beneficiary Address `json:"beneficiary"`
Negative bool `json:"negative,omitempty"`
AmountChange quantity.Quantity `json:"amount_change"`
}
Fields:
beneficiary
specifies the beneficiary account address.amount_change
specifies the absolute value of the amount of base units to change the allowance for.negative
specifies whether theamount_change
should be subtracted instead of added.
The transaction signer implicitly specifies the general account. Upon executing the allow the following actions are performed:
-
If either the
disable_transfers
staking consensus parameter is set totrue
or themax_allowances
staking consensus parameter is set to zero, the method fails withErrForbidden
. -
It is checked whether either the transaction signer address or the
beneficiary
address are reserved. If any are reserved, the method fails withErrForbidden
. -
Address specified by
beneficiary
is compared with the transaction signer address. If the addresses are the same, the method fails withErrInvalidArgument
. -
The account indicated by the signer is loaded.
-
If the allow would create a new allowance and the maximum number of allowances for an account has been reached, the method fails with
ErrTooManyAllowances
. -
The set of allowances is updated so that the allowance is updated as specified by
amount_change
/negative
. In case the change would cause the allowance to be equal to zero or negative, the allowance is removed. -
The account is saved.
-
The corresponding
AllowanceChangeEvent
is emitted with the following structure:type AllowanceChangeEvent struct { Owner Address `json:"owner"` Beneficiary Address `json:"beneficiary"` Allowance quantity.Quantity `json:"allowance"` Negative bool `json:"negative,omitempty"` AmountChange quantity.Quantity `json:"amount_change"` }
Where
allowance
contains the new total allowance, theamount_change
contains the absolute amount the allowance has changed for andnegative
specifies whether the allowance has been reduced rather than increased. The event is emitted even if the new allowance is zero.
Withdraw enables a beneficiary to withdraw from the given account.
Method name:
staking.Withdraw
Body:
type Withdraw struct {
From Address `json:"from"`
Amount quantity.Quantity `json:"amount"`
}
Fields:
from
specifies the account address to withdraw from.amount
specifies the amount of base units to withdraw.
The transaction signer implicitly specifies the destination general account. Upon executing the withdrawal the following actions are performed:
-
If either the
disable_transfers
staking consensus parameter is set totrue
or themax_allowances
staking consensus parameter is set to zero, the method fails withErrForbidden
. -
It is checked whether either the transaction signer address or the
from
address are reserved. If any are reserved, the method fails withErrForbidden
. -
Address specified by
from
is compared with the transaction signer address. If the addresses are the same, the method fails withErrInvalidArgument
. -
The source account indicated by
from
is loaded. -
The destination account indicated by the transaction signer is loaded.
-
amount
is deducted from the corresponding allowance in the source account. If this would cause the allowance to go negative, the method fails withErrForbidden
. -
amount
is deducted from the source general account balance. If this would cause the balance to go negative, the method fails withErrInsufficientBalance
. -
amount
is added to the destination general account balance. -
Both source and destination accounts are saved.
-
The corresponding
TransferEvent
is emitted. -
The corresponding
AllowanceChangeEvent
is emitted with the updated allowance.
This proposal adds the following new query methods in the staking module by
updating the staking.Backend
interface as follows:
type Backend interface {
// ... existing methods omitted ...
// Allowance looks up the allowance for the given owner/beneficiary combination.
Allowance(ctx context.Context, query *AllowanceQuery) (*quantity.Quantity, error)
}
// AllowanceQuery is an allowance query.
type AllowanceQuery struct {
Height int64 `json:"height"`
Owner Address `json:"owner"`
Beneficiary Address `json:"beneficiary"`
}
Since this is the first proposal that introduces a new runtime message type that can be emitted from a runtime during a round, it also defines some general properties of runtime messages and the dispatch mechanism:
-
Each message has an associated gas cost that needs to be paid by the submitter (e.g. as part of the
roothash.ExecutorCommit
method call). The gas cost is split among the committee members. -
There is a maximum number of messages that can be emitted by a runtime during a given round. The limit is defined both globally (e.g. a roothash consensus parameter) and per-runtime (which needs to be equal to or lower than the global limit).
-
Messages are serialized using a sum type describing all possible messages, where each message type is assigned a field name:
type Message struct { Message1 *Message1 `json:"message1,omitempty"` Message2 *Message2 `json:"message2,omitempty"` // ... }
-
All messages are versioned by embeding the
cbor.Versioned
structure which provides a singleuint16
fieldv
. -
A change is made to how messages are included in commitments, to reduce the size of submitted transactions.
The
ComputeResultsHeader
is changed so that theMessages
field is replaced with aMessagesHash
field containing a hash of the CBOR-encoded messages emitted by the runtime.At the same time
ComputeBody
is changed to include an additional fieldMessages
as follows:type ComputeBody struct { // ... existing fields omitted ... Messages []*block.Message `json:"messages,omitempty"` }
The
Messages
field must only be populated in the commitment by the transaction scheduler and must match theMessagesHash
. -
If any of the included messages is deemed malformed, the round fails and the runtime state is not updated.
-
In order to support messages that fail to execute, a new roothash event is emitted for each executed message:
type MessageEvent struct { Index uint32 `json:"index,omitempty"` Module string `json:"module,omitempty"` Code uint32 `json:"code,omitempty"` }
Where the
index
specifies the index of the executed message and themodule
andcode
specify the module and error code accoording to Oasis Core error encoding convention (note that the usual human readable message field is not included).
This proposal introduces the following runtime messages:
The staking method call message enables a runtime to call one of the supported staking module methods.
Field name:
staking
Body:
type StakingMessage struct {
cbor.Versioned
Transfer *staking.Transfer `json:"transfer,omitempty"`
Withdraw *staking.Withdraw `json:"withdraw,omitempty"`
}
Fields:
v
must be set to0
.transfer
indicates that thestaking.Transfer
method should be executed.withdraw
indicates that thestaking.Withdraw
method should be executed.
Exactly one of the supported method fields needs to be non-nil, otherwise the message is considered malformed.
This proposal introduces the following new consensus parameters in the staking module:
max_allowances
(uint32) specifies the maximum number of allowances an account can store. Zero means that allowance functionality is disabled.
This proposal introduces the following new consensus parameters in the roothash module:
max_runtime_messages
(uint32) specifies the global limit on the number of messages that can be emitted in each round by the runtime. The default value of0
disables the use of runtime messages.
This proposal modifies the runtime host protocol as follows:
The existing RuntimeInfoRequest
message body is updated to contain a field
denoting the consensus backend used by the host and its consensus protocol
version as follows:
type RuntimeInfoRequest struct {
ConsensusBackend string `json:"consensus_backend"`
ConsensusProtocolVersion uint64 `json:"consensus_protocol_version"`
// ... existing fields omitted ...
}
This information can be used by the runtime to ensure that it supports the consensus layer used by the host. In case the backend and/or protocol version is not supported, the runtime should return an error and terminate. In case the runtime does not interact with the consensus layer it may ignore the consensus layer information.
The existing RuntimeExecuteTxBatchRequest
and RuntimeCheckTxBatchRequest
message bodies are updated to include the consensus layer light block at the
last finalized round height (specified in .Block.Header.Round
) and the list of
MessageEvent
s emitted while processing the runtime messages emitted in the
previous round as follows:
type RuntimeExecuteTxBatchRequest struct {
// ConsensusBlock is the consensus light block at the last finalized round
// height (e.g., corresponding to .Block.Header.Round).
ConsensusBlock consensus.LightBlock `json:"consensus_block"`
// MessageResults are the results of executing messages emitted by the
// runtime in the previous round (sorted by .Index).
MessageResults []roothash.MessageEvent `json:"message_results,omitempty"`
// ... existing fields omitted ...
}
type RuntimeCheckTxBatchRequest struct {
// ConsensusBlock is the consensus light block at the last finalized round
// height (e.g., corresponding to .Block.Header.Round).
ConsensusBlock consensus.LightBlock `json:"consensus_block"`
// ... existing fields omitted ...
}
The information from the light block can be used to access consensus layer state.
The existing HostStorageSyncRequest
message body is updated to include an
endpoint identifier as follows:
type HostStorageSyncRequest struct {
// Endpoint is the storage endpoint to which this request should be routed.
Endpoint string `json:"endpoint,omitempty"`
// ... existing fields omitted ...
}
The newly introduced endpoint
field can take the following values:
-
runtime
(or empty string) denotes the runtime state endpoint. The empty value is allowed for backwards compatibility as this was the only endpoint available before this proposal. -
consensus
denotes the consensus state endpoint, providing access to consensus state.
The Rust runtime support library (oasis-core-runtime
) must be updated to
support the updated message structures. Additionally, there needs to be basic
support for interpreting the data from the Tendermint consensus layer backend:
-
Decoding light blocks.
-
Decoding staking-related state structures.
The Tendermint-specific functionality should be part of a separate crate.
Scenario:
Account holder has 100 tokens in her account in the consensus layer staking ledger and would like to spend 50 tokens to execute an action in runtime X.
Flow:
-
Account holder sets an allowance of 50 tokens for runtime X by submitting an allow transaction to the consensus layer.
-
Account holder submits a runtime transaction that performs some action costing 50 tokens.
-
Account holder's runtime transaction is executed in runtime X round R:
-
Runtime X emits a message to transfer 50 tokens from the user's account to the runtime's own account.
As an optimization runtime X can verify current consensus layer state and reject the transaction early to prevent paying for needless consensus layer message processing.
-
Runtime X updates its state to indicate a pending transfer of 50 tokens from the user. It uses the index of the emitted message to be able to match the message execution result once it arrives.
-
Runtime X submits commitments to the consensus layer.
-
-
When finalizing round R for runtime X, the consensus layer transfers 50 tokens from the account holder's account to the runtime X account.
-
Corresponding message result event is emitted, indicating success.
-
When runtime X processes round R+1, the runtime receives the set of emitted message result events.
-
Runtime X processes message result events, using the index field to match the corresponding pending action and executes whatever action it queued.
- In case the message result event would indicate failure, the pending action can be pruned.
-
Consensus layer tokens can be transferred into and out of runtimes, enabling more use cases.
-
Any tokens must be explicitly made available to the runtime which limits the damage from badly written or malicious runtimes.
-
Account holders can change the allowance at any time.
-
A badly written or malicious runtime could steal the tokens explicitly deposited into the runtime. This includes any actions by the runtime owner which would modify the runtime's security parameters.
-
A badly written, malicious or forever suspended runtime can lock tokens in the runtime account forever. This could be mitigated via an unspecified consensus layer governance mechanism.
-
Account holders may mistakenly transfer tokens directly into a runtime account which may cause such tokens to be locked forever.
-
Account holders may change the allowance or reduce their account balance right before the runtime round is finalized, causing the emitted messages to fail while the runtime still needs to pay for gas to execute the messages.
- The runtime must handle all message results in the next round as otherwise it cannot easily get past messages.