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