penumbra_sdk_distributions/
component.rs

1pub mod state_key;
2pub use view::{StateReadExt, StateWriteExt};
3pub mod rpc;
4
5mod view;
6
7use std::sync::Arc;
8
9use crate::event;
10use crate::genesis;
11use anyhow::{Context, Result};
12use async_trait::async_trait;
13use cnidarium::StateWrite;
14use cnidarium_component::Component;
15use penumbra_sdk_num::Amount;
16use penumbra_sdk_proto::{DomainType as _, StateWriteProto};
17use penumbra_sdk_sct::component::clock::EpochRead;
18use tendermint::v0_37::abci;
19use tracing::instrument;
20
21pub struct Distributions {}
22
23#[async_trait]
24impl Component for Distributions {
25    type AppState = genesis::Content;
26
27    #[instrument(name = "distributions", skip(state, app_state))]
28    async fn init_chain<S: StateWrite>(mut state: S, app_state: Option<&Self::AppState>) {
29        match app_state {
30            None => { /* Checkpoint -- no-op */ }
31            Some(genesis) => {
32                state.put_distributions_params(genesis.distributions_params.clone());
33            }
34        };
35    }
36
37    #[instrument(name = "distributions", skip(_state, _begin_block))]
38    async fn begin_block<S: StateWrite + 'static>(
39        _state: &mut Arc<S>,
40        _begin_block: &abci::request::BeginBlock,
41    ) {
42    }
43
44    #[instrument(name = "distributions", skip(state))]
45    async fn end_block<S: StateWrite + 'static>(
46        state: &mut Arc<S>,
47        end_block: &abci::request::EndBlock,
48    ) {
49        let state = Arc::get_mut(state).expect("the state should not be shared");
50
51        let current_epoch = state
52            .get_current_epoch()
53            .await
54            .expect("failed to retrieve current epoch from state");
55        let epoch_index = current_epoch.index;
56
57        let params = state
58            .get_distributions_params()
59            .await
60            .expect("distribution parameters should be available");
61        let increase = Amount::from(params.liquidity_tournament_incentive_per_block);
62        let current_block = u64::try_from(end_block.height).unwrap_or_default();
63        // If the end_block is defined, and the current block is at least past there,
64        // then we want to make sure to not issue any rewards.
65        if let Some(end_block) = params
66            .liquidity_tournament_end_block
67            .filter(|b| current_block >= b.get())
68        {
69            // Doing this unconditionally for robustness. This should, in theory,
70            // only need to be an edge trigger though, like the event.
71            tracing::debug!(epoch_index, "zeroing out LQT reward issuance");
72            state.set_lqt_reward_issuance_for_epoch(epoch_index, 0u64.into());
73            if current_block == end_block.get() {
74                state.record_proto(
75                    event::EventLqtPoolSizeIncrease {
76                        epoch_index,
77                        increase: 0u64.into(),
78                        new_total: 0u64.into(),
79                    }
80                    .to_proto(),
81                )
82            }
83            return;
84        }
85
86        let new_total = state.increment_lqt_issuance(epoch_index, increase).await;
87
88        // Emit an event for LQT pool size increase at the end of the block.
89        state.record_proto(
90            event::EventLqtPoolSizeIncrease {
91                epoch_index,
92                increase,
93                new_total,
94            }
95            .to_proto(),
96        )
97    }
98
99    #[instrument(name = "distributions", skip(state))]
100    async fn end_epoch<S: StateWrite + 'static>(state: &mut Arc<S>) -> Result<()> {
101        let state = Arc::get_mut(state).context("state should be unique")?;
102
103        // Define staking budget.
104        state.define_staking_budget().await?;
105
106        // The lqt issuance budget is adjusted every block instead.
107
108        Ok(())
109    }
110}
111
112#[async_trait]
113trait DistributionManager: StateWriteExt {
114    /// Compute the total new issuance of staking tokens for this epoch.
115    async fn compute_new_staking_issuance(&self) -> Result<Amount> {
116        use penumbra_sdk_sct::component::clock::EpochRead;
117
118        let current_block_height = self.get_block_height().await?;
119        let current_epoch = self.get_current_epoch().await?;
120        let num_blocks = current_block_height
121            .checked_sub(current_epoch.start_height)
122            .unwrap_or_else(|| panic!("epoch start height is greater than current block height (epoch_start={}, current_height={}", current_epoch.start_height, current_block_height));
123
124        // TODO(erwan): Will make the distribution chain param an `Amount`
125        // in a subsequent PR. Want to avoid conflicts with other in-flight changes.
126        let staking_issuance_per_block = self
127            .get_distributions_params()
128            .await?
129            .staking_issuance_per_block as u128;
130
131        tracing::debug!(
132            number_of_blocks_in_epoch = num_blocks,
133            staking_issuance_per_block,
134            "calculating staking issuance per epoch"
135        );
136
137        let new_staking_issuance_for_epoch = staking_issuance_per_block
138            .checked_mul(num_blocks as u128) /* Safe to cast a `u64` to `u128` */
139            .expect("infallible unless issuance is pathological");
140
141        tracing::debug!(
142            ?new_staking_issuance_for_epoch,
143            "computed new staking issuance for epoch"
144        );
145
146        Ok(Amount::from(new_staking_issuance_for_epoch))
147    }
148
149    /// Update the object store with the new issuance of staking tokens for this epoch.
150    async fn define_staking_budget(&mut self) -> Result<()> {
151        let new_issuance = self.compute_new_staking_issuance().await?;
152        tracing::debug!(
153            ?new_issuance,
154            "computed new staking issuance for current epoch"
155        );
156        Ok(self.set_staking_token_issuance_for_epoch(new_issuance))
157    }
158
159    async fn increment_lqt_issuance(&mut self, epoch_index: u64, by: Amount) -> Amount {
160        let current = self.get_lqt_reward_issuance_for_epoch(epoch_index).await;
161        let new = current
162            .unwrap_or_default()
163            .checked_add(&by)
164            .expect("LQT issuance should never exceed an Amount");
165        tracing::debug!(
166            ?by,
167            ?current,
168            ?new,
169            epoch_index,
170            "incrementing lqt issuance"
171        );
172        self.set_lqt_reward_issuance_for_epoch(epoch_index, new);
173        new
174    }
175}
176
177impl<T: StateWrite + ?Sized> DistributionManager for T {}