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                },
72            fee_params:
73                FeeParameters {
74                    fixed_gas_prices: _,
75                    fixed_alt_gas_prices: _,
76                },
77            funding_params: FundingParameters {},
78            governance_params:
79                GovernanceParameters {
80                    proposal_voting_blocks: _,
81                    proposal_deposit_amount: _,
82                    proposal_valid_quorum,
83                    proposal_pass_threshold,
84                    proposal_slash_threshold,
85                },
86            ibc_params:
87                IBCParameters {
88                    ibc_enabled: _,
89                    inbound_ics20_transfers_enabled: _,
90                    outbound_ics20_transfers_enabled: _,
91                },
92            sct_params: SctParameters { epoch_duration },
93            shielded_pool_params: ShieldedPoolParameters { fmd_meta_params: _ },
94            stake_params:
95                StakeParameters {
96                    active_validator_limit,
97                    slashing_penalty_misbehavior: _,
98                    slashing_penalty_downtime: _,
99                    signed_blocks_window_len,
100                    missed_blocks_maximum: _,
101                    min_validator_stake: _,
102                    unbonding_delay: _,
103                },
104            dex_params:
105                DexParameters {
106                    is_enabled: _,
107                    fixed_candidates: _,
108                    max_hops: _,
109                    max_positions_per_pair: _,
110                    max_execution_budget: _,
111                },
112            // IMPORTANT: Don't use `..` here! We want to ensure every single field is verified!
113        } = self;
114
115        // Ensure that certain parameters are not changed by the update:
116        check_invariant([(chain_id, &new.chain_id, "chain ID")])?;
117        check_invariant([
118            (
119                epoch_duration,
120                &new.sct_params.epoch_duration,
121                "epoch duration",
122            ),
123            (
124                active_validator_limit,
125                &new.stake_params.active_validator_limit,
126                "active validator limit",
127            ),
128            (
129                signed_blocks_window_len,
130                &new.stake_params.signed_blocks_window_len,
131                "signed blocks window length",
132            ),
133        ])?;
134        check_invariant([
135            (
136                proposal_valid_quorum,
137                &new.governance_params.proposal_valid_quorum,
138                "proposal valid quorum",
139            ),
140            (
141                proposal_pass_threshold,
142                &new.governance_params.proposal_pass_threshold,
143                "proposal pass threshold",
144            ),
145            (
146                proposal_slash_threshold,
147                &new.governance_params.proposal_slash_threshold,
148                "proposal slash threshold",
149            ),
150        ])?;
151
152        Ok(())
153    }
154
155    pub fn check_valid(&self) -> Result<()> {
156        let AppParameters {
157            chain_id,
158            auction_params: AuctionParameters {},
159            community_pool_params:
160                CommunityPoolParameters {
161                    community_pool_spend_proposals_enabled: _,
162                },
163            distributions_params:
164                DistributionsParameters {
165                    staking_issuance_per_block: _,
166                },
167            fee_params:
168                FeeParameters {
169                    fixed_gas_prices: _,
170                    fixed_alt_gas_prices: _,
171                },
172            funding_params: FundingParameters {},
173            governance_params:
174                GovernanceParameters {
175                    proposal_voting_blocks,
176                    proposal_deposit_amount,
177                    proposal_valid_quorum,
178                    proposal_pass_threshold,
179                    proposal_slash_threshold,
180                },
181            ibc_params:
182                IBCParameters {
183                    ibc_enabled,
184                    inbound_ics20_transfers_enabled,
185                    outbound_ics20_transfers_enabled,
186                },
187            sct_params: SctParameters { epoch_duration },
188            shielded_pool_params: ShieldedPoolParameters { fmd_meta_params: _ },
189            stake_params:
190                StakeParameters {
191                    active_validator_limit,
192                    slashing_penalty_misbehavior,
193                    slashing_penalty_downtime,
194                    signed_blocks_window_len,
195                    missed_blocks_maximum,
196                    min_validator_stake,
197                    unbonding_delay,
198                },
199            dex_params:
200                DexParameters {
201                    is_enabled: _,
202                    fixed_candidates: _,
203                    max_hops: _,
204                    max_positions_per_pair: _,
205                    max_execution_budget: _,
206                },
207            // IMPORTANT: Don't use `..` here! We want to ensure every single field is verified!
208        } = self;
209
210        check_all([
211            (!chain_id.is_empty(), "chain ID must be a non-empty string"),
212            (
213                *epoch_duration >= 1,
214                "epoch duration must be at least one block",
215            ),
216            (
217                *unbonding_delay >= epoch_duration * 2 + 1,
218                "unbonding must take at least two epochs",
219            ),
220            (
221                *active_validator_limit > 3,
222                "active validator limit must be at least 4",
223            ),
224            (
225                *slashing_penalty_misbehavior >= 1,
226                "slashing penalty (misbehavior) must be at least 1 basis point",
227            ),
228            (
229                *slashing_penalty_misbehavior <= 100_000_000,
230                "slashing penalty (misbehavior) must be at most 10,000 basis points^2",
231            ),
232            (
233                *slashing_penalty_downtime >= 1,
234                "slashing penalty (downtime) must be at least 1 basis point",
235            ),
236            (
237                *slashing_penalty_downtime <= 100_000_000,
238                "slashing penalty (downtime) must be at most 10,000 basis points^2",
239            ),
240            (
241                *signed_blocks_window_len >= 2,
242                "signed blocks window length must be at least 2",
243            ),
244            (
245                *missed_blocks_maximum >= 1,
246                "missed blocks maximum must be at least 1",
247            ),
248            (
249                (!*inbound_ics20_transfers_enabled && !*outbound_ics20_transfers_enabled)
250                    || *ibc_enabled,
251                "IBC must be enabled if either inbound or outbound ICS20 transfers are enabled",
252            ),
253            (
254                *proposal_voting_blocks >= 1,
255                "proposal voting blocks must be at least 1",
256            ),
257            (
258                *proposal_deposit_amount >= 1u64.into(),
259                "proposal deposit amount must be at least 1",
260            ),
261            (
262                *proposal_valid_quorum > Ratio::new(0, 1),
263                "proposal valid quorum must be greater than 0",
264            ),
265            (
266                *proposal_pass_threshold >= Ratio::new(1, 2),
267                "proposal pass threshold must be greater than or equal to 1/2",
268            ),
269            (
270                *proposal_slash_threshold > Ratio::new(1, 2),
271                "proposal slash threshold must be greater than 1/2",
272            ),
273            (
274                *min_validator_stake >= 1_000_000u128.into(),
275                "the minimum validator stake must be at least 1penumbra",
276            ),
277        ])
278    }
279}
280
281/// Ensure all of the booleans are true, and if any are false, generate an error describing which
282/// failed, based on the provided descriptions.
283fn check_all<'a>(checks: impl IntoIterator<Item = (bool, impl Display + 'a)>) -> Result<()> {
284    let failed_because = checks
285        .into_iter()
286        .filter_map(|(ok, description)| {
287            if !ok {
288                Some(description.to_string())
289            } else {
290                None
291            }
292        })
293        .collect::<Vec<_>>();
294
295    if !failed_because.is_empty() {
296        anyhow::bail!("invalid chain parameters: {}", failed_because.join(", "));
297    }
298
299    Ok(())
300}
301
302/// Ensure that all of the provided pairs of values are equal, and if any are not, generate an error
303/// stating that the varying names can't be changed.
304fn check_invariant<'a, T: Eq + 'a>(
305    sides: impl IntoIterator<Item = (&'a T, &'a T, impl Display + 'a)>,
306) -> Result<()> {
307    check_all(
308        sides
309            .into_iter()
310            .map(|(old, new, name)| ((*old == *new), format!("{name} can't be changed"))),
311    )
312}