diff --git a/.changelog/unreleased/bug-fixes/3438-fix-ibc-shielding-transfer.md b/.changelog/unreleased/bug-fixes/3438-fix-ibc-shielding-transfer.md new file mode 100644 index 0000000000..021403bf57 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/3438-fix-ibc-shielding-transfer.md @@ -0,0 +1,2 @@ +- Fix IBC shielding transfer for the receiver not to be replaced by a malicious + relayer ([\#3438](https://github.com/anoma/namada/issues/3438)) \ No newline at end of file diff --git a/.github/workflows/scripts/hermes.txt b/.github/workflows/scripts/hermes.txt index f885ad8923..7669eaf98a 100644 --- a/.github/workflows/scripts/hermes.txt +++ b/.github/workflows/scripts/hermes.txt @@ -1 +1 @@ -1.8.2-namada-beta12-rc6 +1.9.0-namada-beta13-rc2 diff --git a/Cargo.lock b/Cargo.lock index 68a9f64c39..e6233ba27e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5040,6 +5040,7 @@ name = "namada_ibc" version = "0.39.0" dependencies = [ "borsh 1.2.1", + "data-encoding", "ibc", "ibc-derive", "ibc-testkit", @@ -5062,6 +5063,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.9.9", + "smooth-operator", "thiserror", "tracing", ] diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index 3d9063004b..2ddab6dfb4 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -298,6 +298,7 @@ pub mod cmds { // Actions .subcommand(SignTx::def().display_order(6)) .subcommand(ShieldedSync::def().display_order(6)) + .subcommand(GenIbcShieldingTransfer::def().display_order(6)) // Utils .subcommand(ClientUtils::def().display_order(7)) } @@ -386,6 +387,8 @@ pub mod cmds { Self::parse_with_ctx(matches, AddToEthBridgePool); let sign_tx = Self::parse_with_ctx(matches, SignTx); let shielded_sync = Self::parse_with_ctx(matches, ShieldedSync); + let gen_ibc_shielding = + Self::parse_with_ctx(matches, GenIbcShieldingTransfer); let utils = SubCmd::parse(matches).map(Self::WithoutContext); tx_custom .or(tx_transparent_transfer) @@ -440,6 +443,7 @@ pub mod cmds { .or(query_account) .or(sign_tx) .or(shielded_sync) + .or(gen_ibc_shielding) .or(utils) } } @@ -530,6 +534,7 @@ pub mod cmds { QueryRewards(QueryRewards), SignTx(SignTx), ShieldedSync(ShieldedSync), + GenIbcShieldingTransfer(GenIbcShieldingTransfer), } #[allow(clippy::large_enum_variant)] @@ -2310,6 +2315,29 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct GenIbcShieldingTransfer( + pub args::GenIbcShieldingTransfer, + ); + + impl SubCmd for GenIbcShieldingTransfer { + const CMD: &'static str = "ibc-gen-shielding"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + GenIbcShieldingTransfer(args::GenIbcShieldingTransfer::parse( + matches, + )) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Generate shielding transfer for IBC.") + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct EpochSleep(pub args::Query); @@ -3373,7 +3401,6 @@ pub mod args { pub const RAW_PUBLIC_KEY_HASH_OPT: ArgOpt = RAW_PUBLIC_KEY_HASH.opt(); pub const RECEIVER: Arg = arg("receiver"); - pub const REFUND: ArgFlag = flag("refund"); pub const REFUND_TARGET: ArgOpt = arg_opt("refund-target"); pub const RELAYER: Arg
= arg("relayer"); @@ -6616,19 +6643,19 @@ pub mod args { } } - impl CliToSdk> - for GenIbcShieldedTransfer + impl CliToSdk> + for GenIbcShieldingTransfer { type Error = std::convert::Infallible; fn to_sdk( self, ctx: &mut Context, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { let query = self.query.to_sdk(ctx)?; let chain_ctx = ctx.borrow_chain_or_exit(); - Ok(GenIbcShieldedTransfer:: { + Ok(GenIbcShieldingTransfer:: { query, output_folder: self.output_folder, target: chain_ctx.get(&self.target), @@ -6636,12 +6663,11 @@ pub mod args { amount: self.amount, port_id: self.port_id, channel_id: self.channel_id, - refund: self.refund, }) } } - impl Args for GenIbcShieldedTransfer { + impl Args for GenIbcShieldingTransfer { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let output_folder = OUTPUT_FOLDER_PATH.parse(matches); @@ -6650,7 +6676,6 @@ pub mod args { let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); let port_id = PORT_ID.parse(matches); let channel_id = CHANNEL_ID.parse(matches); - let refund = REFUND.parse(matches); Self { query, output_folder, @@ -6659,7 +6684,6 @@ pub mod args { amount, port_id, channel_id, - refund, } } @@ -6681,9 +6705,6 @@ pub mod args { .arg(CHANNEL_ID.def().help(wrap!( "The channel ID via which the token is received." ))) - .arg(REFUND.def().help(wrap!( - "Generate the shielded transfer for refunding." - ))) } } diff --git a/crates/apps_lib/src/cli/client.rs b/crates/apps_lib/src/cli/client.rs index 8a9f60223c..24ed0981ff 100644 --- a/crates/apps_lib/src/cli/client.rs +++ b/crates/apps_lib/src/cli/client.rs @@ -370,6 +370,20 @@ impl CliApi { ) .await?; } + Sub::GenIbcShieldingTransfer(GenIbcShieldingTransfer( + args, + )) => { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let ledger_address = + chain_ctx.get(&args.query.ledger_address); + let client = client.unwrap_or_else(|| { + C::from_tendermint_address(&ledger_address) + }); + client.wait_until_node_is_synced(&io).await?; + let args = args.to_sdk(&mut ctx)?; + let namada = ctx.to_sdk(client, io); + tx::gen_ibc_shielding_transfer(&namada, args).await?; + } #[cfg(feature = "namada-eth-bridge")] Sub::AddToEthBridgePool(args) => { let args = args.0; diff --git a/crates/apps_lib/src/cli/context.rs b/crates/apps_lib/src/cli/context.rs index db94b7ad9a..58b5862960 100644 --- a/crates/apps_lib/src/cli/context.rs +++ b/crates/apps_lib/src/cli/context.rs @@ -11,9 +11,8 @@ use namada::core::chain::ChainId; use namada::core::ethereum_events::EthAddress; use namada::core::key::*; use namada::core::masp::*; -use namada::ibc::{is_ibc_denom, is_nft_trace}; +use namada::ibc::trace::{ibc_token, is_ibc_denom, is_nft_trace}; use namada::io::Io; -use namada::ledger::ibc::storage::ibc_token; use namada_sdk::masp::fs::FsShieldedUtils; use namada_sdk::masp::ShieldedContext; use namada_sdk::wallet::Wallet; diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index f15d30426e..083076a28f 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1,4 +1,5 @@ use std::fs::File; +use std::io::Write; use borsh::BorshDeserialize; use borsh_ext::BorshSerializeExt; @@ -11,6 +12,7 @@ use namada::core::key::*; use namada::governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, }; +use namada::ibc::convert_masp_tx_to_ibc_memo; use namada::io::Io; use namada::state::EPOCH_SWITCH_BLOCKS_DELAY; use namada::tx::data::compute_inner_tx_hash; @@ -1395,3 +1397,32 @@ pub async fn submit_tx( ) -> Result { tx::submit_tx(namada, to_broadcast).await } + +/// Generate MASP transaction and output it +pub async fn gen_ibc_shielding_transfer( + context: &impl Namada, + args: args::GenIbcShieldingTransfer, +) -> Result<(), error::Error> { + if let Some(masp_tx) = + tx::gen_ibc_shielding_transfer(context, args.clone()).await? + { + let tx_id = masp_tx.txid().to_string(); + let filename = format!("ibc_masp_tx_{}.memo", tx_id); + let output_path = match &args.output_folder { + Some(path) => path.join(filename), + None => filename.into(), + }; + let mut out = File::create(&output_path) + .expect("Creating a new file for IBC MASP transaction failed."); + let bytes = convert_masp_tx_to_ibc_memo(&masp_tx); + out.write_all(bytes.as_bytes()) + .expect("Writing IBC MASP transaction file failed."); + println!( + "Output IBC shielding transfer for {tx_id} to {}", + output_path.to_string_lossy() + ); + } else { + eprintln!("No shielded transfer for this IBC transfer.") + } + Ok(()) +} diff --git a/crates/ibc/Cargo.toml b/crates/ibc/Cargo.toml index 6822e0b40f..e73160983f 100644 --- a/crates/ibc/Cargo.toml +++ b/crates/ibc/Cargo.toml @@ -32,6 +32,7 @@ namada_storage = { path = "../storage" } namada_token = { path = "../token" } borsh.workspace = true +data-encoding.workspace = true konst.workspace = true linkme = {workspace = true, optional = true} ibc.workspace = true @@ -45,6 +46,7 @@ prost.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true +smooth-operator.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/ibc/src/context/common.rs b/crates/ibc/src/context/common.rs index 14876f5cb2..46a6c1cbba 100644 --- a/crates/ibc/src/context/common.rs +++ b/crates/ibc/src/context/common.rs @@ -9,7 +9,6 @@ use ibc::core::channel::types::commitment::{ }; use ibc::core::channel::types::error::{ChannelError, PacketError}; use ibc::core::channel::types::packet::Receipt; -use ibc::core::channel::types::timeout::TimeoutHeight; use ibc::core::client::types::error::ClientError; use ibc::core::client::types::Height; use ibc::core::connection::types::error::ConnectionError; @@ -23,14 +22,14 @@ use ibc::primitives::Timestamp; use namada_core::address::Address; use namada_core::storage::{BlockHeight, Key}; use namada_core::tendermint::Time as TmTime; +use namada_storage::{Error as StorageError, StorageRead}; use namada_token::storage_key::balance_key; use namada_token::Amount; use prost::Message; -use sha2::Digest; use super::client::{AnyClientState, AnyConsensusState}; use super::storage::IbcStorageContext; -use crate::{storage, NftClass, NftMetadata}; +use crate::{storage, trace, NftClass, NftMetadata}; /// Result of IBC common function call pub type Result = std::result::Result; @@ -408,7 +407,7 @@ pub trait IbcCommonContext: IbcStorageContext { channel_id: &ChannelId, ) -> Result { let key = storage::next_sequence_send_key(port_id, channel_id); - self.read_sequence(&key) + read_sequence(self, &key).map_err(ContextError::from) } /// Store the NextSequenceSend @@ -429,7 +428,7 @@ pub trait IbcCommonContext: IbcStorageContext { channel_id: &ChannelId, ) -> Result { let key = storage::next_sequence_recv_key(port_id, channel_id); - self.read_sequence(&key) + read_sequence(self, &key).map_err(ContextError::from) } /// Store the NextSequenceRecv @@ -450,7 +449,7 @@ pub trait IbcCommonContext: IbcStorageContext { channel_id: &ChannelId, ) -> Result { let key = storage::next_sequence_ack_key(port_id, channel_id); - self.read_sequence(&key) + read_sequence(self, &key).map_err(ContextError::from) } /// Store the NextSequenceAck @@ -464,57 +463,12 @@ pub trait IbcCommonContext: IbcStorageContext { self.store_sequence(&key, seq) } - /// Read a sequence - fn read_sequence(&self, key: &Key) -> Result { - match self.read_bytes(key)? { - Some(value) => { - let value: [u8; 8] = - value.try_into().map_err(|_| ChannelError::Other { - description: format!( - "The sequence value wasn't u64: Key {key}", - ), - })?; - Ok(u64::from_be_bytes(value).into()) - } - // when the sequence has never been used, returns the initial value - None => Ok(1.into()), - } - } - /// Store the sequence fn store_sequence(&mut self, key: &Key, sequence: Sequence) -> Result<()> { let bytes = u64::from(sequence).to_be_bytes().to_vec(); self.write_bytes(key, bytes).map_err(ContextError::from) } - /// Calculate the hash - fn hash(value: &[u8]) -> Vec { - sha2::Sha256::digest(value).to_vec() - } - - /// Calculate the packet commitment - fn compute_packet_commitment( - packet_data: &[u8], - timeout_height: &TimeoutHeight, - timeout_timestamp: &Timestamp, - ) -> PacketCommitment { - let mut hash_input = - timeout_timestamp.nanoseconds().to_be_bytes().to_vec(); - - let revision_number = - timeout_height.commitment_revision_number().to_be_bytes(); - hash_input.append(&mut revision_number.to_vec()); - - let revision_height = - timeout_height.commitment_revision_height().to_be_bytes(); - hash_input.append(&mut revision_height.to_vec()); - - let packet_data_hash = Self::hash(packet_data); - hash_input.append(&mut packet_data_hash.to_vec()); - - Self::hash(&hash_input).into() - } - /// Get the packet commitment fn packet_commitment( &self, @@ -703,7 +657,7 @@ pub trait IbcCommonContext: IbcStorageContext { token_id: &TokenId, owner: &Address, ) -> Result { - let ibc_token = storage::ibc_token_for_nft(class_id, token_id); + let ibc_token = trace::ibc_token_for_nft(class_id, token_id); let balance_key = balance_key(&ibc_token, owner); let amount = self.read::(&balance_key)?; Ok(amount == Some(Amount::from_u64(1))) @@ -753,3 +707,22 @@ pub trait IbcCommonContext: IbcStorageContext { self.write(&key, amount).map_err(ContextError::from) } } + +/// Read and decode the IBC sequence +pub fn read_sequence( + storage: &S, + key: &Key, +) -> std::result::Result { + match storage.read_bytes(key)? { + Some(value) => { + let value: [u8; 8] = value.try_into().map_err(|_| { + StorageError::new_alloc(format!( + "The sequence value wasn't u64: Key {key}", + )) + })?; + Ok(u64::from_be_bytes(value).into()) + } + // when the sequence has never been used, returns the initial value + None => Ok(1.into()), + } +} diff --git a/crates/ibc/src/context/nft_transfer.rs b/crates/ibc/src/context/nft_transfer.rs index 85d9aad44e..e7317ba94e 100644 --- a/crates/ibc/src/context/nft_transfer.rs +++ b/crates/ibc/src/context/nft_transfer.rs @@ -17,7 +17,7 @@ use namada_core::address::Address; use namada_core::token::Amount; use super::common::IbcCommonContext; -use crate::{storage, NftClass, NftMetadata, IBC_ESCROW_ADDRESS}; +use crate::{trace, NftClass, NftMetadata, IBC_ESCROW_ADDRESS}; /// NFT transfer context to handle tokens #[derive(Debug)] @@ -95,8 +95,8 @@ where class_id: &PrefixedClassId, token_id: &TokenId, ) -> Result<(), NftTransferError> { - let ibc_trace = format!("{class_id}/{token_id}"); - let trace_hash = storage::calc_hash(&ibc_trace); + let ibc_trace = trace::ibc_trace_for_nft(class_id, token_id); + let trace_hash = trace::calc_hash(&ibc_trace); self.inner .borrow_mut() @@ -241,7 +241,7 @@ where class_id: &PrefixedClassId, token_id: &TokenId, ) -> Option { - Some(storage::calc_hash(format!("{class_id}/{token_id}"))) + Some(trace::calc_hash(format!("{class_id}/{token_id}"))) } /// Returns the NFT @@ -300,7 +300,7 @@ where token_id: &TokenId, _memo: &Memo, ) -> Result<(), NftTransferError> { - let ibc_token = storage::ibc_token_for_nft(class_id, token_id); + let ibc_token = trace::ibc_token_for_nft(class_id, token_id); self.add_withdraw(&ibc_token)?; @@ -324,7 +324,7 @@ where class_id: &PrefixedClassId, token_id: &TokenId, ) -> Result<(), NftTransferError> { - let ibc_token = storage::ibc_token_for_nft(class_id, token_id); + let ibc_token = trace::ibc_token_for_nft(class_id, token_id); self.add_deposit(&ibc_token)?; @@ -347,7 +347,7 @@ where token_uri: Option<&TokenUri>, token_data: Option<&TokenData>, ) -> Result<(), NftTransferError> { - let ibc_token = storage::ibc_token_for_nft(class_id, token_id); + let ibc_token = trace::ibc_token_for_nft(class_id, token_id); // create or update the metadata let metadata = NftMetadata { @@ -378,7 +378,7 @@ where token_id: &TokenId, _memo: &Memo, ) -> Result<(), NftTransferError> { - let ibc_token = storage::ibc_token_for_nft(class_id, token_id); + let ibc_token = trace::ibc_token_for_nft(class_id, token_id); self.update_mint_amount(&ibc_token, false)?; self.add_withdraw(&ibc_token)?; diff --git a/crates/ibc/src/context/token_transfer.rs b/crates/ibc/src/context/token_transfer.rs index ca1128ea68..7d0ebee13e 100644 --- a/crates/ibc/src/context/token_transfer.rs +++ b/crates/ibc/src/context/token_transfer.rs @@ -17,7 +17,7 @@ use namada_core::uint::Uint; use namada_token::Amount; use super::common::IbcCommonContext; -use crate::{storage, IBC_ESCROW_ADDRESS}; +use crate::{trace, IBC_ESCROW_ADDRESS}; /// Token transfer context to handle tokens #[derive(Debug)] @@ -54,7 +54,7 @@ where ) -> Result<(Address, Amount), TokenTransferError> { let token = match Address::decode(coin.denom.base_denom.as_str()) { Ok(token_addr) if coin.denom.trace_path.is_empty() => token_addr, - _ => storage::ibc_token(coin.denom.to_string()), + _ => trace::ibc_token(coin.denom.to_string()), }; // Convert IBC amount to Namada amount for the token @@ -146,7 +146,7 @@ where return Ok(()); } let ibc_denom = coin.denom.to_string(); - let trace_hash = storage::calc_hash(&ibc_denom); + let trace_hash = trace::calc_hash(&ibc_denom); self.inner .borrow_mut() @@ -224,7 +224,7 @@ where } fn denom_hash_string(&self, denom: &PrefixedDenom) -> Option { - Some(storage::calc_hash(denom.to_string())) + Some(trace::calc_hash(denom.to_string())) } } diff --git a/crates/ibc/src/lib.rs b/crates/ibc/src/lib.rs index 8f1831764c..9037d70f22 100644 --- a/crates/ibc/src/lib.rs +++ b/crates/ibc/src/lib.rs @@ -24,12 +24,12 @@ mod msg; mod nft; pub mod parameters; pub mod storage; +pub mod trace; use std::cell::RefCell; use std::collections::BTreeSet; use std::fmt::Debug; use std::rc::Rc; -use std::str::FromStr; pub use actions::transfer_over_ibc; use borsh::BorshDeserialize; @@ -49,17 +49,14 @@ use ibc::apps::nft_transfer::types::error::NftTransferError; use ibc::apps::nft_transfer::types::msgs::transfer::MsgTransfer as IbcMsgNftTransfer; use ibc::apps::nft_transfer::types::{ ack_success_b64, is_receiver_chain_source as is_nft_receiver_chain_source, - PrefixedClassId, TokenId, TracePath as NftTracePath, - TracePrefix as NftTracePrefix, + PrefixedClassId, TokenId, TracePrefix as NftTracePrefix, }; use ibc::apps::transfer::handler::{ send_transfer_execute, send_transfer_validate, }; use ibc::apps::transfer::types::error::TokenTransferError; use ibc::apps::transfer::types::msgs::transfer::MsgTransfer as IbcMsgTransfer; -use ibc::apps::transfer::types::{ - is_receiver_chain_source, PrefixedDenom, TracePath, TracePrefix, -}; +use ibc::apps::transfer::types::{is_receiver_chain_source, TracePrefix}; use ibc::core::channel::types::acknowledgement::{ Acknowledgement, AcknowledgementStatus, }; @@ -72,12 +69,15 @@ use ibc::core::handler::types::error::ContextError; use ibc::core::handler::types::events::Error as RawIbcEventError; use ibc::core::handler::types::msgs::MsgEnvelope; use ibc::core::host::types::error::IdentifierError; -use ibc::core::host::types::identifiers::{ChannelId, PortId}; +use ibc::core::host::types::identifiers::{ChannelId, PortId, Sequence}; use ibc::core::router::types::error::RouterError; use ibc::primitives::proto::Any; pub use ibc::*; +use masp_primitives::transaction::Transaction as MaspTransaction; pub use msg::*; use namada_core::address::{self, Address}; +use namada_core::arith::checked; +use namada_storage::{Error as StorageError, StorageRead}; use namada_token::Transfer; pub use nft::*; use prost::Message; @@ -154,7 +154,7 @@ where pub fn execute( &mut self, tx_data: &[u8], - ) -> Result, Error> { + ) -> Result<(Option, Option), Error> { let message = decode_message(tx_data)?; match &message { IbcMessage::Transfer(msg) => { @@ -169,7 +169,7 @@ where msg.message.clone(), ) .map_err(Error::TokenTransfer)?; - Ok(msg.transfer.clone()) + Ok((msg.transfer.clone(), None)) } IbcMessage::NftTransfer(msg) => { let mut nft_transfer_ctx = @@ -180,47 +180,45 @@ where msg.message.clone(), ) .map_err(Error::NftTransfer)?; - Ok(msg.transfer.clone()) - } - IbcMessage::RecvPacket(msg) => { - let envelope = - MsgEnvelope::Packet(PacketMsg::Recv(msg.message.clone())); - execute(&mut self.ctx, &mut self.router, envelope) - .map_err(|e| Error::Context(Box::new(e)))?; - let transfer = if self.is_receiving_success(&msg.message)? { - // For receiving the token to a shielded address - msg.transfer.clone() - } else { - None - }; - Ok(transfer) - } - IbcMessage::AckPacket(msg) => { - let envelope = - MsgEnvelope::Packet(PacketMsg::Ack(msg.message.clone())); - execute(&mut self.ctx, &mut self.router, envelope) - .map_err(|e| Error::Context(Box::new(e)))?; - let transfer = - if !is_ack_successful(&msg.message.acknowledgement)? { - // For refunding the token to a shielded address - msg.transfer.clone() - } else { - None - }; - Ok(transfer) - } - IbcMessage::Timeout(msg) => { - let envelope = MsgEnvelope::Packet(PacketMsg::Timeout( - msg.message.clone(), - )); - execute(&mut self.ctx, &mut self.router, envelope) - .map_err(|e| Error::Context(Box::new(e)))?; - Ok(msg.transfer.clone()) + Ok((msg.transfer.clone(), None)) } IbcMessage::Envelope(envelope) => { execute(&mut self.ctx, &mut self.router, *envelope.clone()) .map_err(|e| Error::Context(Box::new(e)))?; - Ok(None) + // Extract MASP tx from the memo in the packet if needed + let masp_tx = match &**envelope { + MsgEnvelope::Packet(packet_msg) => { + match packet_msg { + PacketMsg::Recv(msg) => { + if self.is_receiving_success(msg)? { + extract_masp_tx_from_packet( + &msg.packet, + false, + ) + } else { + None + } + } + PacketMsg::Ack(msg) => { + if is_ack_successful(&msg.acknowledgement)? { + // No refund + None + } else { + extract_masp_tx_from_packet( + &msg.packet, + true, + ) + } + } + PacketMsg::Timeout(msg) => { + extract_masp_tx_from_packet(&msg.packet, true) + } + _ => None, + } + } + _ => None, + }; + Ok((None, masp_tx)) } } } @@ -278,24 +276,6 @@ where ) .map_err(Error::NftTransfer) } - IbcMessage::RecvPacket(msg) => validate( - &self.ctx, - &self.router, - MsgEnvelope::Packet(PacketMsg::Recv(msg.message)), - ) - .map_err(|e| Error::Context(Box::new(e))), - IbcMessage::AckPacket(msg) => validate( - &self.ctx, - &self.router, - MsgEnvelope::Packet(PacketMsg::Ack(msg.message)), - ) - .map_err(|e| Error::Context(Box::new(e))), - IbcMessage::Timeout(msg) => validate( - &self.ctx, - &self.router, - MsgEnvelope::Packet(PacketMsg::Timeout(msg.message)), - ) - .map_err(|e| Error::Context(Box::new(e))), IbcMessage::Envelope(envelope) => { validate(&self.ctx, &self.router, *envelope) .map_err(|e| Error::Context(Box::new(e))) @@ -357,23 +337,28 @@ pub fn decode_message(tx_data: &[u8]) -> Result { return Ok(IbcMessage::NftTransfer(msg)); } - // Receiving packet message with `ShieldingTransfer` - if let Ok(msg) = MsgRecvPacket::try_from_slice(tx_data) { - return Ok(IbcMessage::RecvPacket(msg)); - } - - // Acknowledge packet message with `ShieldingTransfer` - if let Ok(msg) = MsgAcknowledgement::try_from_slice(tx_data) { - return Ok(IbcMessage::AckPacket(msg)); - } - // Timeout packet message with `ShieldingTransfer` - if let Ok(msg) = MsgTimeout::try_from_slice(tx_data) { - return Ok(IbcMessage::Timeout(msg)); - } - Err(Error::DecodingData) } +/// Return the last sequence send +pub fn get_last_sequence_send( + storage: &S, + port_id: &PortId, + channel_id: &ChannelId, +) -> Result { + let seq_key = storage::next_sequence_send_key(port_id, channel_id); + let next_seq: u64 = + context::common::read_sequence(storage, &seq_key)?.into(); + if next_seq <= 1 { + // No transfer heppened + return Err(StorageError::new_alloc(format!( + "No IBC transfer happened: Port ID {port_id}, Channel ID \ + {channel_id}", + ))); + } + Ok(checked!(next_seq - 1)?.into()) +} + fn received_ibc_trace( base_trace: impl AsRef, src_port_id: &PortId, @@ -401,7 +386,7 @@ fn received_ibc_trace( } if let Some((trace_path, base_class_id, token_id)) = - is_nft_trace(&base_trace) + trace::is_nft_trace(&base_trace) { let mut class_id = PrefixedClassId { trace_path, @@ -449,43 +434,8 @@ pub fn received_ibc_token( dest_port_id, dest_channel_id, )?; - if ibc_trace.contains('/') { - Ok(storage::ibc_token(ibc_trace)) - } else { - Address::decode(ibc_trace) - .map_err(|e| Error::Trace(format!("Invalid base token: {e}"))) - } -} - -/// Returns the trace path and the token string if the denom is an IBC -/// denom. -pub fn is_ibc_denom(denom: impl AsRef) -> Option<(TracePath, String)> { - let prefixed_denom = PrefixedDenom::from_str(denom.as_ref()).ok()?; - let base_denom = prefixed_denom.base_denom.to_string(); - if prefixed_denom.trace_path.is_empty() || base_denom.contains('/') { - // The denom is just a token or an NFT trace - return None; - } - // The base token isn't decoded because it could be non Namada token - Some((prefixed_denom.trace_path, base_denom)) -} - -/// Returns the trace path and the token string if the trace is an NFT one -pub fn is_nft_trace( - trace: impl AsRef, -) -> Option<(NftTracePath, String, String)> { - // The trace should be {port}/{channel}/.../{class_id}/{token_id} - if let Some((class_id, token_id)) = trace.as_ref().rsplit_once('/') { - let prefixed_class_id = PrefixedClassId::from_str(class_id).ok()?; - // The base token isn't decoded because it could be non Namada token - Some(( - prefixed_class_id.trace_path, - prefixed_class_id.base_class_id.to_string(), - token_id.to_string(), - )) - } else { - None - } + trace::convert_to_address(ibc_trace) + .map_err(|e| Error::Trace(format!("Invalid base token: {e}"))) } #[cfg(any(test, feature = "testing"))] diff --git a/crates/ibc/src/msg.rs b/crates/ibc/src/msg.rs index 9da6db400d..d3a32330f1 100644 --- a/crates/ibc/src/msg.rs +++ b/crates/ibc/src/msg.rs @@ -1,12 +1,18 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use data_encoding::HEXUPPER; use ibc::apps::nft_transfer::types::msgs::transfer::MsgTransfer as IbcMsgNftTransfer; +use ibc::apps::nft_transfer::types::packet::PacketData as NftPacketData; +use ibc::apps::nft_transfer::types::PORT_ID_STR as NFT_PORT_ID_STR; use ibc::apps::transfer::types::msgs::transfer::MsgTransfer as IbcMsgTransfer; -use ibc::core::channel::types::msgs::{ - MsgAcknowledgement as IbcMsgAcknowledgement, - MsgRecvPacket as IbcMsgRecvPacket, MsgTimeout as IbcMsgTimeout, -}; +use ibc::apps::transfer::types::packet::PacketData; +use ibc::apps::transfer::types::PORT_ID_STR as FT_PORT_ID_STR; +use ibc::core::channel::types::msgs::PacketMsg; +use ibc::core::channel::types::packet::Packet; use ibc::core::handler::types::msgs::MsgEnvelope; +use ibc::core::host::types::identifiers::PortId; use ibc::primitives::proto::Protobuf; +use masp_primitives::transaction::Transaction as MaspTransaction; +use namada_core::borsh::BorshSerializeExt; use namada_token::Transfer; /// The different variants of an Ibc message @@ -18,12 +24,6 @@ pub enum IbcMessage { Transfer(MsgTransfer), /// NFT transfer NftTransfer(MsgNftTransfer), - /// Receiving a packet - RecvPacket(MsgRecvPacket), - /// Acknowledgement - AckPacket(MsgAcknowledgement), - /// Timeout - Timeout(MsgTimeout), } /// IBC transfer message with `Transfer` @@ -92,103 +92,103 @@ impl BorshDeserialize for MsgNftTransfer { } } -/// IBC receiving packet message with `Transfer` -#[derive(Debug, Clone)] -pub struct MsgRecvPacket { - /// IBC receiving packet message - pub message: IbcMsgRecvPacket, - /// Shieleded transfer for MASP transaction - pub transfer: Option, +/// Shielding data in IBC packet memo +#[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] +pub struct IbcShieldingData { + /// MASP transaction for receiving the token + pub shielding: Option, + /// MASP transaction for refunding the token + pub refund: Option, } -impl BorshSerialize for MsgRecvPacket { - fn serialize( - &self, - writer: &mut W, - ) -> std::io::Result<()> { - let encoded_msg = self.message.clone().encode_vec(); - let members = (encoded_msg, self.transfer.clone()); - BorshSerialize::serialize(&members, writer) +impl From for String { + fn from(data: IbcShieldingData) -> Self { + HEXUPPER.encode(&data.serialize_to_vec()) } } -impl BorshDeserialize for MsgRecvPacket { - fn deserialize_reader( - reader: &mut R, - ) -> std::io::Result { - use std::io::{Error, ErrorKind}; - let (msg, transfer): (Vec, Option) = - BorshDeserialize::deserialize_reader(reader)?; - let message = IbcMsgRecvPacket::decode_vec(&msg) - .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; - Ok(Self { message, transfer }) - } -} - -/// IBC acknowledgement message with `Transfer` for refunding to a shielded -/// address -#[derive(Debug, Clone)] -pub struct MsgAcknowledgement { - /// IBC acknowledgement message - pub message: IbcMsgAcknowledgement, - /// Shieleded transfer for MASP transaction - pub transfer: Option, -} - -impl BorshSerialize for MsgAcknowledgement { - fn serialize( - &self, - writer: &mut W, - ) -> std::io::Result<()> { - let encoded_msg = self.message.clone().encode_vec(); - let members = (encoded_msg, self.transfer.clone()); - BorshSerialize::serialize(&members, writer) +/// Extract MASP transaction from IBC envelope +pub fn extract_masp_tx_from_envelope( + envelope: &MsgEnvelope, +) -> Option { + match envelope { + MsgEnvelope::Packet(packet_msg) => match packet_msg { + PacketMsg::Recv(msg) => { + extract_masp_tx_from_packet(&msg.packet, false) + } + PacketMsg::Ack(msg) => { + extract_masp_tx_from_packet(&msg.packet, true) + } + PacketMsg::Timeout(msg) => { + extract_masp_tx_from_packet(&msg.packet, true) + } + _ => None, + }, + _ => None, } } -impl BorshDeserialize for MsgAcknowledgement { - fn deserialize_reader( - reader: &mut R, - ) -> std::io::Result { - use std::io::{Error, ErrorKind}; - let (msg, transfer): (Vec, Option) = - BorshDeserialize::deserialize_reader(reader)?; - let message = IbcMsgAcknowledgement::decode_vec(&msg) - .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; - Ok(Self { message, transfer }) +/// Decode IBC shielding data from the memo string +pub fn decode_masp_tx_from_memo( + memo: impl AsRef, +) -> Option { + let bytes = HEXUPPER.decode(memo.as_ref().as_bytes()).ok()?; + IbcShieldingData::try_from_slice(&bytes).ok() +} + +/// Extract MASP transaction from IBC packet memo +pub fn extract_masp_tx_from_packet( + packet: &Packet, + is_sender: bool, +) -> Option { + let port_id = if is_sender { + &packet.port_id_on_a + } else { + &packet.port_id_on_b + }; + let memo = extract_memo_from_packet(packet, port_id)?; + let shielding_data = decode_masp_tx_from_memo(memo)?; + if is_sender { + shielding_data.refund + } else { + shielding_data.shielding } } -/// IBC timeout packet message with `Transfer` for refunding to a shielded -/// address -#[derive(Debug, Clone)] -pub struct MsgTimeout { - /// IBC timeout message - pub message: IbcMsgTimeout, - /// Shieleded transfer for MASP transaction - pub transfer: Option, -} - -impl BorshSerialize for MsgTimeout { - fn serialize( - &self, - writer: &mut W, - ) -> std::io::Result<()> { - let encoded_msg = self.message.clone().encode_vec(); - let members = (encoded_msg, self.transfer.clone()); - BorshSerialize::serialize(&members, writer) +fn extract_memo_from_packet( + packet: &Packet, + port_id: &PortId, +) -> Option { + match port_id.as_str() { + FT_PORT_ID_STR => { + let packet_data = + serde_json::from_slice::(&packet.data).ok()?; + if packet_data.memo.as_ref().is_empty() { + None + } else { + Some(packet_data.memo.as_ref().to_string()) + } + } + NFT_PORT_ID_STR => { + let packet_data = + serde_json::from_slice::(&packet.data).ok()?; + Some(packet_data.memo?.as_ref().to_string()) + } + _ => { + tracing::warn!( + "Memo couldn't be extracted from the unsupported IBC packet \ + data for Port ID {port_id}" + ); + None + } } } -impl BorshDeserialize for MsgTimeout { - fn deserialize_reader( - reader: &mut R, - ) -> std::io::Result { - use std::io::{Error, ErrorKind}; - let (msg, transfer): (Vec, Option) = - BorshDeserialize::deserialize_reader(reader)?; - let message = IbcMsgTimeout::decode_vec(&msg) - .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; - Ok(Self { message, transfer }) - } +/// Get IBC memo string from MASP transaction for receiving +pub fn convert_masp_tx_to_ibc_memo(transaction: &MaspTransaction) -> String { + let shielding_data = IbcShieldingData { + shielding: Some(transaction.clone()), + refund: None, + }; + shielding_data.into() } diff --git a/crates/ibc/src/storage.rs b/crates/ibc/src/storage.rs index de119e8e95..1519a5f3e4 100644 --- a/crates/ibc/src/storage.rs +++ b/crates/ibc/src/storage.rs @@ -12,8 +12,7 @@ use ibc::core::host::types::path::{ ClientStatePath, CommitmentPath, ConnectionPath, Path, PortPath, ReceiptPath, SeqAckPath, SeqRecvPath, SeqSendPath, }; -use namada_core::address::{Address, InternalAddress, HASH_LEN, SHA_HASH_LEN}; -use namada_core::ibc::IbcTokenHash; +use namada_core::address::{Address, InternalAddress}; use namada_core::storage::{DbKeySeg, Key, KeySeg}; use namada_core::token::Amount; use namada_events::extend::UserAccount; @@ -21,11 +20,11 @@ use namada_events::{EmitEvents, EventLevel}; use namada_state::{StorageRead, StorageResult, StorageWrite}; use namada_token as token; use namada_token::event::{TokenEvent, TokenOperation}; -use sha2::{Digest, Sha256}; use thiserror::Error; use crate::event::TOKEN_EVENT_DESCRIPTOR; use crate::parameters::IbcParameters; +use crate::trace::{ibc_token, ibc_token_for_nft}; const CLIENTS_COUNTER_PREFIX: &str = "clients"; const CONNECTIONS_COUNTER_PREFIX: &str = "connections"; @@ -48,8 +47,8 @@ pub enum Error { StorageKey(namada_core::storage::Error), #[error("Invalid Key: {0}")] InvalidKey(String), - #[error("Port capability error: {0}")] - InvalidPortCapability(String), + #[error("Invalid IBC trace: {0}")] + InvalidIbcTrace(String), } /// IBC storage functions result @@ -481,41 +480,6 @@ pub fn ibc_trace_key( .expect("Cannot obtain a storage key") } -/// Hash the denom -#[inline] -pub fn calc_hash(denom: impl AsRef) -> String { - calc_ibc_token_hash(denom).to_string() -} - -/// Hash the denom -pub fn calc_ibc_token_hash(denom: impl AsRef) -> IbcTokenHash { - let hash = { - let mut hasher = Sha256::new(); - hasher.update(denom.as_ref()); - hasher.finalize() - }; - - let input: &[u8; SHA_HASH_LEN] = hash.as_ref(); - let mut output = [0; HASH_LEN]; - - output.copy_from_slice(&input[..HASH_LEN]); - IbcTokenHash(output) -} - -/// Obtain the IbcToken with the hash from the given denom -pub fn ibc_token(denom: impl AsRef) -> Address { - let hash = calc_ibc_token_hash(&denom); - Address::Internal(InternalAddress::IbcToken(hash)) -} - -/// Obtain the IbcToken with the hash from the given NFT class ID and NFT ID -pub fn ibc_token_for_nft( - class_id: &PrefixedClassId, - token_id: &TokenId, -) -> Address { - ibc_token(format!("{class_id}/{token_id}")) -} - /// Returns true if the given key is for IBC pub fn is_ibc_key(key: &Key) -> bool { matches!(&key.segments[0], diff --git a/crates/ibc/src/trace.rs b/crates/ibc/src/trace.rs new file mode 100644 index 0000000000..6bb9d3bea8 --- /dev/null +++ b/crates/ibc/src/trace.rs @@ -0,0 +1,120 @@ +//! Functions for IBC token + +use std::str::FromStr; + +use ibc::apps::nft_transfer::types::{ + PrefixedClassId, TokenId, TracePath as NftTracePath, +}; +use ibc::apps::transfer::types::{PrefixedDenom, TracePath}; +use ibc::core::host::types::identifiers::{ChannelId, PortId}; +use namada_core::address::{Address, InternalAddress, HASH_LEN, SHA_HASH_LEN}; +use namada_core::ibc::IbcTokenHash; +use sha2::{Digest, Sha256}; + +use crate::storage::Error; + +/// Hash the denom +#[inline] +pub fn calc_hash(trace: impl AsRef) -> String { + calc_ibc_token_hash(trace).to_string() +} + +/// Hash the denom +pub fn calc_ibc_token_hash(trace: impl AsRef) -> IbcTokenHash { + let hash = { + let mut hasher = Sha256::new(); + hasher.update(trace.as_ref()); + hasher.finalize() + }; + + let input: &[u8; SHA_HASH_LEN] = hash.as_ref(); + let mut output = [0; HASH_LEN]; + + output.copy_from_slice(&input[..HASH_LEN]); + IbcTokenHash(output) +} + +/// Obtain the IbcToken with the hash from the given denom +pub fn ibc_token(trace: impl AsRef) -> Address { + let hash = calc_ibc_token_hash(&trace); + Address::Internal(InternalAddress::IbcToken(hash)) +} + +/// Obtain the IbcToken with the hash from the given NFT class ID and NFT ID +pub fn ibc_token_for_nft( + class_id: &PrefixedClassId, + token_id: &TokenId, +) -> Address { + ibc_token(ibc_trace_for_nft(class_id, token_id)) +} + +/// Obtain the IBC trace from the given NFT class ID and NFT ID +pub fn ibc_trace_for_nft( + class_id: &PrefixedClassId, + token_id: &TokenId, +) -> String { + format!("{class_id}/{token_id}") +} + +/// Convert the given IBC trace to [`Address`] +pub fn convert_to_address( + ibc_trace: impl AsRef, +) -> Result { + if ibc_trace.as_ref().contains('/') { + // validation + if is_ibc_denom(&ibc_trace).is_none() + && is_nft_trace(&ibc_trace).is_none() + { + return Err(Error::InvalidIbcTrace(format!( + "This is not IBC denom and NFT trace: {}", + ibc_trace.as_ref() + ))); + } + Ok(ibc_token(ibc_trace.as_ref())) + } else { + Address::decode(ibc_trace.as_ref()) + .map_err(|e| Error::InvalidIbcTrace(e.to_string())) + } +} + +/// Returns the trace path and the token string if the denom is an IBC +/// denom. +pub fn is_ibc_denom(denom: impl AsRef) -> Option<(TracePath, String)> { + let prefixed_denom = PrefixedDenom::from_str(denom.as_ref()).ok()?; + let base_denom = prefixed_denom.base_denom.to_string(); + if prefixed_denom.trace_path.is_empty() || base_denom.contains('/') { + // The denom is just a token or an NFT trace + return None; + } + // The base token isn't decoded because it could be non Namada token + Some((prefixed_denom.trace_path, base_denom)) +} + +/// Returns the trace path and the token string if the trace is an NFT one +pub fn is_nft_trace( + trace: impl AsRef, +) -> Option<(NftTracePath, String, String)> { + // The trace should be {port}/{channel}/.../{class_id}/{token_id} + if let Some((class_id, token_id)) = trace.as_ref().rsplit_once('/') { + let prefixed_class_id = PrefixedClassId::from_str(class_id).ok()?; + // The base token isn't decoded because it could be non Namada token + Some(( + prefixed_class_id.trace_path, + prefixed_class_id.base_class_id.to_string(), + token_id.to_string(), + )) + } else { + None + } +} + +/// Return true if the source of the given IBC trace is this chain +pub fn is_sender_chain_source( + trace: impl AsRef, + src_port_id: &PortId, + src_channel_id: &ChannelId, +) -> bool { + !trace + .as_ref() + .starts_with(&format!("{src_port_id}/{src_channel_id}")) +} diff --git a/crates/namada/src/ledger/ibc/mod.rs b/crates/namada/src/ledger/ibc/mod.rs index 4076a1de92..be7f4f5144 100644 --- a/crates/namada/src/ledger/ibc/mod.rs +++ b/crates/namada/src/ledger/ibc/mod.rs @@ -6,7 +6,7 @@ use namada_ibc::storage::{ channel_counter_key, client_counter_key, connection_counter_key, deposit_prefix, withdraw_prefix, }; -pub use namada_ibc::{parameters, storage}; +pub use namada_ibc::{parameters, storage, trace}; use namada_state::{ DBIter, Key, State, StorageError, StorageHasher, StorageRead, StorageWrite, WlState, DB, diff --git a/crates/namada/src/ledger/mod.rs b/crates/namada/src/ledger/mod.rs index 0353640f4a..71304bc1e8 100644 --- a/crates/namada/src/ledger/mod.rs +++ b/crates/namada/src/ledger/mod.rs @@ -109,6 +109,7 @@ mod dry_run_tx { let ExtendedTxResult { mut tx_result, ref masp_tx_refs, + is_ibc_shielding: _, } = extended_tx_result; let tx_gas_meter = RefCell::new(tx_gas_meter); for cmt in diff --git a/crates/namada/src/ledger/native_vp/ibc/mod.rs b/crates/namada/src/ledger/native_vp/ibc/mod.rs index 9286a6d186..90dd6f4c02 100644 --- a/crates/namada/src/ledger/native_vp/ibc/mod.rs +++ b/crates/namada/src/ledger/native_vp/ibc/mod.rs @@ -28,9 +28,10 @@ use thiserror::Error; use crate::ibc::core::host::types::identifiers::ChainId as IbcChainId; use crate::ledger::ibc::storage::{ - calc_hash, deposit_key, get_limits, is_ibc_key, is_ibc_trace_key, - mint_amount_key, withdraw_key, + deposit_key, get_limits, is_ibc_key, is_ibc_trace_key, mint_amount_key, + withdraw_key, }; +use crate::ledger::ibc::trace::calc_hash; use crate::ledger::native_vp::{self, Ctx, NativeVp}; use crate::ledger::parameters::read_epoch_duration_parameter; use crate::token::storage_key::is_any_token_balance_key; @@ -507,14 +508,14 @@ mod tests { use crate::ibc::primitives::proto::{Any, Protobuf}; use crate::ibc::primitives::{Timestamp, ToProto}; use crate::ibc::storage::{ - ack_key, calc_hash, channel_counter_key, channel_key, - client_connections_key, client_counter_key, client_state_key, - client_update_height_key, client_update_timestamp_key, commitment_key, - connection_counter_key, connection_key, consensus_state_key, ibc_token, - ibc_trace_key, mint_amount_key, next_sequence_ack_key, - next_sequence_recv_key, next_sequence_send_key, nft_class_key, - nft_metadata_key, receipt_key, + ack_key, channel_counter_key, channel_key, client_connections_key, + client_counter_key, client_state_key, client_update_height_key, + client_update_timestamp_key, commitment_key, connection_counter_key, + connection_key, consensus_state_key, ibc_trace_key, mint_amount_key, + next_sequence_ack_key, next_sequence_recv_key, next_sequence_send_key, + nft_class_key, nft_metadata_key, receipt_key, }; + use crate::ibc::trace::{calc_hash, ibc_token}; use crate::ibc::{MsgNftTransfer, MsgTransfer, NftClass, NftMetadata}; use crate::key::testing::keypair_1; use crate::ledger::gas::VpGasMeter; @@ -3008,7 +3009,7 @@ mod tests { let class_id = get_nft_class_id(); let token_id = get_nft_id(); let sender = established_address_1(); - let ibc_token = ibc::storage::ibc_token_for_nft(&class_id, &token_id); + let ibc_token = ibc::trace::ibc_token_for_nft(&class_id, &token_id); let balance_key = balance_key(&ibc_token, &sender); let amount = Amount::from_u64(1); state diff --git a/crates/namada/src/ledger/native_vp/masp.rs b/crates/namada/src/ledger/native_vp/masp.rs index f116ca0057..c550c0a4db 100644 --- a/crates/namada/src/ledger/native_vp/masp.rs +++ b/crates/namada/src/ledger/native_vp/masp.rs @@ -3,7 +3,6 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet}; -use borsh::BorshDeserialize; use masp_primitives::asset_type::AssetType; use masp_primitives::merkle_tree::CommitmentTree; use masp_primitives::sapling::Node; @@ -16,17 +15,30 @@ use namada_core::address::Address; use namada_core::arith::{checked, CheckedAdd, CheckedSub}; use namada_core::booleans::BoolResultUnitExt; use namada_core::collections::HashSet; -use namada_core::ibc::apps::transfer::types::is_sender_chain_source; +use namada_core::ibc::apps::nft_transfer::types::msgs::transfer::MsgTransfer as IbcMsgNftTransfer; +use namada_core::ibc::apps::nft_transfer::types::packet::PacketData as NftPacketData; use namada_core::ibc::apps::transfer::types::msgs::transfer::MsgTransfer as IbcMsgTransfer; use namada_core::ibc::apps::transfer::types::packet::PacketData; use namada_core::masp::{addr_taddr, encode_asset_type, ibc_taddr, MaspEpoch}; use namada_core::storage::Key; use namada_gas::GasMetering; use namada_governance::storage::is_proposal_accepted; -use namada_ibc::core::channel::types::msgs::MsgRecvPacket as IbcMsgRecvPacket; -use namada_ibc::core::host::types::identifiers::Sequence; -use namada_ibc::storage::ibc_token; -use namada_ibc::{IbcCommonContext, IbcMessage}; +use namada_ibc::core::channel::types::commitment::{ + compute_packet_commitment, PacketCommitment, +}; +use namada_ibc::core::channel::types::msgs::{ + MsgRecvPacket as IbcMsgRecvPacket, PacketMsg, +}; +use namada_ibc::core::channel::types::timeout::TimeoutHeight; +use namada_ibc::core::handler::types::msgs::MsgEnvelope; +use namada_ibc::core::host::types::identifiers::{ChannelId, PortId, Sequence}; +use namada_ibc::primitives::Timestamp; +use namada_ibc::trace::{ + convert_to_address, ibc_trace_for_nft, is_sender_chain_source, +}; +use namada_ibc::{ + extract_masp_tx_from_envelope, get_last_sequence_send, IbcMessage, +}; use namada_sdk::masp::TAddrData; use namada_state::{ConversionState, OptionExt, ResultExt, StateRead}; use namada_token::read_denom; @@ -41,24 +53,19 @@ use token::storage_key::{ }; use token::Amount; -use crate::address::{InternalAddress, IBC, MASP}; +use crate::address::{IBC, MASP}; use crate::ledger::ibc::storage; -use crate::ledger::ibc::storage::{ - ibc_trace_key, ibc_trace_key_prefix, is_ibc_commitment_key, - is_ibc_trace_key, -}; +use crate::ledger::ibc::storage::{commitment_key, receipt_key}; use crate::ledger::native_vp; -use crate::ledger::native_vp::ibc::context::VpValidationContext; use crate::ledger::native_vp::{Ctx, NativeVp}; use crate::sdk::ibc::apps::transfer::types::{ack_success_b64, PORT_ID_STR}; use crate::sdk::ibc::core::channel::types::acknowledgement::AcknowledgementStatus; use crate::sdk::ibc::core::channel::types::commitment::{ - compute_ack_commitment, AcknowledgementCommitment, PacketCommitment, + compute_ack_commitment, AcknowledgementCommitment, }; -use crate::sdk::ibc::core::channel::types::packet::Packet; use crate::token; use crate::token::MaspDigitPos; -use crate::uint::{Uint, I320}; +use crate::uint::I320; use crate::vm::WasmCacheAccess; #[allow(missing_docs)] @@ -94,6 +101,78 @@ struct ChangedBalances { post: BTreeMap>, } +/// IBC transfer info +struct IbcTransferInfo { + src_port_id: PortId, + src_channel_id: ChannelId, + timeout_height: TimeoutHeight, + timeout_timestamp: Timestamp, + packet_data: Vec, + ibc_traces: Vec, + amount: Amount, + receiver: String, +} + +impl TryFrom for IbcTransferInfo { + type Error = Error; + + fn try_from( + message: IbcMsgTransfer, + ) -> std::result::Result { + let packet_data = serde_json::to_vec(&message.packet_data) + .map_err(native_vp::Error::new)?; + let ibc_traces = vec![message.packet_data.token.denom.to_string()]; + let amount = message + .packet_data + .token + .amount + .try_into() + .into_storage_result()?; + let receiver = message.packet_data.receiver.to_string(); + Ok(Self { + src_port_id: message.port_id_on_a, + src_channel_id: message.chan_id_on_a, + timeout_height: message.timeout_height_on_b, + timeout_timestamp: message.timeout_timestamp_on_b, + packet_data, + ibc_traces, + amount, + receiver, + }) + } +} + +impl TryFrom for IbcTransferInfo { + type Error = Error; + + fn try_from( + message: IbcMsgNftTransfer, + ) -> std::result::Result { + let packet_data = serde_json::to_vec(&message.packet_data) + .map_err(native_vp::Error::new)?; + let ibc_traces = message + .packet_data + .token_ids + .0 + .iter() + .map(|token_id| { + ibc_trace_for_nft(&message.packet_data.class_id, token_id) + }) + .collect(); + let receiver = message.packet_data.receiver.to_string(); + Ok(Self { + src_port_id: message.port_id_on_a, + src_channel_id: message.chan_id_on_a, + timeout_height: message.timeout_height_on_b, + timeout_timestamp: message.timeout_timestamp_on_b, + packet_data, + ibc_traces, + amount: Amount::from_u64(1), + receiver, + }) + } +} + impl<'a, S, CA> MaspVp<'a, S, CA> where S: StateRead, @@ -296,140 +375,135 @@ where Ok(()) } - /// Look up the IBC denomination from a IbcToken. - pub fn query_ibc_denom( + fn check_ibc_transfer( &self, - token: impl AsRef, - owner: Option<&Address>, - ) -> Result { - let hash = match Address::decode(token.as_ref()) { - Ok(Address::Internal(InternalAddress::IbcToken(hash))) => { - hash.to_string() - } - _ => return Ok(token.as_ref().to_string()), - }; - - if let Some(owner) = owner { - let ibc_trace_key = ibc_trace_key(owner.to_string(), &hash); - if let Some(ibc_denom) = self.ctx.read_pre(&ibc_trace_key)? { - return Ok(ibc_denom); - } + ibc_transfer: &IbcTransferInfo, + keys_changed: &BTreeSet, + ) -> Result<()> { + let IbcTransferInfo { + src_port_id, + src_channel_id, + timeout_height, + timeout_timestamp, + packet_data, + .. + } = ibc_transfer; + let sequence = get_last_sequence_send( + &self.ctx.post(), + src_port_id, + src_channel_id, + )?; + let commitment_key = + commitment_key(src_port_id, src_channel_id, sequence); + + if !keys_changed.contains(&commitment_key) { + return Err(Error::NativeVpError(native_vp::Error::AllocMessage( + format!( + "Expected IBC transfer didn't happen: Port ID \ + {src_port_id}, Channel ID {src_channel_id}, Sequence \ + {sequence}" + ), + ))); } - // No owner is specified or the owner doesn't have the token - let ibc_denom_prefix = ibc_trace_key_prefix(None); - let ibc_denoms = self.ctx.iter_prefix(&ibc_denom_prefix)?; - for (key, ibc_denom, gas) in ibc_denoms { - self.ctx.charge_gas(gas)?; - if let Some((_, token_hash)) = - is_ibc_trace_key(&Key::parse(key).into_storage_result()?) - { - if token_hash == hash { - return String::try_from_slice(&ibc_denom[..]) - .into_storage_result() - .map_err(Error::NativeVpError); - } - } + // The commitment is also validated in IBC VP. Make sure that for when + // IBC VP isn't triggered. + let actual: PacketCommitment = self + .ctx + .read_bytes_post(&commitment_key)? + .ok_or(Error::NativeVpError(native_vp::Error::AllocMessage( + format!( + "Packet commitment doesn't exist: Port ID {src_port_id}, \ + Channel ID {src_channel_id}, Sequence {sequence}" + ), + )))? + .into(); + let expected = compute_packet_commitment( + packet_data, + timeout_height, + timeout_timestamp, + ); + if actual != expected { + return Err(Error::NativeVpError(native_vp::Error::AllocMessage( + format!( + "Packet commitment mismatched: Port ID {src_port_id}, \ + Channel ID {src_channel_id}, Sequence {sequence}" + ), + ))); } - Ok(token.as_ref().to_string()) + Ok(()) } - // Find the given IBC message in the changed keys and return the associated - // sequence number - fn find_ibc_transfer_sequence( + fn check_packet_receiving( &self, - message: &IbcMsgTransfer, + msg: &IbcMsgRecvPacket, keys_changed: &BTreeSet, - ) -> Result> { - // Compute the packet commitment for this message - let packet_data_bytes = serde_json::to_vec(&message.packet_data) - .map_err(native_vp::Error::new)?; - let packet_commitment = - VpValidationContext::<'a, 'a, S, CA>::compute_packet_commitment( - &packet_data_bytes, - &message.timeout_height_on_b, - &message.timeout_timestamp_on_b, - ); - // Try to find a key change with the same port, channel, and commitment - // as this message and note its sequence number - for key in keys_changed { - let Some(path) = is_ibc_commitment_key(key) else { - continue; - }; - if path.port_id == message.port_id_on_a - && path.channel_id == message.chan_id_on_a - { - let Some(storage_commitment): Option = - self.ctx.read_bytes_post(key)?.map(Into::into) - else { - // Ignore this key if the value does not exist - continue; - }; - if packet_commitment == storage_commitment { - return Ok(Some(path.sequence)); - } - } + ) -> Result<()> { + let receipt_key = receipt_key( + &msg.packet.port_id_on_b, + &msg.packet.chan_id_on_b, + msg.packet.seq_on_a, + ); + if !keys_changed.contains(&receipt_key) { + return Err(Error::NativeVpError(native_vp::Error::AllocMessage( + format!( + "The packet has not been received: Port ID {}, Channel \ + ID {}, Sequence {}", + msg.packet.port_id_on_b, + msg.packet.chan_id_on_b, + msg.packet.seq_on_a, + ), + ))); } - Ok(None) + Ok(()) } // Apply the given transfer message to the changed balances structure fn apply_transfer_msg( &self, mut acc: ChangedBalances, - msg: &IbcMsgTransfer, + ibc_transfer: &IbcTransferInfo, keys_changed: &BTreeSet, ) -> Result { - // If a key change with the same port, channel, and commitment as this - // message cannot be found, then ignore this message. Though this check - // is done in the IBC VP, the test is repeated here to avoid making - // assumptions about how the IBC VP interprets the given message. - if self - .find_ibc_transfer_sequence(msg, keys_changed)? - .is_none() - { - return Ok(acc); - }; + self.check_ibc_transfer(ibc_transfer, keys_changed)?; - // Obtain the address corresponding to the packet denomination - let denom = msg.packet_data.token.denom.to_string(); - let token = if denom.contains('/') { - ibc_token(denom) - } else { - Address::decode(denom).into_storage_result()? - }; - // Add currency units to the amount in the packet - let delta = ValueSum::from_pair( - token.clone(), - Amount::from_uint(Uint(*msg.packet_data.token.amount), 0).unwrap(), - ); - // Remove pre-existing balance increases to the IBC account since we - // want to record increases to specific receivers - if is_sender_chain_source( - msg.port_id_on_a.clone(), - msg.chan_id_on_a.clone(), - &msg.packet_data.token.denom, - ) { - let ibc_taddr = addr_taddr(IBC); - let post_entry = acc - .post - .get(&ibc_taddr) - .cloned() - .unwrap_or(ValueSum::zero()); + let IbcTransferInfo { + ibc_traces, + src_port_id, + src_channel_id, + amount, + receiver, + .. + } = ibc_transfer; + + let receiver = ibc_taddr(receiver.clone()); + for ibc_trace in ibc_traces { + let token = convert_to_address(ibc_trace).into_storage_result()?; + let delta = ValueSum::from_pair(token, *amount); + // If there is a transfer to the IBC account, then deduplicate the + // balance increase since we already accounted for it above + if is_sender_chain_source(ibc_trace, src_port_id, src_channel_id) { + let ibc_taddr = addr_taddr(IBC); + let post_entry = acc + .post + .get(&ibc_taddr) + .cloned() + .unwrap_or(ValueSum::zero()); + acc.post.insert( + ibc_taddr, + checked!(post_entry - &delta) + .map_err(native_vp::Error::new)?, + ); + } + // Record an increase to the balance of a specific IBC receiver + let post_entry = + acc.post.get(&receiver).cloned().unwrap_or(ValueSum::zero()); acc.post.insert( - ibc_taddr, - checked!(post_entry - &delta).map_err(native_vp::Error::new)?, + receiver, + checked!(post_entry + &delta).map_err(native_vp::Error::new)?, ); } - // Record an increase to the balance of a specific IBC receiver - let receiver = ibc_taddr(msg.packet_data.receiver.to_string()); - let post_entry = - acc.post.get(&receiver).cloned().unwrap_or(ValueSum::zero()); - acc.post.insert( - receiver, - checked!(post_entry + &delta).map_err(native_vp::Error::new)?, - ); Ok(acc) } @@ -437,19 +511,12 @@ where // Check if IBC message was received successfully in this state transition fn is_receiving_success( &self, - packet: &Packet, - keys_changed: &BTreeSet, + dst_port_id: &PortId, + dst_channel_id: &ChannelId, + sequence: Sequence, ) -> Result { // Ensure that the event corresponds to the current changes to storage - let ack_key = storage::ack_key( - &packet.port_id_on_a, - &packet.chan_id_on_a, - packet.seq_on_a, - ); - if !keys_changed.contains(&ack_key) { - // Ignore packet if it was not acknowledged during this state change - return Ok(false); - } + let ack_key = storage::ack_key(dst_port_id, dst_channel_id, sequence); // If the receive is a success, then the commitment is unique let succ_ack_commitment = compute_ack_commitment( &AcknowledgementStatus::success(ack_success_b64()).into(), @@ -469,42 +536,47 @@ where &self, mut acc: ChangedBalances, msg: &IbcMsgRecvPacket, - packet_data: &PacketData, + ibc_traces: Vec, + amount: Amount, keys_changed: &BTreeSet, ) -> Result { + self.check_packet_receiving(msg, keys_changed)?; + // If the transfer was a failure, then enable funds to // be withdrawn from the IBC internal address - if self.is_receiving_success(&msg.packet, keys_changed)? { - // Mirror how the IBC token is derived in - // gen_ibc_shielded_transfer in the non-refund case - let ibc_denom = self.query_ibc_denom( - packet_data.token.denom.to_string(), - Some(&Address::Internal(InternalAddress::Ibc)), - )?; - let token = namada_ibc::received_ibc_token( - ibc_denom, - &msg.packet.port_id_on_a, - &msg.packet.chan_id_on_a, - &msg.packet.port_id_on_b, - &msg.packet.chan_id_on_b, - ) - .into_storage_result() - .map_err(Error::NativeVpError)?; - let delta = ValueSum::from_pair( - token.clone(), - Amount::from_uint(Uint(*packet_data.token.amount), 0).unwrap(), - ); - // Enable funds to be taken from the IBC internal - // address and be deposited elsewhere - // Required for the IBC internal Address to release - // funds - let ibc_taddr = addr_taddr(IBC); - let pre_entry = - acc.pre.get(&ibc_taddr).cloned().unwrap_or(ValueSum::zero()); - acc.pre.insert( - ibc_taddr, - checked!(pre_entry + &delta).map_err(native_vp::Error::new)?, - ); + if self.is_receiving_success( + &msg.packet.port_id_on_b, + &msg.packet.chan_id_on_b, + msg.packet.seq_on_a, + )? { + for ibc_trace in ibc_traces { + // Get the received token + let token = namada_ibc::received_ibc_token( + ibc_trace, + &msg.packet.port_id_on_a, + &msg.packet.chan_id_on_a, + &msg.packet.port_id_on_b, + &msg.packet.chan_id_on_b, + ) + .into_storage_result() + .map_err(Error::NativeVpError)?; + let delta = ValueSum::from_pair(token.clone(), amount); + // Enable funds to be taken from the IBC internal + // address and be deposited elsewhere + // Required for the IBC internal Address to release + // funds + let ibc_taddr = addr_taddr(IBC); + let pre_entry = acc + .pre + .get(&ibc_taddr) + .cloned() + .unwrap_or(ValueSum::zero()); + acc.pre.insert( + ibc_taddr, + checked!(pre_entry + &delta) + .map_err(native_vp::Error::new)?, + ); + } } Ok(acc) } @@ -513,7 +585,7 @@ where fn apply_ibc_packet( &self, mut acc: ChangedBalances, - ibc_msg: &IbcMessage, + ibc_msg: IbcMessage, keys_changed: &BTreeSet, ) -> Result { match ibc_msg { @@ -521,32 +593,75 @@ where IbcMessage::Transfer(msg) => { // Get the packet commitment from post-storage that corresponds // to this event - let receiver = msg.message.packet_data.receiver.to_string(); + let ibc_transfer = IbcTransferInfo::try_from(msg.message)?; + let receiver = ibc_transfer.receiver.clone(); let addr = TAddrData::Ibc(receiver.clone()); acc.decoder.insert(ibc_taddr(receiver), addr); acc = - self.apply_transfer_msg(acc, &msg.message, keys_changed)?; + self.apply_transfer_msg(acc, &ibc_transfer, keys_changed)?; } - // This event is emitted on the receiver - IbcMessage::RecvPacket(msg) - if msg.message.packet.port_id_on_b.as_str() == PORT_ID_STR => - { - let packet_data = serde_json::from_slice::( - &msg.message.packet.data, - ) - .map_err(native_vp::Error::new)?; - let receiver = packet_data.receiver.to_string(); + IbcMessage::NftTransfer(msg) => { + let ibc_transfer = IbcTransferInfo::try_from(msg.message)?; + let receiver = ibc_transfer.receiver.clone(); let addr = TAddrData::Ibc(receiver.clone()); acc.decoder.insert(ibc_taddr(receiver), addr); - acc = self.apply_recv_msg( - acc, - &msg.message, - &packet_data, - keys_changed, - )?; + acc = + self.apply_transfer_msg(acc, &ibc_transfer, keys_changed)?; + } + // This event is emitted on the receiver + IbcMessage::Envelope(envelope) => { + if let MsgEnvelope::Packet(PacketMsg::Recv(msg)) = *envelope { + if msg.packet.port_id_on_b.as_str() == PORT_ID_STR { + let packet_data = serde_json::from_slice::( + &msg.packet.data, + ) + .map_err(native_vp::Error::new)?; + let receiver = packet_data.receiver.to_string(); + let addr = TAddrData::Ibc(receiver.clone()); + acc.decoder.insert(ibc_taddr(receiver), addr); + let ibc_denom = packet_data.token.denom.to_string(); + let amount = packet_data + .token + .amount + .try_into() + .into_storage_result()?; + acc = self.apply_recv_msg( + acc, + &msg, + vec![ibc_denom], + amount, + keys_changed, + )?; + } else { + let packet_data = + serde_json::from_slice::( + &msg.packet.data, + ) + .map_err(native_vp::Error::new)?; + let receiver = packet_data.receiver.to_string(); + let addr = TAddrData::Ibc(receiver.clone()); + acc.decoder.insert(ibc_taddr(receiver), addr); + let ibc_traces = packet_data + .token_ids + .0 + .iter() + .map(|token_id| { + ibc_trace_for_nft( + &packet_data.class_id, + token_id, + ) + }) + .collect(); + acc = self.apply_recv_msg( + acc, + &msg, + ibc_traces, + Amount::from_u64(1), + keys_changed, + )?; + } + } } - // Ignore all other IBC events - _ => {} } Result::<_>::Ok(acc) } @@ -612,7 +727,7 @@ where fn validate_state_and_get_transfer_data( &self, keys_changed: &BTreeSet, - ibc_msgs: &[IbcMessage], + ibc_msg: Option, ) -> Result { // Get the changed balance keys let mut counterparts_balances = @@ -628,14 +743,13 @@ where // Enable decoding the IBC address hash changed_balances.decoder.insert(addr_taddr(IBC), ibc_addr); - // Go through the IBC events and note the balance changes they imply - let changed_balances = - ibc_msgs.iter().try_fold(changed_balances, |acc, ibc_msg| { - // Apply all IBC packets to the changed balances structure - self.apply_ibc_packet(acc, ibc_msg, keys_changed) - })?; - - Ok(changed_balances) + // Note the balance changes they imply + match ibc_msg { + Some(ibc_msg) => { + self.apply_ibc_packet(changed_balances, ibc_msg, keys_changed) + } + None => Ok(changed_balances), + } } // Check that MASP Transaction and state changes are valid @@ -656,26 +770,33 @@ where Error::NativeVpError(native_vp::Error::new_const(msg)) })?; let conversion_state = self.ctx.state.in_mem().get_conversion_state(); - let ibc_msgs = self.ctx.get_ibc_message(tx_data).ok(); - - // Get the Transaction object from the actions - let masp_section_ref = namada_tx::action::get_masp_section_ref( - &self.ctx, - )? - .ok_or_else(|| { - native_vp::Error::new_const( - "Missing MASP section reference in action", - ) - })?; - let shielded_tx = tx_data - .tx - .get_masp_section(&masp_section_ref) - .cloned() - .ok_or_else(|| { - native_vp::Error::new_const( - "Missing MASP section in transaction", - ) - })?; + let ibc_msg = self.ctx.get_ibc_message(tx_data).ok(); + let shielded_tx = + if let Some(IbcMessage::Envelope(ref envelope)) = ibc_msg { + extract_masp_tx_from_envelope(envelope).ok_or_else(|| { + native_vp::Error::new_const( + "Missing MASP transaction in IBC message", + ) + })? + } else { + // Get the Transaction object from the actions + let masp_section_ref = + namada_tx::action::get_masp_section_ref(&self.ctx)? + .ok_or_else(|| { + native_vp::Error::new_const( + "Missing MASP section reference in action", + ) + })?; + tx_data + .tx + .get_masp_section(&masp_section_ref) + .cloned() + .ok_or_else(|| { + native_vp::Error::new_const( + "Missing MASP section in transaction", + ) + })? + }; if u64::from(self.ctx.get_block_height()?) > u64::from(shielded_tx.expiry_height()) @@ -688,10 +809,8 @@ where } // Check the validity of the keys and get the transfer data - let mut changed_balances = self.validate_state_and_get_transfer_data( - keys_changed, - ibc_msgs.as_slice(), - )?; + let mut changed_balances = + self.validate_state_and_get_transfer_data(keys_changed, ibc_msg)?; let masp_address_hash = addr_taddr(MASP); verify_sapling_balancing_value( diff --git a/crates/namada/src/ledger/native_vp/multitoken.rs b/crates/namada/src/ledger/native_vp/multitoken.rs index 1d88396dd2..e55df8a957 100644 --- a/crates/namada/src/ledger/native_vp/multitoken.rs +++ b/crates/namada/src/ledger/native_vp/multitoken.rs @@ -418,7 +418,7 @@ mod tests { }; use crate::key::testing::keypair_1; use crate::ledger::gas::VpGasMeter; - use crate::ledger::ibc::storage::ibc_token; + use crate::ledger::ibc::trace::ibc_token; use crate::storage::TxIndex; use crate::token::storage_key::{balance_key, minted_balance_key}; use crate::vm::wasm::compilation_cache::common::testing::cache as wasm_cache; diff --git a/crates/namada/src/ledger/protocol/mod.rs b/crates/namada/src/ledger/protocol/mod.rs index 37bf135c7c..97616f3b34 100644 --- a/crates/namada/src/ledger/protocol/mod.rs +++ b/crates/namada/src/ledger/protocol/mod.rs @@ -424,6 +424,9 @@ where .0 .push(masp_section_ref); } + extended_tx_result.is_ibc_shielding = + namada_tx::action::is_ibc_shielding_transfer(state) + .map_err(Error::StateError)?; state.write_log_mut().commit_tx_to_batch(); } else { state.write_log_mut().drop_tx(); diff --git a/crates/node/src/shell/finalize_block.rs b/crates/node/src/shell/finalize_block.rs index 6fbfe07412..c32ab0d12c 100644 --- a/crates/node/src/shell/finalize_block.rs +++ b/crates/node/src/shell/finalize_block.rs @@ -16,7 +16,6 @@ use namada::ledger::gas::GasMetering; use namada::ledger::ibc; use namada::ledger::pos::namada_proof_of_stake; use namada::ledger::protocol::{DispatchArgs, DispatchError}; -use namada::masp::MaspTxRefs; use namada::proof_of_stake; use namada::proof_of_stake::storage::{ find_validator_by_raw_hash, write_last_block_proposer_address, @@ -452,8 +451,7 @@ where commit_batch_hash, is_any_tx_invalid, } = temp_log.check_inner_results( - &extended_tx_result.tx_result, - extended_tx_result.masp_tx_refs, + &extended_tx_result, tx_data.tx_index, tx_data.height, ); @@ -524,8 +522,7 @@ where commit_batch_hash, is_any_tx_invalid: _, } = temp_log.check_inner_results( - &extended_tx_result.tx_result, - extended_tx_result.masp_tx_refs, + &extended_tx_result, tx_data.tx_index, tx_data.height, ); @@ -978,14 +975,17 @@ impl<'finalize> TempTxLogs { fn check_inner_results( &mut self, - tx_result: &namada::tx::data::TxResult, - masp_tx_refs: MaspTxRefs, + extended_tx_result: &namada::tx::data::ExtendedTxResult< + protocol::Error, + >, tx_index: usize, height: BlockHeight, ) -> ValidityFlags { let mut flags = ValidityFlags::default(); - for (cmt_hash, batched_result) in tx_result.batch_results.iter() { + for (cmt_hash, batched_result) in + extended_tx_result.tx_result.batch_results.iter() + { match batched_result { Ok(result) => { if result.is_accepted() { @@ -1048,10 +1048,17 @@ impl<'finalize> TempTxLogs { // If at least one of the inner transactions is a valid masp tx, update // the events - if !masp_tx_refs.0.is_empty() { + if !extended_tx_result.masp_tx_refs.0.is_empty() { + self.tx_event + .extend(MaspTxBlockIndex(TxIndex::must_from_usize(tx_index))); + self.tx_event.extend(MaspTxBatchRefs( + extended_tx_result.masp_tx_refs.clone(), + )); + } + + if extended_tx_result.is_ibc_shielding { self.tx_event .extend(MaspTxBlockIndex(TxIndex::must_from_usize(tx_index))); - self.tx_event.extend(MaspTxBatchRefs(masp_tx_refs)); } flags diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index 9715ff35f1..8bdf73202c 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -2765,9 +2765,9 @@ pub struct ValidatorSetUpdateRelay { pub safe_mode: bool, } -/// IBC shielded transfer generation arguments +/// IBC shielding transfer generation arguments #[derive(Clone, Debug)] -pub struct GenIbcShieldedTransfer { +pub struct GenIbcShieldingTransfer { /// The query parameters. pub query: Query, /// The output directory path to where serialize the data @@ -2782,6 +2782,4 @@ pub struct GenIbcShieldedTransfer { pub port_id: PortId, /// Channel ID via which the token is received pub channel_id: ChannelId, - /// Generate the shielded transfer for refunding - pub refund: bool, } diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index f5380173d2..bcf28db17b 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -51,6 +51,7 @@ use namada_events::extend::{ MaspTxBatchRefs as MaspTxBatchRefsAttr, MaspTxBlockIndex as MaspTxBlockIndexAttr, ReadFromEventAttributes, }; +use namada_ibc::{decode_message, extract_masp_tx_from_envelope, IbcMessage}; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; @@ -657,7 +658,11 @@ impl ShieldedContext { let tx = Tx::try_from(block[idx.0 as usize].as_ref()) .map_err(|e| Error::Other(e.to_string()))?; let extracted_masp_txs = - Self::extract_masp_tx(&tx, &masp_sections_refs).await?; + if let Some(masp_sections_refs) = masp_sections_refs { + Self::extract_masp_tx(&tx, &masp_sections_refs).await? + } else { + Self::extract_masp_tx_from_ibc_message(&tx)? + }; // Collect the current transactions shielded_txs.insert( IndexedTx { @@ -701,6 +706,32 @@ impl ShieldedContext { }) } + /// Extract the relevant shield portions from the IBC messages in [`Tx`] + fn extract_masp_tx_from_ibc_message( + tx: &Tx, + ) -> Result, Error> { + let mut masp_txs = Vec::new(); + for cmt in &tx.header.batch { + let tx_data = tx.data(cmt).ok_or_else(|| { + Error::Other("Missing transaction data".to_string()) + })?; + let ibc_msg = decode_message(&tx_data) + .map_err(|_| Error::Other("Invalid IBC message".to_string()))?; + if let IbcMessage::Envelope(ref envelope) = ibc_msg { + if let Some(masp_tx) = extract_masp_tx_from_envelope(envelope) { + masp_txs.push(masp_tx); + } + } + } + if !masp_txs.is_empty() { + Ok(masp_txs) + } else { + Err(Error::Other( + "IBC message doesn't have masp transaction".to_string(), + )) + } + } + /// Applies the given transaction to the supplied context. More precisely, /// the shielded transaction's outputs are added to the commitment tree. /// Newly discovered notes are associated to the supplied viewing keys. Note @@ -2332,7 +2363,7 @@ async fn get_indexed_masp_events_at_height( client: &C, height: BlockHeight, first_idx_to_query: Option, -) -> Result>, Error> { +) -> Result)>>, Error> { let first_idx_to_query = first_idx_to_query.unwrap_or_default(); Ok(client @@ -2356,7 +2387,7 @@ async fn get_indexed_masp_events_at_height( MaspTxBatchRefsAttr::read_from_event_attributes( &event.attributes, ) - .ok()?; + .ok(); Some((tx_index, masp_section_refs)) } else { diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index 4c3a6f80c1..4856ef12a2 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -33,7 +33,6 @@ use namada_governance::storage::proposal::StorageProposal; use namada_governance::utils::{ compute_proposal_result, ProposalResult, ProposalVotes, Vote, }; -use namada_ibc::is_ibc_denom; use namada_ibc::storage::{ ibc_trace_key, ibc_trace_key_prefix, is_ibc_trace_key, }; @@ -1187,25 +1186,9 @@ pub async fn validate_amount( InputAmount::Unvalidated(amt) => amt.canonical(), InputAmount::Validated(amt) => return Ok(amt), }; - let base_token = - if let Address::Internal(InternalAddress::IbcToken(ibc_token_hash)) = - token - { - extract_base_token(context, ibc_token_hash.clone(), None).await - } else { - Some(token.clone()) - }; - let denom = if let Some(token) = base_token { - convert_response::>( - RPC.vp() - .token() - .denomination(context.client(), &token) - .await, - )? - } else { - None - }; - let denom = match denom { + let denom = match convert_response::>( + RPC.vp().token().denomination(context.client(), token).await, + )? { Some(denom) => Ok(denom), None => { if force { @@ -1391,38 +1374,6 @@ pub async fn query_ibc_tokens( Ok(tokens) } -/// Obtain the base token of the given IBC token hash -pub async fn extract_base_token( - context: &N, - ibc_token_hash: IbcTokenHash, - owner: Option<&Address>, -) -> Option
{ - // First obtain the IBC denomination - let ibc_denom = query_ibc_denom( - context, - Address::Internal(InternalAddress::IbcToken(ibc_token_hash)) - .to_string(), - owner, - ) - .await; - // Then try to extract the base token - if let Some((_trace_path, base_token)) = is_ibc_denom(ibc_denom) { - match Address::decode(&base_token) { - // If the base token successfully decoded into an Address - Ok(base_token) => Some(base_token), - // Otherwise find the Address associated with the base token's alias - Err(_) => context - .wallet() - .await - .find_address(&base_token) - .map(|x| x.into_owned()), - } - } else { - // Otherwise the base token Address is unknown to this client - None - } -} - /// Look up the IBC denomination from a IbcToken. pub async fn query_ibc_denom( context: &N, diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 2fb043bfac..403b99248e 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -20,7 +20,7 @@ use masp_primitives::transaction::components::I128Sum; use masp_primitives::transaction::{builder, Transaction as MaspTransaction}; use masp_primitives::zip32::ExtendedFullViewingKey; use namada_account::{InitAccount, UpdateAccount}; -use namada_core::address::{Address, InternalAddress, MASP}; +use namada_core::address::{Address, IBC, MASP}; use namada_core::arith::checked; use namada_core::collections::HashSet; use namada_core::dec::Dec; @@ -51,8 +51,11 @@ use namada_governance::storage::proposal::{ InitProposalData, ProposalType, VoteProposalData, }; use namada_governance::storage::vote::ProposalVote; -use namada_ibc::storage::{channel_key, ibc_token}; -use namada_ibc::{is_nft_trace, MsgNftTransfer, MsgTransfer}; +use namada_ibc::storage::channel_key; +use namada_ibc::trace::is_nft_trace; +use namada_ibc::{ + decode_masp_tx_from_memo, IbcShieldingData, MsgNftTransfer, MsgTransfer, +}; use namada_proof_of_stake::parameters::{ PosParams, MAX_VALIDATOR_METADATA_LEN, }; @@ -2623,6 +2626,46 @@ pub async fn build_ibc_transfer( (args.source.spending_key().is_some() && refund_target.is_some()) || (args.source.address().is_some() && refund_target.is_none()) ); + // Reconstruct the memo for refunding if needed + let memo = if let Some(refund_target) = refund_target { + // Generate MASP transaction for refunding the token + let masp_transfer_data = vec![MaspTransferData { + source: TransferSource::Address(IBC), + target: TransferTarget::PaymentAddress(refund_target), + token: args.token.clone(), + amount: validated_amount, + }]; + let masp_tx = construct_shielded_parts( + context, + masp_transfer_data, + None, + !(args.tx.dry_run || args.tx.dry_run_wrapper), + ) + .await? + .map(|(shielded_transfer, _)| shielded_transfer.masp_tx); + let shielding_data = match &args.memo { + Some(memo) => { + if let Some(mut shielding_data) = decode_masp_tx_from_memo(memo) + { + shielding_data.refund = masp_tx; + shielding_data + } else { + return Err(Error::Other( + "The memo has been already set. The refunding for the \ + spending key can't be set" + .to_string(), + )); + } + } + None => IbcShieldingData { + shielding: None, + refund: masp_tx, + }, + }; + Some(shielding_data.into()) + } else { + args.memo.clone() + }; // If the refund address is given, set the refund address. It is used only // when refunding and won't affect the actual transfer because the actual // source will be the MASP address and the MASP transaction is generated by @@ -2643,7 +2686,7 @@ pub async fn build_ibc_transfer( token, sender, receiver: args.receiver.clone().into(), - memo: args.memo.clone().unwrap_or_default().into(), + memo: memo.unwrap_or_default().into(), }; let message = IbcMsgTransfer { port_id_on_a: args.port_id.clone(), @@ -3662,35 +3705,25 @@ pub async fn build_custom( /// Generate IBC shielded transfer pub async fn gen_ibc_shielding_transfer( context: &N, - args: args::GenIbcShieldedTransfer, -) -> Result> { - let source = Address::Internal(InternalAddress::Ibc); + args: args::GenIbcShieldingTransfer, +) -> Result> { + let source = IBC; let (src_port_id, src_channel_id) = get_ibc_src_port_channel(context, &args.port_id, &args.channel_id) .await?; let ibc_denom = rpc::query_ibc_denom(context, &args.token, Some(&source)).await; - let token = if args.refund { - if ibc_denom.contains('/') { - ibc_token(ibc_denom) - } else { - // the token is a base token - Address::decode(&ibc_denom) - .map_err(|e| Error::Other(format!("Invalid token: {e}")))? - } - } else { - // Need to check the prefix - namada_ibc::received_ibc_token( - &ibc_denom, - &src_port_id, - &src_channel_id, - &args.port_id, - &args.channel_id, - ) - .map_err(|e| { - Error::Other(format!("Getting IBC Token failed: error {e}")) - })? - }; + // Need to check the prefix + let token = namada_ibc::received_ibc_token( + &ibc_denom, + &src_port_id, + &src_channel_id, + &args.port_id, + &args.channel_id, + ) + .map_err(|e| { + Error::Other(format!("Getting IBC Token failed: error {e}")) + })?; let validated_amount = validate_amount(context, args.amount, &token, false).await?; @@ -3720,13 +3753,7 @@ pub async fn gen_ibc_shielding_transfer( .await .map_err(|err| TxSubmitError::MaspError(err.to_string()))?; - if let Some(shielded_transfer) = shielded_transfer { - let masp_tx_hash = shielded_transfer.masp_tx.txid().into(); - let transfer = token::Transfer::masp(masp_tx_hash); - Ok(Some((transfer, shielded_transfer.masp_tx))) - } else { - Ok(None) - } + Ok(shielded_transfer.map(|st| st.masp_tx)) } async fn get_ibc_src_port_channel( diff --git a/crates/sdk/src/wallet/mod.rs b/crates/sdk/src/wallet/mod.rs index 0b52117be2..3719b97663 100644 --- a/crates/sdk/src/wallet/mod.rs +++ b/crates/sdk/src/wallet/mod.rs @@ -22,7 +22,7 @@ use namada_core::masp::{ ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, }; use namada_core::time::DateTimeUtc; -use namada_ibc::is_ibc_denom; +use namada_ibc::trace::is_ibc_denom; pub use pre_genesis::gen_key_to_store; use rand::CryptoRng; use rand_core::RngCore; diff --git a/crates/tests/src/e2e/ibc_tests.rs b/crates/tests/src/e2e/ibc_tests.rs index 6b5d12cbeb..808288b812 100644 --- a/crates/tests/src/e2e/ibc_tests.rs +++ b/crates/tests/src/e2e/ibc_tests.rs @@ -157,9 +157,8 @@ fn run_ledger_ibc() -> Result<()> { BERTHA, ALBERT, token, - 50000, + 50_000_000_000, BERTHA_KEY, - true, )?; check_balances_after_non_ibc(&port_id_b, &channel_id_b, &test_b)?; @@ -234,7 +233,7 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { Some(ALBERT_KEY), &port_id_a, &channel_id_a, - false, + None, None, None, false, @@ -250,9 +249,8 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { BERTHA, ALBERT, token, - 50000, + 50_000_000_000, BERTHA_KEY, - true, )?; check_balances_after_non_ibc(&port_id_b, &channel_id_b, &test_b)?; @@ -268,11 +266,11 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { BERTHA, receiver.to_string(), ibc_denom, - 50000.0, + 50_000_000_000.0, Some(BERTHA_KEY), &port_id_b, &channel_id_b, - true, + None, None, None, false, @@ -289,7 +287,6 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { BTC, 100, ALBERT_KEY, - false, )?; // Send some token for masp fee payment transfer_on_chain( @@ -300,10 +297,19 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { NAM, 10_000, ALBERT_KEY, - false, )?; shielded_sync(&test_a, AA_VIEWING_KEY)?; // Shieded transfer from Chain A to Chain B + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); + let token_addr = find_address(&test_a, BTC)?.to_string(); + let memo_path = gen_masp_tx( + &test_b, + AB_PAYMENT_ADDRESS, + token_addr, + 1_000_000_000, + &port_id_b, + &channel_id_b, + )?; transfer( &test_a, A_SPENDING_KEY, @@ -313,8 +319,8 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { None, &port_id_a, &channel_id_a, - false, None, + Some(memo_path), None, false, )?; @@ -331,7 +337,7 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { Some(ALBERT_KEY), &port_id_a, &channel_id_a, - false, + None, None, None, false, @@ -354,9 +360,9 @@ fn run_ledger_ibc_with_hermes() -> Result<()> { Some(ALBERT_KEY), &port_id_a, &channel_id_a, - false, Some(Duration::new(10, 0)), None, + None, false, )?; // wait for the timeout @@ -420,7 +426,7 @@ fn ibc_namada_gaia() -> Result<()> { Some(ALBERT_KEY), &port_id_namada, &channel_id_namada, - false, + None, None, None, false, @@ -442,6 +448,7 @@ fn ibc_namada_gaia() -> Result<()> { 100000000, &port_id_gaia, &channel_id_gaia, + None, )?; wait_for_packet_relay(&port_id_gaia, &channel_id_gaia, &test)?; @@ -458,6 +465,7 @@ fn ibc_namada_gaia() -> Result<()> { 200, &port_id_gaia, &channel_id_gaia, + None, )?; wait_for_packet_relay(&port_id_gaia, &channel_id_gaia, &test)?; @@ -476,7 +484,7 @@ fn ibc_namada_gaia() -> Result<()> { Some(ALBERT_KEY), &port_id_namada, &channel_id_namada, - true, + None, None, None, false, @@ -486,6 +494,14 @@ fn ibc_namada_gaia() -> Result<()> { check_gaia_balance(&test_gaia, GAIA_USER, GAIA_COIN, 900)?; // Shielding transfer from Gaia to Namada + let memo_path = gen_masp_tx( + &test, + AA_PAYMENT_ADDRESS, + GAIA_COIN, + 100, + &port_id_namada, + &channel_id_namada, + )?; transfer_from_gaia( &test_gaia, GAIA_USER, @@ -494,6 +510,7 @@ fn ibc_namada_gaia() -> Result<()> { 100, &port_id_gaia, &channel_id_gaia, + Some(memo_path), )?; wait_for_packet_relay(&port_id_gaia, &channel_id_gaia, &test_gaia)?; @@ -510,7 +527,6 @@ fn ibc_namada_gaia() -> Result<()> { &ibc_denom, 50, ALBERT_KEY, - true, )?; check_balance(&test, AA_VIEWING_KEY, &ibc_denom, 50)?; check_balance(&test, AB_VIEWING_KEY, &ibc_denom, 50)?; @@ -525,7 +541,7 @@ fn ibc_namada_gaia() -> Result<()> { Some(BERTHA_KEY), &port_id_namada, &channel_id_namada, - true, + None, None, None, false, @@ -583,7 +599,6 @@ fn pgf_over_ibc_with_hermes() -> Result<()> { NAM, 100, ALBERT_KEY, - false, )?; // Proposal on Chain A @@ -667,7 +682,8 @@ fn proposal_ibc_token_inflation() -> Result<()> { setup_hermes(&test_a, &test_b)?; let port_id_a = "transfer".parse().unwrap(); - let (channel_id_a, _channel_id_b) = + let port_id_b = "transfer".parse().unwrap(); + let (channel_id_a, channel_id_b) = create_channel_with_hermes(&test_a, &test_b)?; // Start relaying @@ -678,6 +694,16 @@ fn proposal_ibc_token_inflation() -> Result<()> { wait_epochs(&test_b, 1)?; // Transfer 1 from Chain A to a z-address on Chain B + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); + let token_addr = find_address(&test_a, APFEL)?.to_string(); + let memo_path = gen_masp_tx( + &test_b, + AB_PAYMENT_ADDRESS, + token_addr, + 1_000_000, + &port_id_b, + &channel_id_b, + )?; transfer( &test_a, ALBERT, @@ -687,8 +713,8 @@ fn proposal_ibc_token_inflation() -> Result<()> { Some(ALBERT_KEY), &port_id_a, &channel_id_a, - false, None, + Some(memo_path), None, false, )?; @@ -755,7 +781,7 @@ fn ibc_rate_limit() -> Result<()> { Some(ALBERT_KEY), &port_id_a, &channel_id_a, - false, + None, None, None, false, @@ -771,7 +797,7 @@ fn ibc_rate_limit() -> Result<()> { Some(ALBERT_KEY), &port_id_a, &channel_id_a, - false, + None, None, // expect an error of the throughput limit Some( @@ -798,7 +824,7 @@ fn ibc_rate_limit() -> Result<()> { Some(ALBERT_KEY), &port_id_a, &channel_id_a, - false, + None, None, None, false, @@ -823,9 +849,9 @@ fn ibc_rate_limit() -> Result<()> { Some(ALBERT_KEY), &port_id_a, &channel_id_a, - false, Some(Duration::new(20, 0)), None, + None, false, )?; wait_for_packet_relay(&port_id_a, &channel_id_a, &test_a)?; @@ -1590,7 +1616,7 @@ fn transfer_token( Some(ALBERT_KEY), port_id_a, channel_id_a, - false, + None, None, None, false, @@ -1668,7 +1694,7 @@ fn try_invalid_transfers( Some(ALBERT_KEY), &"port".parse().unwrap(), channel_id_a, - false, + None, None, // the IBC denom can't be parsed when using an invalid port Some(&format!("Invalid IBC denom: {nam_addr}")), @@ -1685,7 +1711,7 @@ fn try_invalid_transfers( Some(ALBERT_KEY), port_id_a, &"channel-42".parse().unwrap(), - false, + None, None, Some("IBC token transfer error: context error: `ICS04 Channel error"), false, @@ -1703,12 +1729,11 @@ fn transfer_on_chain( token: impl AsRef, amount: u64, signer: impl AsRef, - force: bool, ) -> Result<()> { std::env::set_var(ENV_VAR_CHAIN_ID, test.net.chain_id.to_string()); let rpc = get_actor_rpc(test, Who::Validator(0)); let amount = amount.to_string(); - let mut tx_args = vec![ + let tx_args = vec![ kind.as_ref(), "--source", sender.as_ref(), @@ -1723,9 +1748,6 @@ fn transfer_on_chain( "--node", &rpc, ]; - if force { - tx_args.push("--force"); - } let mut client = run!(test, Bin::Client, tx_args, Some(120))?; client.exp_string(TX_APPLIED_SUCCESS)?; client.assert_success(); @@ -1753,11 +1775,11 @@ fn transfer_back( BERTHA, receiver.to_string(), ibc_denom, - 50000.0, + 50_000_000_000.0, Some(BERTHA_KEY), port_id_b, channel_id_b, - true, + None, None, None, false, @@ -1831,9 +1853,9 @@ fn transfer_timeout( Some(ALBERT_KEY), port_id_a, channel_id_a, - false, Some(Duration::new(5, 0)), None, + None, false, )?; let events = get_events(test_a, height)?; @@ -1966,8 +1988,8 @@ fn transfer( signer: Option<&str>, port_id: &PortId, channel_id: &ChannelId, - force: bool, timeout_sec: Option, + memo_path: Option, expected_err: Option<&str>, wait_reveal_pk: bool, ) -> Result { @@ -1994,9 +2016,6 @@ fn transfer( "--node", &rpc, ]; - if force { - tx_args.push("--force"); - } if let Some(signer) = signer { tx_args.extend_from_slice(&["--signing-keys", signer]); @@ -2010,6 +2029,15 @@ fn transfer( tx_args.push(&timeout); } + let memo = memo_path + .as_ref() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_default(); + if memo_path.is_some() { + tx_args.push("--memo-path"); + tx_args.push(&memo); + } + let mut client = run!(test, Bin::Client, tx_args, Some(300))?; match expected_err { Some(err) => { @@ -2197,12 +2225,13 @@ fn transfer_from_gaia( amount: u64, port_id: &PortId, channel_id: &ChannelId, + memo_path: Option, ) -> Result<()> { let port_id = port_id.to_string(); let channel_id = channel_id.to_string(); let amount = format!("{}{}", amount, token.as_ref()); let rpc = format!("tcp://{GAIA_RPC}"); - let args = vec![ + let mut args = vec![ "tx", "ibc-transfer", "transfer", @@ -2223,6 +2252,17 @@ fn transfer_from_gaia( "--yes", ]; + let memo = memo_path + .as_ref() + .map(|path| { + std::fs::read_to_string(path).expect("Reading memo file failed") + }) + .unwrap_or_default(); + if memo_path.is_some() { + args.push("--memo"); + args.push(&memo); + } + let mut gaia = run_gaia_cmd(test, args, Some(40))?; gaia.assert_success(); Ok(()) @@ -2408,7 +2448,7 @@ fn check_balances( // Check the balance on Chain B let ibc_denom = format!("{dest_port_id}/{dest_channel_id}/nam"); - check_balance(test_b, BERTHA, ibc_denom, 100000)?; + check_balance(test_b, BERTHA, ibc_denom, 100_000_000_000)?; Ok(()) } @@ -2454,10 +2494,10 @@ fn check_balances_after_non_ibc( ) -> Result<()> { // Check the source on Chain B let ibc_denom = format!("{port_id}/{channel_id}/nam"); - check_balance(test_b, BERTHA, &ibc_denom, 50000)?; + check_balance(test_b, BERTHA, &ibc_denom, 50_000_000_000)?; // Check the traget on Chain B - check_balance(test_b, ALBERT, &ibc_denom, 50000)?; + check_balance(test_b, ALBERT, &ibc_denom, 50_000_000_000)?; Ok(()) } @@ -2494,7 +2534,7 @@ fn check_shielded_balances( // Check the shielded balance on Chain B let ibc_denom = format!("{dest_port_id}/{dest_channel_id}/btc"); - check_balance(test_b, AB_VIEWING_KEY, ibc_denom, 10)?; + check_balance(test_b, AB_VIEWING_KEY, ibc_denom, 1_000_000_000)?; Ok(()) } @@ -2647,3 +2687,44 @@ fn shielded_sync(test: &Test, viewing_key: impl AsRef) -> Result<()> { client.assert_success(); Ok(()) } + +/// Get masp proof for the following IBC transfer from the destination chain +fn gen_masp_tx( + dst_test: &Test, + receiver: impl AsRef, + token: impl AsRef, + amount: u64, + port_id: &PortId, + channel_id: &ChannelId, +) -> Result { + std::env::set_var(ENV_VAR_CHAIN_ID, dst_test.net.chain_id.to_string()); + let rpc = get_actor_rpc(dst_test, Who::Validator(0)); + let output_folder = dst_test.test_dir.path().to_string_lossy(); + + let amount = amount.to_string(); + let args = vec![ + "ibc-gen-shielding", + "--output-folder-path", + &output_folder, + "--target", + receiver.as_ref(), + "--token", + token.as_ref(), + "--amount", + &amount, + "--port-id", + port_id.as_ref(), + "--channel-id", + channel_id.as_ref(), + "--node", + &rpc, + ]; + + let mut client = run!(dst_test, Bin::Client, args, Some(120))?; + let (_unread, matched) = + client.exp_regex("Output IBC shielding transfer .*")?; + let file_path = matched.trim().split(' ').last().expect("invalid output"); + client.assert_success(); + + Ok(PathBuf::from_str(file_path).expect("invalid file path")) +} diff --git a/crates/tests/src/vm_host_env/ibc.rs b/crates/tests/src/vm_host_env/ibc.rs index 2f51b431af..c5afecf4b0 100644 --- a/crates/tests/src/vm_host_env/ibc.rs +++ b/crates/tests/src/vm_host_env/ibc.rs @@ -59,9 +59,10 @@ pub use namada::ledger::ibc::storage::{ ack_key, channel_counter_key, channel_key, client_counter_key, client_state_key, client_update_height_key, client_update_timestamp_key, commitment_key, connection_counter_key, connection_key, - consensus_state_key, ibc_token, next_sequence_ack_key, - next_sequence_recv_key, next_sequence_send_key, port_key, receipt_key, + consensus_state_key, next_sequence_ack_key, next_sequence_recv_key, + next_sequence_send_key, port_key, receipt_key, }; +pub use namada::ledger::ibc::trace::ibc_token; use namada::ledger::native_vp::ibc::{ get_dummy_genesis_validator, get_dummy_header as tm_dummy_header, Ibc, }; diff --git a/crates/tests/src/vm_host_env/mod.rs b/crates/tests/src/vm_host_env/mod.rs index c844cb3832..8f7740630b 100644 --- a/crates/tests/src/vm_host_env/mod.rs +++ b/crates/tests/src/vm_host_env/mod.rs @@ -33,7 +33,7 @@ mod tests { use namada::ibc::context::transfer_mod::testing::DummyTransferModule; use namada::ibc::primitives::ToProto; use namada::ibc::Error as IbcActionError; - use namada::ledger::ibc::storage as ibc_storage; + use namada::ledger::ibc::{storage as ibc_storage, trace as ibc_trace}; use namada::ledger::native_vp::ibc::{ get_dummy_header as tm_dummy_header, Error as IbcError, }; @@ -1304,7 +1304,7 @@ mod tests { writes.extend(channel_writes); // the origin-specific token let denom = format!("{}/{}/{}", port_id, channel_id, token); - let ibc_token = ibc_storage::ibc_token(&denom); + let ibc_token = ibc_trace::ibc_token(&denom); let balance_key = token::storage_key::balance_key(&ibc_token, &sender); let init_bal = Amount::native_whole(100); writes.insert(balance_key.clone(), init_bal.serialize_to_vec()); diff --git a/crates/tx/src/action.rs b/crates/tx/src/action.rs index f8826b47c6..dbd2e4c20c 100644 --- a/crates/tx/src/action.rs +++ b/crates/tx/src/action.rs @@ -27,6 +27,7 @@ pub enum Action { Gov(GovAction), Pgf(PgfAction), Masp(MaspAction), + IbcShielding, } /// PoS tx actions. @@ -133,3 +134,13 @@ pub fn get_masp_section_ref( } })) } + +/// Helper function to check if the action is IBC shielding transfer +pub fn is_ibc_shielding_transfer( + reader: &T, +) -> Result::Err> { + Ok(reader + .read_actions()? + .iter() + .any(|action| matches!(action, Action::IbcShielding))) +} diff --git a/crates/tx/src/data/mod.rs b/crates/tx/src/data/mod.rs index 8c6c1b4095..2cf49ec745 100644 --- a/crates/tx/src/data/mod.rs +++ b/crates/tx/src/data/mod.rs @@ -333,6 +333,8 @@ pub struct ExtendedTxResult { pub tx_result: TxResult, /// The optional references to masp sections pub masp_tx_refs: MaspTxRefs, + /// The flag for IBC shielding transfer + pub is_ibc_shielding: bool, } impl Default for ExtendedTxResult { @@ -340,6 +342,7 @@ impl Default for ExtendedTxResult { Self { tx_result: Default::default(), masp_tx_refs: Default::default(), + is_ibc_shielding: Default::default(), } } } @@ -392,6 +395,7 @@ impl TxResult { ExtendedTxResult { tx_result: self, masp_tx_refs: masp_tx_refs.unwrap_or_default(), + is_ibc_shielding: false, } } } diff --git a/crates/tx_prelude/src/ibc.rs b/crates/tx_prelude/src/ibc.rs index 678e27ccf8..b11625656a 100644 --- a/crates/tx_prelude/src/ibc.rs +++ b/crates/tx_prelude/src/ibc.rs @@ -8,9 +8,9 @@ use namada_core::address::Address; use namada_core::token::Amount; pub use namada_ibc::event::{IbcEvent, IbcEventType}; pub use namada_ibc::storage::{ - burn_tokens, ibc_token, is_ibc_key, mint_limit_key, mint_tokens, - throughput_limit_key, + burn_tokens, is_ibc_key, mint_limit_key, mint_tokens, throughput_limit_key, }; +pub use namada_ibc::trace::ibc_token; pub use namada_ibc::{ IbcActions, IbcCommonContext, IbcStorageContext, NftTransferModule, ProofSpec, TransferModule, diff --git a/scripts/get_hermes.sh b/scripts/get_hermes.sh index 1626349358..9ae6c1a829 100755 --- a/scripts/get_hermes.sh +++ b/scripts/get_hermes.sh @@ -2,9 +2,9 @@ set -Eo pipefail -HERMES_MAJORMINOR="1.8" -HERMES_PATCH="2" -HERMES_SUFFIX="-namada-beta12-rc4" +HERMES_MAJORMINOR="1.9" +HERMES_PATCH="0" +HERMES_SUFFIX="-namada-beta13-rc2" HERMES_REPO="https://github.com/heliaxdev/hermes" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 211d4b866f..b1f8b1655c 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3904,6 +3904,7 @@ name = "namada_ibc" version = "0.39.0" dependencies = [ "borsh 1.4.0", + "data-encoding", "ibc", "ibc-derive", "ibc-testkit", @@ -3924,6 +3925,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.9.9", + "smooth-operator", "thiserror", "tracing", ] diff --git a/wasm/tx_ibc/src/lib.rs b/wasm/tx_ibc/src/lib.rs index 65bebff920..78963875f4 100644 --- a/wasm/tx_ibc/src/lib.rs +++ b/wasm/tx_ibc/src/lib.rs @@ -11,10 +11,10 @@ use namada_tx_prelude::*; #[transaction] fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { let data = ctx.get_tx_data(&tx_data)?; - let transfer = + let (transfer, masp_tx) = ibc::ibc_actions(ctx).execute(&data).into_storage_result()?; - if let Some(transfers) = transfer { + let masp_section_ref = if let Some(transfers) = transfer { // Prepare the sources of the multi-transfer let sources = transfers .sources @@ -37,8 +37,14 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { token::multi_transfer(ctx, &sources, &targets) .wrap_err("Token transfer failed")?; - if let Some(masp_section_ref) = transfers.shielded_section_hash { - let shielded = tx_data + transfers.shielded_section_hash + } else { + None + }; + + let shielded = if let Some(masp_section_ref) = masp_section_ref { + Some( + tx_data .tx .get_masp_section(&masp_section_ref) .cloned() @@ -48,13 +54,20 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { .map_err(|err| { ctx.set_commitment_sentinel(); err - })?; - token::utils::handle_masp_tx(ctx, &shielded).wrap_err( - "Encountered error while handling MASP transaction", - )?; - update_masp_note_commitment_tree(&shielded) - .wrap_err("Failed to update the MASP commitment tree")?; + })?, + ) + } else { + masp_tx + }; + if let Some(shielded) = shielded { + token::utils::handle_masp_tx(ctx, &shielded) + .wrap_err("Encountered error while handling MASP transaction")?; + update_masp_note_commitment_tree(&shielded) + .wrap_err("Failed to update the MASP commitment tree")?; + if let Some(masp_section_ref) = masp_section_ref { ctx.push_action(Action::Masp(MaspAction { masp_section_ref }))?; + } else { + ctx.push_action(Action::IbcShielding)?; } } diff --git a/wasm/vp_implicit/src/lib.rs b/wasm/vp_implicit/src/lib.rs index a1787c9665..7a0b237d4a 100644 --- a/wasm/vp_implicit/src/lib.rs +++ b/wasm/vp_implicit/src/lib.rs @@ -102,6 +102,7 @@ fn validate_tx( &addr, )?, Action::Masp(_) => (), + Action::IbcShielding => (), } } diff --git a/wasm/vp_user/src/lib.rs b/wasm/vp_user/src/lib.rs index b20142d1d1..0cfe763c7d 100644 --- a/wasm/vp_user/src/lib.rs +++ b/wasm/vp_user/src/lib.rs @@ -102,6 +102,7 @@ fn validate_tx( &addr, )?, Action::Masp(_) => (), + Action::IbcShielding => (), } } diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index 3759b88470..78fb4a614c 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -3940,6 +3940,7 @@ name = "namada_ibc" version = "0.39.0" dependencies = [ "borsh 1.2.1", + "data-encoding", "ibc", "ibc-derive", "ibc-testkit", @@ -3960,6 +3961,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.9.9", + "smooth-operator", "thiserror", "tracing", ]