pd/migrate/
testnet78.rs

1//! Contains functions related to the migration script of Testnet78.
2#![allow(dead_code)]
3use anyhow::Context;
4use cnidarium::StateRead;
5use cnidarium::{Snapshot, StateDelta, StateWrite, Storage};
6use futures::TryStreamExt as _;
7use futures::{pin_mut, StreamExt};
8use jmt::RootHash;
9use penumbra_sdk_app::app::StateReadExt as _;
10use penumbra_sdk_dex::component::{PositionManager, StateReadExt, StateWriteExt};
11use penumbra_sdk_dex::lp::position;
12use penumbra_sdk_dex::lp::position::Position;
13use penumbra_sdk_governance::proposal_state::State as ProposalState;
14use penumbra_sdk_governance::Proposal;
15use penumbra_sdk_governance::StateReadExt as _;
16use penumbra_sdk_governance::StateWriteExt as _;
17use penumbra_sdk_proto::core::component::governance::v1 as pb_governance;
18use penumbra_sdk_proto::{StateReadProto, StateWriteProto};
19use penumbra_sdk_sct::component::clock::EpochManager;
20use penumbra_sdk_sct::component::clock::EpochRead;
21use penumbra_sdk_stake::validator::Validator;
22use std::fs::OpenOptions;
23use std::io::Write;
24use std::path::PathBuf;
25use tendermint_config::TendermintConfig;
26use tracing::instrument;
27
28use crate::network::generate::NetworkConfig;
29
30/// Run the full migration, given an export path and a start time for genesis.
31///
32/// Menu:
33/// - Truncate various user-supplied `String` fields to a maximum length.
34///   * Validators:
35///    - `name` (140 bytes)
36///    - `website` (70 bytes)
37///    - `description` (280 bytes)
38///   * Governance Parameter Changes:
39///    - `key` (64 bytes)
40///    - `value` (2048 bytes)
41///    - `component` (64 bytes)
42///   * Governance Proposals:
43///    - `title` (80 bytes)
44///    - `description` (10,000 bytes)
45///   * Governance Proposal Withdrawals:
46///    - `reason` (1024 bytes)
47///   * Governance IBC Client Freeze Proposals:
48///    - `client_id` (128 bytes)
49///   * Governance IBC Client Unfreeze Proposals:
50///    - `client_id` (128 bytes)
51///   * Governance Signaling Proposals:
52///    - `commit hash` (255 bytes)
53/// - Close and re-open all *open* positions so that they are re-indexed.
54/// - Update DEX parameters
55#[instrument]
56pub async fn migrate(
57    storage: Storage,
58    pd_home: PathBuf,
59    genesis_start: Option<tendermint::time::Time>,
60) -> anyhow::Result<()> {
61    /* `Migration::prepare`: collect basic migration data, logging, initialize alt-storage if needed */
62    let initial_state = storage.latest_snapshot();
63
64    let chain_id = initial_state.get_chain_id().await?;
65    let root_hash = initial_state
66        .root_hash()
67        .await
68        .expect("chain state has a root hash");
69
70    let pre_upgrade_height = initial_state
71        .get_block_height()
72        .await
73        .expect("chain state has a block height");
74    let post_upgrade_height = pre_upgrade_height.wrapping_add(1);
75
76    let pre_upgrade_root_hash: RootHash = root_hash.into();
77
78    /* `Migration::migrate`: reach into the chain state and perform an offline state transition */
79    let mut delta = StateDelta::new(initial_state);
80
81    let (migration_duration, post_upgrade_root_hash) = {
82        let start_time = std::time::SystemTime::now();
83
84        // Migrate empty (deleted) packet commitments to be deleted at the tree level
85        delete_empty_deleted_packet_commitments(&mut delta).await?;
86
87        // Adjust the length of `Validator` fields.
88        truncate_validator_fields(&mut delta).await?;
89
90        // Adjust the length of governance proposal fields.
91        truncate_proposal_fields(&mut delta).await?;
92
93        // Adjust the length of governance proposal outcome fields.
94        truncate_proposal_outcome_fields(&mut delta).await?;
95
96        // Write the new dex parameters (with the execution budget field) to the state.
97        update_dex_params(&mut delta).await?;
98
99        // Re-index all open positions.
100        reindex_dex_positions(&mut delta).await?;
101
102        // Reset the application height and halt flag.
103        delta.ready_to_start();
104        delta.put_block_height(0u64);
105
106        // Finally, commit the changes to the chain state.
107        let post_upgrade_root_hash = storage.commit_in_place(delta).await?;
108        tracing::info!(?post_upgrade_root_hash, "post-migration root hash");
109
110        (
111            start_time.elapsed().expect("start is set"),
112            post_upgrade_root_hash,
113        )
114    };
115
116    tracing::info!("migration completed, generating genesis and signing state...");
117
118    /* `Migration::complete`: the state transition has been performed, we prepare the checkpointed genesis and signing state */
119    let app_state = penumbra_sdk_app::genesis::Content {
120        chain_id,
121        ..Default::default()
122    };
123    let mut genesis = NetworkConfig::make_genesis(app_state.clone()).expect("can make genesis");
124    genesis.app_hash = post_upgrade_root_hash
125        .0
126        .to_vec()
127        .try_into()
128        .expect("infaillible conversion");
129
130    genesis.initial_height = post_upgrade_height as i64;
131    genesis.genesis_time = genesis_start.unwrap_or_else(|| {
132        let now = tendermint::time::Time::now();
133        tracing::info!(%now, "no genesis time provided, detecting a testing setup");
134        now
135    });
136
137    tracing::info!("generating checkpointed genesis");
138    let checkpoint = post_upgrade_root_hash.0.to_vec();
139    let genesis = NetworkConfig::make_checkpoint(genesis, Some(checkpoint));
140
141    tracing::info!("writing genesis to disk");
142    let genesis_json = serde_json::to_string(&genesis).expect("can serialize genesis");
143    tracing::info!("genesis: {}", genesis_json);
144    let genesis_path = pd_home.join("genesis.json");
145    std::fs::write(genesis_path, genesis_json).expect("can write genesis");
146
147    tracing::info!("updating signing state");
148    let validator_state_path = pd_home.join("priv_validator_state.json");
149    let fresh_validator_state = crate::network::generate::NetworkValidator::initial_state();
150    std::fs::write(validator_state_path, fresh_validator_state).expect("can write validator state");
151
152    tracing::info!(
153        pre_upgrade_height,
154        post_upgrade_height,
155        ?pre_upgrade_root_hash,
156        ?post_upgrade_root_hash,
157        duration = migration_duration.as_secs(),
158        "migration fully complete"
159    );
160
161    Ok(())
162}
163
164async fn delete_empty_deleted_packet_commitments(
165    delta: &mut StateDelta<Snapshot>,
166) -> anyhow::Result<()> {
167    let prefix_key = "ibc-data/commitments/";
168    let stream = delta.prefix_raw(&prefix_key);
169
170    pin_mut!(stream);
171
172    while let Some(entry) = stream.next().await {
173        let (key, value) = entry?;
174        if value.is_empty() {
175            delta.delete(key);
176        }
177    }
178
179    Ok(())
180}
181
182/// Write the updated dex parameters to the chain state.
183async fn update_dex_params(delta: &mut StateDelta<Snapshot>) -> anyhow::Result<()> {
184    let mut dex_params = delta
185        .get_dex_params()
186        .await
187        .expect("chain state is initialized");
188    dex_params.max_execution_budget = 64;
189    delta.put_dex_params(dex_params);
190
191    Ok(())
192}
193
194/// Reindex opened liquidity positions to augment the price indexes.
195async fn reindex_dex_positions(delta: &mut StateDelta<Snapshot>) -> anyhow::Result<()> {
196    tracing::info!("running dex re-indexing migration");
197    let prefix_key_lp = penumbra_sdk_dex::state_key::all_positions();
198    let stream_all_lp = delta.prefix::<Position>(&prefix_key_lp);
199    let stream_open_lp = stream_all_lp.filter_map(|entry| async {
200        match entry {
201            Ok((_, lp)) if lp.state == position::State::Opened => Some(lp),
202            _ => None,
203        }
204    });
205    pin_mut!(stream_open_lp);
206
207    while let Some(lp) = stream_open_lp.next().await {
208        // Re-hash the position, since the key is a bech32 string.
209        let id = lp.id();
210        // Close the position, adjusting all its index entries.
211        delta.close_position_by_id(&id).await?;
212        // Erase the position from the state, so that we circumvent the `update_position` guard.
213        delta.delete(penumbra_sdk_dex::state_key::position_by_id(&id));
214        // Open a position with the adjusted indexing logic.
215        delta.open_position(lp).await?;
216    }
217    tracing::info!("completed dex migration");
218    Ok(())
219}
220
221///   * Validators:
222///    - `name` (140 bytes)
223///    - `website` (70 bytes)
224///    - `description` (280 bytes)
225async fn truncate_validator_fields(delta: &mut StateDelta<Snapshot>) -> anyhow::Result<()> {
226    tracing::info!("truncating validator fields");
227    let key_prefix_validators = penumbra_sdk_stake::state_key::validators::definitions::prefix();
228    let all_validators = delta
229        .prefix_proto::<penumbra_sdk_proto::core::component::stake::v1::Validator>(
230            &key_prefix_validators,
231        )
232        .try_collect::<Vec<(
233            String,
234            penumbra_sdk_proto::core::component::stake::v1::Validator,
235        )>>()
236        .await?;
237
238    for (key, mut validator) in all_validators {
239        validator.name = truncate(&validator.name, 140).to_string();
240        validator.website = truncate(&validator.website, 70).to_string();
241        validator.description = truncate(&validator.description, 280).to_string();
242
243        // Ensure the validator can be serialized back to the domain type:
244        let validator: Validator = validator.try_into()?;
245        tracing::info!("put key {:?}", key);
246        delta.put(key, validator);
247    }
248
249    Ok(())
250}
251
252///   * Governance Proposals:
253///    - `title` (80 bytes)
254///    - `description` (10,000 bytes)
255///   * Governance Parameter Changes:
256///    - `key` (64 bytes)
257///    - `value` (2048 bytes)
258///    - `component` (64 bytes)
259///   * Governance IBC Client Freeze Proposals:
260///    - `client_id` (128 bytes)
261///   * Governance IBC Client Unfreeze Proposals:
262///    - `client_id` (128 bytes)
263///   * Governance Signaling Proposals:
264///    - `commit hash` (255 bytes)
265async fn truncate_proposal_fields(delta: &mut StateDelta<Snapshot>) -> anyhow::Result<()> {
266    tracing::info!("truncating proposal fields");
267    let next_proposal_id: u64 = delta.next_proposal_id().await?;
268
269    // Range each proposal and truncate the fields.
270    for proposal_id in 0..next_proposal_id {
271        tracing::info!("truncating proposal: {}", proposal_id);
272        let proposal = delta
273            .get_proto::<pb_governance::Proposal>(
274                &penumbra_sdk_governance::state_key::proposal_definition(proposal_id),
275            )
276            .await?;
277
278        if proposal.is_none() {
279            break;
280        }
281
282        let mut proposal = proposal.unwrap();
283
284        proposal.title = truncate(&proposal.title, 80).to_string();
285        proposal.description = truncate(&proposal.description, 10_000).to_string();
286
287        // Depending on the proposal type, we may need to truncate additional fields.
288        match proposal
289            .payload
290            .clone()
291            .expect("proposal payload always set")
292        {
293            pb_governance::proposal::Payload::Signaling(commit) => {
294                proposal.payload = Some(pb_governance::proposal::Payload::Signaling(
295                    pb_governance::proposal::Signaling {
296                        commit: truncate(&commit.commit, 255).to_string(),
297                    },
298                ));
299            }
300            pb_governance::proposal::Payload::Emergency(_halt_chain) => {}
301            pb_governance::proposal::Payload::ParameterChange(mut param_change) => {
302                for (i, mut change) in param_change.changes.clone().into_iter().enumerate() {
303                    let key = truncate(&change.key, 64).to_string();
304                    let value = truncate(&change.value, 2048).to_string();
305                    let component = truncate(&change.component, 64).to_string();
306
307                    change.key = key;
308                    change.value = value;
309                    change.component = component;
310
311                    param_change.changes[i] = change;
312                }
313
314                for (i, mut change) in param_change.preconditions.clone().into_iter().enumerate() {
315                    let key = truncate(&change.key, 64).to_string();
316                    let value = truncate(&change.value, 2048).to_string();
317                    let component = truncate(&change.component, 64).to_string();
318
319                    change.key = key;
320                    change.value = value;
321                    change.component = component;
322
323                    param_change.preconditions[i] = change;
324                }
325
326                proposal.payload = Some(pb_governance::proposal::Payload::ParameterChange(
327                    param_change,
328                ));
329            }
330            pb_governance::proposal::Payload::CommunityPoolSpend(_transaction_plan) => {}
331            pb_governance::proposal::Payload::UpgradePlan(_height) => {}
332            pb_governance::proposal::Payload::FreezeIbcClient(client_id) => {
333                proposal.payload = Some(pb_governance::proposal::Payload::FreezeIbcClient(
334                    pb_governance::proposal::FreezeIbcClient {
335                        client_id: truncate(&client_id.client_id, 128).to_string(),
336                    },
337                ));
338            }
339            pb_governance::proposal::Payload::UnfreezeIbcClient(client_id) => {
340                proposal.payload = Some(pb_governance::proposal::Payload::UnfreezeIbcClient(
341                    pb_governance::proposal::UnfreezeIbcClient {
342                        client_id: truncate(&client_id.client_id, 128).to_string(),
343                    },
344                ));
345            }
346        };
347
348        // Store the truncated proposal data
349        tracing::info!(
350            "put key {:?}",
351            penumbra_sdk_governance::state_key::proposal_definition(proposal_id)
352        );
353        // Ensure the proposal can be serialized back to the domain type:
354        let proposal: Proposal = proposal.try_into()?;
355        delta.put(
356            penumbra_sdk_governance::state_key::proposal_definition(proposal_id),
357            proposal,
358        );
359    }
360
361    Ok(())
362}
363
364///   * Governance Proposal Withdrawals:
365///    - `reason` (1024 bytes)
366async fn truncate_proposal_outcome_fields(delta: &mut StateDelta<Snapshot>) -> anyhow::Result<()> {
367    tracing::info!("truncating proposal outcome fields");
368    let next_proposal_id: u64 = delta.next_proposal_id().await?;
369
370    // Range each proposal outcome and truncate the fields.
371    for proposal_id in 0..next_proposal_id {
372        tracing::info!("truncating proposal outcomes: {}", proposal_id);
373        let proposal_state = delta
374            .get_proto::<pb_governance::ProposalState>(
375                &penumbra_sdk_governance::state_key::proposal_state(proposal_id),
376            )
377            .await?;
378
379        if proposal_state.is_none() {
380            break;
381        }
382
383        let mut proposal_state = proposal_state.unwrap();
384
385        match proposal_state
386            .state
387            .clone()
388            .expect("proposal state always set")
389        {
390            pb_governance::proposal_state::State::Withdrawn(reason) => {
391                proposal_state.state = Some(pb_governance::proposal_state::State::Withdrawn(
392                    pb_governance::proposal_state::Withdrawn {
393                        reason: truncate(&reason.reason, 1024).to_string(),
394                    },
395                ));
396            }
397            pb_governance::proposal_state::State::Voting(_) => {}
398            pb_governance::proposal_state::State::Finished(ref outcome) => match outcome
399                .outcome
400                .clone()
401                .expect("proposal outcome always set")
402                .outcome
403                .expect("proposal outcome always set")
404            {
405                pb_governance::proposal_outcome::Outcome::Passed(_) => {}
406                pb_governance::proposal_outcome::Outcome::Failed(withdrawn) => {
407                    match withdrawn.withdrawn {
408                        None => {
409                            // Withdrawn::No
410                        }
411                        Some(pb_governance::proposal_outcome::Withdrawn { reason }) => {
412                            // Withdrawn::WithReason
413                            proposal_state.state =
414                                Some(pb_governance::proposal_state::State::Finished(
415                                    pb_governance::proposal_state::Finished {
416                                        outcome: Some(pb_governance::ProposalOutcome{
417                                            outcome: Some(pb_governance::proposal_outcome::Outcome::Failed(
418                                                pb_governance::proposal_outcome::Failed {
419                                                    withdrawn:
420                                                        Some(pb_governance::proposal_outcome::Withdrawn {
421                                                            reason: truncate(&reason, 1024)
422                                                                .to_string(),
423                                                        }),
424                                                },
425                                            )),
426                                        }),
427                                    },
428                                ));
429                        }
430                    }
431                }
432                pb_governance::proposal_outcome::Outcome::Slashed(withdrawn) => {
433                    match withdrawn.withdrawn {
434                        None => {
435                            // Withdrawn::No
436                        }
437                        Some(pb_governance::proposal_outcome::Withdrawn { reason }) => {
438                            // Withdrawn::WithReason
439                            proposal_state.state = Some(pb_governance::proposal_state::State::Finished(
440                                    pb_governance::proposal_state::Finished {
441                                        outcome: Some(pb_governance::ProposalOutcome{
442                                            outcome: Some(pb_governance::proposal_outcome::Outcome::Slashed(
443                                                pb_governance::proposal_outcome::Slashed {
444                                                    withdrawn:
445                                                        Some(pb_governance::proposal_outcome::Withdrawn {
446                                                            reason: truncate(&reason, 1024)
447                                                                .to_string(),
448                                                        }),
449                                                },
450                                            )),
451                                        }),
452                                    },
453                                ));
454                        }
455                    }
456                }
457            },
458            pb_governance::proposal_state::State::Claimed(ref outcome) => match outcome
459                .outcome
460                .clone()
461                .expect("outcome is set")
462                .outcome
463                .expect("outcome is set")
464            {
465                pb_governance::proposal_outcome::Outcome::Passed(_) => {}
466                pb_governance::proposal_outcome::Outcome::Failed(withdrawn) => {
467                    match withdrawn.withdrawn {
468                        None => {
469                            // Withdrawn::No
470                        }
471                        Some(pb_governance::proposal_outcome::Withdrawn { reason }) => {
472                            // Withdrawn::WithReason
473                            proposal_state.state = Some(pb_governance::proposal_state::State::Claimed(
474                                    pb_governance::proposal_state::Claimed {
475                                        outcome: Some(pb_governance::ProposalOutcome{
476                                            outcome: Some(pb_governance::proposal_outcome::Outcome::Failed(
477                                                pb_governance::proposal_outcome::Failed{
478                                                    withdrawn:
479                                                        Some(pb_governance::proposal_outcome::Withdrawn {
480                                                            reason: truncate(&reason, 1024)
481                                                                .to_string(),
482                                                        }),
483                                                },
484                                            )),
485                                        }),
486                                    },
487                                ));
488                        }
489                    }
490                }
491                pb_governance::proposal_outcome::Outcome::Slashed(withdrawn) => {
492                    match withdrawn.withdrawn {
493                        None => {
494                            // Withdrawn::No
495                        }
496                        Some(pb_governance::proposal_outcome::Withdrawn { reason }) => {
497                            proposal_state.state = Some(pb_governance::proposal_state::State::Claimed(
498                                    pb_governance::proposal_state::Claimed {
499                                        outcome: Some(pb_governance::ProposalOutcome{
500                                            outcome: Some(pb_governance::proposal_outcome::Outcome::Slashed(
501                                                pb_governance::proposal_outcome::Slashed{
502                                                    withdrawn:
503                                                        Some(pb_governance::proposal_outcome::Withdrawn {
504                                                            reason: truncate(&reason, 1024)
505                                                                .to_string(),
506                                                        }),
507                                                },
508                                            )),
509                                        }),
510                                    },
511                                ));
512                        }
513                    }
514                }
515            },
516        }
517
518        // Store the truncated proposal state data
519        tracing::info!(
520            "put key {:?}",
521            penumbra_sdk_governance::state_key::proposal_state(proposal_id)
522        );
523        let proposal_state: ProposalState = proposal_state.try_into()?;
524        delta.put(
525            penumbra_sdk_governance::state_key::proposal_state(proposal_id),
526            proposal_state,
527        );
528    }
529    Ok(())
530}
531
532// Since the limits are based on `String::len`, which returns
533// the number of bytes, we need to truncate the UTF-8 strings at the
534// correct byte boundaries.
535//
536// This can be simplified once https://github.com/rust-lang/rust/issues/93743
537// is stabilized.
538#[inline]
539pub fn floor_char_boundary(s: &str, index: usize) -> usize {
540    if index >= s.len() {
541        s.len()
542    } else {
543        let lower_bound = index.saturating_sub(3);
544        let new_index = s.as_bytes()[lower_bound..=index]
545            .iter()
546            .rposition(|b| is_utf8_char_boundary(*b));
547
548        // SAFETY: we know that the character boundary will be within four bytes
549        lower_bound + new_index.unwrap()
550    }
551}
552
553#[inline]
554pub(crate) const fn is_utf8_char_boundary(b: u8) -> bool {
555    // This is bit magic equivalent to: b < 128 || b >= 192
556    (b as i8) >= -0x40
557}
558
559// Truncates a utf-8 string to the nearest character boundary,
560// not exceeding max_bytes
561fn truncate(s: &str, max_bytes: usize) -> &str {
562    let closest = floor_char_boundary(s, max_bytes);
563
564    &s[..closest]
565}
566
567/// Edit the node's CometBFT config file to set two values:
568///
569///   * mempool.max_tx_bytes
570///   * mempool.max_txs_bytes
571///
572/// These values will affect consensus, but the config settings are specified for CometBFT
573/// specifically.
574#[instrument]
575pub(crate) fn update_cometbft_mempool_settings(cometbft_home: PathBuf) -> anyhow::Result<()> {
576    let cometbft_config_path = cometbft_home.join("config").join("config.toml");
577    tracing::debug!(cometbft_config_path = %cometbft_config_path.display(), "opening cometbft config file");
578    let mut cometbft_config = TendermintConfig::load_toml_file(&cometbft_config_path)
579        .context("failed to load pre-migration cometbft config file")?;
580    // The new values were updated in GH4594 & GH4632.
581    let desired_max_txs_bytes = 10485760;
582    let desired_max_tx_bytes = 30720;
583    // Set new value
584    cometbft_config.mempool.max_txs_bytes = desired_max_txs_bytes;
585    cometbft_config.mempool.max_tx_bytes = desired_max_tx_bytes;
586    // Overwrite file
587    let mut fh = OpenOptions::new()
588        .create(false)
589        .write(true)
590        .truncate(true)
591        .open(cometbft_config_path.clone())
592        .context("failed to open cometbft config file for writing")?;
593    fh.write_all(toml::to_string(&cometbft_config)?.as_bytes())
594        .context("failed to write updated cometbft config to toml file")?;
595    Ok(())
596}
597
598mod tests {
599    #[test]
600    fn truncation() {
601        use super::truncate;
602        let s = "Hello, world!";
603
604        assert_eq!(truncate(s, 5), "Hello");
605
606        let s = "โค๏ธ๐Ÿงก๐Ÿ’›๐Ÿ’š๐Ÿ’™๐Ÿ’œ";
607        assert_eq!(s.len(), 26);
608        assert_eq!("โค".len(), 3);
609
610        assert_eq!(truncate(s, 2), "");
611        assert_eq!(truncate(s, 3), "โค");
612        assert_eq!(truncate(s, 4), "โค");
613    }
614}