Skip to content
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

Reverse staking rate calculations #2909

Closed
wants to merge 25 commits into from
Closed

Conversation

plaidfinch
Copy link
Collaborator

@plaidfinch plaidfinch commented Aug 9, 2023

Resolves #1481, resolves #2802. This happens in two steps:

  • Remove all remaining references to next-rate-data
  • In the staking module, determine the correct modification of the base rate data to effect a particular amount of inflation specified in penumbra by the distributions module
  • In the distributions module, determine the correct amount of inflation in penumbra based on the chain parameter specified base rate and send it to the staking module (this is temporary until we transition to dynamic distributions in a follow-up PR)

The formula to adjust the previous_base_rate_data to reflect a particular desired issuance is:

let total_active_stake = 
    todo!("sum over only active validators of `unbonded_stake` of their delegation amount");
const BPS_SQUARED: u64 = 1_0000_0000; // reward rate is measured in basis points squared
let upcoming_base_reward_rate = issuance * BPS_SQUARED / total_active_stake;
let upcoming_base_rate_data = previous_base_rate_data.next(upcoming_base_reward_rate);

Rewards are issued by inflating the stake bonded only to active validators, so to invert the staking calculation, we need to calculate a reward rate (in basis points of basis points) such that the upcoming base rate data when applied to the active validator set will inflate the value of their stake by the desired issuance. This works out to be that we should multiply the base rate by (issuance + total_active_stake) / total_active_stake, or in other words calculate the next base rate using a reward rate of issuance * BPS_SQUARED / total_active_stake.

Efficiently summing over all active validators requires an index tracking which validators are active. This could be done in this PR, or stubbed out with a filter over all validators and fixed up in #2921 to use the index of active validators defined there.

It is likely that the calculation of the new base rate data will result in a not-perfectly-precise creation of inflation (it will undershoot due to precision loss in division in several places). This can't be fixed in any given epoch, but to ensure that the total amount issued over time ends up being correct, we can carry forward to the next epoch a remainder to be fed forward into that epoch's issuance. This remainder can be calculated by summing the unbonded_amount across all active validators' upcoming RateData (i.e. the new ones generated from the new BaseRateData) of each validator's entire delegation pool, and then subtracting that amount from the actual desired issuance. It is not correct to calculate a hypothetical unbonded_amount based only on the BaseRateData, because there are additional precision losses in the conversion to RateData for each validator. The sum should always be positive, because division rounds down. The remainder should be stored in the state — it should not be fed forward into the next epoch's staking issuance directly, but rather should be treated as an additional input to the distributions module (because this gives the distributions module freedom to re-allocate that issuance elsewhere in the upcoming epoch, which it might want to do).

Finally, the temporary determination of the correct issuance of staking rewards in the distributions module should be given by the product of the per-epoch staking reward rate set as a chain parameter, and the total value of all delegation tokens to any validator. This is, as noted above, temporary, and will be replaced with a per-epoch allocation that is scaled to epoch duration, and dynamically allocated based on other factors considered by the distributions module.

@plaidfinch plaidfinch self-assigned this Aug 9, 2023
@plaidfinch plaidfinch temporarily deployed to smoke-test August 9, 2023 14:39 — with GitHub Actions Inactive
Copy link
Collaborator Author

@plaidfinch plaidfinch left a comment

Choose a reason for hiding this comment

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

I've done a self-review to point out the important parts of this change. Seeking review and testing to ensure this looks right. The desire of this change is that absolutely nothing should change observationally; this merely finishes the job of removing the next-rate-data calculations.

crates/bin/pd/src/info/specific.rs Show resolved Hide resolved
crates/bin/pd/src/info/specific.rs Show resolved Hide resolved
Comment on lines 325 to 332
// We are transitioning to the next epoch, so set "cur_base_rate" to the previous "next_base_rate", and
// update "next_base_rate".
let current_base_rate = self.next_base_rate().await?;
// We are transitioning to the next epoch, so set "cur_base_rate" to the base rate derived
// from the allocation given by the distributions module, and remember the previous base
// rate.
let previous_base_rate = self.current_base_rate().await?; // now looking backwards, our current rate is the previous rate
let current_base_rate: BaseRateData =
previous_base_rate.next(chain_params.base_reward_rate);

let next_base_rate = current_base_rate.next(chain_params.base_reward_rate);

// rename to curr_rate so it lines up with next_rate (same # chars)
tracing::debug!(curr_base_rate = ?current_base_rate);
tracing::debug!(?next_base_rate);
tracing::debug!(current_base_rate = ?current_base_rate);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Please check this: The intent of this change is to effect the exact same base rate change dynamics as previously, but without defining a next_base_rate. Instead, we copy the current base rate (of the previous epoch) into previous_base_rate, then redefine current_base_rate to be the next base rate of that.

Comment on lines 329 to 331
let current_base_rate: BaseRateData =
previous_base_rate.next(chain_params.base_reward_rate);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When this PR is finished, this definition of current_base_rate will be replaced with a derivation from the allocation of penumbra tokens from the distributions module, converting that into the base rate needed to effect that inflation.

Comment on lines -197 to -205
message NextValidatorRateRequest {
// The expected chain id (empty string if no expectation).
string chain_id = 1;
core.crypto.v1alpha1.IdentityKey identity_key = 2;
}

message NextValidatorRateResponse {
core.stake.v1alpha1.RateData data = 1;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These messages aren't useful anymore. Deleting them causes a proto lint failure, but I think this is one of the acceptable cases, because they should never be used anymore (and aren't).

crates/core/component/stake/src/state_key.rs Outdated Show resolved Hide resolved
crates/core/component/stake/src/state_key.rs Show resolved Hide resolved
crates/core/component/stake/src/rate.rs Outdated Show resolved Hide resolved
crates/core/component/stake/src/identity_key.rs Outdated Show resolved Hide resolved
@plaidfinch plaidfinch temporarily deployed to smoke-test August 9, 2023 15:38 — with GitHub Actions Inactive
@plaidfinch plaidfinch force-pushed the reverse-staking-rate branch from afbf20c to f48cdfb Compare August 18, 2023 19:34
@plaidfinch plaidfinch marked this pull request as ready for review August 18, 2023 19:34
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I made implementing each of these methods optional, so that Components don't have to implement the ones they don't do anything in.

Comment on lines -65 to +72
// TODO: replace with a single checked_add_signed call when mixed_integer_ops lands in stable (1.66)
let new_supply: Amount = if change < 0 {
current_supply
.value()
.checked_sub(change.unsigned_abs())
.ok_or_else(|| {
anyhow::anyhow!(
"underflow updating token {} supply {} with delta {}",
asset_id,
current_supply,
change
)
})?
.into()
} else {
current_supply
.value()
.checked_add(change as u128)
.ok_or_else(|| {
anyhow::anyhow!(
"overflow updating token {} supply {} with delta {}",
asset_id,
current_supply,
change
)
})?
.into()
};
tracing::debug!(?current_supply, ?new_supply, ?change);
let new_supply = current_supply.checked_add_signed(change).ok_or_else(|| {
anyhow::anyhow!(
"over-/under-flow updating token {} supply {} with delta {}",
asset_id,
current_supply,
change
)
})?;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I followed the TODO, since we are now on a version of Rust that supports checked_add_signed.

Comment on lines +160 to +169
// This currently computes the new issuance by multiplying the total staking token ever
// issued by the base reward rate. This is a stand-in for a more accurate and good model of
// issuance, which will be implemented later. For now, this inflates the total issuance of
// staking tokens by a fixed ratio per epoch.
let base_reward_rate = self.get_chain_params().await?.base_reward_rate;
let total_issued = self.total_issued().await?;
const BPS_SQUARED: u64 = 1_0000_0000; // reward rate is measured in basis points squared
let new_issuance = total_issued * base_reward_rate / BPS_SQUARED;
let issuance = new_issuance + remainder;
Ok((issuance, 0))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This new issuance algorithm is not numerically equivalent to the previous one, but that does not matter because we're going to entirely replace it with a time-based control mechanism soon. This algorithm just inflates the penumbra token at a particular rate every epoch (3 basis points is the default).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This standalone algorithm does optimal allocation of an integer amount to a list of weighted labels. This is overkill for the place where it's used in the distributions component, but will be crucial in implementing distributions within individual components.

@conorsch
Copy link
Contributor

conorsch commented Sep 8, 2023

We're worried about drift making rebasing difficult. Let's aim to get this one in during 61.

@hdevalence
Copy link
Member

This won't be mergeable any more, following #3077, and the work will have to be redone on top of the current main. We should try to avoid leaving large PRs like this open for so long to prevent this in the future.

@erwanor
Copy link
Member

erwanor commented Nov 13, 2023

Picking this up in a cherry-picked branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

staking: remove NextRate APIs staking: reverse causality of rate calculations
4 participants