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 for _change in &self.changes {
28 }
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 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#[deny(unused)] impl AppParameters {
57 pub fn check_valid_update(&self, new: &AppParameters) -> Result<()> {
58 new.check_valid()?;
59 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 } = self;
114
115 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 } = 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
281fn 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
302fn 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}