pd/migrate/
mainnet1.rs

1//! Migration for shipping consensus-breaking IBC changes, fixing
2//! how withdrawals from Penumbra to Noble are handled, and ensures that IBC
3//! error messages from counterparty chains are processed.
4use cnidarium::Snapshot;
5use cnidarium::{StateDelta, Storage};
6use ibc_types::core::channel::{Packet, PortId};
7use ibc_types::transfer::acknowledgement::TokenTransferAcknowledgement;
8use jmt::RootHash;
9use penumbra_sdk_app::app::StateReadExt as _;
10use penumbra_sdk_app::app_version::migrate_app_version;
11use penumbra_sdk_governance::StateWriteExt;
12use penumbra_sdk_ibc::{component::ChannelStateWriteExt as _, IbcRelay};
13use penumbra_sdk_sct::component::clock::EpochManager;
14use penumbra_sdk_sct::component::clock::EpochRead;
15use penumbra_sdk_transaction::{Action, Transaction};
16use std::path::PathBuf;
17use tracing::instrument;
18
19use crate::network::generate::NetworkConfig;
20
21/// The block where proposal #2 passed, enabling outbound ICS20 transfers.
22const ICS20_TRANSFER_START_HEIGHT: u64 = 411616;
23
24/// Find all of the lost transfers inside of a transaction.
25///
26/// In other words, look for relayed packet acknowledgements relating to ICS20 transfers containing an error.
27/// These packets were not correctly handled, being deleted when the ack had an error,
28/// as if the ack were successful.
29fn tx_lost_transfers(transaction: Transaction) -> impl Iterator<Item = Packet> {
30    transaction
31        .transaction_body()
32        .actions
33        .into_iter()
34        .filter_map(move |action| match action {
35            Action::IbcRelay(IbcRelay::Acknowledgement(m)) => {
36                // Make sure we're only looking at ICS20 related packets
37                if m.packet.port_on_b != PortId::transfer() {
38                    return None;
39                }
40                // This shouldn't fail to parse, because the transaction wouldn't have been
41                // included otherwise, but if for some reason it doesn't, ignore it.
42                let transfer: TokenTransferAcknowledgement =
43                    match serde_json::from_slice(m.acknowledgement.as_slice()) {
44                        Err(_) => return None,
45                        Ok(x) => x,
46                    };
47                // If the ack was successful, then that packet was correctly handled, so don't
48                // consider it.
49                match transfer {
50                    TokenTransferAcknowledgement::Success(_) => None,
51                    TokenTransferAcknowledgement::Error(_) => Some(m.packet),
52                }
53            }
54            _ => None,
55        })
56}
57
58/// Retrieve all the packets resulting in a locked transfer because of error acks.
59///
60/// This does so by looking at all transactions, looking for the relayed acknowledgements.
61async fn lost_transfers(state: &StateDelta<Snapshot>) -> anyhow::Result<Vec<Packet>> {
62    let mut out = Vec::new();
63    let end_height = state.get_block_height().await?;
64    // We only need to start from the height where transfers were enabled via governance.
65    for height in ICS20_TRANSFER_START_HEIGHT..=end_height {
66        let transactions = state.transactions_by_height(height).await?.transactions;
67        for tx in transactions.into_iter() {
68            for lost in tx_lost_transfers(tx.try_into()?) {
69                out.push(lost);
70            }
71        }
72    }
73    Ok(out)
74}
75
76/// Replace all the packets that were erroneously removed from the state.
77async fn replace_lost_packets(delta: &mut StateDelta<Snapshot>) -> anyhow::Result<()> {
78    let lost_packets = lost_transfers(delta).await?;
79    for packet in lost_packets {
80        // This will undo what happens in https://github.com/penumbra-zone/penumbra/blob/882a061bd69ce14b01711041bbc0c0ce209e2823/crates/core/component/ibc/src/component/msg_handler/acknowledgement.rs#L99.
81        delta.put_packet_commitment(&packet);
82    }
83    Ok(())
84}
85
86/// Run the full migration, emitting a new genesis event, representing historical state.
87///
88/// This will have the effect of reinserting packets which had acknowledgements containing
89/// errors, and erroneously removed from state, as if the acknowledgements had contained successes.
90#[instrument]
91pub async fn migrate(
92    storage: Storage,
93    pd_home: PathBuf,
94    genesis_start: Option<tendermint::time::Time>,
95) -> anyhow::Result<()> {
96    // Setup:
97    let initial_state = storage.latest_snapshot();
98    let chain_id = initial_state.get_chain_id().await?;
99    let root_hash = initial_state
100        .root_hash()
101        .await
102        .expect("chain state has a root hash");
103    // We obtain the pre-upgrade hash solely to log it as a result.
104    let pre_upgrade_root_hash: RootHash = root_hash.into();
105    let pre_upgrade_height = initial_state
106        .get_block_height()
107        .await
108        .expect("chain state has a block height");
109    let post_upgrade_height = pre_upgrade_height.wrapping_add(1);
110
111    let mut delta = StateDelta::new(initial_state);
112    let (migration_duration, post_upgrade_root_hash) = {
113        let start_time = std::time::SystemTime::now();
114
115        // Note, when this bit of code was added, the upgrade happened months ago,
116        // and the version safeguard mechanism was not in place. However,
117        // adding this will prevent someone running version 0.80.X with the
118        // safeguard patch from accidentally running the migraton again, since they
119        // will already have version 8 written into the state. But, if someone is syncing
120        // up from genesis, then version 0.79 will not have written anything into the safeguard,
121        // and this method will not complain. So, this addition provides a safeguard
122        // for existing nodes, while also not impeding syncing up a node from scratch.
123        migrate_app_version(&mut delta, 8).await?;
124
125        // Reinsert all of the erroneously removed packets
126        replace_lost_packets(&mut delta).await?;
127
128        // Reset the application height and halt flag.
129        delta.ready_to_start();
130        delta.put_block_height(0u64);
131
132        // Finally, commit the changes to the chain state.
133        let post_upgrade_root_hash = storage.commit_in_place(delta).await?;
134        tracing::info!(?post_upgrade_root_hash, "post-migration root hash");
135
136        (
137            start_time.elapsed().expect("start is set"),
138            post_upgrade_root_hash,
139        )
140    };
141    storage.release().await;
142
143    // The migration is complete, now we need to generate a genesis file. To do this, we need
144    // to lookup a validator view from the chain, and specify the post-upgrade app hash and
145    // initial height.
146    let app_state = penumbra_sdk_app::genesis::Content {
147        chain_id,
148        ..Default::default()
149    };
150    let mut genesis = NetworkConfig::make_genesis(app_state.clone()).expect("can make genesis");
151    genesis.app_hash = post_upgrade_root_hash
152        .0
153        .to_vec()
154        .try_into()
155        .expect("infallible conversion");
156
157    genesis.initial_height = post_upgrade_height as i64;
158    genesis.genesis_time = genesis_start.unwrap_or_else(|| {
159        let now = tendermint::time::Time::now();
160        tracing::info!(%now, "no genesis time provided, detecting a testing setup");
161        now
162    });
163    let checkpoint = post_upgrade_root_hash.0.to_vec();
164    let genesis = NetworkConfig::make_checkpoint(genesis, Some(checkpoint));
165    let genesis_json = serde_json::to_string(&genesis).expect("can serialize genesis");
166    tracing::info!("genesis: {}", genesis_json);
167    let genesis_path = pd_home.join("genesis.json");
168    std::fs::write(genesis_path, genesis_json).expect("can write genesis");
169
170    let validator_state_path = pd_home.join("priv_validator_state.json");
171    let fresh_validator_state = crate::network::generate::NetworkValidator::initial_state();
172    std::fs::write(validator_state_path, fresh_validator_state).expect("can write validator state");
173
174    tracing::info!(
175        pre_upgrade_height,
176        post_upgrade_height,
177        ?pre_upgrade_root_hash,
178        ?post_upgrade_root_hash,
179        duration = migration_duration.as_secs(),
180        "successful migration!"
181    );
182
183    Ok(())
184}