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
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
65cb328
Completely remove next-epoch rate calculations
plaidfinch Aug 9, 2023
6a831dc
Fix: per-validator staking rate wasn't being updated correctly
plaidfinch Aug 9, 2023
b625cde
Rename some variables for clarity, remove assertion about next rates
plaidfinch Aug 9, 2023
206fdcd
Fix incorrect voting power calculation
plaidfinch Aug 9, 2023
29395bf
Documentation improvements
plaidfinch Aug 9, 2023
34ff20c
TODO notes about tracking total supply of staking token
plaidfinch Aug 11, 2023
ebde009
Follow TODO instruction to replace amount update with checked_add_signed
plaidfinch Aug 11, 2023
7e9e9ad
Rearrange calculations in validator loop to allow staking rate inversion
plaidfinch Aug 16, 2023
0ec6d53
Fully set up staking component to take inputs from distributions
plaidfinch Aug 17, 2023
524a917
Begin implementing distribution to staking module
plaidfinch Aug 17, 2023
5e016ef
Exact distribution algorithm
plaidfinch Aug 17, 2023
785a50a
Do in-place scaling of the weights
plaidfinch Aug 17, 2023
eeff117
Comment tweaks
plaidfinch Aug 18, 2023
c2a24a6
More documentation in comments about the scaling algorithm
plaidfinch Aug 18, 2023
10f8b89
Docs note about swap_remove
plaidfinch Aug 18, 2023
5c3b900
Return iterator from exact allocation
plaidfinch Aug 18, 2023
4b5596f
Move the allocate function to the num crate
plaidfinch Aug 18, 2023
e8c9a50
Remove proptest regressions from moved test
plaidfinch Aug 18, 2023
b6a5ea5
Add default implementations to `Component` trait
plaidfinch Aug 18, 2023
f48cdfb
Finish first version of inverting staking rate calculations
plaidfinch Aug 18, 2023
1ea9944
Fix incorrect computation of unissued remainder in staking component
plaidfinch Aug 18, 2023
47e04a5
Regenerate Go code from the proto files
plaidfinch Aug 18, 2023
a1be5b8
Remove unused import
plaidfinch Aug 18, 2023
939c105
Allow a couple warnings in the distributions component
plaidfinch Aug 18, 2023
227b910
Change tracing::info messages to tracing::debug
plaidfinch Aug 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 0 additions & 32 deletions crates/bin/pd/src/info/specific.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ use proto::client::v1alpha1::LiquidityPositionsByPriceRequest;
use proto::client::v1alpha1::LiquidityPositionsByPriceResponse;
use proto::client::v1alpha1::LiquidityPositionsRequest;
use proto::client::v1alpha1::LiquidityPositionsResponse;
use proto::client::v1alpha1::NextValidatorRateRequest;
use proto::client::v1alpha1::NextValidatorRateResponse;
plaidfinch marked this conversation as resolved.
Show resolved Hide resolved
use proto::client::v1alpha1::PrefixValueRequest;
use proto::client::v1alpha1::PrefixValueResponse;
use proto::client::v1alpha1::SimulateTradeRequest;
Expand Down Expand Up @@ -494,36 +492,6 @@ impl SpecificQueryService for Info {
}
}

#[instrument(skip(self, request))]
async fn next_validator_rate(
&self,
request: tonic::Request<NextValidatorRateRequest>,
) -> Result<tonic::Response<NextValidatorRateResponse>, Status> {
let state = self.storage.latest_snapshot();
state
.check_chain_id(&request.get_ref().chain_id)
.await
.map_err(|e| tonic::Status::unknown(format!("chain_id not OK: {e}")))?;
let identity_key = request
.into_inner()
.identity_key
.ok_or_else(|| tonic::Status::invalid_argument("empty message"))?
.try_into()
.map_err(|_| tonic::Status::invalid_argument("invalid identity key"))?;

let rate_data = state
.next_validator_rate(&identity_key)
.await
.map_err(|e| tonic::Status::internal(e.to_string()))?;

match rate_data {
Some(r) => Ok(tonic::Response::new(NextValidatorRateResponse {
data: Some(r.into()),
})),
None => Err(Status::not_found("next validator rate not found")),
}
}

plaidfinch marked this conversation as resolved.
Show resolved Hide resolved
#[instrument(skip(self, request))]
/// Get the batch swap data associated with a given trading pair and height.
async fn batch_swap_output_data(
Expand Down
20 changes: 12 additions & 8 deletions crates/core/component/component/src/component.rs
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.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub trait Component {
/// This method is called once per chain, and should only perform
/// writes, since the backing tree for the [`State`] will
/// be empty.
async fn init_chain<S: StateWrite>(state: S, app_state: &Self::AppState);
async fn init_chain<S: StateWrite>(_state: S, _app_state: &Self::AppState) {}

/// Begins a new block, optionally inspecting the ABCI
/// [`BeginBlock`](abci::request::BeginBlock) request.
Expand All @@ -31,9 +31,10 @@ pub trait Component {
/// implementor MUST ensure that any clones of the `Arc` are dropped before
/// it returns, so that `state.get_mut().is_some()` on completion.
async fn begin_block<S: StateWrite + 'static>(
state: &mut Arc<S>,
begin_block: &abci::request::BeginBlock,
);
_state: &mut Arc<S>,
_begin_block: &abci::request::BeginBlock,
) {
}

/// Ends the block, optionally inspecting the ABCI
/// [`EndBlock`](abci::request::EndBlock) request, and performing any batch
Expand All @@ -50,9 +51,10 @@ pub trait Component {
/// implementor MUST ensure that any clones of the `Arc` are dropped before
/// it returns, so that `state.get_mut().is_some()` on completion.
async fn end_block<S: StateWrite + 'static>(
state: &mut Arc<S>,
end_block: &abci::request::EndBlock,
);
_state: &mut Arc<S>,
_end_block: &abci::request::EndBlock,
) {
}

/// Ends the epoch, applying component-specific state transitions that should occur when an epoch ends.
///
Expand All @@ -63,5 +65,7 @@ pub trait Component {
/// called, `state.get_mut().is_some()`, i.e., the `Arc` is not shared. The
/// implementor MUST ensure that any clones of the `Arc` are dropped before
/// it returns, so that `state.get_mut().is_some()` on completion.
async fn end_epoch<S: StateWrite + 'static>(state: &mut Arc<S>) -> Result<()>;
async fn end_epoch<S: StateWrite + 'static>(_state: &mut Arc<S>) -> Result<()> {
Ok(())
}
}
5 changes: 5 additions & 0 deletions crates/core/component/distributions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ penumbra-storage = { path = "../../../storage", optional = true }
penumbra-component = { path = "../component", optional = true }
penumbra-chain = { path = "../chain", default-features = false }
penumbra-shielded-pool = { path = "../shielded-pool", default-features = false }
penumbra-stake = { path = "../stake", default-features = false }
penumbra-dex = { path = "../dex", default-features = false }
penumbra-num = { path = "../../num" }
penumbra-asset = { path = "../../asset" }

# Crates.io deps
async-trait = "0.1.52"
Expand All @@ -26,3 +30,4 @@ tracing = "0.1"
tendermint = "0.32.0"

[dev-dependencies]
proptest = "1"
198 changes: 183 additions & 15 deletions crates/core/component/distributions/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,205 @@ pub mod state_key;

mod view;

use std::sync::Arc;
use std::{
fmt::{self, Display, Formatter},
sync::Arc,
};

use anyhow::Result;
use async_trait::async_trait;
use penumbra_chain::genesis;
use penumbra_asset::{asset, STAKING_TOKEN_ASSET_ID};
use penumbra_chain::{component::StateReadExt as _, genesis};
use penumbra_component::Component;
use penumbra_storage::StateWrite;
use tendermint::abci;
use penumbra_proto::{StateReadProto, StateWriteProto};
use penumbra_storage::{StateRead, StateWrite};
use tracing::instrument;
pub use view::{StateReadExt, StateWriteExt};

use penumbra_dex::{component::StateReadExt as _, component::StateWriteExt as _};
use penumbra_stake::{component::StateWriteExt as _, StateReadExt as _};

pub struct Distributions {}

#[async_trait]
impl Component for Distributions {
type AppState = genesis::AppState;

async fn init_chain<S: StateWrite>(_state: S, _app_state: &Self::AppState) {}
#[instrument(name = "distributions", skip(state, app_state))]
async fn init_chain<S: StateWrite>(mut state: S, app_state: &Self::AppState) {
// Tally up the total issuance of the staking token from the genesis allocations, so that we
// can accurately track the total amount issued in the future.
let genesis_issuance = app_state
.allocations
.iter()
.filter(|alloc| {
// Filter only for allocations of the staking token
asset::REGISTRY.parse_denom(&alloc.denom).map(|d| d.id())
== Some(*STAKING_TOKEN_ASSET_ID)
})
.fold(0u64, |sum, alloc| {
// Total the allocations
sum.checked_add(
u128::from(alloc.amount)
.try_into()
.expect("genesis issuance does not overflow `u64`"),
)
.expect("genesis issuance does not overflow `u64`")
});
tracing::info!(
"total genesis issuance of staking token: {}",
genesis_issuance
);
state.set_total_issued(genesis_issuance);
}

#[instrument(name = "distributions", skip(state))]
async fn end_epoch<S: StateWrite + 'static>(state: &mut Arc<S>) -> Result<()> {
let state = Arc::get_mut(state).expect("state `Arc` is unique");

// Get the remainders of issuances that couldn't be distributed last epoch, due to precision
// loss or lack of activity.
let staking_remainder: u64 = state.staking_issuance().await?;
let dex_remainder: u64 = 0; // TODO: get this from the dex once LP rewards are implemented

// Sum all the per-component remainders together, including any remainder in the
// distribution component itself left over undistributed in the previous epoch
let last_epoch_remainder =
staking_remainder
.checked_add(dex_remainder)
.ok_or_else(|| {
anyhow::anyhow!("staking and dex remainders overflowed when added together")
})?;

// The remainder from the previous epoch could not be issued, so subtract it from the total
// issuance for all time.
let total_issued = state
.total_issued()
.await?
.checked_sub(last_epoch_remainder)
.expect(
"total issuance is greater than or equal to the remainder from the previous epoch",
);
state.set_total_issued(total_issued);

// Add the remainder from the previous epoch to the remainder carried over from before then.
let remainder = last_epoch_remainder
.checked_add(state.remainder().await?)
.expect("remainder does not overflow `u64`");

tracing::info!(
?remainder,
?last_epoch_remainder,
?staking_remainder,
?dex_remainder,
);

// Clear out the remaining issuances, so that if we don't issue anything to one of them, we
// don't leave the remainder there.
state.set_staking_issuance(0);
// TODO: clear dex issuance

async fn begin_block<S: StateWrite + 'static>(
_state: &mut Arc<S>,
_begin_block: &abci::request::BeginBlock,
) {
// Get the total issuance and new remainder for this epoch
let (issuance, remainder) = state.total_issuance_and_remainder(remainder).await?;

tracing::info!(new_issuance = ?issuance, new_remainder = ?remainder);

// Set the remainder to be carried over to the next epoch
state.set_remainder(remainder);

// Set the cumulative total issuance (pending receipt of remainders, which may decrease it
// next epoch)
state.set_total_issued(total_issued + issuance);

// Determine the allocation of the issuance between the different components: this returns a
// set of weights, which we'll use to scale the total issuance
let weights = state.issuance_weights().await?;

// Allocate the issuance according to the weights
if let Some(allocation) = penumbra_num::allocate(issuance.into(), weights) {
for (component, issuance) in allocation {
use ComponentName::*;
let issuance: u64 = issuance.try_into().expect("total issuance is within `u64`");
tracing::info!(%component, ?issuance, "issuing tokens to component"
);
match component {
Staking => state.set_staking_issuance(issuance),
Dex => todo!("set dex issuance"),
}
}
}

Ok(())
}
}

async fn end_block<S: StateWrite + 'static>(
_state: &mut Arc<S>,
_end_block: &abci::request::EndBlock,
) {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum ComponentName {
Staking,
Dex,
}

impl Display for ComponentName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ComponentName::Staking => write!(f, "staking"),
ComponentName::Dex => write!(f, "dex"),
}
}
}

async fn end_epoch<S: StateWrite + 'static>(_state: &mut Arc<S>) -> Result<()> {
Ok(())
#[async_trait]
trait DistributionsImpl
where
Self: StateRead + StateWrite,
{
// Compute the total issuance for this epoch, and the remainder that will be carried over to
// the next epoch, given the remainder that was carried forward from the preceding epoch.
async fn total_issuance_and_remainder(&self, remainder: u64) -> Result<(u64, u64)> {
// 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))
Comment on lines +162 to +171
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).

}

// Determine in each epoch what the relative weight of issuance per component should be. The
// returned list of weights is used to allocate the total issuance between the different
// components, and does not need to sum to any particular total; it will be rescaled to the
// total issuance determined by `total_issuance_and_remainder`.
async fn issuance_weights(&self) -> Result<Vec<(ComponentName, u128)>> {
// Currently, only issue staking rewards:
Ok(vec![(ComponentName::Staking, 1)])
}

// Get the remainder of the issuance that couldn't be distributed in the previous epoch.
async fn remainder(&self) -> Result<u64> {
self.get_proto(state_key::remainder())
.await
.map(Option::unwrap_or_default)
}

// Set the remainder of the issuance that will be carried forward to the next epoch.
fn set_remainder(&mut self, remainder: u64) {
self.put_proto(state_key::remainder().to_string(), remainder)
}

// Get the total issuance of staking tokens for all time.
async fn total_issued(&self) -> Result<u64> {
self.get_proto(state_key::total_issued())
.await
.map(Option::unwrap_or_default)
}

// Set the total issuance of staking tokens for all time.
fn set_total_issued(&mut self, total_issued: u64) {
self.put_proto(state_key::total_issued().to_string(), total_issued)
}
}

impl<S: StateRead + StateWrite> DistributionsImpl for S {}
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
// The remainder that has yet to be distributed
pub fn remainder() -> &'static str {
"distributions/remainder"
}

// The cumulative total issuance
pub fn total_issued() -> &'static str {
"distributions/total_issued"
}
Loading