Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Forcibly reduce credits_observed to the last epoch#13233

Closed
ryoqun wants to merge 3 commits intosolana-labs:masterfrom
ryoqun:credits-observed-overwrite
Closed

Forcibly reduce credits_observed to the last epoch#13233
ryoqun wants to merge 3 commits intosolana-labs:masterfrom
ryoqun:credits-observed-overwrite

Conversation

@ryoqun
Copy link
Copy Markdown
Contributor

@ryoqun ryoqun commented Oct 28, 2020

Problem

The initial inflation reward could be unexpected if inflation has not been enabled since genesis. (i.e. Virtually all of live clusters)
Subsequent inflation rewards are correct, though.

Conceptually, inflation rewards should be a fair distribution of a stake-weighted contribution metrics (called points) across validators for some reasonable recent timespan.

The current implementation fulfils it for all inflation rewards but the first one.
The reason is that the initial inflation reward calculation is based on the full timespan since genesis.
Specifically, the initial inflation calculation uses all epochs since genesis as the timespan for points summation.

This is due to implantation remnant before periodic forced inflation rewards distribution (the solana redeem age)
Technically, this is caused because stake account's credits_observed (key component of points calculation) aren't updated at all, until the very first inflation reward.
After that, as credits_observed is initialized, the inflation is calculated for each last epoch.

This creates the following counter intuitive inflation rewards.
As an extreme example, we reward currently undelegated stake account. (But this happens only once at the first epoch after inflation is enabled)

Currently, HDAhLXUZmFyvxT2gnGv36opsU6ZAXEEmcEzHMAghTBoT is undelegated:

$ ./target/release/solana stake-account HDAhLXUZmFyvxT2gnGv36opsU6ZAXEEmcEzHMAghTBoT
Balance: 0.00228288 SOL
Rent Exempt Reserve: 0.00228288 SOL
Stake account is undelegated
Stake Authority: 9LQhe3fHy9i7RVWMiFiyon3GfC9rCzoXiwc9DjaAY3b7
Withdraw Authority: 9LQhe3fHy9i7RVWMiFiyon3GfC9rCzoXiwc9DjaAY3b7

But, if we would enable the inflation now, the stake account receives +0.000016081 SOL. That's because initial inflation doesn't only look at the last epoch, but the entire blockchain history. So, this matches to the fact the stake account was actually delegating some SOLs (=~ ◎60) from epoch 58 to epoch 65:

        HDAhLXUZmFyvxT2gnGv36opsU6ZAXEEmcEzHMAghTBoT: Delegation {
            voter_pubkey: FKsC411dik9ktS6xPADxs4Fk2SCENvAiuccQHLAPndvk,
            stake: 59987617120,
            activation_epoch: 58,
            deactivation_epoch: 65,
            warmup_cooldown_rate: 0.25,
        },

HDAhLXUZmFyvxT2gnGv36opsU6ZAXEEmcEzHMAghTBoT(Stake11111111111111111111111111111111111111): ◎0.002282880 => ◎0.002298961 (+◎0.000016081 0.704417227%)

For comparison, similarly-sized stake account only receives 0.000000014 SOL.

H6rtteTo41ifz9XwMRx3yHXywssiGQizZYNCDQr4wZj1(Stake11111111111111111111111111111111111111): ◎0.010000000 => ◎0.010000014 (+◎0.000000014 0.000140000%)

In other words, staker will be rewarded proportional to the full delegated duration, not only the last epoch, with current implementation. So, this is a situation of act sooner and earn more.

Summary of Changes

Ideally, I think we should only consider the last epoch?
To realize that, this pr forcibly overwrites credits_observed before calculating inflation.

But this phenomenon is only one-time. and pico-inflation reward is very small. So, might not worth to fix this...

Context

Found via ledger-tool capitalization check.

@codecov
Copy link
Copy Markdown

codecov Bot commented Oct 28, 2020

Codecov Report

Merging #13233 (d9a07e7) into master (f5e0adc) will increase coverage by 0.1%.
The diff coverage is 92.5%.

@@            Coverage Diff            @@
##           master   #13233     +/-   ##
=========================================
+ Coverage    82.0%    82.2%   +0.1%     
=========================================
  Files         378      378             
  Lines       90905    91258    +353     
=========================================
+ Hits        74630    75073    +443     
+ Misses      16275    16185     -90     

@mvines
Copy link
Copy Markdown
Contributor

mvines commented Oct 29, 2020

But this phenomenon is only one-time. and pico-inflation reward is very small. So, might not worth to fix this...

Yep we want to fix this! The effect is small but we don't need a muddled story for how inflation gets turned on and having to explain why that first epoch after enabling inflation is special.

@ryoqun
Copy link
Copy Markdown
Contributor Author

ryoqun commented Oct 29, 2020

Wow, test passed.

Comment thread runtime/src/bank.rs Outdated
self.get_account(&delegation.voter_pubkey),
) {
(Some(stake_account), Some(vote_account)) => {
(Some(mut stake_account), Some(vote_account)) => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is least intrusive way. but a hacky way at the same time.

I'll do more commenting

@ryoqun ryoqun force-pushed the credits-observed-overwrite branch from 070712d to 24ecc57 Compare October 29, 2020 04:54
@mvines
Copy link
Copy Markdown
Contributor

mvines commented Oct 30, 2020

This'll need a feature gate so it can be deployed out on testnet/devnet right?

@ryoqun
Copy link
Copy Markdown
Contributor Author

ryoqun commented Oct 30, 2020

This'll need a feature gate so it can be deployed out on testnet/devnet right?

Yeah, it seems so. I originally thought that this shouldn't cause any calculation changes for cluster where inflation is already enabled. But it seems not. ;) I'm now working on this.

@mvines
Copy link
Copy Markdown
Contributor

mvines commented Oct 31, 2020

This'll need a feature gate so it can be deployed out on testnet/devnet right?

Yeah, it seems so. I originally thought that this shouldn't cause any calculation changes for cluster where inflation is already enabled. But it seems not. ;) I'm now working on this.

We turned off inflation for devnet/testnet to test the inflation kill switch, and to make it easier to deploy this across all clusters. No gating required now I think!

Comment thread programs/stake/src/stake_state.rs
@ryoqun ryoqun force-pushed the credits-observed-overwrite branch from 5e14228 to 9bf5c3e Compare November 8, 2020 14:00
stake_context: Option<(Epoch, &StakeHistory)>,
inflation_point_calc_tracer: &mut Option<impl FnMut(&InflationPointCalculationEvent)>,
) -> (u128, u64) {
// if there is no newer credits since observed, return no point
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

move comment along...

@ryoqun
Copy link
Copy Markdown
Contributor Author

ryoqun commented Nov 8, 2020

  • Also, add comment to note that now that VoteState.epoch_credits can be retired after checking this is really the case?

Comment on lines +485 to +541
if epoch != target_epoch {
return (0, 0);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

write a test for this; this bug is later found after manual testing.

Copy link
Copy Markdown
Contributor Author

@ryoqun ryoqun Nov 21, 2020

Choose a reason for hiding this comment

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

well, if we don't exclude historical vote_state's epoch credits by doing like this early return, this way, we might accidentally reward stale delegations using its (last and stale) epoch_credits, which is well past to target_epoch.

@ryoqun ryoqun force-pushed the credits-observed-overwrite branch from 9bf5c3e to 8c41fac Compare November 15, 2020 19:09
@t-nelson
Copy link
Copy Markdown
Contributor

t-nelson commented Nov 18, 2020

I happened back across this bug while investigating the viability of allowing fully-activated stake accounts to be merged. Would an alternate (and cleaner?) solution be to ensure that the stake account's observed_credits field is updated even when there were no rewards to be paid out, regardless of split? Caveat being that it needs feature gated, but stake-v2 isn't enabled yet 🎉

I think this can be achieved by changing the early return here...

if rewards == 0 {
return None;
}
let (voter_rewards, staker_rewards, is_split) = vote_state.commission_split(rewards);

... to something like ...

let (voter_rewards, staker_rewards, is_split) = if rewards == 0 {
    (0, 0, false)
} else {
    vote_state.commission_split(rewards)
};

... then everything resolves itself at the next epoch boundary.

I say, "something like," because I haven't yet checked whether rewards is accounting for a sub-lamport situation here.

This solution has the side-effect that I believe we can safely merge two qualified fully activated stake accounts with just a few extra checks from the deactivated case.

@ryoqun @CriesofCarrots @mvines WDYT?

let mut points = 0;
let mut new_credits_observed = self.credits_observed;

for (epoch, final_epoch_credits, initial_epoch_credits) in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looking into this further... I believe this change can deny small stakes delegated to validators with small, but non-zero commissions ever getting paid out, as per

if (voter_rewards == 0 || staker_rewards == 0) && is_split {
// don't collect if we lose a whole lamport somewhere
// is_split means there should be tokens on both sides,
// uncool to move credits_observed if one side didn't get paid
return None;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah! correct! I intentionally done this way now. My two cent is posted here: #13680 (comment)

@CriesofCarrots
Copy link
Copy Markdown
Contributor

CriesofCarrots commented Nov 19, 2020

I think this can be achieved by changing the early return here...

if rewards == 0 {
return None;
}
let (voter_rewards, staker_rewards, is_split) = vote_state.commission_split(rewards);

... to something like ...

let (voter_rewards, staker_rewards, is_split) = if rewards == 0 {
    (0, 0, false)
} else {
    vote_state.commission_split(rewards)
};

... then everything resolves itself at the next epoch boundary.

@t-nelson , it looks to me like your suggestion will resolve the issue motivating this PR nicely. The only caveat, as we discussed in DM, is that we need to activate your fix at least one epoch before we activate inflation (to prevent the additional rewards that @ryoqun describes on the epoch that observed_credits gets bumped).

I will hold off activating stake-program-v2 on testnet so that we don't have to plumb an additional feature.

@ryoqun
Copy link
Copy Markdown
Contributor Author

ryoqun commented Nov 21, 2020

hehe. what a baroque work compared to the state of art pr #13680 by @t-nelson.

Closing this, because this has happily superseded. :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants