penumbra_sdk_governance/
change.rs

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/// An encoded parameter.
8#[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        // TODO: what are max key/value lengths here?
24        // Validation:
25        // - Key has max length of 64 chars
26        if value.key.len() > 64 {
27            anyhow::bail!("key length must be less than or equal to 64 characters");
28        }
29
30        // - Value has max length of 2048 chars
31        if value.value.len() > 2048 {
32            anyhow::bail!("value length must be less than or equal to 2048 characters");
33        }
34
35        // - Component has max length of 64 chars
36        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/// A set of changes to the app parameters.
59#[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    /// Generates a set of encoded parameters for the given object.
111    ///
112    /// This is useful for generating template changes.
113    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    /// Applies a set of changes to the "raw" app parameters.
131    ///
132    /// The app parameters are input as a [`serde_json::Value`] object, so that the
133    /// parameter change code does not need to know about the structure of the entire
134    /// application.
135    ///
136    /// If the changes can be successfully applied, the new app parameters are returned.
137    /// By taking ownership of the input `app_parameters`, we ensure that the caller cannot
138    /// access any partially-mutated app parameters in the event of an error applying one of them.
139    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            // We want to insert into the map to handle the case where the existing value
174            // is missing (e.g., it had a default value and so was not encoded)
175            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!(&parameters);
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        // Make changes to the gov parameters specifically since they're
301        // local to this crate so we can also inspect the decoded parameters.
302        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            // Now we have a json Value with two keys, proposalVotingBlocks and proposal_voting_blocks
370            .expect("the bad changes are still a valid json modification");
371
372        let new_gov_parameters_raw = new_parameters_raw["governanceParams"].clone();
373
374        // We ensure that such a json Value cannot be deserialized because the pbjson
375        // Deserialize impl will treat it as a duplicate key.
376        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}