Skip to content

SIMD-0185: Vote Account v4 #185

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

Merged
merged 15 commits into from
Jul 11, 2025
245 changes: 245 additions & 0 deletions proposals/0185-vote-account-v4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
---
simd: '0185'
title: Vote Account v4
authors: Justin Starry (Anza)
category: Standard
type: Core
status: Review
created: 2024-10-17
feature: (fill in with feature tracking issues once accepted)
---

## Summary

Add a new version of vote account state which clears unused state to make
room for new state information.

## Motivation

A new update for the vote program is proposed to improve authorized voter
bookkeeping as well as initialize state fields that will allow validators to set
commission rates and collector accounts for different revenue sources in the
future.

### Authorized voter bookkeeping

- Over 40% of vote state size is reserved for tracking a history of 32 prior
authorized voters for the vote account in the `prior_voters` field. Having such
a long history of prior voters is arguably not very useful, tracking the most
recent previous epoch's voter is probably sufficient and can be stored in the
`authorized_voters` field instead.

- The `authorized_voters` field doesn't store the voter for the previous epoch
so it's impossible to have a transition epoch where both the previous and newly
assigned voter can both sign votes.

### Revenue Collection Customization

- There is only one commission rate stored in vote account state but validators
want to be able to use different commission rates for different income streams
like block revenue.

- It's not possible to customize which accounts income is collected into.
Currently all block fee revenue is collected into the validator identity account
which cannot be a cold wallet since the identity needs to sign a lot of messages
for various network protocols used in Solana like turbine, gossip, and QUIC.

## Alternatives Considered

### Reuse vote commission

Vote accounts already allow validators to set a commission rate for inflation
rewards and so it's not unreasonable to expect that this commission rate could
also be used to distribute block revenue. However, some validators have
expressed a desire to be able to tune revenue streams independently.

## New Terminology

- Block Revenue Commission: The commission rate that determines how much of
block base fee and priority fee revenue is collected by the validator before
distributing remaining funds to stake delegators. Previously 100% of block
revenue was distributed to the validator identity account.

- Inflation Rewards Commission: The commission rate that determines how much of
stake inflation rewards are collected by the validator before distributing the
remaining rewards to stake delegators. Previously referred to as simply the
"commission" since there was no need to differentiate from other types of
commission.

- Block Revenue Commission Collector: The account used to collect commissioned
block revenue for validators. Previously collected by default into the validator
identity account.

- Inflation Rewards Commission Collector: The account used to collect
commissioned inflation rewards for validators. Previously collected by default
into the vote account.

## Detailed Design

Currently, all block revenue, including both transaction base fees and priority
fees, is collected into a validator's node id account. This proposal details
changes to the vote account state that will allow validators to specify how
different sources of income are collected in future SIMD's. This proposal also
updates the bookkeeping for authorized voters in the vote account state.

### Vote Account

A new version of vote state will be introduced with the enum discriminant value
of `3u32` which will be little endian encoded in the first 4 bytes of account
data.

```rust
pub enum VoteStateVersions {
V1(..),
V2(..),
V3(..),
V4(..), // <- new variant
}
```

This new version of vote state will include new fields for setting the
commission and collector account for the following sources of validator income:
inflation rewards and block revenue. It will also remove the `prior_voters`
field.

```rust
pub struct VoteStateV4 {
pub node_pubkey: Pubkey,
pub authorized_withdrawer: Pubkey,

/// REMOVED
/// commission: u8,

/// NEW: the collector accounts for validator income
pub inflation_rewards_collector: Pubkey,
pub block_revenue_collector: Pubkey,

/// NEW: basis points (0-10,000) that represent how much of each income
/// source should be given to this VoteAccount
pub inflation_rewards_commission_bps: u16,
pub block_revenue_commission_bps: u16,

/// NEW: reward amount pending distribution to stake delegators
pub pending_delegator_rewards: u64,

pub votes: VecDeque<LandedVote>,
pub root_slot: Option<Slot>,
pub authorized_voters: AuthorizedVoters,

/// REMOVED
/// prior_voters: CircBuf<(Pubkey, Epoch, Epoch)>,

pub epoch_credits: Vec<(Epoch, u64, u64)>,
pub last_timestamp: BlockTimestamp,
}
```

### Vote Program

All vote instructions MUST be updated to support deserializing v4 vote accounts.

Whenever a vote account is initialized OR modified by the vote program in a
transaction AND hasn't been updated to v4 yet, the account state MUST be saved
in the new format with the following default values for the new fields described
above:

```rust
VoteStateV4 {
// ..

inflation_rewards_collector: Pubkey::default(),
block_rewards_collector: Pubkey::default(),
inflation_rewards_commission_bps: 100u16 * (old_vote_state.commission as u16),
block_rewards_commission_bps: 10_000u16,
pending_delegator_rewards: 0u64,

// ..
}
```

If a modified vote account's size is smaller than `3762` bytes, first resize the
account to `3762` bytes before updating the account data. The vote program does
not need to check if the resulting account is rent exempt, the runtime will
enforce that check. This differs from the prior vote program implementation which
falls back to store vote state as v2 if the account size cannot be resized while
keeping the account rent exempt.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt this was nice to have because it simplifies the vote program implementation and ensures that all v4 vote accounts have the same size. Here's the current code in question that this will clean up:

https://github.com/anza-xyz/agave/blame/0df4a2d963e06ced795247719ef1aa16c6d71d15/programs/vote/src/vote_state/mod.rs#L40-L54



### `InitializeAccount`

The required size for vote accounts previously set to `3762` bytes MUST remain
unchanged despite freeing up space with the removal of the prior voters field.
Keeping the same size requirement simplifies this proposal and leaves extra
space for future fields.

Note that a v4 vote account is ALWAYS considered initialized, because unlike
other vote state versions, it's never stored with uninitialized state.

#### `UpdateCommission`

The existing `UpdateCommission` instruction (with enum discriminant `5u32`) will
will continue to only update the inflation rewards commission in integer
percentage values.

When updating vote state v4 accounts, the new `inflation_rewards_commission_bps`
field should be used instead of the old generic `commission` field.
Additionally, the new commission value MUST be multiplied by `100` before being
stored.

#### `Authorize`, `AuthorizeChecked`, `AuthorizeWithSeed`, `AuthorizeCheckedWithSeed`

Existing authorize instructions will be processed differently when setting new
authorized voters. Rather than purging authorized voter entries from the
`authorized_voters` field that correspond to epochs less than the current epoch,
only purge entries less than the previous epoch (current epoch - 1). This will
mean that the `authorized_voters` field can now hold up to 4 entries for the
epochs in the range `[current_epoch - 1, current_epoch + 2]`. Keeping the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason we need to store current_epoch + 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's the current design and I think it kinda makes sense because otherwise you could set a new authorized voter at the end of an epoch and immediately start using it in the next block in the new epoch and leaders would need an up to date view on any new authorized voters for each fork crossing the epoch boundary

authorized voter around from the previous epoch will allow the protocol to
accept votes from both the current and previous authorized voters to make voter
transitions smoother.

Additionally, since the `prior_voters` field is removed from vote state v4,
there's no need to read or modify this field when processing authorize
instructions.

#### `Withdraw`

The existing withdraw instruction MUST be modified to completely zero vote
account data for fully withdrawn vote accounts. The old behavior partially
zeroed the account data following the vote state version discriminant and is
less intuitive.
Comment on lines +213 to +216
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt this was a nice to have change. This way we know that any vote account with the version discriminant bytes 03 00 00 00 is still initialized. Other vote state versions require checking fields for initialized values to be sure of initialization which is a footgun IMO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me, unless there's some reason it's done like this currently? Seems strange to keep that discriminator set afterward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No explicit reason that I'm aware of. We will need an extra look from auditors at this though.


### Stake Program

The builtin stake program reads vote account state when creating, delegating,
and deactivating stake accounts. The program MUST be updated to support v4 vote
accounts.

### Other

The runtime stakes cache and epoch stakes stored in snapshots MUST also be updated
to support initialized v4 vote accounts.

The tower serialization format MUST remain unchanged and continue serializing tower
vote state as vote state v2.

## Impact

This is a prerequisite for implementing other SIMD's like block reward
distribution in [SIMD-0123] which give validators more flexibility in how
inflation rewards and block revenue is collected and distributed.

[SIMD-0123]: https://github.com/solana-foundation/solana-improvement-documents/pull/123

## Security Considerations

NA

## Drawbacks *(Optional)*

NA

## Backwards Compatibility *(Optional)*

Existing programs that read vote state will need to be updated to support the
latest account state version.
Loading