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 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 } = self;
119
120 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 } = 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
291fn 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
312fn 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}