The staking service is responsible for managing the staking ledger in the consensus layer. It enables operations like transferring stake between accounts and escrowing stake for specific needs (e.g., operating nodes).
The service interface definition lives in go/staking/api
. It defines the
supported queries and transactions. For more information you can also check out
the consensus service API documentation.
Stake amounts can be denominated in tokens and base units.
Tokens are used in user-facing scenarios (e.g. CLI commands) where the token
amount is prefixed with the token's ticker symbol as defined by the Genesis
'
TokenSymbol
field.
Another Genesis
' field, TokenValueExponent
, defines the
token's value base-10 exponent.
For example, if TokenValueExponent
is 6, then 1 token equals 10^6 (i.e. one
million) base units.
Internally, base units are used for all stake calculation and processing.
A staking account is an entry in the staking ledger. It can hold both general and escrow accounts.
Each staking account has an address which is derived from the corresponding public key as follows:
[ 1 byte <ctx-version> ][ first 20 bytes of SHA512-256(<ctx-identifier> || <ctx-version> || <data>) ]
Where <ctx-version>
and <ctx-identifier>
represent the staking account
address' context version and identifier and <data>
represents the data
specific to the address kind.
There are two kinds of accounts:
- User accounts linked to a specific public key.
- Runtime accounts linked to a specific runtime identifier.
Addresses use Bech32 encoding for text serialization with oasis
as its human
readable part (HRP) prefix (for both kinds of accounts).
In case of user accounts, the <ctx-version>
and <ctx-identifier>
are as
defined by the AddressV0Context
variable, and <data>
represents the
account signer's public key (e.g. entity id).
For more details, see the NewAddress
function.
:::info
When generating an account's private/public key pair, follow ADR 0008: Standard Account Key Generation.
:::
In case of runtime accounts, the <ctx-version>
and <ctx-identifier>
are as
defined by the AddressRuntimeV0Context
variable, and <data>
represents the
runtime identifier.
For more details, see the NewRuntimeAddress
function.
The runtime accounts belong to runtimes and can only be manipulated by the runtime by emitting messages to the consensus layer.
Some staking account addresses are reserved to prevent them from being accidentally used in the actual ledger.
Currently, they are:
oasis1qrmufhkkyyf79s5za2r8yga9gnk4t446dcy3a5zm
: common pool address (defined byCommonPoolAddress
variable).oasis1qqnv3peudzvekhulf8v3ht29z4cthkhy7gkxmph5
: per-block fee accumulator address (defined byFeeAccumulatorAddress
variable).oasis1qp65laz8zsa9a305wxeslpnkh9x4dv2h2qhjz0ec
: governance deposits address (defined by theGovernanceDeposits
variable).
General accounts store account's general balance and nonce. Nonce is the incremental number that must be unique for each account's transaction.
Escrow accounts are used to hold stake delegated for specific consensus-layer operations (e.g., registering and running nodes). Their balance is subject to special delegation provisions and a debonding period.
Delegation provisions, also called commissions, are specified by the
CommissionSchedule
field.
An escrow account also has a corresponding stake accumulator. It stores stake claims for an escrow account and ensures all claims are satisfied at any given point. Adding a new claim is only possible if all of the existing claims plus the new claim can be satisfied.
When a delegator wants to delegate some of amount of stake to a staking account, he needs to escrow stake using Add Escrow method.
Similarly, when a delegator wants to reclaim some amount of escrowed stake back to his general account, he needs to reclaim stake using Reclaim Escrow method.
To simplify accounting, each escrow results in the delegator account being issued shares which can be converted back to stake during the reclaim escrow operation.
When a delegator delegates some amount of stake to an escrow account, the delegator receives the number of shares proportional to the current share price (in base units) calculated from the total number of stake delegated to an escrow account so far and the number of shares issued so far:
shares_per_base_unit = account_issued_shares / account_delegated_base_units
For example, if an escrow account has the following state:
"escrow": {
"active": {
"balance": "250",
"total_shares": "1000"
},
...
}
then the current share price (i.e. shares_per_base_unit
) is 1000 / 250 = 4.
Delegating 500 base units to this escrow account would result in 500 * 4 = 2000 newly issued shares.
Thus, the escrow account would have the following state afterwards:
"escrow": {
"active": {
"balance": "750",
"total_shares": "3000"
},
...
}
When a delegator wants to reclaim a certain number of escrowed stake, the base unit price (in shares) must be calculated based on the escrow account's current active balance and the number of issued shares:
base_units_per_share = account_delegated_base_units / account_issued_shares
Returning to our example escrow account, the current base unit price (i.e.
base_units_per_share
) is 750 / 3000 = 0.25.
Reclaiming 1200 shares would result in 1200 * 0.25 = 300 base units being reclaimed.
The escrow account would have the following state afterwards:
"escrow": {
"active": {
"balance": "450",
"total_shares": "1800"
},
...
}
Reclaiming escrow does not complete immediately, but may be subject to a debonding period during in which the stake still remains escrowed.
A staking account can be configured to take a commission on staking rewards
given to its node(s). They are defined by the CommissionRateStep
type.
The commission rate must be within bounds, which the staking account can also
specify using the CommissionRateBoundStep
type.
The commission rates and rate bounds can change over time which is defined
by the CommissionSchedule
type.
To prevent unexpected changes in commission rates and rate bounds, they must
be specified a number of epochs in the future, controlled by the
CommissionScheduleRules
consensus parameter.
The following sections describe the methods supported by the consensus staking service.
Transfer enables stake transfer between different accounts in the staking
ledger. A new transfer transaction can be generated using
NewTransferTx
function.
Method name:
staking.Transfer
Body:
type Transfer struct {
To Address `json:"to"`
Amount quantity.Quantity `json:"amount"`
}
Fields:
to
specifies the destination account's address.amount
specifies the amount of base units to transfer.
The transaction signer implicitly specifies the source account.
Burn destroys some stake in the caller's account. A new burn transaction can be
generated using NewBurnTx
function.
Method name:
staking.Burn
Body:
type Burn struct {
Amount quantity.Quantity `json:"amount"`
}
Fields:
amount
specifies the amount of base units to burn.
The transaction signer implicitly specifies the caller's account.
Escrow transfers stake into an escrow account.
For more details, see the Delegation section of this document.
A new add escrow transaction can be generated using NewAddEscrowTx
function.
Method name:
staking.AddEscrow
Body:
type Escrow struct {
Account Address `json:"account"`
Amount quantity.Quantity `json:"amount"`
}
Fields:
account
specifies the destination escrow account's address.amount
specifies the amount of base units to transfer.
The transaction signer implicitly specifies the source account.
Reclaim escrow starts the escrow reclamation process.
For more details, see the Delegation section of this document.
A new reclaim escrow transaction can be generated using
NewReclaimEscrowTx
function.
Method name:
staking.ReclaimEscrow
Body:
type ReclaimEscrow struct {
Account Address `json:"account"`
Shares quantity.Quantity `json:"shares"`
}
Fields:
account
specifies the source escrow account's address.shares
specifies the number of shares to reclaim.
The transaction signer implicitly specifies the destination account.
Amend commission schedule updates the commission schedule specified for the
given escrow account.
For more details, see the Commission Schedule section of this document.
A new amend commission schedule transaction can be
generated using NewAmendCommissionScheduleTx
function.
Method name:
staking.AmendCommissionSchedule
Body:
type AmendCommissionSchedule struct {
Amendment CommissionSchedule `json:"amendment"`
}
Fields:
amendment
defines the amended commission schedule.
The transaction signer implicitly specifies the escrow account.
Allow enables an account holder to set an allowance for a beneficiary. A new
allow transaction can be generated using NewAllowTx
function.
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.
Withdraw enables a beneficiary to withdraw from the given account. A new
withdraw transaction can be generated using NewWithdrawTx
function.
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.
The transfer event is emitted when tokens are transferred from a source account to a destination account.
Body:
type TransferEvent struct {
From Address `json:"from"`
To Address `json:"to"`
Amount quantity.Quantity `json:"amount"`
}
Fields:
from
contains the address of the source account.to
contains the address of the destination account.amount
contains the amount (in base units) transferred.
The burn event is emitted when tokens are burned.
Body:
type BurnEvent struct {
Owner Address `json:"owner"`
Amount quantity.Quantity `json:"amount"`
}
Fields:
owner
contains the address of the account that burned tokens.amount
contains the amount (in base units) burned.
Escrow events are emitted when tokens are escrowed, taken from escrow by the protocol or reclaimed from escrow by the account owner.
Body:
type EscrowEvent struct {
Add *AddEscrowEvent `json:"add,omitempty"`
Take *TakeEscrowEvent `json:"take,omitempty"`
Reclaim *ReclaimEscrowEvent `json:"reclaim,omitempty"`
}
Fields:
add
is set if the emitted event is an Add Escrow event.take
is set if the emitted event is a Take Escrow event.reclaim
is set if the emitted event is a Reclaim Escrow event.
The add escrow event is emitted when funds are escrowed.
Body:
type AddEscrowEvent struct {
Owner Address `json:"owner"`
Escrow Address `json:"escrow"`
Amount quantity.Quantity `json:"amount"`
NewShares quantity.Quantity `json:"new_shares"`
}
Fields:
owner
contains the address of the source account.escrow
contains the address of the destination account the tokens are being escrowed to.amount
contains the amount (in base units) escrowed.new_shares
contains the amount of shares created as a result of the added escrow event. Can be zero in case of (non-commissioned) rewards, where stake is added without new shares to increase share price.
The take escrow event is emitted by the protocol when escrowed funds are slashed for whatever reason.
Body:
type TakeEscrowEvent struct {
Owner Address `json:"owner"`
Amount quantity.Quantity `json:"amount"`
DebondingAmount quantity.Quantity `json:"debonding_amount"`
}
Fields:
owner
contains the address of the account escrow has been taken from.amount
contains the total amount (in base units) taken. The debonding and active escrow balances are slashed in equal proportions.debonding_amount
contains the amount (in base units) taken from just the debonding escrow balance.
The reclaim escrow event is emitted when a reclaim escrow operation completes successfully (after the debonding period has passed).
Body:
type ReclaimEscrowEvent struct {
Owner Address `json:"owner"`
Escrow Address `json:"escrow"`
Amount quantity.Quantity `json:"amount"`
Shares quantity.Quantity `json:"shares"`
}
Fields:
owner
contains the address of the account that reclaimed tokens from escrow.escrow
contains the address of the account escrow has been reclaimed from.amount
contains the amount (in base units) reclaimed.shares
contains the amount of shares reclaimed.
Body:
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"`
}
Fields:
owner
contains the address of the account owner where allowance has been changed.beneficiary
contains the address of the beneficiary.allowance
contains the new total allowance.amount_change
contains the absolute amount the allowance has changed for.negative
specifies whether the allowance has been reduced rather than increased.
The event is emitted even if the new allowance is zero.
max_allowances
(uint32) specifies the maximum number of allowances an account can store. Zero means that allowance functionality is disabled.
To generate test vectors for various staking transactions, run:
make -C go staking/gen_vectors
For more information about the structure of the test vectors see the section on Transaction Test Vectors.