diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 029cf15e59..76436ebaac 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -7,3 +7,5 @@ | 5 (Testnet 76) | v0.76.x | v0.37.5 | v1 | | 6 (Testnet 77) | v0.77.x | v0.37.5 | v1 | | 7 (Testnet 78) | v0.78.x | v0.37.5 | v1 | +| 7 (Mainnet) | v0.79.x | v0.37.x | v1 | +| 8 (Mainnet) | v0.80.x | v0.37.x | v1 | diff --git a/Cargo.lock b/Cargo.lock index 03f23e2f3c..fff79b3eb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1233,7 +1233,7 @@ dependencies = [ [[package]] name = "cnidarium" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "cnidarium-component" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -1307,7 +1307,7 @@ dependencies = [ [[package]] name = "cometindex" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -1668,7 +1668,7 @@ dependencies = [ [[package]] name = "decaf377-fmd" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "ark-ff", "ark-serialize", @@ -1683,7 +1683,7 @@ dependencies = [ [[package]] name = "decaf377-frost" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "decaf377-ka" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "ark-ff", "decaf377", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "pcli" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4295,7 +4295,7 @@ dependencies = [ [[package]] name = "pclientd" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "assert_cmd", @@ -4347,7 +4347,7 @@ dependencies = [ [[package]] name = "pd" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4500,7 +4500,7 @@ dependencies = [ [[package]] name = "penumbra-app" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4588,7 +4588,7 @@ dependencies = [ [[package]] name = "penumbra-asset" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4628,7 +4628,7 @@ dependencies = [ [[package]] name = "penumbra-auction" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4683,7 +4683,7 @@ dependencies = [ [[package]] name = "penumbra-auto-https" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "axum-server", @@ -4695,7 +4695,7 @@ dependencies = [ [[package]] name = "penumbra-bench" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-bls12-377", @@ -4739,7 +4739,7 @@ dependencies = [ [[package]] name = "penumbra-community-pool" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4771,7 +4771,7 @@ dependencies = [ [[package]] name = "penumbra-compact-block" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4806,7 +4806,7 @@ dependencies = [ [[package]] name = "penumbra-custody" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "argon2", @@ -4842,7 +4842,7 @@ dependencies = [ [[package]] name = "penumbra-dex" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4904,7 +4904,7 @@ dependencies = [ [[package]] name = "penumbra-distributions" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -4922,7 +4922,7 @@ dependencies = [ [[package]] name = "penumbra-eddy" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4940,7 +4940,7 @@ dependencies = [ [[package]] name = "penumbra-fee" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -4967,7 +4967,7 @@ dependencies = [ [[package]] name = "penumbra-funding" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -4990,7 +4990,7 @@ dependencies = [ [[package]] name = "penumbra-governance" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -5044,7 +5044,7 @@ dependencies = [ [[package]] name = "penumbra-ibc" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -5081,7 +5081,7 @@ dependencies = [ [[package]] name = "penumbra-keys" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "aes", "anyhow", @@ -5128,7 +5128,7 @@ dependencies = [ [[package]] name = "penumbra-measure" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "bytesize", @@ -5146,7 +5146,7 @@ dependencies = [ [[package]] name = "penumbra-mock-client" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "cnidarium", @@ -5163,7 +5163,7 @@ dependencies = [ [[package]] name = "penumbra-mock-consensus" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "bytes", @@ -5178,7 +5178,7 @@ dependencies = [ [[package]] name = "penumbra-mock-tendermint-proxy" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "penumbra-mock-consensus", "penumbra-proto", @@ -5190,7 +5190,7 @@ dependencies = [ [[package]] name = "penumbra-num" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -5227,7 +5227,7 @@ dependencies = [ [[package]] name = "penumbra-proof-params" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ec", @@ -5256,7 +5256,7 @@ dependencies = [ [[package]] name = "penumbra-proof-setup" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ec", @@ -5283,7 +5283,7 @@ dependencies = [ [[package]] name = "penumbra-proto" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "async-trait", @@ -5317,7 +5317,7 @@ dependencies = [ [[package]] name = "penumbra-sct" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -5353,7 +5353,7 @@ dependencies = [ [[package]] name = "penumbra-shielded-pool" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -5407,7 +5407,7 @@ dependencies = [ [[package]] name = "penumbra-stake" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -5460,7 +5460,7 @@ dependencies = [ [[package]] name = "penumbra-tct" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "ark-ed-on-bls12-377", "ark-ff", @@ -5492,7 +5492,7 @@ dependencies = [ [[package]] name = "penumbra-tct-property-test" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "futures", @@ -5504,7 +5504,7 @@ dependencies = [ [[package]] name = "penumbra-tct-visualize" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -5534,7 +5534,7 @@ dependencies = [ [[package]] name = "penumbra-tendermint-proxy" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "chrono", @@ -5566,7 +5566,7 @@ dependencies = [ [[package]] name = "penumbra-test-subscriber" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "tracing", "tracing-subscriber 0.3.18", @@ -5574,7 +5574,7 @@ dependencies = [ [[package]] name = "penumbra-tower-trace" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "futures", "hex", @@ -5595,7 +5595,7 @@ dependencies = [ [[package]] name = "penumbra-transaction" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-ff", @@ -5648,7 +5648,7 @@ dependencies = [ [[package]] name = "penumbra-txhash" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "blake2b_simd 1.0.2", @@ -5661,7 +5661,7 @@ dependencies = [ [[package]] name = "penumbra-view" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-std", @@ -5719,7 +5719,7 @@ dependencies = [ [[package]] name = "penumbra-wallet" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-std", @@ -5805,7 +5805,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pindexer" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -7654,7 +7654,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "summonerd" -version = "0.79.3" +version = "0.80.0-alpha.1" dependencies = [ "anyhow", "ark-groth16", diff --git a/Cargo.toml b/Cargo.toml index 11081c95ab..92e69855a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ push = false [workspace.package] authors = ["Penumbra Labs "] edition = "2021" -version = "0.79.3" +version = "0.80.0-alpha.1" repository = "https://github.com/penumbra-zone/penumbra" homepage = "https://penumbra.zone" license = "MIT OR Apache-2.0" diff --git a/crates/bin/pcli/src/command/tx.rs b/crates/bin/pcli/src/command/tx.rs index 4e8fa5f6af..80a54d00fb 100644 --- a/crates/bin/pcli/src/command/tx.rs +++ b/crates/bin/pcli/src/command/tx.rs @@ -253,6 +253,10 @@ pub enum TxCmd { /// The selected fee tier to multiply the fee amount by. #[clap(short, long, default_value_t)] fee_tier: FeeTier, + /// Whether to use a Bech32(non-m) address for the withdrawal. + /// Required for some chains for a successful acknowledgement. + #[clap(long)] + use_compat_address: bool, }, /// Broadcast a saved transaction to the network #[clap(display_order = 1000)] @@ -974,6 +978,7 @@ impl TxCmd { channel, source, fee_tier, + use_compat_address, } => { let destination_chain_address = to; @@ -1087,6 +1092,7 @@ impl TxCmd { return_address: ephemeral_return_address, // TODO: impl From for ChannelId source_channel: ChannelId::from_str(format!("channel-{}", channel).as_ref())?, + use_compat_address: *use_compat_address, }; let plan = Planner::new(OsRng) diff --git a/crates/bin/pd/src/main.rs b/crates/bin/pd/src/main.rs index 223c17d41c..a203d8f175 100644 --- a/crates/bin/pd/src/main.rs +++ b/crates/bin/pd/src/main.rs @@ -12,7 +12,7 @@ use cnidarium::Storage; use metrics_exporter_prometheus::PrometheusBuilder; use pd::{ cli::{NetworkCommand, Opt, RootCommand}, - migrate::Migration::{ReadyToStart, Testnet78}, + migrate::Migration::{Mainnet1, ReadyToStart}, network::{ config::{get_network_dir, parse_tm_address, url_has_necessary_parts}, generate::NetworkConfig, @@ -243,11 +243,11 @@ async fn main() -> anyhow::Result<()> { } => { let network_dir = get_network_dir(network_dir); if network_dir.exists() { - tracing::info!("Removing testnet directory: {}", network_dir.display()); + tracing::info!("removing network directory: {}", network_dir.display()); std::fs::remove_dir_all(network_dir)?; } else { tracing::info!( - "Testnet directory does not exist, so not removing: {}", + "network directory does not exist, so not removing: {}", network_dir.display() ); } @@ -468,7 +468,7 @@ async fn main() -> anyhow::Result<()> { let genesis_start = pd::migrate::last_block_timestamp(pd_home.clone()).await?; tracing::info!(?genesis_start, "last block timestamp"); - Testnet78 + Mainnet1 .migrate(pd_home.clone(), comet_home, Some(genesis_start), force) .instrument(pd_migrate_span) .await diff --git a/crates/bin/pd/src/migrate.rs b/crates/bin/pd/src/migrate.rs index a4d1310fd9..ad6e612a9e 100644 --- a/crates/bin/pd/src/migrate.rs +++ b/crates/bin/pd/src/migrate.rs @@ -4,6 +4,7 @@ //! node operators must coordinate to perform a chain upgrade. //! This module declares how local `pd` state should be altered, if at all, //! in order to be compatible with the network post-chain-upgrade. +mod mainnet1; mod reset_halt_bit; mod simple; mod testnet72; @@ -52,6 +53,9 @@ pub enum Migration { /// - Truncate various user-supplied `String` fields to a maximum length. /// - Populate the DEX NV price idnexes with position data Testnet78, + /// Mainnet-1 migration: + /// - Restore IBC packet commitments for improperly handled withdrawal attempts + Mainnet1, } impl Migration { @@ -87,19 +91,8 @@ impl Migration { Migration::SimpleMigration => { simple::migrate(storage, pd_home.clone(), genesis_start).await? } - - Migration::Testnet78 => { - testnet78::migrate(storage, pd_home.clone(), genesis_start).await?; - // Testnet78 migration munges CometBFT config TOML directly - if let Some(comet_home) = comet_home.clone() { - // Don't bail out on error: the TendermintConfig struct doesn't understand some - // valid config files, so we should just warn, not abort the overall migration. - let _ = testnet78::update_cometbft_mempool_settings(comet_home).map_err(|e| { - tracing::warn!(%e, "failed to update 'max_txs_bytes' value in cometbft config, set it manually before restarting") - }); - } else { - tracing::warn!("cometbft home not specified, update 'max_txs_bytes' value manually before restarting"); - } + Migration::Mainnet1 => { + mainnet1::migrate(storage, pd_home.clone(), genesis_start).await?; } // We keep historical migrations around for now, this will help inform an abstracted // design. Feel free to remove it if it's causing you trouble. diff --git a/crates/bin/pd/src/migrate/mainnet1.rs b/crates/bin/pd/src/migrate/mainnet1.rs new file mode 100644 index 0000000000..2045c7f14c --- /dev/null +++ b/crates/bin/pd/src/migrate/mainnet1.rs @@ -0,0 +1,173 @@ +//! 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_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 { + 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) -> anyhow::Result> { + 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) -> 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, +) -> 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(); + + // 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(()) +} diff --git a/crates/bin/pd/src/migrate/testnet78.rs b/crates/bin/pd/src/migrate/testnet78.rs index 0af5ff8746..3d6bc886ce 100644 --- a/crates/bin/pd/src/migrate/testnet78.rs +++ b/crates/bin/pd/src/migrate/testnet78.rs @@ -1,4 +1,5 @@ //! Contains functions related to the migration script of Testnet78. +#![allow(dead_code)] use anyhow::Context; use cnidarium::StateRead; use cnidarium::{Snapshot, StateDelta, StateWrite, Storage}; diff --git a/crates/core/app/src/action_handler/transaction.rs b/crates/core/app/src/action_handler/transaction.rs index 5bb585dfdc..3806c86b12 100644 --- a/crates/core/app/src/action_handler/transaction.rs +++ b/crates/core/app/src/action_handler/transaction.rs @@ -19,8 +19,8 @@ use self::stateful::{ claimed_anchor_is_valid, fmd_parameters_valid, tx_parameters_historical_check, }; use stateless::{ - check_memo_exists_if_outputs_absent_if_not, num_clues_equal_to_num_outputs, - valid_binding_signature, + check_memo_exists_if_outputs_absent_if_not, check_non_empty_transaction, + num_clues_equal_to_num_outputs, valid_binding_signature, }; #[async_trait] @@ -46,6 +46,8 @@ impl AppActionHandler for Transaction { // Other checks probably too cheap to be worth splitting into tasks. num_clues_equal_to_num_outputs(self)?; check_memo_exists_if_outputs_absent_if_not(self)?; + // This check ensures that transactions contain at least one action. + check_non_empty_transaction(self)?; let context = self.context(); diff --git a/crates/core/app/src/action_handler/transaction/stateless.rs b/crates/core/app/src/action_handler/transaction/stateless.rs index 1df8c90e98..6ba541753d 100644 --- a/crates/core/app/src/action_handler/transaction/stateless.rs +++ b/crates/core/app/src/action_handler/transaction/stateless.rs @@ -48,3 +48,14 @@ pub fn check_memo_exists_if_outputs_absent_if_not(tx: &Transaction) -> anyhow::R )) } } + +pub fn check_non_empty_transaction(tx: &Transaction) -> anyhow::Result<()> { + let num_actions = tx.actions().count(); + if num_actions > 0 { + Ok(()) + } else { + Err(anyhow::anyhow!( + "consensus rule violated: transaction must have more than 0 actions" + )) + } +} diff --git a/crates/core/app/src/lib.rs b/crates/core/app/src/lib.rs index f72fcef032..d915e9eaee 100644 --- a/crates/core/app/src/lib.rs +++ b/crates/core/app/src/lib.rs @@ -22,7 +22,7 @@ use once_cell::sync::Lazy; /// Representation of the Penumbra application version. Notably, this is distinct /// from the crate version(s). This number should only ever be incremented. -pub const APP_VERSION: u64 = 7; +pub const APP_VERSION: u64 = 8; pub static SUBSTORE_PREFIXES: Lazy> = Lazy::new(|| { vec![ diff --git a/crates/core/component/ibc/src/component.rs b/crates/core/component/ibc/src/component.rs index 00a7a9f4c2..556df345cc 100644 --- a/crates/core/component/ibc/src/component.rs +++ b/crates/core/component/ibc/src/component.rs @@ -25,6 +25,7 @@ use msg_handler::MsgHandler; pub use self::metrics::register_metrics; pub use channel::StateReadExt as ChannelStateReadExt; +pub use channel::StateWriteExt as ChannelStateWriteExt; pub use client::StateReadExt as ClientStateReadExt; pub use client::StateWriteExt as ClientStateWriteExt; pub use connection::StateReadExt as ConnectionStateReadExt; diff --git a/crates/core/component/ibc/src/component/app_handler.rs b/crates/core/component/ibc/src/component/app_handler.rs index 40db8959b5..453ab37a72 100644 --- a/crates/core/component/ibc/src/component/app_handler.rs +++ b/crates/core/component/ibc/src/component/app_handler.rs @@ -55,7 +55,10 @@ pub trait AppHandlerExecute: Send + Sync { async fn recv_packet_execute(state: S, msg: &MsgRecvPacket) -> Result<()>; async fn timeout_packet_execute(state: S, msg: &MsgTimeout) -> Result<()>; - async fn acknowledge_packet_execute(state: S, msg: &MsgAcknowledgement); + async fn acknowledge_packet_execute( + state: S, + msg: &MsgAcknowledgement, + ) -> Result<()>; } pub trait AppHandler: AppHandlerCheck + AppHandlerExecute {} diff --git a/crates/core/component/ibc/src/component/client.rs b/crates/core/component/ibc/src/component/client.rs index 4e22db8565..b6f6ffd327 100644 --- a/crates/core/component/ibc/src/component/client.rs +++ b/crates/core/component/ibc/src/component/client.rs @@ -577,7 +577,12 @@ mod tests { async fn timeout_packet_execute(_state: S, _msg: &MsgTimeout) -> Result<()> { Ok(()) } - async fn acknowledge_packet_execute(_state: S, _msg: &MsgAcknowledgement) {} + async fn acknowledge_packet_execute( + _state: S, + _msg: &MsgAcknowledgement, + ) -> Result<()> { + Ok(()) + } } #[async_trait] diff --git a/crates/core/component/ibc/src/component/msg_handler/acknowledgement.rs b/crates/core/component/ibc/src/component/msg_handler/acknowledgement.rs index f43c25f6ec..6a02b5c3fe 100644 --- a/crates/core/component/ibc/src/component/msg_handler/acknowledgement.rs +++ b/crates/core/component/ibc/src/component/msg_handler/acknowledgement.rs @@ -45,6 +45,16 @@ impl MsgHandler for MsgAcknowledgement { if channel.counterparty().port_id().ne(&self.packet.port_on_b) { anyhow::bail!("packet destination port does not match channel"); } + if channel + .counterparty() + .channel_id() + .ok_or_else(|| { + anyhow::anyhow!("missing channel id for counterparty channel in acknowledgement") + })? + .ne(&self.packet.chan_on_b) + { + anyhow::bail!("packet destination channel does not match channel"); + } let connection = state .get_connection(&channel.connection_hops[0]) @@ -119,7 +129,7 @@ impl MsgHandler for MsgAcknowledgement { let transfer = PortId::transfer(); if self.packet.port_on_b == transfer { - AH::acknowledge_packet_execute(state, self).await; + AH::acknowledge_packet_execute(state, self).await?; } else { anyhow::bail!("invalid port id"); } diff --git a/crates/core/component/shielded-pool/Cargo.toml b/crates/core/component/shielded-pool/Cargo.toml index 79462e77f1..d1a9b72f81 100644 --- a/crates/core/component/shielded-pool/Cargo.toml +++ b/crates/core/component/shielded-pool/Cargo.toml @@ -51,7 +51,7 @@ decaf377-rdsa = {workspace = true} futures = {workspace = true} hex = {workspace = true} ibc-proto = {workspace = true, default-features = false} -ibc-types = {workspace = true, default-features = false} +ibc-types = {workspace = true, features = ["with_serde"], default-features = false} im = {workspace = true} metrics = {workspace = true} once_cell = {workspace = true} diff --git a/crates/core/component/shielded-pool/src/component/transfer.rs b/crates/core/component/shielded-pool/src/component/transfer.rs index 9b6a594532..702c0c6065 100644 --- a/crates/core/component/shielded-pool/src/component/transfer.rs +++ b/crates/core/component/shielded-pool/src/component/transfer.rs @@ -7,6 +7,7 @@ use crate::{ use anyhow::{Context, Result}; use async_trait::async_trait; use cnidarium::{StateRead, StateWrite}; +use ibc_types::core::channel::Packet; use ibc_types::{ core::channel::{ channel::Order as ChannelOrder, @@ -265,7 +266,7 @@ async fn recv_transfer_packet_inner( // NOTE: spec says proto but this is actually JSON according to the ibc-go implementation let packet_data: FungibleTokenPacketData = serde_json::from_slice(msg.packet.data.as_slice()) .with_context(|| "failed to decode FTPD packet")?; - let denom: asset::Metadata = packet_data + let packet_denom: asset::Metadata = packet_data .denom .as_str() .try_into() @@ -279,7 +280,12 @@ async fn recv_transfer_packet_inner( // NOTE: here we assume we are chain A. // 2. check if we are the source chain for the denom. - if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom, false) { + if is_source( + &msg.packet.port_on_a, + &msg.packet.chan_on_a, + &packet_denom, + false, + ) { // mint tokens to receiver in the amount of packet_data.amount in the denom of denom (with // the source removed, since we're the source) let prefix = format!( @@ -288,7 +294,7 @@ async fn recv_transfer_packet_inner( source_chan = msg.packet.chan_on_a ); - let unprefixed_denom: asset::Metadata = packet_data + let denom: asset::Metadata = packet_data .denom .strip_prefix(&prefix) .context(format!( @@ -300,7 +306,7 @@ async fn recv_transfer_packet_inner( let value: Value = Value { amount: receiver_amount, - asset_id: unprefixed_denom.id(), + asset_id: denom.id(), }; // assume AppHandlerCheck has already been called, and we have enough balance to mint tokens to receiver @@ -308,7 +314,7 @@ async fn recv_transfer_packet_inner( let value_balance: Amount = state .get(&state_key::ics20_value_balance::by_asset_id( &msg.packet.chan_on_b, - &unprefixed_denom.id(), + &denom.id(), )) .await? .unwrap_or_else(Amount::zero); @@ -333,14 +339,6 @@ async fn recv_transfer_packet_inner( .context("unable to mint note when receiving ics20 transfer packet")?; // update the value balance - let value_balance: Amount = state - .get(&state_key::ics20_value_balance::by_asset_id( - &msg.packet.chan_on_b, - &unprefixed_denom.id(), - )) - .await? - .unwrap_or_else(Amount::zero); - // note: this arithmetic was checked above, but we do it again anyway. let new_value_balance = value_balance .checked_sub(&receiver_amount) @@ -406,8 +404,8 @@ async fn recv_transfer_packet_inner( } // see: https://github.com/cosmos/ibc/blob/8326e26e7e1188b95c32481ff00348a705b23700/spec/app/ics-020-fungible-token-transfer/README.md?plain=1#L297 -async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> Result<()> { - let packet_data: FungibleTokenPacketData = serde_json::from_slice(msg.packet.data.as_slice())?; +async fn refund_tokens(mut state: S, packet: &Packet) -> Result<()> { + let packet_data: FungibleTokenPacketData = serde_json::from_slice(packet.data.as_slice())?; let denom: asset::Metadata = packet_data // CRITICAL: verify that this denom is validated in upstream timeout handling .denom .as_str() @@ -430,11 +428,11 @@ async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> asset_id: denom.id(), }; - if is_source(&msg.packet.port_on_a, &msg.packet.chan_on_a, &denom, true) { + if is_source(&packet.port_on_a, &packet.chan_on_a, &denom, true) { // sender was source chain, unescrow tokens back to sender let value_balance: Amount = state .get(&state_key::ics20_value_balance::by_asset_id( - &msg.packet.chan_on_a, + &packet.chan_on_a, &denom.id(), )) .await? @@ -449,8 +447,8 @@ async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> value, &receiver, CommitmentSource::Ics20Transfer { - packet_seq: msg.packet.sequence.0, - channel_id: msg.packet.chan_on_a.0.clone(), + packet_seq: packet.sequence.0, + channel_id: packet.chan_on_a.0.clone(), sender: packet_data.sender.clone(), }, ) @@ -458,26 +456,18 @@ async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> .context("couldn't mint note in timeout_packet_inner")?; // update the value balance - let value_balance: Amount = state - .get(&state_key::ics20_value_balance::by_asset_id( - &msg.packet.chan_on_a, - &denom.id(), - )) - .await? - .unwrap_or_else(Amount::zero); - // note: this arithmetic was checked above, but we do it again anyway. let new_value_balance = value_balance .checked_sub(&amount) .context("underflow in ics20 timeout packet value balance subtraction")?; state.put( - state_key::ics20_value_balance::by_asset_id(&msg.packet.chan_on_a, &denom.id()), + state_key::ics20_value_balance::by_asset_id(&packet.chan_on_a, &denom.id()), new_value_balance, ); } else { let value_balance: Amount = state .get(&state_key::ics20_value_balance::by_asset_id( - &msg.packet.chan_on_a, + &packet.chan_on_a, &denom.id(), )) .await? @@ -489,8 +479,8 @@ async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> &receiver, // NOTE: should this be Ics20TransferTimeout? CommitmentSource::Ics20Transfer { - packet_seq: msg.packet.sequence.0, - channel_id: msg.packet.chan_on_a.0.clone(), + packet_seq: packet.sequence.0, + channel_id: packet.chan_on_a.0.clone(), sender: packet_data.sender.clone(), }, ) @@ -499,7 +489,7 @@ async fn timeout_packet_inner(mut state: S, msg: &MsgTimeout) -> let new_value_balance = value_balance.saturating_add(&value.amount); state.put( - state_key::ics20_value_balance::by_asset_id(&msg.packet.chan_on_a, &denom.id()), + state_key::ics20_value_balance::by_asset_id(&packet.chan_on_a, &denom.id()), new_value_balance, ); } @@ -540,14 +530,30 @@ impl AppHandlerExecute for Ics20Transfer { async fn timeout_packet_execute(mut state: S, msg: &MsgTimeout) -> Result<()> { // timeouts may fail due to counterparty chains sending transfers of u128-1 - timeout_packet_inner(&mut state, msg) + refund_tokens(&mut state, &msg.packet) .await .context("able to timeout packet")?; Ok(()) } - async fn acknowledge_packet_execute(_state: S, _msg: &MsgAcknowledgement) {} + async fn acknowledge_packet_execute( + mut state: S, + msg: &MsgAcknowledgement, + ) -> Result<()> { + let ack: TokenTransferAcknowledgement = + serde_json::from_slice(msg.acknowledgement.as_slice())?; + if !ack.is_successful() { + // in the case where a counterparty chain acknowledges a packet with an error, + // for example due to a middleware processing issue or other behavior, + // the funds should be unescrowed back to the packet sender. + refund_tokens(&mut state, &msg.packet) + .await + .context("unable to refund packet acknowledgement")?; + } + + Ok(()) + } } impl AppHandler for Ics20Transfer {} diff --git a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs index 60256c6a74..c452d5d703 100644 --- a/crates/core/component/shielded-pool/src/ics20_withdrawal.rs +++ b/crates/core/component/shielded-pool/src/ics20_withdrawal.rs @@ -37,6 +37,10 @@ pub struct Ics20Withdrawal { pub timeout_time: u64, // the source channel used for the withdrawal pub source_channel: ChannelId, + + // Whether to use a "compat" (bech32, non-m) address for the return address in the withdrawal, + // for compatability with chains that expect to be able to parse the return address as bech32. + pub use_compat_address: bool, } #[cfg(feature = "component")] @@ -113,6 +117,7 @@ impl From for pb::Ics20Withdrawal { timeout_height: Some(w.timeout_height.into()), timeout_time: w.timeout_time, source_channel: w.source_channel.to_string(), + use_compat_address: w.use_compat_address, } } } @@ -142,17 +147,23 @@ impl TryFrom for Ics20Withdrawal { .try_into()?, timeout_time: s.timeout_time, source_channel: ChannelId::from_str(&s.source_channel)?, + use_compat_address: s.use_compat_address, }) } } impl From for pb::FungibleTokenPacketData { fn from(w: Ics20Withdrawal) -> Self { + let return_address = match w.use_compat_address { + true => w.return_address.compat_encoding(), + false => w.return_address.to_string(), + }; + pb::FungibleTokenPacketData { amount: w.value().amount.to_string(), denom: w.denom.to_string(), receiver: w.destination_chain_address, - sender: w.return_address.to_string(), + sender: return_address, memo: "".to_string(), } } diff --git a/crates/proto/src/gen/penumbra.core.component.ibc.v1.rs b/crates/proto/src/gen/penumbra.core.component.ibc.v1.rs index df993e3cf0..320e259a47 100644 --- a/crates/proto/src/gen/penumbra.core.component.ibc.v1.rs +++ b/crates/proto/src/gen/penumbra.core.component.ibc.v1.rs @@ -69,6 +69,10 @@ pub struct Ics20Withdrawal { /// The source channel used for the withdrawal #[prost(string, tag = "7")] pub source_channel: ::prost::alloc::string::String, + /// Whether to use a "compat" (bech32, non-m) address for the return address in the withdrawal, + /// for compatability with chains that expect to be able to parse the return address as bech32. + #[prost(bool, tag = "8")] + pub use_compat_address: bool, } impl ::prost::Name for Ics20Withdrawal { const NAME: &'static str = "Ics20Withdrawal"; diff --git a/crates/proto/src/gen/penumbra.core.component.ibc.v1.serde.rs b/crates/proto/src/gen/penumbra.core.component.ibc.v1.serde.rs index a12442b052..b4bcbf3624 100644 --- a/crates/proto/src/gen/penumbra.core.component.ibc.v1.serde.rs +++ b/crates/proto/src/gen/penumbra.core.component.ibc.v1.serde.rs @@ -1054,6 +1054,9 @@ impl serde::Serialize for Ics20Withdrawal { if !self.source_channel.is_empty() { len += 1; } + if self.use_compat_address { + len += 1; + } let mut struct_ser = serializer.serialize_struct("penumbra.core.component.ibc.v1.Ics20Withdrawal", len)?; if let Some(v) = self.amount.as_ref() { struct_ser.serialize_field("amount", v)?; @@ -1077,6 +1080,9 @@ impl serde::Serialize for Ics20Withdrawal { if !self.source_channel.is_empty() { struct_ser.serialize_field("sourceChannel", &self.source_channel)?; } + if self.use_compat_address { + struct_ser.serialize_field("useCompatAddress", &self.use_compat_address)?; + } struct_ser.end() } } @@ -1099,6 +1105,8 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { "timeoutTime", "source_channel", "sourceChannel", + "use_compat_address", + "useCompatAddress", ]; #[allow(clippy::enum_variant_names)] @@ -1110,6 +1118,7 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { TimeoutHeight, TimeoutTime, SourceChannel, + UseCompatAddress, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -1139,6 +1148,7 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { "timeoutHeight" | "timeout_height" => Ok(GeneratedField::TimeoutHeight), "timeoutTime" | "timeout_time" => Ok(GeneratedField::TimeoutTime), "sourceChannel" | "source_channel" => Ok(GeneratedField::SourceChannel), + "useCompatAddress" | "use_compat_address" => Ok(GeneratedField::UseCompatAddress), _ => Ok(GeneratedField::__SkipField__), } } @@ -1165,6 +1175,7 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { let mut timeout_height__ = None; let mut timeout_time__ = None; let mut source_channel__ = None; + let mut use_compat_address__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Amount => { @@ -1211,6 +1222,12 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { } source_channel__ = Some(map_.next_value()?); } + GeneratedField::UseCompatAddress => { + if use_compat_address__.is_some() { + return Err(serde::de::Error::duplicate_field("useCompatAddress")); + } + use_compat_address__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -1224,6 +1241,7 @@ impl<'de> serde::Deserialize<'de> for Ics20Withdrawal { timeout_height: timeout_height__, timeout_time: timeout_time__.unwrap_or_default(), source_channel: source_channel__.unwrap_or_default(), + use_compat_address: use_compat_address__.unwrap_or_default(), }) } } diff --git a/crates/proto/src/gen/proto_descriptor.bin.no_lfs b/crates/proto/src/gen/proto_descriptor.bin.no_lfs index 0a63961d63..294b190c53 100644 Binary files a/crates/proto/src/gen/proto_descriptor.bin.no_lfs and b/crates/proto/src/gen/proto_descriptor.bin.no_lfs differ diff --git a/proto/penumbra/penumbra/core/component/ibc/v1/ibc.proto b/proto/penumbra/penumbra/core/component/ibc/v1/ibc.proto index 2506eba219..733ca5a3af 100644 --- a/proto/penumbra/penumbra/core/component/ibc/v1/ibc.proto +++ b/proto/penumbra/penumbra/core/component/ibc/v1/ibc.proto @@ -50,6 +50,10 @@ message Ics20Withdrawal { // The source channel used for the withdrawal string source_channel = 7; + + // Whether to use a "compat" (bech32, non-m) address for the return address in the withdrawal, + // for compatability with chains that expect to be able to parse the return address as bech32. + bool use_compat_address = 8; } message ClientData {