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 VoteState VoteState::authorized_vote_signer is initialized to account[0] other VoteState members defaulted

VoteInstruction::AuthorizeVoteSigner(Pubkey)

  • account[0] - RW - The VoteState VoteState::authorized_vote_signer is set to to Pubkey, instruction must by signed by Pubkey

StakeState

A StakeState takes one of two forms, StakeState::Stake and StakeState::MiningPool.

StakeState::Stake

Stake is the current delegation preference of the staker. Stake contains the following state information:

  • voter_pubkey - The pubkey of the VoteState instance the lamports are delegated to.

  • credits_observed - The total credits claimed over the lifetime of the program.

  • stake - The actual activated stake.

  • Account::lamports - Lamports available for staking, including any earned as rewards.

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::DelegateStake(stake)

  • account[0] - RW - The StakeState::Stake instance. StakeState::Stake::credits_observed is initialized to VoteState::credits. StakeState::Stake::voter_pubkey is initialized to account[1] StakeState::Stake::stake is initialized to stake, as long as it's less than account[0].lamports

  • 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::Stake instance that is redeeming votes credits.
  • account[2] - R - The VoteState instance, must be the same as StakeState::voter_pubkey

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::Stake 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::Stake::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::Stake 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::Stake::voter_pubkey.

Example Callflow

Passive Staking 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_pubkey and credits_observed entry in the StakeState program, the program can be initialized with a vector of tuples.

Voter {
    voter_pubkey: 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.