-
Notifications
You must be signed in to change notification settings - Fork 253
SIMD-0123: Block Revenue Distribution #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0131156
b726b37
6024552
80d7fa3
e69c317
9028617
1a7ab60
8d4dbc9
57edcab
9ae2000
691c4d3
9ffac84
03f20ac
8e1f147
712ce19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| --- | ||
| simd: '0123' | ||
| title: Block Revenue Sharing | ||
| authors: Justin Starry (Anza) | ||
| category: Standard | ||
| type: Core | ||
| status: Review | ||
| created: 2024-03-10 | ||
| feature: (fill in with feature tracking issues once accepted) | ||
| --- | ||
|
|
||
| ## Summary | ||
|
|
||
| A new mechanism is proposed to allow validators to share part of their block | ||
| revenue with their delegators. Commission rates from validator vote accounts | ||
| will be used by the protocol to calculate post-commission rewards that will be | ||
| automatically distributed to delegated stake accounts after an epoch is | ||
| completed. | ||
|
|
||
| ## Motivation | ||
|
|
||
| Delegated stake directly increases the number of blocks that a validator is | ||
| allocated in an epoch leader schedule but the core protocol doesn't support | ||
| diverting any of that extra revenue to stake delegators. | ||
|
|
||
| ## Dependencies | ||
|
|
||
| This proposal depends on the following previously accepted proposals: | ||
|
|
||
| - **[SIMD-0180]: Use Vote Account Address To Key Leader Schedule** | ||
|
|
||
| Necessary for looking up a block producer's vote account | ||
|
|
||
| - **[SIMD-0185]: Vote Account v4** | ||
|
|
||
| Introduces version 4 of the vote account state, which adds new fields | ||
| for block revenue commission and pending delegation rewards | ||
|
|
||
| - **[SIMD-0232]: Custom Commission Collector Account** | ||
|
|
||
| Necessary for looking up a block producer's commission collector account | ||
|
|
||
| - **[SIMD-0291]: Commssion Rate in Basis Points** | ||
|
|
||
| Introduces a new instruction type for setting commission rates in basis | ||
| points | ||
|
|
||
| [SIMD-0180]: https://github.com/solana-foundation/solana-improvement-documents/pull/180 | ||
| [SIMD-0185]: https://github.com/solana-foundation/solana-improvement-documents/pull/185 | ||
| [SIMD-0232]: https://github.com/solana-foundation/solana-improvement-documents/pull/232 | ||
| [SIMD-0291]: https://github.com/solana-foundation/solana-improvement-documents/pull/291 | ||
|
|
||
| ## Alternatives Considered | ||
|
|
||
| ### Distribute Rewards as Activated Stake | ||
|
|
||
| The runtime could ensure that any distributed stake rewards get activated as | ||
| well but it would require extra complexity in the protocol to support that | ||
| feature. Instead, stakers will receive inactive SOL in their stake accounts that | ||
| they will have to manage themselves. [SIMD-0022] aims to make this experience | ||
| better for stakers by allowing stake accounts to separately delegate any | ||
| unstaked balance in their accounts. | ||
|
|
||
| [SIMD-0022]: https://github.com/solana-foundation/solana-improvement-documents/pull/22 | ||
|
|
||
| ### Out of protocol reward distribution | ||
|
|
||
| Due to the lack of core protocol support for distributing block revenue to | ||
| stakers, validators have developed their own solutions which are not enforced by | ||
| the core protocol. For example, the Cogent validator diverts part of its fee | ||
| revenue to NFT holders. But it's up the NFT holders to audit and hold Cogent | ||
| accountable to a specific commission rate. | ||
|
|
||
| Another alternative is Jito's mechanism for block "tips" (not fees, but the idea | ||
| is similar). Jito's validator implementation includes a tip distribution program | ||
| which it instructs validator operators to divert all of their tips to but cannot | ||
| enforce perfect compliance. It's up to stakers and the Jito team to audit | ||
| compliance by validator operators. This mechanism requires trusting a | ||
| third-party (in this case Jito) to calculate reward distribution in an accurate | ||
| and fair manner. It also relies on using a merkle tree to distribute fees to all | ||
| stake accounts and the distributed fees are not automatically staked in | ||
| recipient stake accounts. | ||
|
|
||
| ## New Terminology | ||
|
|
||
| NA | ||
|
|
||
| ## Detailed Design | ||
|
|
||
| ### Runtime: Block Revenue Collection | ||
|
|
||
| After all transactions are processed in a block for a given leader, rather than | ||
| collecting all block revenue into the validator identity account, the protocol | ||
| will look up the block producer's vote account as described in [SIMD-0180]. Then | ||
| it MUST check if the validator's vote account has specified a block revenue | ||
| commission rate and collector addresses in the new vote account version | ||
| described in [SIMD-0185]. As described in [SIMD-0232], the latest block revenue | ||
| commission rate and collector address MUST be loaded from the vote account state | ||
| at the beginning of the previous epoch. This is the same vote account state used | ||
| to build the leader schedule for the current epoch. | ||
|
|
||
| If the block revenue commission rate and collector account aren't set (e.g., the | ||
| vote account state version has not been updated to v4 yet), all revenue will be | ||
| collected into the validator's identity account as before. If the block revenue | ||
| commission rate and collector account *are* specified, the rewards MUST be | ||
| distributed according to the commission and delegator rewards collection | ||
| sections below. | ||
|
|
||
| #### Commission Collection | ||
|
|
||
| The commission amount MUST be calculated by first multiplying the amount of | ||
| revenue by the lesser of the vote account's block revenue commission rate or the | ||
| maximum of `10,000` basis points. Then use integer division to divide by | ||
| `10,000` and discard the remainder. If the commission amount is non-zero, the | ||
| block revenue commission collector account MUST be loaded and checked for the | ||
| following conditions: | ||
|
|
||
| 1. account is system program owned AND | ||
| 2. account is rent-exempt after depositing the commission. | ||
|
|
||
| If the conditions are met, the commission amount MUST be deposited into the | ||
| block revenue commission collector account. If either of these conditions is | ||
| violated, the commission amount MUST be burned. | ||
|
|
||
| #### Delegator Rewards Collection | ||
|
|
||
| The delegator rewards amount MUST be calculated by subtracting the calculated | ||
| commission from the block fee revenue. If the delegator rewards amount is | ||
| non-zero, the vote account must be loaded and checked for the following | ||
| conditions: | ||
|
|
||
| 1. account is vote program owned AND | ||
| 2. account is initialized with vote state v4 or later | ||
|
|
||
| If the conditions are met, the delegator rewards amount MUST be added to the | ||
| vote state field `pending_delegator_rewards` and added to the balance of vote | ||
| account. If either of these conditions is violated, the delegator rewards amount | ||
| MUST be burned. | ||
|
|
||
| ### Runtime: Delegator Rewards Distribution | ||
|
|
||
| When calculating stake delegation rewards for a particular completed reward | ||
| epoch, construct a list of all vote accounts that were initialized at the | ||
| beginning of the reward epoch and had a non-zero active stake delegation. For | ||
| each vote account, retrieve its state at the end of the reward epoch and check | ||
| the `pending_delegator_rewards` field in its vote state. Let this value be `P`. | ||
| If `P` is non-zero, use it to calculate rewards for each of the stake accounts | ||
| delegated to the vote account as follows: | ||
|
|
||
| 1. Sum all active stake delegated to the vote account during the reward epoch | ||
| epoch. Let this total be `A`. | ||
|
|
||
| 2. For each individual stake account, multiply its active stake from the | ||
| reward epoch by `P`, and divide the result by `A` using integer division. | ||
| Discard any fractional lamports. | ||
|
|
||
| After calculating all individual stake rewards, sum them to obtain `D`, the | ||
| total distribution amount. Because of integer division, the full amount `P` may | ||
| not be distributed so compute the amount to be burned, `B`, as the difference | ||
| between `P` and `D`. | ||
|
|
||
| If no blocks in the epoch following the completed reward epoch have been | ||
| processed yet, subtract `B` from both the vote account’s lamport balance and its | ||
| `pending_delegator_rewards` field and store the updated vote account. Finally, | ||
| the burn amount `B` should also be deducted from the cluster capitalization. | ||
|
Comment on lines
+162
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t think this is correct, or I didn’t understand the logic here. Secondly, when we calculate rewards for stake accounts, we simply increase capitalization at the same moment. That is, if for some reason, by the time of calculation, the stake account or vote account no longer exists, it just won’t increase the capitalization (which reduces real inflation). However, in the case of the vote account, this will lead to unfair distribution (some stake accounts of the removed vote account may still manage to receive their rewards). In the case of additional rewards, we move amounts from the vote account to the stake accounts. By the way, this will lead to writing all vote accounts every block (assuming the partition reward distribution is uniformly distributed), right? I think it’s better to follow the approach of SIMD-0118, but we’ll have to use a sysvar for that (since capitalization is not increased). That is, in the first block of the epoch, record the reward distribution amounts by vote accounts into the sysvar data, and move that amount to the sysvar. This way, we can avoid the error of double discarding the fractional part (which I described above), reduce the number of accounts written to in each block, and overall allows us to lift the restriction on deleting a vote account before distribution is complete (I think this was mentioned in the description of the v4 vote program).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps I wasn’t entirely precise. The two-line example above is actually a single example. If there were only one field pending_delegator_rewards, it wouldn’t be sufficient. The example assumes a total of 7 lamports to be distributed and two stakers with shares of 0.1 and 0.9 for the validator. If calculated at the beginning of the epoch, the stakers would receive 0 and 6 lamports, respectively. And we would immediately decrease pending_delegator_rewards by 1 lamport. But then, during actual distribution, we would get 0 and 5 lamports due to truncation. And 1 lamport would remain undistributed. To avoid this, we need to store the original value of pending_delegator_rewards. Thus, we need two fields - one to store new rewards for the current epoch and another to store the original value from the previous epoch. The remaining amount to be distributed is not strictly necessary. In any case, using a sysvar moves these problems out of the vote account state. The sysvar data can store only the original unmodified amount, and the balance transfer from the vote account can be done using the reduced value.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use the original value of pending rewards from the epoch boundary for each of the stake account calculations There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, yes, I understand. But the entire paragraph refers to “If no blocks in the epoch…” and then “…and store the updated vote account”. So it looks like the actual value will already be lost by the second block of the epoch. But the actual funding of stake accounts will happen later. And at that point, the original value will be needed again (because without it, the error I described in the example will occur). And yes, I’m assuming here that this value is stored somewhere in the state, not just in the Banks at validator runtime. But I saw the new thread. I think it makes sense to move the discussion there. |
||
|
|
||
| #### Individual Delegator Reward | ||
|
|
||
| The stake reward distribution amounts for each stake account calculated above | ||
| can then be used to construct a list of stake reward entries which MUST be | ||
| partitioned and distributed according to [SIMD-0118]. | ||
|
|
||
| When reward entries are used to distribute rewards pool funds during partitioned | ||
| rewards distribution, the delegated vote account for each rewarded stake account | ||
| must have its `pending_delegator_rewards` field and its balance deducted with | ||
| the amount of rewards distributed to keep capitalization consistent. | ||
|
|
||
| [SIMD-0118]: https://github.com/solana-foundation/solana-improvement-documents/pull/118 | ||
|
|
||
| ### Vote Program | ||
|
|
||
| #### Withdraw | ||
|
|
||
| Since pending delegator rewards will be stored in the validator's vote account | ||
| until distribution at the next epoch boundary, those funds will be unable to be | ||
| withdrawn. | ||
|
|
||
| The `Withdraw` instruction must be modified so that if the balance indicated by | ||
| the `pending_delegator_rewards` field is non-zero, the vote account will no | ||
| longer be closeable by fully withdrawing funds. The withdrawable balance when | ||
| `pending_delegator_rewards` is non-zero will be equal to the vote account's | ||
| balance minus `pending_delegator_rewards` and the minimum rent exempt balance. | ||
|
|
||
| #### UpdateCommissionBps | ||
|
|
||
| The `UpdateCommissionBps` instruction added in [SIMD-0291] must be updated to | ||
| add support for updating the block revenue commission rate. | ||
|
|
||
| When the specified commission kind is `CommissionKind::BlockRevenue`, update the | ||
| `block_revenue_commission_bps` field instead of the previous behavior of | ||
| returning an `InstructionError::InvalidInstructionData`. | ||
|
|
||
| Note that the commission rate is allowed to be set and stored as any `u16` value | ||
| but as detailed above, it will capped at 10,000 during the actual commission | ||
| calculation. | ||
|
|
||
| #### DepositDelegatorRewards | ||
|
|
||
| A new instruction for distributing lamports to stake delegators will be added to | ||
| the vote program with the enum discriminant value of `18u32` little endian | ||
| encoded in the first 4 bytes. | ||
|
|
||
| ```rust | ||
| pub enum VoteInstruction { | ||
| /// # Account references | ||
| /// 0. `[WRITE]` Vote account to be updated with the deposit | ||
| /// 1. `[SIGNER, WRITE]` Source account for deposit funds | ||
| DepositDelegatorRewards { // 18u32 | ||
| deposit: u64, | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| Perform the following checks: | ||
|
|
||
| - If the number of account inputs is less than 2, return | ||
| `InstructionError::NotEnoughAccountKeys` | ||
| - If the vote account (index `0`) fails to deserialize, return | ||
| `InstructionError::InvalidAccountData` | ||
| - If the vote account is not initialized with state version 4, return | ||
| `InstructionError::InvalidAccountData` | ||
|
|
||
| Then the processor should perform a system transfer CPI of `deposit` lamports | ||
| from the source account (index `1`) to the vote account. Lastly, increment the | ||
| `pending_delegator_rewards` value by `deposit`. | ||
|
|
||
| ## Impact | ||
|
|
||
| Stake delegators will receive additional income when delegating to validators | ||
| who adopt this new feature by setting a block revenue commission rate less than | ||
| the default of `100%`. | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| NA | ||
|
|
||
| ## Backwards Compatibility | ||
|
|
||
| A feature gate will be used to enable block reward distribution at an epoch | ||
| boundary. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s not very clear here, especially if you’re not a native speaker, I suspect. The version that refers to the previous epoch, the current epoch, and the first block of the current epoch is more unambiguous. Variants: the epoch for which rewards are earned - rewarded epoch, the epoch in which rewards are distributed - distribution epoch. Or you can use N and N-1 terminology for clarity. I generally prefer the approach used in SIMD-0118. It clearly defines what and when is taken from the state, what load from the snapshot (and save to), and how it all fits together.