pd/migrate/
testnet72.rs

1//! Contains functions related to the migration script of Testnet72
2#![allow(dead_code)]
3use anyhow;
4use cnidarium::{Snapshot, StateDelta, StateRead, StateWrite, Storage};
5use futures::StreamExt as _;
6use jmt::RootHash;
7use penumbra_sdk_app::app::StateReadExt as _;
8use penumbra_sdk_app::SUBSTORE_PREFIXES;
9use penumbra_sdk_proto::core::component::sct::v1::query_service_server::QueryService;
10use penumbra_sdk_proto::penumbra::core::component as pb;
11use penumbra_sdk_proto::StateWriteProto;
12use penumbra_sdk_sct::component::clock::{EpochManager, EpochRead};
13use penumbra_sdk_sct::component::rpc::Server as SctServer;
14use penumbra_sdk_tct::Position;
15use prost::Message;
16use std::path::PathBuf;
17use std::sync::Arc;
18use tonic::IntoRequest;
19
20use crate::network::generate::NetworkConfig;
21
22/// The context holding various query services we need to help perform the migration.
23#[derive(Clone)]
24struct Context {
25    sct_server: Arc<SctServer>,
26}
27
28impl Context {
29    /// Create a new context from the state storage.
30    fn new(storage: Storage) -> Self {
31        Self {
32            sct_server: Arc::new(SctServer::new(storage)),
33        }
34    }
35
36    /// Use storage to lookup the index of an epoch based on its starting heights
37    async fn epoch_height_to_index(&self, epoch_starting_height: u64) -> anyhow::Result<u64> {
38        Ok(self
39            .sct_server
40            .epoch_by_height(
41                pb::sct::v1::EpochByHeightRequest {
42                    height: epoch_starting_height,
43                }
44                .into_request(),
45            )
46            .await?
47            .into_inner()
48            .epoch
49            .expect(&format!(
50                "epoch at height {} should be present",
51                epoch_starting_height
52            ))
53            .index)
54    }
55
56    /// Translate the protobuf for a BSOD by populating the correct data and emptying the
57    /// deprecated field.
58    #[allow(deprecated)]
59    async fn translate_bsod(
60        &self,
61        bsod: pb::dex::v1::BatchSwapOutputData,
62    ) -> anyhow::Result<pb::dex::v1::BatchSwapOutputData> {
63        let sct_position_prefix: u64 = {
64            let epoch = self
65                .epoch_height_to_index(bsod.epoch_starting_height)
66                .await?;
67            Position::from((
68                u16::try_from(epoch).expect("epoch should fit in 16 bits"),
69                u16::try_from(bsod.height - bsod.epoch_starting_height)
70                    .expect("block index should fit in 16 bits"),
71                0,
72            ))
73            .into()
74        };
75        Ok(pb::dex::v1::BatchSwapOutputData {
76            sct_position_prefix,
77            epoch_starting_height: Default::default(),
78            ..bsod
79        })
80    }
81
82    async fn translate_compact_block(
83        &self,
84        compact_block: pb::compact_block::v1::CompactBlock,
85    ) -> anyhow::Result<pb::compact_block::v1::CompactBlock> {
86        let mut swap_outputs = Vec::with_capacity(compact_block.swap_outputs.len());
87        for bsod in compact_block.swap_outputs {
88            swap_outputs.push(self.translate_bsod(bsod).await?);
89        }
90        Ok(pb::compact_block::v1::CompactBlock {
91            swap_outputs,
92            ..compact_block
93        })
94    }
95}
96
97/// Translate all of the BSODs inside dex storage to the new format.
98async fn translate_dex_storage(
99    ctx: Context,
100    delta: &mut StateDelta<Snapshot>,
101) -> anyhow::Result<()> {
102    let mut stream = delta.prefix_raw("dex/output/");
103    while let Some(r) = stream.next().await {
104        let (key, bsod_bytes) = r?;
105        let bsod = pb::dex::v1::BatchSwapOutputData::decode(bsod_bytes.as_slice())?;
106        let bsod = ctx.translate_bsod(bsod).await?;
107        delta.put_proto(key, bsod);
108    }
109    Ok(())
110}
111
112/// Translate all of the compact block storage to hold the new BSOD data inside the compact blocks.
113async fn translate_compact_block_storage(
114    ctx: Context,
115    delta: &mut StateDelta<Snapshot>,
116) -> anyhow::Result<()> {
117    let mut stream = delta.nonverifiable_prefix_raw("compactblock/".as_bytes());
118    while let Some(r) = stream.next().await {
119        let (key, compactblock_bytes) = r?;
120        let block = pb::compact_block::v1::CompactBlock::decode(compactblock_bytes.as_slice())?;
121        let block = ctx.translate_compact_block(block).await?;
122        delta.nonverifiable_put_raw(key, block.encode_to_vec());
123    }
124    Ok(())
125}
126
127/// Run the full migration, given an export path and a start time for genesis.
128pub async fn migrate(
129    storage: Storage,
130    path_to_export: PathBuf,
131    genesis_start: Option<tendermint::time::Time>,
132) -> anyhow::Result<()> {
133    let export_state = storage.latest_snapshot();
134    let root_hash = export_state.root_hash().await.expect("can get root hash");
135    let pre_upgrade_root_hash: RootHash = root_hash.into();
136    let pre_upgrade_height = export_state
137        .get_block_height()
138        .await
139        .expect("can get block height");
140    let post_upgrade_height = pre_upgrade_height.wrapping_add(1);
141
142    let mut delta = StateDelta::new(export_state);
143    let (migration_duration, post_upgrade_root_hash) = {
144        let start_time = std::time::SystemTime::now();
145        let ctx = Context::new(storage.clone());
146
147        // Translate inside dex storage.
148        translate_dex_storage(ctx.clone(), &mut delta).await?;
149        // Translate inside compact block storage.
150        translate_compact_block_storage(ctx.clone(), &mut delta).await?;
151
152        delta.put_block_height(0u64);
153        let post_upgrade_root_hash = storage.commit_in_place(delta).await?;
154        tracing::info!(?post_upgrade_root_hash, "post-upgrade root hash");
155
156        (
157            start_time.elapsed().expect("start time not set"),
158            post_upgrade_root_hash,
159        )
160    };
161
162    storage.release().await;
163
164    let rocksdb_dir = path_to_export.join("rocksdb");
165    let storage = Storage::load(rocksdb_dir, SUBSTORE_PREFIXES.to_vec()).await?;
166    let migrated_state = storage.latest_snapshot();
167
168    // The migration is complete, now we need to generate a genesis file. To do this, we need
169    // to lookup a validator view from the chain, and specify the post-upgrade app hash and
170    // initial height.
171    let chain_id = migrated_state.get_chain_id().await?;
172    let app_state = penumbra_sdk_app::genesis::Content {
173        chain_id,
174        ..Default::default()
175    };
176    let mut genesis = NetworkConfig::make_genesis(app_state.clone()).expect("can make genesis");
177    genesis.app_hash = post_upgrade_root_hash
178        .0
179        .to_vec()
180        .try_into()
181        .expect("infaillible conversion");
182    genesis.initial_height = post_upgrade_height as i64;
183    genesis.genesis_time = genesis_start.unwrap_or_else(|| {
184        let now = tendermint::time::Time::now();
185        tracing::info!(%now, "no genesis time provided, detecting a testing setup");
186        now
187    });
188    let checkpoint = post_upgrade_root_hash.0.to_vec();
189    let genesis = NetworkConfig::make_checkpoint(genesis, Some(checkpoint));
190
191    let genesis_json = serde_json::to_string(&genesis).expect("can serialize genesis");
192    tracing::info!("genesis: {}", genesis_json);
193    let genesis_path = path_to_export.join("genesis.json");
194    std::fs::write(genesis_path, genesis_json).expect("can write genesis");
195
196    let validator_state_path = path_to_export.join("priv_validator_state.json");
197    let fresh_validator_state = crate::network::generate::NetworkValidator::initial_state();
198    std::fs::write(validator_state_path, fresh_validator_state).expect("can write validator state");
199
200    tracing::info!(
201        pre_upgrade_height,
202        post_upgrade_height,
203        ?pre_upgrade_root_hash,
204        ?post_upgrade_root_hash,
205        duration = migration_duration.as_secs(),
206        "successful migration!"
207    );
208
209    Ok(())
210}