7.9 KiB
Stake Delegation and Reward
This design proposal focuses on the software architecture for the on-chain voting and staking programs. Incentives for staking is covered in staking rewards.
The current architecture requires a vote for each delegated stake from the validator, and therefore does not scale to allow replicator clients to automatically delegate their rewards.
The design proposes a new set of programs for voting and stake delegation, The proposed programs allow many stake accounts to passively earn rewards with a single validator vote without permission or active involvement from the validator.
Current Design Problems
In the current design each staker creates their own VoteState, and assigns a delegate in the VoteState that can submit votes. Since the validator has to actively vote for each stake delegated to it, validators can censor stakes by not voting for them.
The number of votes is equal to the number of stakers, and not the number of validators. Replicator clients are expected to delegate their replication rewards as they are earned, and therefore the number of stakes is expected to be large compared to the number of validators in a long running cluster.
Proposed changes to the current design.
The general idea is that instead of the staker, the validator will own the VoteState program. In this proposal the VoteState program is there to track validator votes, count validator generated credits and to provide any additional validator specific state. The VoteState program is not aware of any stakes delegated to it, and has no staking weight.
The rewards generated are proportional to the amount of lamports staked. In this proposal stake state is stored as part of the StakeState program. This program is owned by the staker only. Lamports stored in this program are the stake. Unlike the current design, this program contains a new field to indicate which VoteState program the stake is delegated to.
VoteState
VoteState is the current state of all the votes the delegate has submitted to the bank. VoteState contains the following state information:
-
votes - The submitted votes data structure.
-
credits - The total number of rewards this vote program has generated over its lifetime.
-
root_slot - The last slot to reach the full lockout commitment necessary for rewards.
-
commission - The commission taken by this VoteState for any rewards claimed by staker's StakeState accounts. This is the percentage ceiling of the reward.
-
Account::lamports - The accumulated lamports from the commission. These do not count as stakes.
-
authorized_vote_signer
- Only this identity is authorized to submit votes, and this field can only modified by this entity
VoteInstruction::Initialize
account[0]
- RW - The VoteStateVoteState::authorized_vote_signer
is initialized toaccount[0]
other VoteState members defaulted
VoteInstruction::AuthorizeVoteSigner(Pubkey)
account[0]
- RW - The VoteStateVoteState::authorized_vote_signer
is set to toPubkey
, instruction must by signed by Pubkey
StakeState
A StakeState takes one of two forms, StakeState::Delegate and StakeState::MiningPool.
StakeState::Delegate
StakeState is the current delegation preference of the staker. StakeState contains the following state information:
-
Account::lamports - The staked lamports.
-
voter_id
- The pubkey of the VoteState instance the lamports are delegated to. -
credits_observed
- The total credits claimed over the lifetime of the program.
StakeState::MiningPool
There are two approaches to the mining pool. The bank could allow the StakeState program to bypass the token balance check, or a program representing the mining pool could run on the network. To avoid a single network wide lock, the pool can be split into several mining pools. This design focuses on using a StakeState::MiningPool as the cluster wide mining pools.
- 256 StakeState::MiningPool are initialized, each with 1/256 number of mining pool
tokens stored as
Account::lamports
.
The stakes and the MiningPool are accounts that are owned by the same Stake
program.
StakeInstruction::Initialize
-
account[0]
- RW - The StakeState::Delegate instance.StakeState::Delegate::credits_observed
is initialized toVoteState::credits
.StakeState::Delegate::voter_id
is initialized toaccount[1]
-
account[1]
- R - The VoteState instance.
StakeInstruction::RedeemVoteCredits
The VoteState program and the StakeState programs maintain a lifetime counter
of total rewards generated and claimed. Therefore an explicit Clear
instruction is not necessary. When claiming rewards, the total lamports
deposited into the StakeState and as validator commission is proportional to
VoteState::credits - StakeState::credits_observed
.
account[0]
- RW - The StakeState::MiningPool instance that will fulfill the reward.account[1]
- RW - The StakeState::Delegate instance that is redeeming votes credits.account[2]
- R - The VoteState instance, must be the same asStakeState::voter_id
Reward is payed out for the difference between VoteState::credits
to
StakeState::Delgate.credits_observed
, and credits_observed
is updated to
VoteState::credits
. The commission is deposited into the VoteState
token
balance, and the reward is deposited to the StakeState::Delegate
token balance. The
reward and the commission is weighted by the StakeState::lamports
divided by total lamports staked.
The Staker or the owner of the Stake program sends a transaction with this instruction to claim the reward.
Any random MiningPool can be used to redeem the credits.
let credits_to_claim = vote_state.credits - stake_state.credits_observed;
stake_state.credits_observed = vote_state.credits;
credits_to_claim
is used to compute the reward and commission, and
StakeState::Delegate::credits_observed
is updated to the latest
VoteState::credits
value.
Collecting network fees into the MiningPool
At the end of the block, before the bank is frozen, but after it processed all the transactions for the block, a virtual instruction is executed to collect the transaction fees.
- A portion of the fees are deposited into the leader's account.
- A portion of the fees are deposited into the smallest StakeState::MiningPool account.
Benefits
-
Single vote for all the stakers.
-
Clearing of the credit variable is not necessary for claiming rewards.
-
Each delegated stake can claim its rewards independently.
-
Commission for the work is deposited when a reward is claimed by the delegated stake.
This proposal would benefit from the read-only
accounts proposal to allow for
many rewards to be claimed concurrently.
Passive Delegation
Any number of instances of StakeState::Delegate programs can delegate to a single VoteState program without an interactive action from the identity controlling the VoteState program or submitting votes to the program.
The total stake allocated to a VoteState program can be calculated by the sum of
all the StakeState programs that have the VoteState pubkey as the
StakeState::Delegate::voter_id
.
Example Callflow
Future work
Validators may want to split the stake delegated to them amongst many validator nodes since stake is used as weight in the network control and data planes. One way to implement this would be for the StakeState to delegate to a pool of validators instead of a single one.
Instead of a single vote_id
and credits_observed
entry in the StakeState
program, the program can be initialized with a vector of tuples.
Voter {
voter_id: Pubkey,
credits_observed: u64,
weight: u8,
}
- voters: Vec - Array of VoteState accounts that are voting rewards with this stake.
A StakeState program would claim a fraction of the reward from each voter in
the voters
array, and each voter would be delegated a fraction of the stake.