Skip to content

runtime: Implement rent-adjusted stake delegations for amended SIMD-0392 (formerly SIMD-0488)#11332

Draft
joncinque wants to merge 9 commits intoanza-xyz:masterfrom
joncinque:rad
Draft

runtime: Implement rent-adjusted stake delegations for amended SIMD-0392 (formerly SIMD-0488)#11332
joncinque wants to merge 9 commits intoanza-xyz:masterfrom
joncinque:rad

Conversation

@joncinque
Copy link
Copy Markdown

Problem

As described in SIMD-0488, we need a mechanism for adjusting stake delegations during epoch rewards payout.

Summary of Changes

Implement the new calculation logic. The commits can be read in order:

  • add the feature gate
  • adjust the calculation
  • tweak existing tests, add calculation test
  • add distribution test

Note: leaving this in draft until the SIMD goes through, there's still one more open question about respecting the minimum SOL delegation amount

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 16, 2026

Codecov Report

❌ Patch coverage is 98.57143% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.2%. Comparing base (0ef8fb3) to head (d8a8278).
⚠️ Report is 10 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff            @@
##           master   #11332    +/-   ##
========================================
  Coverage    83.1%    83.2%            
========================================
  Files         847      847            
  Lines      320546   321076   +530     
========================================
+ Hits       266680   267261   +581     
+ Misses      53866    53815    -51     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joncinque
Copy link
Copy Markdown
Author

Although the SIMD isn't in yet, for the sake of moving things forward quickly, please take a look when you have a moment. The actual code changes are pretty small, so this should be an easy backport if we make that decision.


#[derive(Default)]
struct RewardsAccumulator {
/// Accounts receiving voting commissions at the epoch boundary
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

please avoid unrelated changes. this should be a separate pr

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The other comments "/// Number of stake rewards, used to figure out size of rewards vector" and "/// Accumulated reward lamports, only used for sanity checks" are very important, since it helps show what's actually important in all of these structs.

Adding comments to two out of three fields in a struct felt silly, so I added this one too. The other ones are important to this change though.

stake.delegation.stake = new_delegation;
// Deactivate stake if needed
if new_delegation == 0 {
stake.delegation.deactivation_epoch = rewarded_epoch;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

since phone guys don't know what encapsulation is, can we have a comment here about this deactivation being immediate, unlike an authority-requested deactivation which is cast to the next epoch boundary

}
}

#[allow(clippy::too_many_arguments)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

i think it'd be worthwhile to do a cleanup preliminary pr where we just pass the whole stake account down

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't think it would really help to be honest -- it's either we pass the whole account and rent sysvar, or we pass the current and minimum lamport amount. I found the lamport amounts to be much simpler

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ok but you literally added the "fuck you guys i'm lazy" attribute

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I was answering the suggestion -- if you really want to refactor this function, I can do it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

#11681 if you want it

})
))
} else {
// No rewards to pay out, but still might need to modify delegation
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

seems like it'd be more maintainable to combine these blocks. for the sake of this operation, we don't care whether the account has earned rewards

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I implemented your suggestion, but it makes the code much harder to understand (and thus, maintain) in my opinion. I've pushed it up, let me know what you think

@joncinque joncinque changed the title runtime: Implement rent-adjusted stake delegations for SIMD-0488 runtime: Implement rent-adjusted stake delegations for amended SIMD-0392 (formerly SIMD-0488) Mar 19, 2026
@joncinque
Copy link
Copy Markdown
Author

Noting that this will be rebased on #9380 to use the feature gate from there

Comment on lines +140 to +145
let new_delegation = std::cmp::min(
stake.delegation.stake.saturating_add(staker_rewards),
current_lamports
.saturating_add(staker_rewards)
.saturating_sub(minimum_lamports),
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this looks correct to me

joncinque added a commit to joncinque/solana-sdk that referenced this pull request Mar 23, 2026
#### Problem

As mentioned during this
[SIMD-0392 amendment](solana-foundation/solana-improvement-documents#488 (comment)),
block metadata should carry the information of when a stake account is
deactivting.

#### Summary of changes

Add a new `DeactivatingStake` variant of `RewardType`, to be used in
anza-xyz/agave#11332.
joncinque added a commit to anza-xyz/solana-sdk that referenced this pull request Mar 24, 2026
* reward-info: Add variant for deactivating stake

#### Problem

As mentioned during this
[SIMD-0392 amendment](solana-foundation/solana-improvement-documents#488 (comment)),
block metadata should carry the information of when a stake account is
deactivting.

#### Summary of changes

Add a new `DeactivatingStake` variant of `RewardType`, to be used in
anza-xyz/agave#11332.

* Change wording
@mergify
Copy link
Copy Markdown

mergify bot commented Mar 24, 2026

If this PR represents a change to the public RPC API:

  1. Make sure it includes a complementary update to rpc-client/ (example)
  2. Open a follow-up PR to update the JavaScript client @solana/kit (example)

Thank you for keeping the RPC clients in sync with the server API @joncinque.

@joncinque
Copy link
Copy Markdown
Author

This now contains the new reward type of DeactivatedStake, to be used when a stake becomes inactive, so that all deactivations (include forced ones due to rent) can be tracked. It will still require #9380

}
}

pub fn new_with_pre_stake_account(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
pub fn new_with_pre_stake_account(
#[cfg(feature = "dev-context-only-utils")]
pub fn new_with_pre_stake_account(

'cause it calls Pubkey::unique()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is already gated with dev-context-only-utils on line 97

let vote_state =
vote_state::VoteStateV4::deserialize(vote_account.data(), voter_pubkey).unwrap();
let credits_observed = vote_state.credits();
let last_epoch = vote_state.epoch_credits.last().map(|c| c.0).unwrap_or(0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

is the _or(0) the right thing to do here? seems like current_epoch - 1 is more correct, but this doesn't not work? just unclear whether it will encourage us to write more misleading tests

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sure why not, it can't hurt to pass in the epoch too. I was going with the simplest way to just create a deactivated stake account

Comment on lines +783 to +784
&& (reward_type == RewardType::Staking
|| reward_type == RewardType::DeactivatedStake))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

in retrospect, we should have added a helper on RewardType

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The thought did cross my mind 🙃

))
.map_err(|_| DistributionError::UnableToSetState)?;

let reward_type = if partitioned_stake_reward.stake.delegation.stake(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

thanks for reminding me how worthlessly ambiguous the stake interface is 🫠

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants