1use std::str::FromStr;
2
3use anyhow::Context;
4use penumbra_sdk_proto::{core::component::governance::v1 as pb, DomainType};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(try_from = "pb::EncodedParameter", into = "pb::EncodedParameter")]
10pub struct EncodedParameter {
11 pub component: String,
12 pub key: String,
13 pub value: String,
14}
15
16impl DomainType for EncodedParameter {
17 type Proto = pb::EncodedParameter;
18}
19
20impl TryFrom<pb::EncodedParameter> for EncodedParameter {
21 type Error = anyhow::Error;
22 fn try_from(value: pb::EncodedParameter) -> Result<Self, Self::Error> {
23 if value.key.len() > 64 {
27 anyhow::bail!("key length must be less than or equal to 64 characters");
28 }
29
30 if value.value.len() > 2048 {
32 anyhow::bail!("value length must be less than or equal to 2048 characters");
33 }
34
35 if value.component.len() > 64 {
37 anyhow::bail!("component length must be less than or equal to 64 characters");
38 }
39
40 Ok(EncodedParameter {
41 component: value.component,
42 key: value.key,
43 value: value.value,
44 })
45 }
46}
47
48impl From<EncodedParameter> for pb::EncodedParameter {
49 fn from(value: EncodedParameter) -> Self {
50 pb::EncodedParameter {
51 component: value.component,
52 key: value.key,
53 value: value.value,
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(
61 try_from = "pb::proposal::ParameterChange",
62 into = "pb::proposal::ParameterChange"
63)]
64pub struct ParameterChange {
65 pub changes: Vec<EncodedParameter>,
66 pub preconditions: Vec<EncodedParameter>,
67}
68
69impl DomainType for ParameterChange {
70 type Proto = pb::proposal::ParameterChange;
71}
72
73impl TryFrom<pb::proposal::ParameterChange> for ParameterChange {
74 type Error = anyhow::Error;
75 fn try_from(value: pb::proposal::ParameterChange) -> Result<Self, Self::Error> {
76 Ok(ParameterChange {
77 changes: value
78 .changes
79 .into_iter()
80 .map(EncodedParameter::try_from)
81 .collect::<Result<_, _>>()?,
82 preconditions: value
83 .preconditions
84 .into_iter()
85 .map(EncodedParameter::try_from)
86 .collect::<Result<_, _>>()?,
87 })
88 }
89}
90
91impl From<ParameterChange> for pb::proposal::ParameterChange {
92 fn from(value: ParameterChange) -> Self {
93 pb::proposal::ParameterChange {
94 changes: value
95 .changes
96 .into_iter()
97 .map(pb::EncodedParameter::from)
98 .collect(),
99 preconditions: value
100 .preconditions
101 .into_iter()
102 .map(pb::EncodedParameter::from)
103 .collect(),
104 ..Default::default()
105 }
106 }
107}
108
109impl ParameterChange {
110 pub fn encode_parameters(parameters: serde_json::Value) -> Self {
114 let mut encoded_parameters = Vec::new();
115 for (component, value) in parameters.as_object().into_iter().flatten() {
116 for (key, value) in value.as_object().into_iter().flatten() {
117 encoded_parameters.push(EncodedParameter {
118 component: component.to_string(),
119 key: key.to_string(),
120 value: value.to_string(),
121 });
122 }
123 }
124 Self {
125 changes: encoded_parameters.clone(),
126 preconditions: encoded_parameters,
127 }
128 }
129
130 pub fn apply_changes_raw(
140 &self,
141 mut app_parameters: serde_json::Value,
142 ) -> Result<serde_json::Value, anyhow::Error> {
143 for precondition in &self.preconditions {
144 let expected_value = serde_json::Value::from_str(&precondition.value)
145 .context("could not decode existing value as JSON value")?;
146
147 match get_component(&mut app_parameters, precondition)?.get(&precondition.key) {
148 Some(current_value) => {
149 anyhow::ensure!(
150 current_value == &expected_value,
151 "precondition failed: key {} in component {} has value {} but expected {}",
152 precondition.key,
153 precondition.component,
154 current_value,
155 expected_value
156 )
157 }
158 None => {
159 anyhow::bail!(
160 "precondition failed: key {} not found in component {}",
161 precondition.key,
162 precondition.component
163 );
164 }
165 }
166 }
167 for change in &self.changes {
168 let component = get_component(&mut app_parameters, change)?;
169
170 let new_value = serde_json::Value::from_str(&change.value)
171 .context("could not decode new value as JSON value")?;
172
173 component.insert(change.key.clone(), new_value);
176 }
177 Ok(app_parameters)
178 }
179}
180
181fn get_component<'a>(
182 app_parameters: &'a mut serde_json::Value,
183 change: &EncodedParameter,
184) -> Result<&'a mut serde_json::Map<String, serde_json::Value>, anyhow::Error> {
185 app_parameters
186 .get_mut(&change.component)
187 .ok_or_else(|| {
188 anyhow::anyhow!("component {} not found in app parameters", change.component)
189 })?
190 .as_object_mut()
191 .ok_or_else(|| {
192 anyhow::anyhow!(
193 "expected component {} to be an object in app parameters",
194 change.component
195 )
196 })
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use penumbra_sdk_num::Amount;
203
204 use crate::params::GovernanceParameters;
205
206 const SAMPLE_JSON_PARAMETERS: &'static str = r#"
207 {
208 "chainId": "penumbra-testnet-deimos-6-b295771a",
209 "sctParams": {
210 "epochDuration": "719"
211 },
212 "communityPoolParams": {
213 "communityPoolSpendProposalsEnabled": true
214 },
215 "governanceParams": {
216 "proposalVotingBlocks": "17280",
217 "proposalDepositAmount": {
218 "lo": "10000000"
219 },
220 "proposalValidQuorum": "40/100",
221 "proposalPassThreshold": "50/100",
222 "proposalSlashThreshold": "80/100"
223 },
224 "ibcParams": {
225 "ibcEnabled": true,
226 "inboundIcs20TransfersEnabled": true,
227 "outboundIcs20TransfersEnabled": true
228 },
229 "stakeParams": {
230 "activeValidatorLimit": "80",
231 "baseRewardRate": "30000",
232 "slashingPenaltyMisbehavior": "10000000",
233 "slashingPenaltyDowntime": "10000",
234 "signedBlocksWindowLen": "10000",
235 "missedBlocksMaximum": "9500",
236 "minValidatorStake": {
237 "lo": "1000000"
238 },
239 "unbondingDelay": "2158"
240 },
241 "feeParams": {
242 "fixedGasPrices": {}
243 },
244 "distributionsParams": {
245 "stakingIssuancePerBlock": "1"
246 },
247 "fundingParams": {},
248 "shieldedPoolParams": {
249 "fixedFmdParams": {
250 "asOfBlockHeight": "1"
251 }
252 },
253 "dexParams": {
254 "isEnabled": true,
255 "fixedCandidates": [
256 {
257 "inner": "KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA="
258 },
259 {
260 "inner": "reum7wQmk/owgvGMWMZn/6RFPV24zIKq3W6In/WwZgg="
261 },
262 {
263 "inner": "HW2Eq3UZVSBttoUwUi/MUtE7rr2UU7/UH500byp7OAc="
264 },
265 {
266 "inner": "nwPDkQq3OvLnBwGTD+nmv1Ifb2GEmFCgNHrU++9BsRE="
267 },
268 {
269 "inner": "ypUT1AOtjfwMOKMATACoD9RSvi8jY/YnYGi46CZ/6Q8="
270 },
271 {
272 "inner": "pmpygqUf4DL+z849rGPpudpdK/+FAv8qQ01U2C73kAw="
273 },
274 {
275 "inner": "o2gZdbhCH70Ry+7iBhkSeHC/PB1LZhgkn7LHC2kEhQc="
276 }
277 ],
278 "maxHops": 4,
279 "maxPositionsPerPair": 10
280 },
281 "auctionParams": {}
282 }
283 "#;
284
285 #[test]
286 fn dump_encoded_parameters() {
287 let parameters = serde_json::from_str(SAMPLE_JSON_PARAMETERS).unwrap();
288 dbg!(¶meters);
289 let encoded_parameters = ParameterChange::encode_parameters(parameters);
290 for encoded_parameter in encoded_parameters.changes.iter() {
291 println!("{}", serde_json::to_string(&encoded_parameter).unwrap());
292 }
293 }
294
295 #[test]
296 fn apply_changes_to_gov_params() {
297 let old_parameters_raw: serde_json::Value =
298 serde_json::from_str(SAMPLE_JSON_PARAMETERS).unwrap();
299
300 let changes = ParameterChange {
303 changes: vec![
304 super::EncodedParameter {
305 component: "governanceParams".to_string(),
306 key: "proposalVotingBlocks".to_string(),
307 value: r#""17281""#.to_string(),
308 },
309 super::EncodedParameter {
310 component: "governanceParams".to_string(),
311 key: "proposalDepositAmount".to_string(),
312 value: r#"{"lo":"10000001"}"#.to_string(),
313 },
314 ],
315 preconditions: vec![],
316 };
317 let new_parameters_raw = changes
318 .apply_changes_raw(old_parameters_raw.clone())
319 .unwrap();
320
321 println!(
322 "{}",
323 serde_json::to_string_pretty(&old_parameters_raw).unwrap()
324 );
325 println!(
326 "{}",
327 serde_json::to_string_pretty(&new_parameters_raw).unwrap()
328 );
329
330 let old_gov_parameters_raw = old_parameters_raw["governanceParams"].clone();
331 let new_gov_parameters_raw = new_parameters_raw["governanceParams"].clone();
332
333 let old_gov_parameters: GovernanceParameters =
334 serde_json::value::from_value(old_gov_parameters_raw).unwrap();
335 let new_gov_parameters: GovernanceParameters =
336 serde_json::value::from_value(new_gov_parameters_raw).unwrap();
337
338 dbg!(&old_gov_parameters);
339 dbg!(&new_gov_parameters);
340
341 assert_eq!(old_gov_parameters.proposal_voting_blocks, 17280);
342 assert_eq!(
343 old_gov_parameters.proposal_deposit_amount,
344 Amount::from(10_000_000u64)
345 );
346 assert_eq!(new_gov_parameters.proposal_voting_blocks, 17281);
347 assert_eq!(
348 new_gov_parameters.proposal_deposit_amount,
349 Amount::from(10_000_001u64)
350 );
351 }
352
353 #[test]
354 fn protojson_rules_block_snake_case_parameter_changes() {
355 let old_parameters_raw: serde_json::Value =
356 serde_json::from_str(SAMPLE_JSON_PARAMETERS).unwrap();
357
358 let bad_change_1 = ParameterChange {
359 changes: vec![super::EncodedParameter {
360 component: "governanceParams".to_string(),
361 key: "proposal_voting_blocks".to_string(),
362 value: r#""17281""#.to_string(),
363 }],
364 preconditions: vec![],
365 };
366
367 let new_parameters_raw = bad_change_1
368 .apply_changes_raw(old_parameters_raw.clone())
369 .expect("the bad changes are still a valid json modification");
371
372 let new_gov_parameters_raw = new_parameters_raw["governanceParams"].clone();
373
374 let new_gov_parameters: Result<GovernanceParameters, _> =
377 serde_json::value::from_value(new_gov_parameters_raw);
378
379 dbg!(&new_gov_parameters);
380
381 assert!(new_gov_parameters.is_err());
382 }
383
384 #[test]
385 fn preconditions_prevent_applying_changes() {
386 let old_parameters_raw: serde_json::Value =
387 serde_json::from_str(SAMPLE_JSON_PARAMETERS).unwrap();
388
389 let satisfied_precondition = ParameterChange {
390 preconditions: vec![super::EncodedParameter {
391 component: "governanceParams".to_string(),
392 key: "proposalVotingBlocks".to_string(),
393 value: r#""17280""#.to_string(),
394 }],
395 changes: vec![super::EncodedParameter {
396 component: "governanceParams".to_string(),
397 key: "proposalVotingBlocks".to_string(),
398 value: r#""17281""#.to_string(),
399 }],
400 };
401
402 let unsatisfied_precondition = ParameterChange {
403 preconditions: vec![super::EncodedParameter {
404 component: "governanceParams".to_string(),
405 key: "proposalVotingBlocks".to_string(),
406 value: r#""17281""#.to_string(),
407 }],
408 changes: vec![super::EncodedParameter {
409 component: "governanceParams".to_string(),
410 key: "proposalVotingBlocks".to_string(),
411 value: r#""17282""#.to_string(),
412 }],
413 };
414
415 let satisfied_result = satisfied_precondition.apply_changes_raw(old_parameters_raw.clone());
416 let unsatisfied_result =
417 unsatisfied_precondition.apply_changes_raw(old_parameters_raw.clone());
418
419 assert!(satisfied_result.is_ok());
420 assert!(unsatisfied_result.is_err());
421 }
422}