From 019048456b06bec12c0bee77a82f7911adb3fe45 Mon Sep 17 00:00:00 2001 From: Erwan Date: Thu, 16 Nov 2023 17:46:35 -0500 Subject: [PATCH 1/6] distributions: add scaffolding for ext read/write Co-authored-by: Finch Foner --- .../core/component/distributions/Cargo.toml | 6 +- .../component/distributions/src/component.rs | 133 +++++++++++++++++- .../distributions/src/component/state_key.rs | 5 +- 3 files changed, 137 insertions(+), 7 deletions(-) diff --git a/crates/core/component/distributions/Cargo.toml b/crates/core/component/distributions/Cargo.toml index 5987949350..a881283bdc 100644 --- a/crates/core/component/distributions/Cargo.toml +++ b/crates/core/component/distributions/Cargo.toml @@ -13,11 +13,13 @@ docsrs = [] [dependencies] # Workspace dependencies -penumbra-proto = { path = "../../../proto", default-features = false } -penumbra-storage = { path = "../../../storage", optional = true } +penumbra-asset = { path = "../../asset" } penumbra-component = { path = "../component", optional = true } penumbra-chain = { path = "../chain", default-features = false } +penumbra-num = { path = "../../../core/num", default-features = false } +penumbra-proto = { path = "../../../proto", default-features = false } penumbra-shielded-pool = { path = "../shielded-pool", default-features = false } +penumbra-storage = { path = "../../../storage", optional = true } # Crates.io deps async-trait = "0.1.52" diff --git a/crates/core/component/distributions/src/component.rs b/crates/core/component/distributions/src/component.rs index 573a5f6049..8c2d680cce 100644 --- a/crates/core/component/distributions/src/component.rs +++ b/crates/core/component/distributions/src/component.rs @@ -7,9 +7,16 @@ use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; use penumbra_component::Component; -use penumbra_storage::StateWrite; -use tendermint::abci; -pub use view::{StateReadExt, StateWriteExt}; +// use penumbra_dex::{component::StateReadExt as _, component::StateWriteExt as _}; +// use penumbra_stake::{component::StateWriteExt as _, StateReadExt as _}; +use penumbra_proto::{StateReadProto, StateWriteProto}; +use penumbra_storage::{StateRead, StateWrite}; +use tendermint::v0_37::abci; +use tracing::instrument; +// pub use view::{StateReadExt, StateWriteExt}; +use penumbra_asset::STAKING_TOKEN_ASSET_ID; +use penumbra_num::Amount; +use penumbra_shielded_pool::component::SupplyRead; pub struct Distributions {} @@ -17,21 +24,139 @@ pub struct Distributions {} impl Component for Distributions { type AppState = (); - async fn init_chain(_state: S, _app_state: Option<&()>) {} + #[instrument(name = "distributions", skip(state, app_state))] + async fn init_chain(mut state: S, app_state: Option<&Self::AppState>) { + match app_state { + None => { /* Checkpoint -- no-op */ } + Some(_) => { + let genesis_issuance = state + .token_supply(&*STAKING_TOKEN_ASSET_ID) + .await + .expect("supply is valid") + .expect("shielded pool component has tallied genesis issuance"); + tracing::debug!( + "total genesis issuance of staking token: {}", + genesis_issuance + ); + // TODO(erwan): it's not yet totally clear if it is necessary, or even desirable, for the + // distributions component to track the total issuance. The shielded pool component + // already does that. We do it anyway for now so that we can write the rest of the scaffolding. + state.set_total_issued(genesis_issuance); + } + }; + } + #[instrument(name = "distributions", skip(_state, _begin_block))] async fn begin_block( _state: &mut Arc, _begin_block: &abci::request::BeginBlock, ) { } + #[instrument(name = "distributions", skip(_state, _end_block))] async fn end_block( _state: &mut Arc, _end_block: &abci::request::EndBlock, ) { } + #[instrument(name = "distributions", skip(_state))] async fn end_epoch(_state: &mut Arc) -> 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::debug!( + ?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 + + // Get the total issuance and new remainder for this epoch + let (issuance, remainder) = state.total_issuance_and_remainder(remainder).await?; + + tracing::debug!(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::debug!(%component, ?issuance, "issuing tokens to component" + ); + match component { + Staking => state.set_staking_issuance(issuance), + Dex => todo!("set dex issuance"), + } + } + } + */ Ok(()) } } + +#[async_trait] +pub trait StateReadExt: StateRead { + async fn total_issued(&self) -> Result> { + self.get_proto(&state_key::total_issued()).await + } +} + +impl StateReadExt for T {} + +#[async_trait] + +pub trait StateWriteExt: StateWrite + StateReadExt { + /// Set the total amount of staking tokens issued. + fn set_total_issued(&mut self, total_issued: u64) { + let total = Amount::from(total_issued); + self.put(state_key::total_issued().to_string(), total) + } +} +impl StateWriteExt for T {} diff --git a/crates/core/component/distributions/src/component/state_key.rs b/crates/core/component/distributions/src/component/state_key.rs index 8b13789179..7569065a01 100644 --- a/crates/core/component/distributions/src/component/state_key.rs +++ b/crates/core/component/distributions/src/component/state_key.rs @@ -1 +1,4 @@ - +// The cumulative total issuance of staking token. +pub fn total_issued() -> &'static str { + "distributions/total_issued" +} From 4ce3d97731e2b375a21ccf25b8c6e9dc9e825d9a Mon Sep 17 00:00:00 2001 From: Erwan Date: Thu, 16 Nov 2023 19:09:31 -0500 Subject: [PATCH 2/6] distributions: add basic `DistributionManager::compute_issuance` Co-authored-by: Finch Foner --- Cargo.lock | 2 + .../component/distributions/src/component.rs | 42 +++++++++---------- .../distributions/src/component/view.rs | 22 ++++++++-- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6ca0741ef..5ddff26b11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5256,8 +5256,10 @@ version = "0.63.1" dependencies = [ "anyhow", "async-trait", + "penumbra-asset", "penumbra-chain", "penumbra-component", + "penumbra-num", "penumbra-proto", "penumbra-shielded-pool", "penumbra-storage", diff --git a/crates/core/component/distributions/src/component.rs b/crates/core/component/distributions/src/component.rs index 8c2d680cce..3c8155b6cd 100644 --- a/crates/core/component/distributions/src/component.rs +++ b/crates/core/component/distributions/src/component.rs @@ -9,14 +9,12 @@ use async_trait::async_trait; use penumbra_component::Component; // use penumbra_dex::{component::StateReadExt as _, component::StateWriteExt as _}; // use penumbra_stake::{component::StateWriteExt as _, StateReadExt as _}; -use penumbra_proto::{StateReadProto, StateWriteProto}; -use penumbra_storage::{StateRead, StateWrite}; -use tendermint::v0_37::abci; -use tracing::instrument; -// pub use view::{StateReadExt, StateWriteExt}; use penumbra_asset::STAKING_TOKEN_ASSET_ID; -use penumbra_num::Amount; use penumbra_shielded_pool::component::SupplyRead; +use penumbra_storage::StateWrite; +use tendermint::v0_37::abci; +use tracing::instrument; +pub use view::{StateReadExt, StateWriteExt}; pub struct Distributions {} @@ -142,21 +140,21 @@ impl Component for Distributions { } #[async_trait] -pub trait StateReadExt: StateRead { - async fn total_issued(&self) -> Result> { - self.get_proto(&state_key::total_issued()).await - } -} - -impl StateReadExt for T {} - -#[async_trait] - -pub trait StateWriteExt: StateWrite + StateReadExt { - /// Set the total amount of staking tokens issued. - fn set_total_issued(&mut self, total_issued: u64) { - let total = Amount::from(total_issued); - self.put(state_key::total_issued().to_string(), total) +trait DistributionManager: StateWriteExt { + /// Compute the total issuance for this epoch. + // TODO(erwan): this is a stub implementation. + async fn compute_new_issuance(&self) -> Result { + // 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: u64 = 0; + let total_issued = self + .total_issued() + .await? + .expect("total issuance has been initialized"); + 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; + Ok(new_issuance) } } -impl StateWriteExt for T {} diff --git a/crates/core/component/distributions/src/component/view.rs b/crates/core/component/distributions/src/component/view.rs index d2f042fb8f..433261cd47 100644 --- a/crates/core/component/distributions/src/component/view.rs +++ b/crates/core/component/distributions/src/component/view.rs @@ -1,13 +1,27 @@ use async_trait::async_trait; +use crate::component::state_key; +use anyhow::Result; +use penumbra_num::Amount; +use penumbra_proto::{StateReadProto, StateWriteProto}; use penumbra_storage::{StateRead, StateWrite}; #[async_trait] -pub trait StateReadExt: StateRead {} +pub trait StateReadExt: StateRead { + async fn total_issued(&self) -> Result> { + self.get_proto(&state_key::total_issued()).await + } +} -impl StateReadExt for T where T: StateRead + ?Sized {} +impl StateReadExt for T {} #[async_trait] -pub trait StateWriteExt: StateWrite {} -impl StateWriteExt for T where T: StateWrite + ?Sized {} +pub trait StateWriteExt: StateWrite + StateReadExt { + /// Set the total amount of staking tokens issued. + fn set_total_issued(&mut self, total_issued: u64) { + let total = Amount::from(total_issued); + self.put(state_key::total_issued().to_string(), total) + } +} +impl StateWriteExt for T {} From 8acc31519a6dda1cefa755e9fc84d1e549c1eec2 Mon Sep 17 00:00:00 2001 From: Erwan Date: Thu, 16 Nov 2023 20:18:08 -0500 Subject: [PATCH 3/6] distributions: stub out `DistributionManager::distribute` --- .../component/distributions/src/component.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/core/component/distributions/src/component.rs b/crates/core/component/distributions/src/component.rs index 3c8155b6cd..1c1082f575 100644 --- a/crates/core/component/distributions/src/component.rs +++ b/crates/core/component/distributions/src/component.rs @@ -141,13 +141,9 @@ impl Component for Distributions { #[async_trait] trait DistributionManager: StateWriteExt { - /// Compute the total issuance for this epoch. - // TODO(erwan): this is a stub implementation. + /// Compute the total new issuance of staking tokens for this epoch. + /// TODO(erwan): this is a stub implementation. async fn compute_new_issuance(&self) -> Result { - // 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: u64 = 0; let total_issued = self .total_issued() @@ -157,4 +153,13 @@ trait DistributionManager: StateWriteExt { let new_issuance = total_issued * base_reward_rate / BPS_SQUARED; Ok(new_issuance) } + + /// Update the object store with the new issuance of staking tokens for this epoch. + /// TODO(erwan): this is a stub implementation. + async fn distribute(&mut self) -> Result<()> { + let new_issuance = self.compute_new_issuance().await?; + tracing::debug!(?new_issuance, "computed new issuance for epoch"); + self.set_total_issued(new_issuance); + todo!() + } } From 7368f5f69103592417203deb38dec48e462d025d Mon Sep 17 00:00:00 2001 From: Erwan Date: Thu, 16 Nov 2023 20:43:59 -0500 Subject: [PATCH 4/6] app: make the shielded pool component run first This is exploratory. If we go ahead and commit to having ordering requirements on component execution, we will need to document it and motivate why. --- crates/core/app/src/app/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/core/app/src/app/mod.rs b/crates/core/app/src/app/mod.rs index c6fa0046b2..18b2330c08 100644 --- a/crates/core/app/src/app/mod.rs +++ b/crates/core/app/src/app/mod.rs @@ -127,9 +127,9 @@ impl App { }, ); - Distributions::init_chain(&mut state_tx, Some(&())).await; ShieldedPool::init_chain(&mut state_tx, Some(&app_state.shielded_pool_content)) .await; + Distributions::init_chain(&mut state_tx, Some(&())).await; Staking::init_chain(&mut state_tx, Some(&app_state.stake_content)).await; IBCComponent::init_chain(&mut state_tx, Some(&())).await; Dex::init_chain(&mut state_tx, Some(&())).await; @@ -138,8 +138,8 @@ impl App { } genesis::AppState::Checkpoint(_) => { /* perform upgrade specific check */ - Distributions::init_chain(&mut state_tx, None).await; ShieldedPool::init_chain(&mut state_tx, None).await; + Distributions::init_chain(&mut state_tx, None).await; Staking::init_chain(&mut state_tx, None).await; IBCComponent::init_chain(&mut state_tx, None).await; Dex::init_chain(&mut state_tx, None).await; @@ -196,10 +196,10 @@ impl App { // Run each of the begin block handlers for each component, in sequence: let mut arc_state_tx = Arc::new(state_tx); + ShieldedPool::begin_block(&mut arc_state_tx, begin_block).await; Distributions::begin_block(&mut arc_state_tx, begin_block).await; IBCComponent::begin_block(&mut arc_state_tx, begin_block).await; Governance::begin_block(&mut arc_state_tx, begin_block).await; - ShieldedPool::begin_block(&mut arc_state_tx, begin_block).await; Staking::begin_block(&mut arc_state_tx, begin_block).await; Fee::begin_block(&mut arc_state_tx, begin_block).await; @@ -301,11 +301,11 @@ impl App { let state_tx = StateDelta::new(self.state.clone()); let mut arc_state_tx = Arc::new(state_tx); + ShieldedPool::end_block(&mut arc_state_tx, end_block).await; Distributions::end_block(&mut arc_state_tx, end_block).await; IBCComponent::end_block(&mut arc_state_tx, end_block).await; Dex::end_block(&mut arc_state_tx, end_block).await; Governance::end_block(&mut arc_state_tx, end_block).await; - ShieldedPool::end_block(&mut arc_state_tx, end_block).await; Staking::end_block(&mut arc_state_tx, end_block).await; Fee::end_block(&mut arc_state_tx, end_block).await; let mut state_tx = Arc::try_unwrap(arc_state_tx) From b96006fcff215f334709c62e42025619d6928307 Mon Sep 17 00:00:00 2001 From: Erwan Date: Fri, 17 Nov 2023 17:30:01 -0500 Subject: [PATCH 5/6] staking: refactor epoch transition logic Co-authored-by: Henry de Valence --- .../component/distributions/src/component.rs | 5 +- .../distributions/src/component/view.rs | 2 +- .../shielded-pool/src/component/supply.rs | 6 +- .../action_handler/validator_definition.rs | 11 +- crates/core/component/stake/src/component.rs | 152 ++++++------------ .../component/stake/src/funding_stream.rs | 24 ++- 6 files changed, 78 insertions(+), 122 deletions(-) diff --git a/crates/core/component/distributions/src/component.rs b/crates/core/component/distributions/src/component.rs index 1c1082f575..1e164a592a 100644 --- a/crates/core/component/distributions/src/component.rs +++ b/crates/core/component/distributions/src/component.rs @@ -10,6 +10,7 @@ use penumbra_component::Component; // use penumbra_dex::{component::StateReadExt as _, component::StateWriteExt as _}; // use penumbra_stake::{component::StateWriteExt as _, StateReadExt as _}; use penumbra_asset::STAKING_TOKEN_ASSET_ID; +use penumbra_num::Amount; use penumbra_shielded_pool::component::SupplyRead; use penumbra_storage::StateWrite; use tendermint::v0_37::abci; @@ -143,7 +144,7 @@ impl Component for Distributions { trait DistributionManager: StateWriteExt { /// Compute the total new issuance of staking tokens for this epoch. /// TODO(erwan): this is a stub implementation. - async fn compute_new_issuance(&self) -> Result { + async fn compute_new_issuance(&self) -> Result { let base_reward_rate: u64 = 0; let total_issued = self .total_issued() @@ -151,7 +152,7 @@ trait DistributionManager: StateWriteExt { .expect("total issuance has been initialized"); 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; - Ok(new_issuance) + Ok(new_issuance.into()) } /// Update the object store with the new issuance of staking tokens for this epoch. diff --git a/crates/core/component/distributions/src/component/view.rs b/crates/core/component/distributions/src/component/view.rs index 433261cd47..21149b18df 100644 --- a/crates/core/component/distributions/src/component/view.rs +++ b/crates/core/component/distributions/src/component/view.rs @@ -19,7 +19,7 @@ impl StateReadExt for T {} pub trait StateWriteExt: StateWrite + StateReadExt { /// Set the total amount of staking tokens issued. - fn set_total_issued(&mut self, total_issued: u64) { + fn set_total_issued(&mut self, total_issued: Amount) { let total = Amount::from(total_issued); self.put(state_key::total_issued().to_string(), total) } diff --git a/crates/core/component/shielded-pool/src/component/supply.rs b/crates/core/component/shielded-pool/src/component/supply.rs index aacd3fec73..2f512454c6 100644 --- a/crates/core/component/shielded-pool/src/component/supply.rs +++ b/crates/core/component/shielded-pool/src/component/supply.rs @@ -12,8 +12,8 @@ use crate::state_key; #[async_trait] pub trait SupplyRead: StateRead { - async fn token_supply(&self, asset_id: &asset::Id) -> Result> { - self.get_proto(&state_key::token_supply(asset_id)).await + async fn token_supply(&self, asset_id: &asset::Id) -> Result> { + self.get(&state_key::token_supply(asset_id)).await } // TODO: refactor for new state model -- no more list of known asset IDs with fixed key @@ -60,7 +60,7 @@ pub trait SupplyWrite: StateWrite { // #[instrument(skip(self, change))] async fn update_token_supply(&mut self, asset_id: &asset::Id, change: i128) -> Result<()> { let key = state_key::token_supply(asset_id); - let current_supply: Amount = self.get_proto(&key).await?.unwrap_or(0u64).into(); + let current_supply: Amount = self.get(&key).await?.unwrap_or(0u64.into()); // 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 { diff --git a/crates/core/component/stake/src/action_handler/validator_definition.rs b/crates/core/component/stake/src/action_handler/validator_definition.rs index 75de2f7fba..eeeb9439b9 100644 --- a/crates/core/component/stake/src/action_handler/validator_definition.rs +++ b/crates/core/component/stake/src/action_handler/validator_definition.rs @@ -113,24 +113,15 @@ impl ActionHandler for validator::Definition { // Set the default rates and state. let validator_key = v.validator.identity_key; - // Delegations require knowing the rates for the - // next epoch, so pre-populate with 0 reward => exchange rate 1 for - // the current and next epochs. let cur_rate_data = RateData { identity_key: validator_key, epoch_index: cur_epoch.index, validator_reward_rate: 0, validator_exchange_rate: 1_0000_0000, // 1 represented as 1e8 }; - let next_rate_data = RateData { - identity_key: validator_key, - epoch_index: cur_epoch.index + 1, - validator_reward_rate: 0, - validator_exchange_rate: 1_0000_0000, // 1 represented as 1e8 - }; state - .add_validator(v.validator.clone(), cur_rate_data, next_rate_data) + .add_validator(v.validator.clone(), cur_rate_data) .await .context("should be able to add validator during validator definition execution")?; } diff --git a/crates/core/component/stake/src/component.rs b/crates/core/component/stake/src/component.rs index fbfbbb0a79..474ba20383 100644 --- a/crates/core/component/stake/src/component.rs +++ b/crates/core/component/stake/src/component.rs @@ -333,50 +333,49 @@ pub(crate) trait StakingImpl: StateWriteExt { let chain_params = self.get_stake_params().await?; + // We are transitioning to the next epoch, so the "current" base rate in + // the state is now the previous base rate. + let prev_base_rate = self.current_base_rate().await?; + // TODO: This will change to be based on issuance. tracing::debug!("processing base rate"); - // 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?; + let next_base_rate = prev_base_rate.next(chain_params.base_reward_rate); + tracing::debug!(?prev_base_rate, ?next_base_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); - - // Update the base rates in the JMT: - self.set_base_rates(current_base_rate.clone(), next_base_rate.clone()) - .await; + // Set the next base rate as the new "current" base rate. + self.set_base_rate(next_base_rate.clone()); let validator_list = self.validator_list().await?; for validator in &validator_list { - // The old epoch's "next rate" is now the "current rate"... - let old_next_rate = self - .next_validator_rate(&validator.identity_key) + // Grab the current validator state. + let validator_state = self + .validator_state(&validator.identity_key) .await? .ok_or_else(|| { - anyhow::anyhow!("validator had ID in validator_list but rate not found in JMT") + anyhow::anyhow!("validator had ID in validator_list but state not found in JMT") })?; - // ... as soon as we apply any penalties recorded in the previous epoch. - let penalty = self - .penalty_in_epoch(&validator.identity_key, epoch_to_end.index) - .await? - .unwrap_or_default(); - let current_rate = old_next_rate.slash(penalty); - let validator_state = self - .validator_state(&validator.identity_key) + // We are transitioning to the next epoch, so the "current" validator + // rate in the state is now the previous validator rate. + let prev_validator_rate = self + .current_validator_rate(&validator.identity_key) .await? .ok_or_else(|| { - anyhow::anyhow!("validator had ID in validator_list but state not found in JMT") + anyhow::anyhow!("validator had ID in validator_list but current_validator_rate not found in JMT") })?; - tracing::debug!(?validator, "processing validator rate updates"); - let funding_streams = validator.funding_streams.clone(); + // First, apply any penalty recorded in the epoch we are ending. + let penalty = self + .penalty_in_epoch(&validator.identity_key, epoch_to_end.index) + .await? + .unwrap_or_default(); + let prev_validator_rate_with_penalty = prev_validator_rate.slash(penalty); - let next_rate = - current_rate.next(&next_base_rate, funding_streams.as_ref(), &validator_state); - assert!(next_rate.epoch_index == epoch_to_end.index + 2); + // Then compute the next validator rate, accounting for funding streams and validator state. + let next_validator_rate = prev_validator_rate_with_penalty.next( + &next_base_rate, + validator.funding_streams.as_ref(), + &validator_state, + ); let total_delegations = delegations_by_validator .get(&validator.identity_key) @@ -397,8 +396,12 @@ pub(crate) trait StakingImpl: StateWriteExt { delegation_delta ); + // Delegations and undelegations created in the previous epoch were created + // with the prev_validator_rate. To compute the staking delta, we need to take + // an absolute value and then re-apply the sign, since .unbonded_amount operates + // on unsigned values. let abs_unbonded_amount = - current_rate.unbonded_amount(delegation_delta.unsigned_abs()) as i128; + prev_validator_rate.unbonded_amount(delegation_delta.unsigned_abs()) as i128; let staking_delta = if delegation_delta >= 0 { // Net delegation: subtract the unbonded amount from the staking token supply -abs_unbonded_amount @@ -417,6 +420,7 @@ pub(crate) trait StakingImpl: StateWriteExt { self.update_token_supply(&STAKING_TOKEN_ASSET_ID, staking_delta) .await?; + // Get the updated delegation token supply for use calculating voting power. let delegation_token_supply = self .token_supply(&DelegationToken::from(validator.identity_key).id()) .await? @@ -424,30 +428,24 @@ pub(crate) trait StakingImpl: StateWriteExt { // Calculate the voting power in the newly beginning epoch let voting_power = - current_rate.voting_power(delegation_token_supply.into(), ¤t_base_rate); + next_validator_rate.voting_power(delegation_token_supply.into(), &next_base_rate); tracing::debug!(?voting_power); // Update the state of the validator within the validator set // with the newly starting epoch's calculated voting rate and power. - self.set_validator_rates( - &validator.identity_key, - current_rate.clone(), - next_rate.clone(), - ); + self.set_validator_rates(&validator.identity_key, next_validator_rate.clone()); self.set_validator_power(&validator.identity_key, voting_power) .await?; - // Only Active validators produce commission rewards - // The validator *may* drop out of Active state during the next epoch, - // but the commission rewards for the ending epoch in which it was Active - // should still be rewarded. + // If the validator is Active, distribute its funding stream rewards + // for the preceding epoch. if validator_state == validator::State::Active { // distribute validator commission - for stream in funding_streams { + for stream in &validator.funding_streams { let commission_reward_amount = stream.reward_amount( - delegation_token_supply, + &prev_base_rate, &next_base_rate, - ¤t_base_rate, + delegation_token_supply, ); match stream.recipient() { @@ -479,8 +477,8 @@ pub(crate) trait StakingImpl: StateWriteExt { // rename to curr_rate so it lines up with next_rate (same # chars) let delegation_denom = DelegationToken::from(&validator.identity_key).denom(); - tracing::debug!(curr_rate = ?current_rate); - tracing::debug!(?next_rate); + tracing::debug!(?prev_validator_rate); + tracing::debug!(?next_validator_rate); tracing::debug!(?delegation_delta); tracing::debug!(?delegation_token_supply); tracing::debug!(?delegation_denom); @@ -760,21 +758,12 @@ pub(crate) trait StakingImpl: StateWriteExt { genesis_base_rate: &BaseRateData, validator: Validator, ) -> Result<()> { - // Delegations require knowing the rates for the - // next epoch, so pre-populate with 0 reward => exchange rate 1 for - // the current and next epochs. let cur_rate_data = RateData { identity_key: validator.identity_key.clone(), epoch_index: genesis_base_rate.epoch_index, validator_reward_rate: 0, validator_exchange_rate: 1_0000_0000, // 1 represented as 1e8 }; - let next_rate_data = RateData { - identity_key: validator.identity_key.clone(), - epoch_index: genesis_base_rate.epoch_index + 1, - validator_reward_rate: 0, - validator_exchange_rate: 1_0000_0000, // 1 represented as 1e8 - }; // The initial allocations to the validator are specified in `genesis_allocations`. // We use these to determine the initial voting power for each validator. @@ -790,7 +779,6 @@ pub(crate) trait StakingImpl: StateWriteExt { self.add_validator_inner( validator.clone(), cur_rate_data, - next_rate_data, // All genesis validators start in the "Active" state: validator::State::Active, // All genesis validators start in the "Bonded" bonding state: @@ -812,19 +800,13 @@ pub(crate) trait StakingImpl: StateWriteExt { /// Add a validator after genesis, which will start in Inactive /// state with no power assigned. - async fn add_validator( - &mut self, - validator: Validator, - cur_rate_data: RateData, - next_rate_data: RateData, - ) -> Result<()> { + async fn add_validator(&mut self, validator: Validator, cur_rate_data: RateData) -> Result<()> { // We explicitly do not call `update_tm_validator_power` here, // as a post-genesis validator should not have power reported // to Tendermint until it becomes Active. self.add_validator_inner( validator.clone(), cur_rate_data, - next_rate_data, // All post-genesis validators start in the "Inactive" state: validator::State::Inactive, // And post-genesis validators have "Unbonded" bonding state: @@ -911,22 +893,12 @@ impl Component for Staking { .expect("should be able to get initial epoch"); let epoch_index = starting_epoch.index; - // Delegations require knowing the rates for the next epoch, so - // pre-populate with 0 reward => exchange rate 1 for the current - // (index 0) and next (index 1) epochs for base rate data. let genesis_base_rate = BaseRateData { epoch_index, base_reward_rate: 0, base_exchange_rate: 1_0000_0000, }; - let next_base_rate = BaseRateData { - epoch_index: epoch_index + 1, - base_reward_rate: 0, - base_exchange_rate: 1_0000_0000, - }; - state - .set_base_rates(genesis_base_rate.clone(), next_base_rate) - .await; + state.set_base_rate(genesis_base_rate.clone()); // Compile totals of genesis allocations by denom, which we can use // to compute the delegation tokens for each validator. @@ -947,7 +919,7 @@ impl Component for Staking { .await .expect("should be able to get token supply") .expect("should have token supply for known denom") - as u128; + .value(); genesis_allocations.insert(denom, genesis_allocation); } @@ -1111,12 +1083,7 @@ pub trait StateReadExt: StateRead { .map(|rate_data| rate_data.expect("rate data must be set after init_chain")) } - async fn next_base_rate(&self) -> Result { - self.get(state_key::next_base_rate()) - .await - .map(|rate_data| rate_data.expect("rate data must be set after init_chain")) - } - + // This is a normal fn returning a boxed future so it can be 'static with no inferred lifetime bound fn current_validator_rate( &self, identity_key: &IdentityKey, @@ -1125,11 +1092,6 @@ pub trait StateReadExt: StateRead { .boxed() } - async fn next_validator_rate(&self, identity_key: &IdentityKey) -> Result> { - self.get(&state_key::next_rate_by_validator(identity_key)) - .await - } - fn validator_power(&self, identity_key: &IdentityKey) -> ProtoFuture { self.get_proto(&state_key::power_by_validator(identity_key)) } @@ -1187,7 +1149,7 @@ pub trait StateReadExt: StateRead { async fn validator_info(&self, identity_key: &IdentityKey) -> Result> { let validator = self.validator(identity_key).await?; let status = self.validator_status(identity_key).await?; - let rate_data = self.next_validator_rate(identity_key).await?; + let rate_data = self.current_validator_rate(identity_key).await?; match (validator, status, rate_data) { (Some(validator), Some(status), Some(rate_data)) => Ok(Some(validator::Info { validator, @@ -1361,10 +1323,9 @@ pub trait StateWriteExt: StateWrite { } #[instrument(skip(self))] - async fn set_base_rates(&mut self, current: BaseRateData, next: BaseRateData) { - tracing::debug!("setting base rates"); + fn set_base_rate(&mut self, current: BaseRateData) { + tracing::debug!("setting base rate"); self.put(state_key::current_base_rate().to_owned(), current); - self.put(state_key::next_base_rate().to_owned(), next); } #[instrument(skip(self))] @@ -1384,18 +1345,12 @@ pub trait StateWriteExt: StateWrite { } #[instrument(skip(self))] - fn set_validator_rates( - &mut self, - identity_key: &IdentityKey, - current_rates: RateData, - next_rates: RateData, - ) { + fn set_validator_rates(&mut self, identity_key: &IdentityKey, current_rates: RateData) { tracing::debug!("setting validator rates"); self.put( state_key::current_rate_by_validator(identity_key), current_rates, ); - self.put(state_key::next_rate_by_validator(identity_key), next_rates); } async fn register_consensus_key( @@ -1444,7 +1399,6 @@ pub trait StateWriteExt: StateWrite { &mut self, validator: Validator, current_rates: RateData, - next_rates: RateData, state: validator::State, bonding_state: validator::BondingState, power: u64, @@ -1458,7 +1412,7 @@ pub trait StateWriteExt: StateWrite { self.register_denom(&DelegationToken::from(&id).denom()) .await?; - self.set_validator_rates(&id, current_rates, next_rates); + self.set_validator_rates(&id, current_rates); // We can't call `set_validator_state` here because it requires an existing validator state, // so we manually initialize the state for new validators. diff --git a/crates/core/component/stake/src/funding_stream.rs b/crates/core/component/stake/src/funding_stream.rs index 1afb23fa33..f641402f11 100644 --- a/crates/core/component/stake/src/funding_stream.rs +++ b/crates/core/component/stake/src/funding_stream.rs @@ -1,4 +1,5 @@ use penumbra_keys::Address; +use penumbra_num::Amount; use penumbra_proto::{penumbra::core::component::stake::v1alpha1 as pb, DomainType, TypeUrl}; use serde::{Deserialize, Serialize}; @@ -51,18 +52,18 @@ impl FundingStream { /// Computes the amount of reward at the epoch specified by base_rate_data pub fn reward_amount( &self, - total_delegation_tokens: u64, - base_rate_data: &BaseRateData, - prev_epoch_rate_data: &BaseRateData, + prev_base_rate: &BaseRateData, + next_base_rate: &BaseRateData, + total_delegation_tokens: Amount, ) -> u64 { - if prev_epoch_rate_data.epoch_index != base_rate_data.epoch_index - 1 { + if prev_base_rate.epoch_index != next_base_rate.epoch_index - 1 { panic!("wrong base rate data for previous epoch") } // take yv*cve*re*psi(e-1) let mut r = - (total_delegation_tokens as u128 * (self.rate_bps() as u128 * 1_0000)) / 1_0000_0000; - r = (r * base_rate_data.base_reward_rate as u128) / 1_0000_0000; - r = (r * prev_epoch_rate_data.base_exchange_rate as u128) / 1_0000_0000; + (total_delegation_tokens.value() * (self.rate_bps() as u128 * 1_0000)) / 1_0000_0000; + r = (r * next_base_rate.base_reward_rate as u128) / 1_0000_0000; + r = (r * prev_base_rate.base_exchange_rate as u128) / 1_0000_0000; r as u64 } @@ -187,3 +188,12 @@ impl IntoIterator for FundingStreams { self.funding_streams.into_iter() } } + +impl<'a> IntoIterator for &'a FundingStreams { + type Item = &'a FundingStream; + type IntoIter = std::slice::Iter<'a, FundingStream>; + + fn into_iter(self) -> Self::IntoIter { + (&self.funding_streams).into_iter() + } +} From 5988be6c4d15e99112f989ee970b5c1a47f7c959 Mon Sep 17 00:00:00 2001 From: Erwan Date: Fri, 17 Nov 2023 17:40:28 -0500 Subject: [PATCH 6/6] distributions: remove dead code comment Co-authored-by: Henry de Valence --- .../component/distributions/src/component.rs | 75 ------------------- 1 file changed, 75 deletions(-) diff --git a/crates/core/component/distributions/src/component.rs b/crates/core/component/distributions/src/component.rs index 1e164a592a..70ef24ab5f 100644 --- a/crates/core/component/distributions/src/component.rs +++ b/crates/core/component/distributions/src/component.rs @@ -61,81 +61,6 @@ impl Component for Distributions { #[instrument(name = "distributions", skip(_state))] async fn end_epoch(_state: &mut Arc) -> 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::debug!( - ?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 - - // Get the total issuance and new remainder for this epoch - let (issuance, remainder) = state.total_issuance_and_remainder(remainder).await?; - - tracing::debug!(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::debug!(%component, ?issuance, "issuing tokens to component" - ); - match component { - Staking => state.set_staking_issuance(issuance), - Dex => todo!("set dex issuance"), - } - } - } - */ Ok(()) } }