penumbra_sdk_app/params/
change.rs

1use std::fmt::Display;
2
3use anyhow::Context;
4use anyhow::Result;
5use penumbra_sdk_auction::params::AuctionParameters;
6use penumbra_sdk_community_pool::params::CommunityPoolParameters;
7use penumbra_sdk_dex::DexParameters;
8use penumbra_sdk_distributions::params::DistributionsParameters;
9use penumbra_sdk_fee::FeeParameters;
10use penumbra_sdk_funding::params::FundingParameters;
11use penumbra_sdk_governance::change::ParameterChange;
12use penumbra_sdk_governance::{params::GovernanceParameters, tally::Ratio};
13use penumbra_sdk_ibc::params::IBCParameters;
14use penumbra_sdk_sct::params::SctParameters;
15use penumbra_sdk_shielded_pool::params::ShieldedPoolParameters;
16use penumbra_sdk_stake::params::StakeParameters;
17
18use super::AppParameters;
19
20pub trait ParameterChangeExt {
21    fn apply_changes(&self, app_parameters: AppParameters) -> Result<AppParameters, anyhow::Error>;
22}
23
24impl ParameterChangeExt for ParameterChange {
25    fn apply_changes(&self, app_parameters: AppParameters) -> Result<AppParameters, anyhow::Error> {
26        // Check the changes against the denylist of banned parameters
27        for _change in &self.changes {
28            // TODO: implement denylist. each component should have a list of &'static str denied fields
29        }
30
31        let app_parameters_raw = serde_json::value::to_value(app_parameters.clone())
32            .context("could not encode app parameters to json value")?;
33        let new_app_parameters_raw = self
34            .apply_changes_raw(app_parameters_raw)
35            .context("error applying parameter changes")?;
36
37        let new_app_parameters = serde_json::value::from_value(new_app_parameters_raw)
38            .context("error parsing changed app parameters")?;
39
40        // TODO: validation should be done in the components themselves,
41        // in the domain type conversions (#3593).
42        // Rather than checking against old parameters we should use a denylist
43        // of fields that can't be changed.
44        app_parameters
45            .check_valid_update(&new_app_parameters)
46            .context("parameter change was invalid")?;
47
48        Ok(new_app_parameters)
49    }
50}
51
52// The checks below validate that a parameter change is valid, since some parameter settings or
53// combinations are nonsensical and should be rejected outright, regardless of governance.
54
55#[deny(unused)] // We want to be really careful here to not examine fields!
56impl AppParameters {
57    pub fn check_valid_update(&self, new: &AppParameters) -> Result<()> {
58        new.check_valid()?;
59        // TODO: move the checks below into their respective components.
60        // Tracked by #3593
61        let AppParameters {
62            chain_id,
63            auction_params: AuctionParameters {},
64            community_pool_params:
65                CommunityPoolParameters {
66                    community_pool_spend_proposals_enabled: _,
67                },
68            distributions_params:
69                DistributionsParameters {
70                    staking_issuance_per_block: _,
71                    liquidity_tournament_incentive_per_block: _,
72                    liquidity_tournament_end_block: _,
73                },
74            fee_params:
75                FeeParameters {
76                    fixed_gas_prices: _,
77                    fixed_alt_gas_prices: _,
78                },
79            funding_params:
80                FundingParameters {
81                    liquidity_tournament: _,
82                },
83            governance_params:
84                GovernanceParameters {
85                    proposal_voting_blocks: _,
86                    proposal_deposit_amount: _,
87                    proposal_valid_quorum,
88                    proposal_pass_threshold,
89                    proposal_slash_threshold,
90                },
91            ibc_params:
92                IBCParameters {
93                    ibc_enabled: _,
94                    inbound_ics20_transfers_enabled: _,
95                    outbound_ics20_transfers_enabled: _,
96                },
97            sct_params: SctParameters { epoch_duration },
98            shielded_pool_params: ShieldedPoolParameters { fmd_meta_params: _ },
99            stake_params:
100                StakeParameters {
101                    active_validator_limit,
102                    slashing_penalty_misbehavior: _,
103                    slashing_penalty_downtime: _,
104                    signed_blocks_window_len,
105                    missed_blocks_maximum: _,
106                    min_validator_stake: _,
107                    unbonding_delay: _,
108                },
109            dex_params:
110                DexParameters {
111                    is_enabled: _,
112                    fixed_candidates: _,
113                    max_hops: _,
114                    max_positions_per_pair: _,
115                    max_execution_budget: _,
116                },
117            // IMPORTANT: Don't use `..` here! We want to ensure every single field is verified!
118        } = self;
119
120        // Ensure that certain parameters are not changed by the update:
121        check_invariant([(chain_id, &new.chain_id, "chain ID")])?;
122        check_invariant([
123            (
124                epoch_duration,
125                &new.sct_params.epoch_duration,
126                "epoch duration",
127            ),
128            (
129                active_validator_limit,
130                &new.stake_params.active_validator_limit,
131                "active validator limit",
132            ),
133            (
134                signed_blocks_window_len,
135                &new.stake_params.signed_blocks_window_len,
136                "signed blocks window length",
137            ),
138        ])?;
139        check_invariant([
140            (
141                proposal_valid_quorum,
142                &new.governance_params.proposal_valid_quorum,
143                "proposal valid quorum",
144            ),
145            (
146                proposal_pass_threshold,
147                &new.governance_params.proposal_pass_threshold,
148                "proposal pass threshold",
149            ),
150            (
151                proposal_slash_threshold,
152                &new.governance_params.proposal_slash_threshold,
153                "proposal slash threshold",
154            ),
155        ])?;
156
157        Ok(())
158    }
159
160    pub fn check_valid(&self) -> Result<()> {
161        let AppParameters {
162            chain_id,
163            auction_params: AuctionParameters {},
164            community_pool_params:
165                CommunityPoolParameters {
166                    community_pool_spend_proposals_enabled: _,
167                },
168            distributions_params:
169                DistributionsParameters {
170                    staking_issuance_per_block: _,
171                    liquidity_tournament_incentive_per_block: _,
172                    liquidity_tournament_end_block: _,
173                },
174            fee_params:
175                FeeParameters {
176                    fixed_gas_prices: _,
177                    fixed_alt_gas_prices: _,
178                },
179            funding_params:
180                FundingParameters {
181                    liquidity_tournament: _,
182                },
183            governance_params:
184                GovernanceParameters {
185                    proposal_voting_blocks,
186                    proposal_deposit_amount,
187                    proposal_valid_quorum,
188                    proposal_pass_threshold,
189                    proposal_slash_threshold,
190                },
191            ibc_params:
192                IBCParameters {
193                    ibc_enabled,
194                    inbound_ics20_transfers_enabled,
195                    outbound_ics20_transfers_enabled,
196                },
197            sct_params: SctParameters { epoch_duration },
198            shielded_pool_params: ShieldedPoolParameters { fmd_meta_params: _ },
199            stake_params:
200                StakeParameters {
201                    active_validator_limit,
202                    slashing_penalty_misbehavior,
203                    slashing_penalty_downtime,
204                    signed_blocks_window_len,
205                    missed_blocks_maximum,
206                    min_validator_stake,
207                    unbonding_delay,
208                },
209            dex_params:
210                DexParameters {
211                    is_enabled: _,
212                    fixed_candidates: _,
213                    max_hops: _,
214                    max_positions_per_pair: _,
215                    max_execution_budget: _,
216                },
217            // IMPORTANT: Don't use `..` here! We want to ensure every single field is verified!
218        } = self;
219
220        check_all([
221            (!chain_id.is_empty(), "chain ID must be a non-empty string"),
222            (
223                *epoch_duration >= 1,
224                "epoch duration must be at least one block",
225            ),
226            (
227                *unbonding_delay >= epoch_duration * 2 + 1,
228                "unbonding must take at least two epochs",
229            ),
230            (
231                *active_validator_limit > 3,
232                "active validator limit must be at least 4",
233            ),
234            (
235                *slashing_penalty_misbehavior >= 1,
236                "slashing penalty (misbehavior) must be at least 1 basis point",
237            ),
238            (
239                *slashing_penalty_misbehavior <= 100_000_000,
240                "slashing penalty (misbehavior) must be at most 10,000 basis points^2",
241            ),
242            (
243                *slashing_penalty_downtime >= 1,
244                "slashing penalty (downtime) must be at least 1 basis point",
245            ),
246            (
247                *slashing_penalty_downtime <= 100_000_000,
248                "slashing penalty (downtime) must be at most 10,000 basis points^2",
249            ),
250            (
251                *signed_blocks_window_len >= 2,
252                "signed blocks window length must be at least 2",
253            ),
254            (
255                *missed_blocks_maximum >= 1,
256                "missed blocks maximum must be at least 1",
257            ),
258            (
259                (!*inbound_ics20_transfers_enabled && !*outbound_ics20_transfers_enabled)
260                    || *ibc_enabled,
261                "IBC must be enabled if either inbound or outbound ICS20 transfers are enabled",
262            ),
263            (
264                *proposal_voting_blocks >= 1,
265                "proposal voting blocks must be at least 1",
266            ),
267            (
268                *proposal_deposit_amount >= 1u64.into(),
269                "proposal deposit amount must be at least 1",
270            ),
271            (
272                *proposal_valid_quorum > Ratio::new(0, 1),
273                "proposal valid quorum must be greater than 0",
274            ),
275            (
276                *proposal_pass_threshold >= Ratio::new(1, 2),
277                "proposal pass threshold must be greater than or equal to 1/2",
278            ),
279            (
280                *proposal_slash_threshold > Ratio::new(1, 2),
281                "proposal slash threshold must be greater than 1/2",
282            ),
283            (
284                *min_validator_stake >= 1_000_000u128.into(),
285                "the minimum validator stake must be at least 1penumbra",
286            ),
287        ])
288    }
289}
290
291/// Ensure all of the booleans are true, and if any are false, generate an error describing which
292/// failed, based on the provided descriptions.
293fn check_all<'a>(checks: impl IntoIterator<Item = (bool, impl Display + 'a)>) -> Result<()> {
294    let failed_because = checks
295        .into_iter()
296        .filter_map(|(ok, description)| {
297            if !ok {
298                Some(description.to_string())
299            } else {
300                None
301            }
302        })
303        .collect::<Vec<_>>();
304
305    if !failed_because.is_empty() {
306        anyhow::bail!("invalid chain parameters: {}", failed_because.join(", "));
307    }
308
309    Ok(())
310}
311
312/// Ensure that all of the provided pairs of values are equal, and if any are not, generate an error
313/// stating that the varying names can't be changed.
314fn check_invariant<'a, T: Eq + 'a>(
315    sides: impl IntoIterator<Item = (&'a T, &'a T, impl Display + 'a)>,
316) -> Result<()> {
317    check_all(
318        sides
319            .into_iter()
320            .map(|(old, new, name)| ((*old == *new), format!("{name} can't be changed"))),
321    )
322}