From 0a6f16feb303a2854456ce8a9df8f0dc46638c13 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 24 Mar 2025 13:10:03 +0000 Subject: [PATCH 01/40] Add FMD flag ciphertext sechash to transfer data --- crates/core/src/masp.rs | 39 ++++++++++++++++++++ crates/ibc/src/msg.rs | 1 + crates/light_sdk/src/transaction/transfer.rs | 8 +++- crates/node/src/bench_utils.rs | 33 ++++++++++------- crates/sdk/src/signing.rs | 35 ++++++++++-------- crates/sdk/src/tx.rs | 32 +++++++++++++--- crates/shielded_token/src/lib.rs | 4 +- crates/token/src/lib.rs | 24 +++++++++--- crates/token/src/tx.rs | 4 +- wasm/tx_ibc/src/lib.rs | 2 +- 10 files changed, 137 insertions(+), 45 deletions(-) diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index fe8fecee579..8f992e9b3c1 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -23,6 +23,7 @@ use sha2::Sha256; use crate::address::{Address, DecodeError, HASH_HEX_LEN, IBC, MASP}; use crate::borsh::BorshSerializeExt; use crate::chain::Epoch; +use crate::hash::Hash; use crate::impl_display_and_from_str_via_format; use crate::string_encoding::{ self, MASP_EXT_FULL_VIEWING_KEY_HRP, MASP_EXT_SPENDING_KEY_HRP, @@ -85,6 +86,44 @@ impl Display for MaspTxId { } } +/// Pointers to MASP data included in Namada transactions. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Serialize, + Deserialize, + Clone, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Debug, + Eq, + PartialEq, + Copy, + Ord, + PartialOrd, + Hash, +)] +pub struct MaspTxData { + /// Id of the MASP transaction. + /// + /// This is used to look-up a MASP transaction section. + pub masp_tx_id: MaspTxId, + /// Section hash of the FMD flag ciphertext. + /// + /// This is used to look-up a transaction data section + /// containing an FMD flag ciphertext. + pub flag_ciphertext_sechash: Hash, +} + +impl Display for MaspTxData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("") + .field("masp_tx_id", &self.masp_tx_id) + .field("flag_ciphertext_sechash", &self.flag_ciphertext_sechash) + .finish() + } +} + /// Wrapper type around `Epoch` for type safe operations involving the masp /// epoch #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] diff --git a/crates/ibc/src/msg.rs b/crates/ibc/src/msg.rs index d38466bb5e5..00b7610461f 100644 --- a/crates/ibc/src/msg.rs +++ b/crates/ibc/src/msg.rs @@ -238,6 +238,7 @@ impl BorshSchema for MsgNftTransfer { /// Shielding data in IBC packet memo #[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] +// TODO: add flag ciphertext here pub struct IbcShieldingData(pub MaspTransaction); impl From<&IbcShieldingData> for String { diff --git a/crates/light_sdk/src/transaction/transfer.rs b/crates/light_sdk/src/transaction/transfer.rs index 2e0e6833908..e3c51c6a08d 100644 --- a/crates/light_sdk/src/transaction/transfer.rs +++ b/crates/light_sdk/src/transaction/transfer.rs @@ -2,7 +2,7 @@ use namada_sdk::address::Address; use namada_sdk::hash::Hash; use namada_sdk::key::common; pub use namada_sdk::token::{ - DenominatedAmount, MaspTransaction, MaspTxId, Transfer, + DenominatedAmount, MaspTransaction, MaspTxData, MaspTxId, Transfer, }; use namada_sdk::tx::data::GasLimit; use namada_sdk::tx::{Authorization, TX_TRANSFER_WASM, Tx, TxError}; @@ -27,10 +27,14 @@ impl TransferBuilder { /// Build a shielded transfer transaction from the given parameters pub fn shielded( shielded_section_hash: MaspTxId, + flag_ciphertext_sechash: Hash, transaction: MaspTransaction, args: GlobalArgs, ) -> Self { - let data = Transfer::masp(shielded_section_hash); + let data = Transfer::masp(MaspTxData { + masp_tx_id: shielded_section_hash, + flag_ciphertext_sechash, + }); let mut tx = transaction::build_tx(args, data, TX_TRANSFER_WASM.to_string()); tx.add_masp_tx_section(transaction); diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index 2e746b18459..a4478290509 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -89,7 +89,9 @@ use namada_sdk::state::StorageRead; use namada_sdk::state::write_log::StorageModification; use namada_sdk::storage::{Key, KeySeg, TxIndex}; use namada_sdk::time::DateTimeUtc; -use namada_sdk::token::{self, Amount, DenominatedAmount, Transfer}; +use namada_sdk::token::{ + self, Amount, DenominatedAmount, MaspTxData, Transfer, +}; use namada_sdk::tx::data::pos::Bond; use namada_sdk::tx::data::{ BatchedTxResult, Fee, TxResult, VpsResult, compute_inner_tx_hash, @@ -1279,13 +1281,18 @@ impl BenchShieldedCtx { ) .expect("MASP must have shielded part"); - let shielded_section_hash = shielded.txid().into(); + let shielded_data = MaspTxData { + masp_tx_id: shielded.txid().into(), + // TODO: change this to the actual sechash + // of the fmd flag + flag_ciphertext_sechash: Default::default(), + }; let tx = if source.effective_address() == MASP && target.effective_address() == MASP { namada.client().read().generate_tx( TX_TRANSFER_WASM, - Transfer::masp(shielded_section_hash), + Transfer::masp(shielded_data), Some(shielded), None, vec![&defaults::albert_keypair()], @@ -1293,7 +1300,7 @@ impl BenchShieldedCtx { } else if target.effective_address() == MASP { namada.client().read().generate_tx( TX_TRANSFER_WASM, - Transfer::masp(shielded_section_hash) + Transfer::masp(shielded_data) .transfer( source.effective_address(), MASP, @@ -1308,7 +1315,7 @@ impl BenchShieldedCtx { } else { namada.client().read().generate_tx( TX_TRANSFER_WASM, - Transfer::masp(shielded_section_hash) + Transfer::masp(shielded_data) .transfer( MASP, target.effective_address(), @@ -1382,6 +1389,7 @@ impl BenchShieldedCtx { let vectorized_transfer = Transfer::deserialize(&mut tx.tx.data(&tx.cmt).unwrap().as_slice()) .unwrap(); + let masp_tx_id = vectorized_transfer.masp_tx_id().unwrap(); let sources = vec![vectorized_transfer.sources.into_iter().next().unwrap()] .into_iter() @@ -1393,15 +1401,14 @@ impl BenchShieldedCtx { let transfer = Transfer { sources, targets, - shielded_section_hash: Some( - vectorized_transfer.shielded_section_hash.unwrap(), - ), + shielded_data: Some(MaspTxData { + masp_tx_id, + // TODO: change this to the actual sechash + // of the fmd flag + flag_ciphertext_sechash: Default::default(), + }), }; - let masp_tx = tx - .tx - .get_masp_section(&transfer.shielded_section_hash.unwrap()) - .unwrap() - .clone(); + let masp_tx = tx.tx.get_masp_section(&masp_tx_id).unwrap().clone(); let msg = MsgTransfer:: { message: msg, transfer: Some(transfer), diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index abca6c7d978..0513876f566 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -1426,12 +1426,9 @@ pub async fn to_ledger_vector( // To facilitate lookups of MASP AssetTypes let mut asset_types = HashMap::new(); - let builder = find_masp_builder( - tx, - transfer.shielded_section_hash, - &mut asset_types, - ) - .map_err(|_| Error::Other("Invalid Data".to_string()))?; + let builder = + find_masp_builder(tx, transfer.masp_tx_id(), &mut asset_types) + .map_err(|_| Error::Other("Invalid Data".to_string()))?; make_ledger_token_transfer_endpoints( tokens, &mut tv.output, @@ -1526,7 +1523,7 @@ pub async fn to_ledger_vector( let mut asset_types = HashMap::new(); let builder = find_masp_builder( tx, - transfer.shielded_section_hash, + transfer.masp_tx_id(), &mut asset_types, ) .map_err(|_| Error::Other("Invalid Data".to_string()))?; @@ -1695,7 +1692,7 @@ pub async fn to_ledger_vector( let mut asset_types = HashMap::new(); let builder = find_masp_builder( tx, - transfer.shielded_section_hash, + transfer.masp_tx_id(), &mut asset_types, ) .map_err(|_| Error::Other("Invalid Data".to_string()))?; @@ -2190,7 +2187,7 @@ mod test_signing { use namada_core::hash::Hash; use namada_core::ibc::PGFIbcTarget; use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; - use namada_core::masp::TxIdInner; + use namada_core::masp::{MaspTxData, TxIdInner}; use namada_core::token::{Denomination, MaspDigitPos}; use namada_governance::storage::proposal::PGFInternalTarget; use namada_io::client::EncodedResponseQuery; @@ -2616,7 +2613,7 @@ mod test_signing { }, DenominatedAmount::new(Amount::from_u64(2), 0.into()), )]), - shielded_section_hash: None, + shielded_data: None, }; let tokens = HashMap::from([ (Address::Internal(InternalAddress::Governance), "SuperMoney"), @@ -2768,15 +2765,19 @@ mod test_signing { fn test_find_masp_builder() { let mut tx = Tx::new(ChainId::default(), None); let mut asset_types = Default::default(); - let shielded_section_hash = MaspTxId::from(TxIdInner::from_bytes([ + let masp_tx_id = MaspTxId::from(TxIdInner::from_bytes([ 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ])); + let shielded_data = MaspTxData { + masp_tx_id, + flag_ciphertext_sechash: Hash::zero(), + }; // no masp builder present assert_eq!( find_masp_builder( &tx, - Some(shielded_section_hash), + Some(shielded_data.masp_tx_id), &mut asset_types ) .expect("Test failed"), @@ -2837,9 +2838,13 @@ mod test_signing { assert!(asset_types.is_empty()); // now we should find the builder - find_masp_builder(&tx, Some(shielded_section_hash), &mut asset_types) - .expect("Test failed") - .expect("Test failed"); + find_masp_builder( + &tx, + Some(shielded_data.masp_tx_id), + &mut asset_types, + ) + .expect("Test failed") + .expect("Test failed"); assert_eq!( asset_types .values() diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index dc79c58e6da..09a554c22e2 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -41,7 +41,9 @@ use namada_core::ibc::core::client::types::Height as IbcHeight; use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; use namada_core::ibc::primitives::{IntoTimestamp, Timestamp as IbcTimestamp}; use namada_core::key::{self, *}; -use namada_core::masp::{AssetData, MaspEpoch, TransferSource, TransferTarget}; +use namada_core::masp::{ + AssetData, MaspEpoch, MaspTxData, TransferSource, TransferTarget, +}; use namada_core::storage; use namada_core::time::DateTimeUtc; use namada_events::extend::EventAttributeEntry; @@ -2819,7 +2821,12 @@ pub async fn build_ibc_transfer( .map(|(shielded_transfer, asset_types)| { let masp_tx_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx.clone()).1; - transfer.shielded_section_hash = Some(masp_tx_hash); + transfer.shielded_data = Some(MaspTxData { + masp_tx_id: masp_tx_hash, + // TODO: change this to the actual sechash + // of the fmd flag + flag_ciphertext_sechash: Hash::zero(), + }); signing_data.shielded_hash = Some(masp_tx_hash); tx.add_masp_builder(MaspBuilder { asset_types, @@ -3265,7 +3272,12 @@ pub async fn build_shielded_transfer( target: section_hash, }); - data.shielded_section_hash = Some(section_hash); + data.shielded_data = Some(MaspTxData { + masp_tx_id: section_hash, + // TODO: change this to the actual sechash + // of the fmd flag + flag_ciphertext_sechash: Hash::zero(), + }); signing_data.shielded_hash = Some(section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) @@ -3435,7 +3447,12 @@ pub async fn build_shielding_transfer( target: shielded_section_hash, }); - data.shielded_section_hash = Some(shielded_section_hash); + data.shielded_data = Some(MaspTxData { + masp_tx_id: shielded_section_hash, + // TODO: change this to the actual sechash + // of the fmd flag + flag_ciphertext_sechash: Hash::zero(), + }); signing_data.shielded_hash = Some(shielded_section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) @@ -3558,7 +3575,12 @@ pub async fn build_unshielding_transfer( target: shielded_section_hash, }); - data.shielded_section_hash = Some(shielded_section_hash); + data.shielded_data = Some(MaspTxData { + masp_tx_id: shielded_section_hash, + // TODO: change this to the actual sechash + // of the fmd flag + flag_ciphertext_sechash: Hash::zero(), + }); signing_data.shielded_hash = Some(shielded_section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) diff --git a/crates/shielded_token/src/lib.rs b/crates/shielded_token/src/lib.rs index cf9f837961e..45850916508 100644 --- a/crates/shielded_token/src/lib.rs +++ b/crates/shielded_token/src/lib.rs @@ -31,7 +31,9 @@ use std::str::FromStr; use namada_core::borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; pub use namada_core::dec::Dec; -pub use namada_core::masp::{MaspEpoch, MaspTransaction, MaspTxId, MaspValue}; +pub use namada_core::masp::{ + MaspEpoch, MaspTransaction, MaspTxData, MaspTxId, MaspValue, +}; pub use namada_state::{ ConversionLeaf, ConversionState, Error, Key, OptionExt, Result, ResultExt, StorageRead, StorageWrite, WithConversionState, diff --git a/crates/token/src/lib.rs b/crates/token/src/lib.rs index 361a1424ff6..24a36d5d27d 100644 --- a/crates/token/src/lib.rs +++ b/crates/token/src/lib.rs @@ -182,8 +182,8 @@ pub struct Transfer { pub sources: BTreeMap, /// Targets of this transfer pub targets: BTreeMap, - /// Hash of tx section that contains the MASP transaction - pub shielded_section_hash: Option, + /// Pointers to MASP data within a transfer tx + pub shielded_data: Option, } /// References to the transparent sections of a [`Transfer`]. @@ -197,13 +197,19 @@ pub struct TransparentTransfersRef<'a> { impl Transfer { /// Create a MASP transaction - pub fn masp(hash: MaspTxId) -> Self { + pub fn masp(shielded_data: MaspTxData) -> Self { Self { - shielded_section_hash: Some(hash), + shielded_data: Some(shielded_data), ..Self::default() } } + /// Return the (optional) MASP tx id associated with this [`Transfer`]. + pub fn masp_tx_id(&self) -> Option { + self.shielded_data + .map(|shielded_data| shielded_data.masp_tx_id) + } + /// Set the key to the given amount fn set( map: &mut BTreeMap, @@ -351,7 +357,10 @@ pub mod testing { use namada_core::address::testing::arb_non_internal_address; use namada_core::address::{Address, MASP}; use namada_core::collections::HashMap; - use namada_core::masp::{AssetData, TAddrData, encode_asset_type}; + use namada_core::hash::Hash; + use namada_core::masp::{ + AssetData, MaspTxData, TAddrData, encode_asset_type, + }; pub use namada_core::token::*; use namada_shielded_token::masp::testing::{ MockTxProver, TestCsprng, arb_masp_epoch, arb_output_descriptions, @@ -522,7 +531,10 @@ pub mod testing { &mut rng, &mut rng_build_params, ).unwrap(); - transfer.shielded_section_hash = Some(masp_tx.txid().into()); + transfer.shielded_data = Some(MaspTxData { + masp_tx_id: masp_tx.txid().into(), + flag_ciphertext_sechash: Hash::zero(), + }); (transfer, ShieldedTransfer { builder: builder.map_builder(WalletMap), metadata, diff --git a/crates/token/src/tx.rs b/crates/token/src/tx.rs index d60eeb1a247..0cc2488a63d 100644 --- a/crates/token/src/tx.rs +++ b/crates/token/src/tx.rs @@ -42,10 +42,10 @@ where }; // Apply the shielded transfer if there is a link to one - if let Some(masp_section_ref) = transfers.shielded_section_hash { + if let Some(shielded_data) = transfers.shielded_data { apply_shielded_transfer( env, - masp_section_ref, + shielded_data.masp_tx_id, debited_accounts, tokens, tx_data, diff --git a/wasm/tx_ibc/src/lib.rs b/wasm/tx_ibc/src/lib.rs index 01ca4f9733c..ee2bcb60c28 100644 --- a/wasm/tx_ibc/src/lib.rs +++ b/wasm/tx_ibc/src/lib.rs @@ -22,7 +22,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { Default::default() }; - (transfers.shielded_section_hash, tokens) + (transfers.masp_tx_id(), tokens) } else { (None, Default::default()) }; From bf818efdf9f19ac5044624fa2c61790e291b0457 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 15 Apr 2025 10:40:23 +0100 Subject: [PATCH 02/40] Add flag ciphertexts domain type --- crates/core/src/masp.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index 8f992e9b3c1..67188b98751 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -124,6 +124,26 @@ impl Display for MaspTxData { } } +/// FMD flag ciphertexts. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Serialize, + Deserialize, + Clone, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Debug, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, +)] +pub struct FlagCiphertext { + inner: Vec, +} + /// Wrapper type around `Epoch` for type safe operations involving the masp /// epoch #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] From 1398d696e375691908c0d4713d48272e34b8445a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Apr 2025 10:07:23 +0100 Subject: [PATCH 03/40] Allow large client ctx enum variant --- crates/apps_lib/src/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index b918a1b34e3..d6d2c7a607e 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -500,6 +500,7 @@ pub mod cmds { } #[derive(Clone, Debug)] + #[allow(clippy::large_enum_variant)] pub enum NamadaClientWithContext { // Ledger cmds TxCustom(TxCustom), From f2a0b7db9d3690ff6e2338f7b6133edd23b4f45c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Apr 2025 10:09:16 +0100 Subject: [PATCH 04/40] Extend IBC shielding data with flag ciphertext --- crates/apps_lib/src/client/tx.rs | 6 +++++- crates/core/src/masp.rs | 2 ++ crates/ibc/src/msg.rs | 24 +++++++++++++++++++----- crates/sdk/src/args.rs | 6 +++++- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 6689cd51191..3f420cc0769 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1945,7 +1945,11 @@ pub async fn gen_ibc_shielding_transfer( }; 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); + let bytes = convert_masp_tx_to_ibc_memo( + masp_tx, + // TODO: add actual flag ciphertext + Default::default(), + ); out.write_all(bytes.as_bytes()) .expect("Writing IBC MASP transaction file failed."); println!( diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index 67188b98751..42450d46b47 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -140,6 +140,8 @@ impl Display for MaspTxData { PartialOrd, Hash, )] +// TODO: remove Default derive +#[derive(Default)] pub struct FlagCiphertext { inner: Vec, } diff --git a/crates/ibc/src/msg.rs b/crates/ibc/src/msg.rs index 00b7610461f..b93fd17d373 100644 --- a/crates/ibc/src/msg.rs +++ b/crates/ibc/src/msg.rs @@ -20,6 +20,7 @@ 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_core::masp::FlagCiphertext; use namada_core::string_encoding::StringEncoded; use serde::{Deserialize, Serialize}; @@ -238,8 +239,12 @@ impl BorshSchema for MsgNftTransfer { /// Shielding data in IBC packet memo #[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] -// TODO: add flag ciphertext here -pub struct IbcShieldingData(pub MaspTransaction); +pub struct IbcShieldingData { + /// MASP transaction forwarded over IBC. + pub masp_tx: MaspTransaction, + /// Flag ciphertext to signal the owner of the new note(s). + pub flag_ciphertext: FlagCiphertext, +} impl From<&IbcShieldingData> for String { fn from(data: &IbcShieldingData) -> Self { @@ -302,7 +307,9 @@ pub fn decode_ibc_shielding_data( /// Extract MASP transaction from IBC packet memo pub fn extract_masp_tx_from_packet(packet: &Packet) -> Option { let memo = extract_memo_from_packet(packet, &packet.port_id_on_b)?; - decode_ibc_shielding_data(memo).map(|data| data.0) + + decode_ibc_shielding_data(memo) + .map(|IbcShieldingData { masp_tx, .. }| masp_tx) } fn extract_memo_from_packet( @@ -367,6 +374,13 @@ pub fn extract_traces_from_recv_msg( } /// Get IBC memo string from MASP transaction for receiving -pub fn convert_masp_tx_to_ibc_memo(transaction: &MaspTransaction) -> String { - IbcShieldingData(transaction.clone()).into() +pub fn convert_masp_tx_to_ibc_memo( + masp_tx: MaspTransaction, + flag_ciphertext: FlagCiphertext, +) -> String { + IbcShieldingData { + masp_tx, + flag_ciphertext, + } + .into() } diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index b9ba33fc322..5d228b7c6fe 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -752,7 +752,11 @@ impl TxOsmosisSwap { serde_json::to_value(&NamadaMemo { namada: NamadaMemoData::OsmosisSwap { shielding_data: StringEncoded::new( - IbcShieldingData(shielding_tx), + IbcShieldingData { + masp_tx: shielding_tx, + // TODO: add actual flag ciphertext here + flag_ciphertext: Default::default(), + }, ), shielded_amount: amount_to_shield, overflow_receiver, From 1e9c6c2776c40133888bb47fe77bff537d5d575c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Apr 2025 09:30:01 +0100 Subject: [PATCH 05/40] Import FMD dependency to workspace --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index ddfb6bf3a3a..b82993b90b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -204,6 +204,7 @@ owo-colors = "4.1" parity-wasm = { version = "0.45", features = ["sign_ext"] } paste = "1.0" patricia_tree = "0.8" +polyfuzzy = "0.5.0" pretty_assertions = "1.4" primitive-types = "0.13" proc-macro2 = "1.0" From 402c423c4d43e1a7aaedbf5aea9153f568e2dadf Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Apr 2025 09:33:57 +0100 Subject: [PATCH 06/40] Move FMD code to new module --- crates/core/src/masp.rs | 25 +++---------------------- crates/core/src/masp/fmd.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 crates/core/src/masp/fmd.rs diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index 42450d46b47..4fe585d8e0e 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -1,5 +1,7 @@ //! MASP types +mod fmd; + use std::collections::BTreeMap; use std::fmt::Display; use std::num::ParseIntError; @@ -20,6 +22,7 @@ use ripemd::Digest as RipemdDigest; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::Sha256; +pub use self::fmd::FlagCiphertext; use crate::address::{Address, DecodeError, HASH_HEX_LEN, IBC, MASP}; use crate::borsh::BorshSerializeExt; use crate::chain::Epoch; @@ -124,28 +127,6 @@ impl Display for MaspTxData { } } -/// FMD flag ciphertexts. -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Serialize, - Deserialize, - Clone, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Debug, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, -)] -// TODO: remove Default derive -#[derive(Default)] -pub struct FlagCiphertext { - inner: Vec, -} - /// Wrapper type around `Epoch` for type safe operations involving the masp /// epoch #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs new file mode 100644 index 00000000000..53f56eb0684 --- /dev/null +++ b/crates/core/src/masp/fmd.rs @@ -0,0 +1,26 @@ +//! Fuzzy message detection MASP primitives. + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use serde::{Deserialize, Serialize}; + +/// FMD flag ciphertexts. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Serialize, + Deserialize, + Clone, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Debug, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, +)] +// TODO: remove Default derive +#[derive(Default)] +pub struct FlagCiphertext { + inner: Vec, +} From 3420539c8c52700646706f7860a42452c36acc2a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Apr 2025 11:34:40 +0100 Subject: [PATCH 07/40] Import new deps --- Cargo.lock | 17 +++++++++++ Cargo.toml | 3 +- crates/core/Cargo.toml | 2 ++ wasm/Cargo.lock | 55 +++++++++++++++++++++++++++++++++-- wasm_for_tests/Cargo.lock | 61 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 133 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4539542bdb7..794ff3d62bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1740,7 +1740,9 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", + "rand_core", "rustc_version", + "serde", "subtle", "zeroize", ] @@ -5624,6 +5626,7 @@ dependencies = [ "arbitrary", "assert_matches", "bech32 0.11.0", + "bincode", "borsh", "chrono", "data-encoding", @@ -5647,6 +5650,7 @@ dependencies = [ "num-traits", "num256", "num_enum", + "polyfuzzy", "primitive-types 0.13.1", "proptest", "prost-types 0.13.5", @@ -7301,6 +7305,19 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyfuzzy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ad377e0383a3332e1deb427b97c8670a954efa00add87d2071daab421dae24" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", +] + [[package]] name = "portable-atomic" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index b82993b90b9..8e7451febec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,13 +117,14 @@ base58 = "0.2" base64 = "0.22" bech32 = "0.11" bimap = {version = "0.6", features = ["serde"]} +bincode = "1.3.3" bit-set = "0.8" bitflags = { version = "2.5", features = ["serde"] } blake2b-rs = "0.2" +borsh = {version = "1.2", features = ["unstable__schema", "derive"]} byte-unit = "5.1" byteorder = "1.4" bytes = "1.1" -borsh = {version = "1.2", features = ["unstable__schema", "derive"]} cargo_metadata = "0.19" chrono = {version = "0.4", default-features = false, features = ["clock", "std"]} circular-queue = "0.2" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9d3ce842959..de9312025da 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -39,6 +39,7 @@ namada_migrations = { workspace = true, optional = true } arbitrary = { workspace = true, optional = true } arse-merkle-tree.workspace = true bech32.workspace = true +bincode.workspace = true borsh.workspace = true chrono.workspace = true data-encoding.workspace = true @@ -60,6 +61,7 @@ num_enum.workspace = true num-integer.workspace = true num-rational.workspace = true num-traits.workspace = true +polyfuzzy = { workspace = true, features = ["serde"] } primitive-types.workspace = true proptest = { workspace = true, optional = true } prost-types.workspace = true diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 9d082bffa66..426bffd929a 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -486,6 +486,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip0039" version = "0.12.0" @@ -1290,6 +1299,33 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rand_core", + "rustc_version", + "serde", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -4409,6 +4445,7 @@ name = "namada_core" version = "0.149.1" dependencies = [ "bech32 0.11.0", + "bincode", "borsh", "chrono", "data-encoding", @@ -4430,6 +4467,7 @@ dependencies = [ "num-traits", "num256", "num_enum", + "polyfuzzy", "primitive-types 0.13.1", "proptest", "prost-types", @@ -5661,6 +5699,19 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyfuzzy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ad377e0383a3332e1deb427b97c8670a954efa00add87d2071daab421dae24" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", +] + [[package]] name = "portable-atomic" version = "1.10.0" @@ -5816,7 +5867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools 0.11.0", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -5836,7 +5887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.98", diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index d03c51deaf6..c07ae3be193 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -297,6 +297,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bip0039" version = "0.12.0" @@ -721,6 +730,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rand_core", + "rustc_version", + "serde", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1103,6 +1139,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -2486,6 +2528,7 @@ name = "namada_core" version = "0.149.1" dependencies = [ "bech32", + "bincode", "borsh", "chrono", "data-encoding", @@ -2506,6 +2549,7 @@ dependencies = [ "num-traits", "num256", "num_enum", + "polyfuzzy", "primitive-types 0.13.1", "prost-types", "rayon", @@ -3177,6 +3221,19 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyfuzzy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ad377e0383a3332e1deb427b97c8670a954efa00add87d2071daab421dae24" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", +] + [[package]] name = "postcard" version = "1.1.1" @@ -3275,7 +3332,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck", - "itertools 0.13.0", + "itertools 0.14.0", "log", "multimap", "once_cell", @@ -3295,7 +3352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.98", From 0ca4b486bdc81363dc7ff3dcc9e608607223f51c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Apr 2025 11:36:06 +0100 Subject: [PATCH 08/40] Wrap polyfuzzy's flag ciphertext type --- crates/core/src/masp/fmd.rs | 123 ++++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 18 deletions(-) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 53f56eb0684..173d17a31e4 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -1,26 +1,113 @@ //! Fuzzy message detection MASP primitives. +use std::collections::BTreeMap; +use std::io; + +use borsh::schema::Definition; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use polyfuzzy::fmd2_compact::FlagCiphertexts as PolyfuzzyFlagCiphertext; use serde::{Deserialize, Serialize}; +pub mod parameters { + //! Fuzzy message detection parameters used by Namada. + + /// Gamma parameter. + pub const GAMMA: usize = 20; + + /// Threshold parameter. + pub const THRESHOLD: usize = 1; +} + /// FMD flag ciphertexts. -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Serialize, - Deserialize, - Clone, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Debug, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, -)] -// TODO: remove Default derive -#[derive(Default)] +//#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct FlagCiphertext { - inner: Vec, + inner: PolyfuzzyFlagCiphertext, +} + +impl AsRef for FlagCiphertext { + fn as_ref(&self) -> &PolyfuzzyFlagCiphertext { + &self.inner + } +} + +// TODO: use polyfuzzy PartialEq impl once available, +// and simply derive it in FlagCiphertext +impl PartialEq for FlagCiphertext { + fn eq(&self, other: &Self) -> bool { + let this = bincode::serialize(&self.inner).unwrap(); + let other = bincode::serialize(&other.inner).unwrap(); + + this == other + } +} + +#[cfg(feature = "rand")] +impl Default for FlagCiphertext { + // TODO: improve this default impl + fn default() -> Self { + use polyfuzzy::fmd2_compact::MultiFmd2CompactScheme; + use polyfuzzy::{FmdKeyGen, MultiFmdScheme}; + use rand_core::OsRng; + + let mut scheme = MultiFmd2CompactScheme::new( + parameters::GAMMA, + parameters::THRESHOLD, + ); + let (_sk, pk) = scheme.generate_keys(&mut OsRng); + + Self { + inner: scheme.flag(&pk, &mut OsRng), + } + } +} + +impl BorshSerialize for FlagCiphertext { + #[inline] + fn serialize(&self, writer: &mut W) -> io::Result<()> { + // NOTE: serialize the size. borsh will only see an + // opaque vector of bytes + let size: u32 = bincode::serialized_size(&self.inner) + .map_err(from_bincode_err)? + .try_into() + .map_err(io::Error::other)?; + writer.write_all(&size.to_le_bytes())?; + + bincode::serialize_into(writer, &self.inner).map_err(from_bincode_err) + } +} + +impl BorshDeserialize for FlagCiphertext { + #[inline] + fn deserialize_reader(reader: &mut R) -> io::Result { + // NOTE: skip the length of the fake vector of bytes + reader.read_exact(&mut [0u8; 4])?; + + bincode::deserialize_from(reader).map_err(from_bincode_err) + } +} + +impl BorshSchema for FlagCiphertext { + fn add_definitions_recursively( + definitions: &mut BTreeMap, + ) { + let def = { + >::add_definitions_recursively(definitions); + definitions.get(&>::declaration()).unwrap().clone() + }; + + definitions.insert(Self::declaration(), def); + } + + fn declaration() -> String { + std::any::type_name::().into() + } +} + +#[allow(clippy::boxed_local)] +fn from_bincode_err(err: bincode::Error) -> io::Error { + match *err { + bincode::ErrorKind::Io(err) => err, + other => io::Error::other(other), + } } From 0ced13aa70ae5797ad85fa69476df2f1f516d55a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Apr 2025 12:03:00 +0100 Subject: [PATCH 09/40] Improve parameter docs --- crates/core/src/masp/fmd.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 173d17a31e4..04a6b736671 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -8,13 +8,22 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use polyfuzzy::fmd2_compact::FlagCiphertexts as PolyfuzzyFlagCiphertext; use serde::{Deserialize, Serialize}; +#[allow(dead_code)] pub mod parameters { //! Fuzzy message detection parameters used by Namada. /// Gamma parameter. + /// + /// This parameter defines the minimum false positive rate, + /// which is given by `2^-GAMMA`. pub const GAMMA: usize = 20; /// Threshold parameter. + /// + /// This parameter affects the length of payment addresses. + /// The raw data of payment addresses will contain `THRESHOLD + 1` + /// extra compressed curve points (32 bytes each), to allow + /// flagging note ownership to their respective owner. pub const THRESHOLD: usize = 1; } From 8d25a749f9387736e1f15d53acc6cb3b7f680b26 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Apr 2025 13:04:43 +0100 Subject: [PATCH 10/40] Improve default implementation of flag ciphertext --- crates/core/Cargo.toml | 5 +++-- crates/core/src/masp/fmd.rs | 18 +++++------------- crates/sdk/Cargo.toml | 4 ++-- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index de9312025da..2669402cfe2 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -16,13 +16,14 @@ rust-version.workspace = true [features] default = [] mainnet = [] -rand = ["dep:rand", "rand_core"] +rand = ["dep:rand", "dep:rand_core"] +default-flag-ciphertext = ["dep:rand_core", "polyfuzzy/random-flag-ciphertexts"] ethers-derive = ["ethbridge-structs/ethers-derive"] # for tests and test utilities testing = ["rand", "proptest"] migrations = ["namada_migrations", "linkme"] benches = ["proptest"] -control_flow = ["lazy_static", "tokio", "wasmtimer"] +control_flow = ["dep:lazy_static", "tokio", "wasmtimer"] arbitrary = [ "dep:arbitrary", "chrono/arbitrary", diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 04a6b736671..538d74cdbc1 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -51,22 +51,14 @@ impl PartialEq for FlagCiphertext { } } -#[cfg(feature = "rand")] +#[cfg(feature = "default-flag-ciphertext")] impl Default for FlagCiphertext { - // TODO: improve this default impl fn default() -> Self { - use polyfuzzy::fmd2_compact::MultiFmd2CompactScheme; - use polyfuzzy::{FmdKeyGen, MultiFmdScheme}; - use rand_core::OsRng; - - let mut scheme = MultiFmd2CompactScheme::new( - parameters::GAMMA, - parameters::THRESHOLD, - ); - let (_sk, pk) = scheme.generate_keys(&mut OsRng); - Self { - inner: scheme.flag(&pk, &mut OsRng), + inner: PolyfuzzyFlagCiphertext::random( + &mut rand_core::OsRng, + parameters::GAMMA, + ), } } } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 65c39f6d64a..24d82c759d2 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -72,7 +72,7 @@ migrations = [ [dependencies] namada_account.workspace = true -namada_core.workspace = true +namada_core = { workspace = true, features = ["default-flag-ciphertext"] } namada_ethereum_bridge.workspace = true namada_events.workspace = true namada_gas.workspace = true @@ -150,7 +150,7 @@ getrandom = { workspace = true, features = ["wasm_js"] } [dev-dependencies] namada_account = { path = "../account", features = ["testing"] } -namada_core = { path = "../core", features = ["rand", "testing"] } +namada_core = { path = "../core", features = ["default-flag-ciphertext", "rand", "testing"] } namada_ethereum_bridge = { path = "../ethereum_bridge", features = ["testing"] } namada_governance = { path = "../governance", features = ["testing"] } namada_ibc = { path = "../ibc", features = ["testing"] } From 2fa1459228f5abe312b98a98383bf0d73b97f8b8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Apr 2025 13:52:22 +0100 Subject: [PATCH 11/40] Validate compressed bit ciphertexts --- crates/core/src/masp/fmd.rs | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 538d74cdbc1..e3efb389eab 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -25,6 +25,25 @@ pub mod parameters { /// extra compressed curve points (32 bytes each), to allow /// flagging note ownership to their respective owner. pub const THRESHOLD: usize = 1; + + /// Evaluate whether the given compressed bit ciphertext is valid. + #[allow(clippy::arithmetic_side_effects)] + pub const fn valid_compressed_bit_ciphertext(bits: &[u8]) -> bool { + // Number of bytes required to represent a polyfuzzy bit ciphertext + // with a `GAMMA` parameter. + const COMPRESSED_BIT_CIPHERTEXT_LEN: usize = + GAMMA / 8 + (GAMMA % 8 != 0) as usize; + + // Mask with the padding bits that should be set to 0 (or, + // in other words, unset) in the bit ciphertext produced + // by polyfuzzy. Since the library doesn't set any of the + // upper bits, if they have been set it means someone has + // tampered with the flag ciphertext. + const UNSET_BITS_MASK: u8 = 0xff << (GAMMA % 8); + + bits.len() == COMPRESSED_BIT_CIPHERTEXT_LEN + && bits[COMPRESSED_BIT_CIPHERTEXT_LEN - 1] & UNSET_BITS_MASK == 0 + } } /// FMD flag ciphertexts. @@ -112,3 +131,62 @@ fn from_bincode_err(err: bincode::Error) -> io::Error { other => io::Error::other(other), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_flag_ciphertext_bits_validation() { + let mut bits: Vec = { + let mut bits = [false; parameters::GAMMA]; + + // set some random bits + bits[0] = true; + bits[5] = true; + bits[10] = true; + bits[parameters::GAMMA - 1] = true; + bits[parameters::GAMMA - 2] = true; + bits[parameters::GAMMA - 3] = true; + + // compress the bits + bits.chunks(8) + .map(|bits| { + bits.iter().copied().enumerate().fold( + 0u8, + |accum_byte, (i, bit)| { + #[allow(clippy::cast_lossless)] + { + accum_byte ^ ((bit as u8) << i) + } + }, + ) + }) + .collect() + }; + + // check validation of a proper flag ciphertext + assert!(parameters::valid_compressed_bit_ciphertext(&bits)); + + let all_bits_unset = (0..8 - (parameters::GAMMA % 8)) + .map(|i| bits[bits.len() - 1] & (0b1000_0000_u8 >> i)) + .all(|bit| bit == 0); + + assert!(all_bits_unset, "Invalid bit ciphertext"); + + if parameters::GAMMA % 8 != 0 { + let n = bits.len(); + bits[n - 1] |= 0b1000_0000_u8; + + // check validation of a flag ciphertext with padding bits + // that has been tampered with + assert!(!parameters::valid_compressed_bit_ciphertext(&bits)); + + let some_bit_unset = (0..8 - (parameters::GAMMA % 8)) + .map(|i| bits[bits.len() - 1] & (0b1000_0000_u8 >> i)) + .any(|bit| bit != 0); + + assert!(some_bit_unset, "Valid bit ciphertext"); + } + } +} From 1ad37fd42a1b687b0bcf92aaed373e9afdb2c5e6 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 24 Apr 2025 14:23:52 +0100 Subject: [PATCH 12/40] Export flag ciphertext from sdk --- crates/sdk/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 06c2dce010a..3894644c8c7 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -52,8 +52,8 @@ use namada_core::ethereum_events::EthAddress; use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; use namada_core::key::*; pub use namada_core::masp::{ - ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, TransferSource, - TransferTarget, + ExtendedSpendingKey, ExtendedViewingKey, FlagCiphertext, PaymentAddress, + TransferSource, TransferTarget, }; pub use namada_core::{control_flow, task_env}; use namada_io::{Client, Io, NamadaIo}; From 34f2a3c31220a5719f310a3a3605880741f7a8a0 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 24 Apr 2025 14:24:22 +0100 Subject: [PATCH 13/40] Create tx data section from borsh encodable data --- crates/tx/src/section.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/tx/src/section.rs b/crates/tx/src/section.rs index d288de3e058..d4aeef17597 100644 --- a/crates/tx/src/section.rs +++ b/crates/tx/src/section.rs @@ -275,6 +275,12 @@ impl Data { } } + /// Make a new data section with the given borsh encodable data + #[inline] + pub fn from_borsh_encoded(data: &T) -> Self { + Self::new(data.serialize_to_vec()) + } + /// Hash this data section pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { hasher.update(self.serialize_to_vec()); From 93e883821a73a9f72fdf654d866245a1ce5c241b Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 24 Apr 2025 14:25:07 +0100 Subject: [PATCH 14/40] Extended bench txs with fmd flag section --- crates/node/src/bench_utils.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index a4478290509..b187c4f92f1 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -115,8 +115,8 @@ pub use namada_sdk::tx::{ }; use namada_sdk::wallet::{DatedSpendingKey, Wallet}; use namada_sdk::{ - Namada, NamadaImpl, PaymentAddress, TransferSource, TransferTarget, - parameters, proof_of_stake, tendermint, + FlagCiphertext, Namada, NamadaImpl, PaymentAddress, TransferSource, + TransferTarget, parameters, proof_of_stake, tendermint, }; use namada_test_utils::tx_data::TxWriteData; use namada_vm::wasm::run; @@ -188,7 +188,7 @@ impl BenchShellInner { if let Some(sections) = extra_sections { for section in sections { - if let Section::ExtraData(_) = section { + if let Section::ExtraData(_) | Section::Data(_) = section { tx.add_section(section); } } @@ -1281,12 +1281,14 @@ impl BenchShieldedCtx { ) .expect("MASP must have shielded part"); + let fmd_section = Section::Data(Data::from_borsh_encoded(&vec![ + FlagCiphertext::default(), + ])); let shielded_data = MaspTxData { masp_tx_id: shielded.txid().into(), - // TODO: change this to the actual sechash - // of the fmd flag - flag_ciphertext_sechash: Default::default(), + flag_ciphertext_sechash: fmd_section.get_hash(), }; + let tx = if source.effective_address() == MASP && target.effective_address() == MASP { @@ -1294,7 +1296,7 @@ impl BenchShieldedCtx { TX_TRANSFER_WASM, Transfer::masp(shielded_data), Some(shielded), - None, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) } else if target.effective_address() == MASP { @@ -1309,7 +1311,7 @@ impl BenchShieldedCtx { ) .unwrap(), Some(shielded), - None, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) } else { @@ -1324,7 +1326,7 @@ impl BenchShieldedCtx { ) .unwrap(), Some(shielded), - None, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) }; @@ -1398,14 +1400,15 @@ impl BenchShieldedCtx { vec![vectorized_transfer.targets.into_iter().next().unwrap()] .into_iter() .collect(); + let fmd_section = Section::Data(Data::from_borsh_encoded(&vec![ + FlagCiphertext::default(), + ])); let transfer = Transfer { sources, targets, shielded_data: Some(MaspTxData { masp_tx_id, - // TODO: change this to the actual sechash - // of the fmd flag - flag_ciphertext_sechash: Default::default(), + flag_ciphertext_sechash: fmd_section.get_hash(), }), }; let masp_tx = tx.tx.get_masp_section(&masp_tx_id).unwrap().clone(); @@ -1419,6 +1422,7 @@ impl BenchShieldedCtx { .read() .generate_ibc_tx(TX_IBC_WASM, msg.serialize_to_vec()); ibc_tx.tx.add_masp_tx_section(masp_tx); + ibc_tx.tx.add_section(fmd_section); (ctx, ibc_tx) } From 6f488d99171b877149736eda58f8946ada06b8c4 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 12:53:03 +0100 Subject: [PATCH 15/40] Switch to transparent repr in flag ciphertext --- crates/core/src/masp/fmd.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index e3efb389eab..b29c8bed3a4 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -49,6 +49,7 @@ pub mod parameters { /// FMD flag ciphertexts. //#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Serialize, Deserialize, Clone, Debug)] +#[repr(transparent)] pub struct FlagCiphertext { inner: PolyfuzzyFlagCiphertext, } From efcb4a9fd29266582118f94bf72f2de4e528a58d Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 12:54:30 +0100 Subject: [PATCH 16/40] Improve flag ciphertext PartialEq impl --- crates/core/src/masp/fmd.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index b29c8bed3a4..280880373a4 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -48,7 +48,7 @@ pub mod parameters { /// FMD flag ciphertexts. //#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[repr(transparent)] pub struct FlagCiphertext { inner: PolyfuzzyFlagCiphertext, @@ -60,17 +60,6 @@ impl AsRef for FlagCiphertext { } } -// TODO: use polyfuzzy PartialEq impl once available, -// and simply derive it in FlagCiphertext -impl PartialEq for FlagCiphertext { - fn eq(&self, other: &Self) -> bool { - let this = bincode::serialize(&self.inner).unwrap(); - let other = bincode::serialize(&other.inner).unwrap(); - - this == other - } -} - #[cfg(feature = "default-flag-ciphertext")] impl Default for FlagCiphertext { fn default() -> Self { From 3dc53d6287bb3cd94e07c69268df51a28cef6c43 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 13:06:48 +0100 Subject: [PATCH 17/40] Test if flag ciphertext is valid --- crates/core/src/masp/fmd.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 280880373a4..2531254a1d7 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -54,6 +54,15 @@ pub struct FlagCiphertext { inner: PolyfuzzyFlagCiphertext, } +impl FlagCiphertext { + /// Check if the flag ciphertext is valid, according to Namada's consensus + /// rules. + #[inline] + pub fn is_valid(&self) -> bool { + parameters::valid_compressed_bit_ciphertext(self.inner.bits()) + } +} + impl AsRef for FlagCiphertext { fn as_ref(&self) -> &PolyfuzzyFlagCiphertext { &self.inner @@ -126,6 +135,16 @@ fn from_bincode_err(err: bincode::Error) -> io::Error { mod tests { use super::*; + #[test] + #[cfg(feature = "default-flag-ciphertext")] + fn test_random_flag_ciphertext_is_valid() { + // run this test a couple of times + for _ in 0..5 { + let random_flag_ciphertext = FlagCiphertext::default(); + assert!(random_flag_ciphertext.is_valid()); + } + } + #[test] fn test_flag_ciphertext_bits_validation() { let mut bits: Vec = { From 4aa8067f1ad142ff7c87de4b2789b626c68f6d0a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 13:10:14 +0100 Subject: [PATCH 18/40] Generate random flag ciphertext --- crates/core/Cargo.toml | 4 ++-- crates/core/src/masp/fmd.rs | 21 +++++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 2669402cfe2..5c9611d4730 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -16,8 +16,8 @@ rust-version.workspace = true [features] default = [] mainnet = [] -rand = ["dep:rand", "dep:rand_core"] -default-flag-ciphertext = ["dep:rand_core", "polyfuzzy/random-flag-ciphertexts"] +rand = ["dep:rand", "dep:rand_core", "polyfuzzy/random-flag-ciphertexts"] +default-flag-ciphertext = ["rand"] ethers-derive = ["ethbridge-structs/ethers-derive"] # for tests and test utilities testing = ["rand", "proptest"] diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 2531254a1d7..b663147c2e4 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -6,6 +6,8 @@ use std::io; use borsh::schema::Definition; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use polyfuzzy::fmd2_compact::FlagCiphertexts as PolyfuzzyFlagCiphertext; +#[cfg(feature = "rand")] +use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; #[allow(dead_code)] @@ -61,6 +63,17 @@ impl FlagCiphertext { pub fn is_valid(&self) -> bool { parameters::valid_compressed_bit_ciphertext(self.inner.bits()) } + + /// Generate a random [`FlagCiphertext`]. + #[cfg(feature = "rand")] + pub fn random(rng: &mut R) -> Self + where + R: CryptoRng + RngCore, + { + Self { + inner: PolyfuzzyFlagCiphertext::random(rng, parameters::GAMMA), + } + } } impl AsRef for FlagCiphertext { @@ -71,13 +84,9 @@ impl AsRef for FlagCiphertext { #[cfg(feature = "default-flag-ciphertext")] impl Default for FlagCiphertext { + #[inline] fn default() -> Self { - Self { - inner: PolyfuzzyFlagCiphertext::random( - &mut rand_core::OsRng, - parameters::GAMMA, - ), - } + Self::random(&mut rand_core::OsRng) } } From 2982e9bce0002e329a8e769cc747e3669468eec0 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 13:14:28 +0100 Subject: [PATCH 19/40] Impl conversion traits between polyfuzzy and namada_core --- crates/core/src/masp/fmd.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index b663147c2e4..86b2e723976 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -76,6 +76,20 @@ impl FlagCiphertext { } } +impl From for FlagCiphertext { + fn from(flag_ciphertext: PolyfuzzyFlagCiphertext) -> Self { + Self { + inner: flag_ciphertext, + } + } +} + +impl From for PolyfuzzyFlagCiphertext { + fn from(flag_ciphertext: FlagCiphertext) -> Self { + flag_ciphertext.inner + } +} + impl AsRef for FlagCiphertext { fn as_ref(&self) -> &PolyfuzzyFlagCiphertext { &self.inner From 7399e2abea4c6903d7966e28aa45344d3f8afc82 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 13:26:08 +0100 Subject: [PATCH 20/40] Refactor FMD borsh impls --- crates/core/src/masp/fmd.rs | 95 +++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 86b2e723976..048168367a8 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -104,54 +104,87 @@ impl Default for FlagCiphertext { } } -impl BorshSerialize for FlagCiphertext { - #[inline] - fn serialize(&self, writer: &mut W) -> io::Result<()> { +mod borsh_impls { + use super::*; + + macro_rules! borsh_derive_from_bincode { + ($($type:ty),+) => { + $( + impl BorshSerialize for $type { + #[inline] + fn serialize( + &self, + writer: &mut W, + ) -> io::Result<()> { + bincode_as_borsh_serialize(writer, self) + } + } + + impl BorshDeserialize for $type { + #[inline] + fn deserialize_reader( + reader: &mut R, + ) -> io::Result { + bincode_as_borsh_deserialize(reader) + } + } + + impl BorshSchema for $type { + fn add_definitions_recursively( + definitions: &mut BTreeMap, + ) { + let def = { + >::add_definitions_recursively(definitions); + definitions.get(&>::declaration()).unwrap().clone() + }; + + definitions.insert(Self::declaration(), def); + } + + fn declaration() -> String { + std::any::type_name::().into() + } + } + )+ + }; + } + + fn bincode_as_borsh_serialize( + writer: &mut W, + data: &T, + ) -> io::Result<()> { // NOTE: serialize the size. borsh will only see an // opaque vector of bytes - let size: u32 = bincode::serialized_size(&self.inner) + let size: u32 = bincode::serialized_size(data) .map_err(from_bincode_err)? .try_into() .map_err(io::Error::other)?; writer.write_all(&size.to_le_bytes())?; - bincode::serialize_into(writer, &self.inner).map_err(from_bincode_err) + bincode::serialize_into(writer, data).map_err(from_bincode_err) } -} -impl BorshDeserialize for FlagCiphertext { - #[inline] - fn deserialize_reader(reader: &mut R) -> io::Result { + fn bincode_as_borsh_deserialize< + R: io::Read, + T: for<'de> Deserialize<'de>, + >( + reader: &mut R, + ) -> io::Result { // NOTE: skip the length of the fake vector of bytes reader.read_exact(&mut [0u8; 4])?; bincode::deserialize_from(reader).map_err(from_bincode_err) } -} - -impl BorshSchema for FlagCiphertext { - fn add_definitions_recursively( - definitions: &mut BTreeMap, - ) { - let def = { - >::add_definitions_recursively(definitions); - definitions.get(&>::declaration()).unwrap().clone() - }; - - definitions.insert(Self::declaration(), def); - } - fn declaration() -> String { - std::any::type_name::().into() + #[allow(clippy::boxed_local)] + fn from_bincode_err(err: bincode::Error) -> io::Error { + match *err { + bincode::ErrorKind::Io(err) => err, + other => io::Error::other(other), + } } -} -#[allow(clippy::boxed_local)] -fn from_bincode_err(err: bincode::Error) -> io::Error { - match *err { - bincode::ErrorKind::Io(err) => err, - other => io::Error::other(other), - } + borsh_derive_from_bincode!(FlagCiphertext); } #[cfg(test)] From 840e2c8a53d3d126b05e05a7327944d04d7556f8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 13:30:52 +0100 Subject: [PATCH 21/40] Test fmd flag ciphertext borsh roundtrip --- crates/core/src/masp/fmd.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 048168367a8..b6edecf680f 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -191,6 +191,23 @@ mod borsh_impls { mod tests { use super::*; + #[test] + #[cfg(feature = "default-flag-ciphertext")] + fn test_flag_ciphertext_borsh_roundtrip() { + use crate::borsh::BorshSerializeExt; + + // run this test a couple of times + for _ in 0..5 { + let random_flag_ciphertext = FlagCiphertext::default(); + + let serialized = random_flag_ciphertext.serialize_to_vec(); + let deserialized = + FlagCiphertext::try_from_slice(&serialized).unwrap(); + + assert_eq!(random_flag_ciphertext, deserialized); + } + } + #[test] #[cfg(feature = "default-flag-ciphertext")] fn test_random_flag_ciphertext_is_valid() { From d77876ea3cd8893f196c4ae6cf0a2dcad6252ecf Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 16:26:48 +0100 Subject: [PATCH 22/40] Derive FMD secret keys from input vks --- Cargo.toml | 2 +- crates/core/src/masp/fmd.rs | 50 ++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e7451febec..fb71a9976ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -248,7 +248,7 @@ test-log = {version = "0.2", default-features = false, features = ["trace"]} textwrap-macros = "0.3" tiny-bip39 = {version = "2.0"} tiny-hderive = {package = "nam-tiny-hderive", version = "0.3.1-nam.0"} -tiny-keccak = { version = "2.0", features = ["keccak"] } +tiny-keccak = { version = "2.0", features = ["keccak", "k12"] } thiserror = "2.0" tokio = {version = "1.8", default-features = false} tokio-test = "0.4" diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index b6edecf680f..c91c38f46d9 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -5,10 +5,15 @@ use std::io; use borsh::schema::Definition; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use polyfuzzy::fmd2_compact::FlagCiphertexts as PolyfuzzyFlagCiphertext; +use masp_primitives::sapling::SaplingIvk; +use polyfuzzy::fmd2_compact::{ + CompactSecretKey as PolyfuzzyCompactSecretKey, + FlagCiphertexts as PolyfuzzyFlagCiphertext, +}; #[cfg(feature = "rand")] use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; +use tiny_keccak::{Hasher, IntoXof, KangarooTwelve, Xof}; #[allow(dead_code)] pub mod parameters { @@ -48,6 +53,49 @@ pub mod parameters { } } +/// FMD secret key. +//#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct SecretKey { + inner: PolyfuzzyCompactSecretKey, +} + +impl SecretKey { + /// Hash personalization string used when deriving a [`SecretKey`] + /// from a [`SaplingIvk`]. + const KDF_PERSONALIZATION: &str = "Namada FMD Secret Key"; +} + +impl From<&SaplingIvk> for SecretKey { + fn from(ivk: &SaplingIvk) -> Self { + let mut xof_stream = { + let mut hasher = KangarooTwelve::new(Self::KDF_PERSONALIZATION); + + // derive key material from input viewing key + hasher.update(&ivk.to_repr()); + + hasher.into_xof() + }; + + Self { + inner: PolyfuzzyCompactSecretKey::derive_from_xof_stream( + parameters::THRESHOLD, + |buf| { + xof_stream.squeeze(buf); + }, + ), + } + } +} + +impl From for SecretKey { + #[inline] + fn from(ivk: SaplingIvk) -> Self { + (&ivk).into() + } +} + /// FMD flag ciphertexts. //#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] From 569134b0412994ec6f107e4c47656ea6d1adf96d Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 21:24:32 +0100 Subject: [PATCH 23/40] Derive FMD secret key from extended spending key --- crates/core/src/masp/fmd.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index c91c38f46d9..fe659661b17 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -6,6 +6,7 @@ use std::io; use borsh::schema::Definition; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use masp_primitives::sapling::SaplingIvk; +use masp_primitives::zip32::{ExtendedKey, PseudoExtendedKey}; use polyfuzzy::fmd2_compact::{ CompactSecretKey as PolyfuzzyCompactSecretKey, FlagCiphertexts as PolyfuzzyFlagCiphertext, @@ -96,6 +97,18 @@ impl From for SecretKey { } } +impl From<&PseudoExtendedKey> for SecretKey { + fn from(psk: &PseudoExtendedKey) -> Self { + psk.to_viewing_key().fvk.vk.ivk().into() + } +} + +impl From for SecretKey { + fn from(psk: PseudoExtendedKey) -> Self { + (&psk).into() + } +} + /// FMD flag ciphertexts. //#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] From a92d2c5415302415f3eaaf3692678eba91bd6e1f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 21:41:44 +0100 Subject: [PATCH 24/40] Move polyfuzzy imports --- crates/core/src/masp/fmd.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index fe659661b17..a055184104d 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -7,15 +7,15 @@ use borsh::schema::Definition; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use masp_primitives::sapling::SaplingIvk; use masp_primitives::zip32::{ExtendedKey, PseudoExtendedKey}; -use polyfuzzy::fmd2_compact::{ - CompactSecretKey as PolyfuzzyCompactSecretKey, - FlagCiphertexts as PolyfuzzyFlagCiphertext, -}; #[cfg(feature = "rand")] use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use tiny_keccak::{Hasher, IntoXof, KangarooTwelve, Xof}; +mod polyfuzzy { + pub(super) use ::polyfuzzy::fmd2_compact::*; +} + #[allow(dead_code)] pub mod parameters { //! Fuzzy message detection parameters used by Namada. @@ -59,7 +59,7 @@ pub mod parameters { #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[repr(transparent)] pub struct SecretKey { - inner: PolyfuzzyCompactSecretKey, + inner: polyfuzzy::CompactSecretKey, } impl SecretKey { @@ -80,7 +80,7 @@ impl From<&SaplingIvk> for SecretKey { }; Self { - inner: PolyfuzzyCompactSecretKey::derive_from_xof_stream( + inner: polyfuzzy::CompactSecretKey::derive_from_xof_stream( parameters::THRESHOLD, |buf| { xof_stream.squeeze(buf); @@ -114,7 +114,7 @@ impl From for SecretKey { #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[repr(transparent)] pub struct FlagCiphertext { - inner: PolyfuzzyFlagCiphertext, + inner: polyfuzzy::FlagCiphertexts, } impl FlagCiphertext { @@ -132,27 +132,27 @@ impl FlagCiphertext { R: CryptoRng + RngCore, { Self { - inner: PolyfuzzyFlagCiphertext::random(rng, parameters::GAMMA), + inner: polyfuzzy::FlagCiphertexts::random(rng, parameters::GAMMA), } } } -impl From for FlagCiphertext { - fn from(flag_ciphertext: PolyfuzzyFlagCiphertext) -> Self { +impl From for FlagCiphertext { + fn from(flag_ciphertext: polyfuzzy::FlagCiphertexts) -> Self { Self { inner: flag_ciphertext, } } } -impl From for PolyfuzzyFlagCiphertext { +impl From for polyfuzzy::FlagCiphertexts { fn from(flag_ciphertext: FlagCiphertext) -> Self { flag_ciphertext.inner } } -impl AsRef for FlagCiphertext { - fn as_ref(&self) -> &PolyfuzzyFlagCiphertext { +impl AsRef for FlagCiphertext { + fn as_ref(&self) -> &polyfuzzy::FlagCiphertexts { &self.inner } } From b7d339115dee79b308ecd6297e9ee0aa2b18bb0b Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 21:46:43 +0100 Subject: [PATCH 25/40] Remove unnecessary bounds on secret keys --- crates/core/src/masp/fmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index a055184104d..fb7e49eaf70 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -56,7 +56,7 @@ pub mod parameters { /// FMD secret key. //#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] #[repr(transparent)] pub struct SecretKey { inner: polyfuzzy::CompactSecretKey, From 397d77dd2be9c47aae61d5680483f3b6536d991f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 22:09:38 +0100 Subject: [PATCH 26/40] Add FMD public keys --- crates/core/src/masp/fmd.rs | 106 ++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index fb7e49eaf70..4a6bc4347aa 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -13,6 +13,8 @@ use serde::{Deserialize, Serialize}; use tiny_keccak::{Hasher, IntoXof, KangarooTwelve, Xof}; mod polyfuzzy { + #[cfg(feature = "rand")] + pub(super) use ::polyfuzzy::MultiFmdScheme; pub(super) use ::polyfuzzy::fmd2_compact::*; } @@ -34,6 +36,9 @@ pub mod parameters { /// flagging note ownership to their respective owner. pub const THRESHOLD: usize = 1; + /// Number of bytes required to store a public key. + pub const PUBLIC_KEY_LEN: usize = (THRESHOLD + 1) * 32; + /// Evaluate whether the given compressed bit ciphertext is valid. #[allow(clippy::arithmetic_side_effects)] pub const fn valid_compressed_bit_ciphertext(bits: &[u8]) -> bool { @@ -54,6 +59,68 @@ pub mod parameters { } } +/// FMD public key. +//#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(transparent)] +pub struct PublicKey { + inner: polyfuzzy::CompactPublicKey, +} + +impl PublicKey { + /// Serialize this public key into its compressed representation. + pub fn into_compressed_bytes(self) -> Vec { + self.inner.compress().to_coeff_repr() + } + + /// Deserialize a public key from the given compressed representation and + /// diversifier. + pub fn from_parts( + diversifier: &[u8], + compressed_bytes: &[u8], + ) -> Option { + if compressed_bytes.len() != parameters::PUBLIC_KEY_LEN { + return None; + } + + let compressed_pk = + polyfuzzy::CompressedCompactPublicKey::from_coeff_repr( + compressed_bytes, + )?; + + Some(PublicKey { + inner: compressed_pk.var_decompress(diversifier), + }) + } + + /// Generate a new [`FlagCiphertext`]. + /// + /// This notifies the owner of a given viewing + /// key that a note can be decrypted by it, with + /// occasional false positives returned for + /// non-decrypting viewing keys. + #[cfg(feature = "rand")] + pub fn flag(&self, rng: &mut R) -> FlagCiphertext + where + R: CryptoRng + RngCore, + { + use polyfuzzy::MultiFmdScheme as _; + + let mut scheme = polyfuzzy::MultiFmd2CompactScheme::new( + parameters::GAMMA, + parameters::THRESHOLD, + ); + + scheme.flag(&self.inner, rng).into() + } +} + +impl AsRef for PublicKey { + fn as_ref(&self) -> &polyfuzzy::CompactPublicKey { + &self.inner + } +} + /// FMD secret key. //#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Clone, Debug, Eq, PartialEq)] @@ -66,6 +133,30 @@ impl SecretKey { /// Hash personalization string used when deriving a [`SecretKey`] /// from a [`SaplingIvk`]. const KDF_PERSONALIZATION: &str = "Namada FMD Secret Key"; + + /// Return the default public key. + /// + /// Flag ciphertexts generated using the default + /// public key are guaranteed to be unique on + /// every call to [`PublicKey::flag`]. + pub fn default_public_key(&self) -> PublicKey { + PublicKey { + inner: self.inner.master_public_key(), + } + } + + /// Return a public key randomized by the given `diversifier`. + pub fn randomized_public_key(&self, diversifier: &[u8]) -> PublicKey { + PublicKey { + inner: self.inner.var_randomized_public_key(diversifier), + } + } +} + +impl AsRef for SecretKey { + fn as_ref(&self) -> &polyfuzzy::CompactSecretKey { + &self.inner + } } impl From<&SaplingIvk> for SecretKey { @@ -252,6 +343,21 @@ mod borsh_impls { mod tests { use super::*; + #[test] + fn test_fmd_pubkey_roundtrip_repr() { + let diversifier = b"bing-bong"; + let sk = { + let ivk = SaplingIvk(Default::default()); + SecretKey::from(ivk) + }; + + let pk = sk.randomized_public_key(diversifier); + let repr = pk.clone().into_compressed_bytes(); + let pk2 = PublicKey::from_parts(diversifier, &repr).unwrap(); + + assert_eq!(pk, pk2); + } + #[test] #[cfg(feature = "default-flag-ciphertext")] fn test_flag_ciphertext_borsh_roundtrip() { From 65e06a4d19b4c97665945b95b472f5527768af10 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 30 Apr 2025 22:23:13 +0100 Subject: [PATCH 27/40] Export FMD keys --- crates/core/src/masp.rs | 4 +++- crates/core/src/masp/fmd.rs | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index 4fe585d8e0e..06ecd7d912f 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -22,7 +22,9 @@ use ripemd::Digest as RipemdDigest; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::Sha256; -pub use self::fmd::FlagCiphertext; +pub use self::fmd::{ + FlagCiphertext, PublicKey as FmdPublicKey, SecretKey as FmdSecretKey, +}; use crate::address::{Address, DecodeError, HASH_HEX_LEN, IBC, MASP}; use crate::borsh::BorshSerializeExt; use crate::chain::Epoch; diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index 4a6bc4347aa..f5ca3917404 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -18,8 +18,7 @@ mod polyfuzzy { pub(super) use ::polyfuzzy::fmd2_compact::*; } -#[allow(dead_code)] -pub mod parameters { +mod parameters { //! Fuzzy message detection parameters used by Namada. /// Gamma parameter. From 724ec42059cc17d30794673c2b94b6fbdb5f31b0 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 5 May 2025 13:48:35 +0100 Subject: [PATCH 28/40] Define HRP of fmd payment addrs --- crates/core/src/string_encoding.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/core/src/string_encoding.rs b/crates/core/src/string_encoding.rs index 82fd5d7b0c2..fe9a3757583 100644 --- a/crates/core/src/string_encoding.rs +++ b/crates/core/src/string_encoding.rs @@ -27,6 +27,8 @@ pub const ADDRESS_HRP: &str = "tnam"; pub const MASP_EXT_FULL_VIEWING_KEY_HRP: &str = "zvknam"; /// MASP payment address human-readable part pub const MASP_PAYMENT_ADDRESS_HRP: &str = "znam"; +/// MASP payment address with FMD public key human-readable part +pub const MASP_FMD_PAYMENT_ADDRESS_HRP: &str = "zfnam"; /// MASP extended spending key human-readable part pub const MASP_EXT_SPENDING_KEY_HRP: &str = "zsknam"; /// `common::PublicKey` human-readable part From 241bb19ea1e5760c343464fa0535425439f5ad31 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 5 May 2025 14:00:05 +0100 Subject: [PATCH 29/40] Add domain type for fmd pubkey bytes --- crates/core/src/masp.rs | 3 ++- crates/core/src/masp/fmd.rs | 39 +++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index 06ecd7d912f..f5de46709db 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -23,7 +23,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::Sha256; pub use self::fmd::{ - FlagCiphertext, PublicKey as FmdPublicKey, SecretKey as FmdSecretKey, + FlagCiphertext, PublicKey as FmdPublicKey, + PublicKeyBytes as FmdPublicKeyBytes, SecretKey as FmdSecretKey, }; use crate::address::{Address, DecodeError, HASH_HEX_LEN, IBC, MASP}; use crate::borsh::BorshSerializeExt; diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index f5ca3917404..c85f5ed4a8e 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::io; +use std::ops::Deref; use borsh::schema::Definition; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -58,6 +59,31 @@ mod parameters { } } +/// FMD public key bytes. +#[derive( + Clone, + Debug, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +#[repr(transparent)] +pub struct PublicKeyBytes(Box<[u8; parameters::PUBLIC_KEY_LEN]>); + +impl Deref for PublicKeyBytes { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &[u8] { + self.0.as_slice() + } +} + /// FMD public key. //#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Clone, Debug, Eq, PartialEq)] @@ -68,8 +94,17 @@ pub struct PublicKey { impl PublicKey { /// Serialize this public key into its compressed representation. - pub fn into_compressed_bytes(self) -> Vec { - self.inner.compress().to_coeff_repr() + pub fn into_compressed_bytes(self) -> PublicKeyBytes { + let repr = self.inner.compress().to_coeff_repr(); + + PublicKeyBytes(repr.into_boxed_slice().try_into().unwrap_or_else( + |_| { + panic!( + "FMD public key length should have been {}", + parameters::PUBLIC_KEY_LEN + ) + }, + )) } /// Deserialize a public key from the given compressed representation and From 336eaddf5a4e44a5011dc1694b07c616b97c9d96 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 5 May 2025 14:31:22 +0100 Subject: [PATCH 30/40] Add payment addrs with fmd pubkeys --- crates/core/src/masp.rs | 138 +++++++++++++++++++++++++++++++++++- crates/core/src/masp/fmd.rs | 24 +++++++ 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index f5de46709db..6914d86cea5 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -33,7 +33,7 @@ use crate::hash::Hash; use crate::impl_display_and_from_str_via_format; use crate::string_encoding::{ self, MASP_EXT_FULL_VIEWING_KEY_HRP, MASP_EXT_SPENDING_KEY_HRP, - MASP_PAYMENT_ADDRESS_HRP, + MASP_FMD_PAYMENT_ADDRESS_HRP, MASP_PAYMENT_ADDRESS_HRP, }; use crate::token::{Denomination, MaspDigitPos, NATIVE_MAX_DECIMAL_PLACES}; @@ -345,6 +345,10 @@ pub type Precision = u128; // enough capacity to store the payment address const PAYMENT_ADDRESS_SIZE: usize = 43; +// enough capacity to store the payment address +const FMD_PAYMENT_ADDRESS_SIZE: usize = + PAYMENT_ADDRESS_SIZE + FmdPublicKeyBytes::LENGTH; + /// Wrapper for masp_primitive's DiversifierIndex #[derive(Clone, Debug, Copy, Eq, PartialEq, Default)] pub struct DiversifierIndex(masp_primitives::zip32::DiversifierIndex); @@ -526,6 +530,54 @@ impl string_encoding::Format for PaymentAddress { impl_display_and_from_str_via_format!(PaymentAddress); +impl string_encoding::Format for FmdPaymentAddress { + type EncodedBytes<'a> = Vec; + + const HRP: string_encoding::Hrp = + string_encoding::Hrp::parse_unchecked(MASP_FMD_PAYMENT_ADDRESS_HRP); + + fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(FMD_PAYMENT_ADDRESS_SIZE); + bytes.extend_from_slice(&self.payment_address.to_bytes()[..]); + bytes.extend_from_slice(&self.fmd_public_key[..]); + bytes + } + + fn decode_bytes( + bytes: &[u8], + ) -> Result { + if bytes.len() != FMD_PAYMENT_ADDRESS_SIZE { + return Err(DecodeError::InvalidInnerEncoding(format!( + "expected {FMD_PAYMENT_ADDRESS_SIZE} bytes for the fmd \ + payment address" + ))); + } + + let payment_address = + masp_primitives::sapling::PaymentAddress::from_bytes(&{ + let mut payment_addr = [0u8; PAYMENT_ADDRESS_SIZE]; + payment_addr.copy_from_slice(&bytes[0..PAYMENT_ADDRESS_SIZE]); + payment_addr + }) + .ok_or_else(|| { + DecodeError::InvalidInnerEncoding( + "invalid fmd payment address provided".to_string(), + ) + })?; + + let fmd_public_key = bytes[PAYMENT_ADDRESS_SIZE..] + .try_into() + .map_err(DecodeError::InvalidInnerEncoding)?; + + Ok(Self { + payment_address, + fmd_public_key, + }) + } +} + +impl_display_and_from_str_via_format!(FmdPaymentAddress); + impl From for masp_primitives::zip32::ExtendedFullViewingKey { @@ -598,6 +650,20 @@ impl PaymentAddress { // hex of the first 40 chars of the hash format!("{:.width$X}", hasher.finalize(), width = HASH_HEX_LEN) } + + /// Create a new [`FmdPaymentAddress`]. + pub fn with_fmd_key(self, sk: &FmdSecretKey) -> FmdPaymentAddress { + let Self(payment_address) = self; + + let fmd_public_key = sk + .randomized_public_key(&payment_address.diversifier().0) + .into_compressed_bytes(); + + FmdPaymentAddress { + payment_address, + fmd_public_key, + } + } } impl From for masp_primitives::sapling::PaymentAddress { @@ -636,6 +702,76 @@ impl<'de> serde::Deserialize<'de> for PaymentAddress { } } +/// [`PaymentAddress`] with FMD public key attached. +#[derive( + Clone, + Debug, + PartialOrd, + Ord, + Eq, + PartialEq, + Hash, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, +)] +pub struct FmdPaymentAddress { + /// Wrapped MASP payment address. + payment_address: masp_primitives::sapling::PaymentAddress, + /// FMD public key. + fmd_public_key: FmdPublicKeyBytes, +} + +impl FmdPaymentAddress { + /// Recover the [`PaymentAddress`] embedded within. + pub fn as_payment_address( + &self, + ) -> &masp_primitives::sapling::PaymentAddress { + &self.payment_address + } + + /// Recover the [`FmdPublicKey`] embedded within. + pub fn to_fmd_public_key(&self) -> Option { + FmdPublicKey::from_parts( + &self.payment_address.diversifier().0, + &self.fmd_public_key, + ) + } + + /// Hash this payment address + pub fn hash(&self) -> String { + let bytes = self.serialize_to_vec(); + let mut hasher = Sha256::new(); + hasher.update(bytes); + // hex of the first 40 chars of the hash + format!("{:.width$X}", hasher.finalize(), width = HASH_HEX_LEN) + } +} + +impl serde::Serialize for FmdPaymentAddress { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + let encoded = self.to_string(); + serde::Serialize::serialize(&encoded, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for FmdPaymentAddress { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let encoded: String = serde::Deserialize::deserialize(deserializer)?; + Self::from_str(&encoded).map_err(D::Error::custom) + } +} + /// Wrapper for masp_primitive's ExtendedSpendingKey #[derive( Clone, diff --git a/crates/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs index c85f5ed4a8e..20d9efc9a94 100644 --- a/crates/core/src/masp/fmd.rs +++ b/crates/core/src/masp/fmd.rs @@ -75,6 +75,30 @@ mod parameters { #[repr(transparent)] pub struct PublicKeyBytes(Box<[u8; parameters::PUBLIC_KEY_LEN]>); +impl PublicKeyBytes { + /// Length of the byte payload. + pub const LENGTH: usize = parameters::PUBLIC_KEY_LEN; +} + +impl TryFrom<&[u8]> for PublicKeyBytes { + type Error = String; + + fn try_from(slice: &[u8]) -> Result { + if slice.len() != parameters::PUBLIC_KEY_LEN { + return Err(format!( + "FMD public key length must be {}", + parameters::PUBLIC_KEY_LEN + )); + } + + let mut bytes = + PublicKeyBytes(Box::new([0u8; parameters::PUBLIC_KEY_LEN])); + bytes.0.copy_from_slice(slice); + + Ok(bytes) + } +} + impl Deref for PublicKeyBytes { type Target = [u8]; From e2c679251f4e3856df01bf58cb11b5d82cd1074e Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 6 May 2025 14:03:35 +0100 Subject: [PATCH 31/40] Add new payment addrs to wallet --- crates/apps_lib/src/cli.rs | 9 +- crates/apps_lib/src/cli/context.rs | 20 ++- crates/apps_lib/src/cli/wallet.rs | 21 +-- crates/benches/native_vps.rs | 2 +- crates/core/src/masp.rs | 164 ++++++++++++++++-- crates/node/src/bench_utils.rs | 7 +- crates/sdk/src/args.rs | 6 +- crates/sdk/src/lib.rs | 6 +- crates/sdk/src/tx.rs | 6 +- .../src/masp/shielded_wallet.rs | 2 +- crates/wallet/src/lib.rs | 13 +- crates/wallet/src/store.rs | 21 ++- 12 files changed, 213 insertions(+), 64 deletions(-) diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index d6d2c7a607e..de7dfdaf7a7 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -3402,7 +3402,9 @@ pub mod args { use data_encoding::HEXUPPER; use either::Either; - use namada_core::masp::{DiversifierIndex, MaspEpoch, PaymentAddress}; + use namada_core::masp::{ + DiversifierIndex, MaspEpoch, UnifiedPaymentAddress, + }; use namada_sdk::address::{Address, EstablishedAddress}; pub use namada_sdk::args::*; use namada_sdk::chain::{ChainId, ChainIdPrefix}; @@ -3653,8 +3655,9 @@ pub mod args { pub const RAW_ADDRESS_ESTABLISHED: Arg = arg("address"); pub const RAW_ADDRESS_OPT: ArgOpt
= RAW_ADDRESS.opt(); pub const RAW_KEY_GEN: ArgFlag = flag("raw"); - pub const RAW_PAYMENT_ADDRESS: Arg = arg("payment-address"); - pub const RAW_PAYMENT_ADDRESS_OPT: ArgOpt = + pub const RAW_PAYMENT_ADDRESS: Arg = + arg("payment-address"); + pub const RAW_PAYMENT_ADDRESS_OPT: ArgOpt = RAW_PAYMENT_ADDRESS.opt(); pub const RAW_PUBLIC_KEY: Arg = arg("public-key"); pub const RAW_PUBLIC_KEY_OPT: ArgOpt = diff --git a/crates/apps_lib/src/cli/context.rs b/crates/apps_lib/src/cli/context.rs index 7c4d570d5c7..0435b7241e0 100644 --- a/crates/apps_lib/src/cli/context.rs +++ b/crates/apps_lib/src/cli/context.rs @@ -12,8 +12,8 @@ use masp_primitives::zip32::{ ExtendedSpendingKey as MaspExtendedSpendingKey, }; use namada_core::masp::{ - BalanceOwner, ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, - TransferSource, TransferTarget, + BalanceOwner, ExtendedSpendingKey, ExtendedViewingKey, TransferSource, + TransferTarget, UnifiedPaymentAddress, }; use namada_sdk::address::{Address, InternalAddress}; use namada_sdk::chain::ChainId; @@ -60,7 +60,7 @@ pub type WalletDatedSpendingKey = FromContext; /// A raw payment address (bech32m encoding) or an alias of a payment address /// in the wallet -pub type WalletPaymentAddr = FromContext; +pub type WalletPaymentAddr = FromContext; /// A raw full viewing key (bech32m encoding) or an alias of a full viewing key /// in the wallet @@ -388,10 +388,11 @@ impl FromContext { } } - /// Converts this TransferTarget argument to a PaymentAddress. Call this - /// function only when certain that raw represents a PaymentAddress. - pub fn to_payment_address(&self) -> FromContext { - FromContext:: { + /// Converts this TransferTarget argument to a UnifiedPaymentAddress. Call + /// this function only when certain that raw represents a + /// UnifiedPaymentAddress. + pub fn to_payment_address(&self) -> FromContext { + FromContext:: { raw: self.raw.clone(), phantom: PhantomData, } @@ -690,7 +691,7 @@ impl ArgFromMutContext for DatedViewingKey { } } -impl ArgFromContext for PaymentAddress { +impl ArgFromContext for UnifiedPaymentAddress { fn arg_from_ctx( ctx: &ChainContext, raw: impl AsRef, @@ -743,7 +744,8 @@ impl ArgFromContext for TransferTarget { Address::arg_from_ctx(ctx, raw) .map(Self::Address) .or_else(|_| { - PaymentAddress::arg_from_ctx(ctx, raw).map(Self::PaymentAddress) + UnifiedPaymentAddress::arg_from_ctx(ctx, raw) + .map(Self::PaymentAddress) }) } } diff --git a/crates/apps_lib/src/cli/wallet.rs b/crates/apps_lib/src/cli/wallet.rs index 19f4261d3a4..07c94a22203 100644 --- a/crates/apps_lib/src/cli/wallet.rs +++ b/crates/apps_lib/src/cli/wallet.rs @@ -12,7 +12,7 @@ use ledger_transport_hid::hidapi::HidApi; use masp_primitives::zip32::ExtendedFullViewingKey; use namada_core::chain::BlockHeight; use namada_core::masp::{ - DiversifierIndex, ExtendedSpendingKey, MaspValue, PaymentAddress, + ExtendedSpendingKey, MaspValue, UnifiedPaymentAddress, }; use namada_sdk::address::{Address, DecodeError}; use namada_sdk::borsh::{BorshDeserialize, BorshSerializeExt}; @@ -421,23 +421,18 @@ fn payment_address_gen( .copied() .unwrap_or_default() }); - let (diversifier_index, masp_payment_addr) = - ExtendedFullViewingKey::from(viewing_key) - .find_address(diversifier_index.into()) - .expect("exhausted payment addresses"); + let (diversifier_index, payment_addr) = + UnifiedPaymentAddress::v1_from_zip32(viewing_key, diversifier_index); let mut next_div_idx = diversifier_index; - next_div_idx - .increment() - .expect("exhausted payment addresses"); - let payment_addr = PaymentAddress::from(masp_payment_addr); + next_div_idx.increment(); let alias = wallet - .insert_payment_addr(alias, payment_addr, alias_force) + .insert_payment_addr(alias, payment_addr.clone(), alias_force) .unwrap_or_else(|| { edisplay_line!(io, "Payment address not added"); cli::safe_exit(1); }); wallet - .insert_diversifier_index(viewing_key_alias, next_div_idx.into()) + .insert_diversifier_index(viewing_key_alias, next_div_idx) .expect( "must be able to save next diversifier index under the alias of \ the viewing key", @@ -447,7 +442,7 @@ fn payment_address_gen( io, "Successfully generated payment address {} at index {} with alias {}", payment_addr, - DiversifierIndex::from(diversifier_index), + diversifier_index, alias, ); } @@ -1095,7 +1090,7 @@ fn payment_address_or_alias_find( ctx: Context, io: &impl Io, alias: Option, - payment_address: Option, + payment_address: Option, ) { let wallet = load_wallet(ctx); if payment_address.is_some() && alias.is_some() { diff --git a/crates/benches/native_vps.rs b/crates/benches/native_vps.rs index 63ba6afebe2..e0b3a223a3e 100644 --- a/crates/benches/native_vps.rs +++ b/crates/benches/native_vps.rs @@ -572,7 +572,7 @@ fn setup_storage_for_masp_verification( let (shielded_ctx, shield_tx) = shielded_ctx.generate_masp_tx( amount, TransferSource::Address(defaults::albert_address()), - TransferTarget::PaymentAddress(albert_payment_addr), + TransferTarget::PaymentAddress(albert_payment_addr.clone()), ); shielded_ctx.shell.write().execute_tx(&shield_tx.to_ref()); diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index 6914d86cea5..3c7e70852aa 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -353,6 +353,13 @@ const FMD_PAYMENT_ADDRESS_SIZE: usize = #[derive(Clone, Debug, Copy, Eq, PartialEq, Default)] pub struct DiversifierIndex(masp_primitives::zip32::DiversifierIndex); +impl DiversifierIndex { + /// Return the next payment address index. + pub fn increment(&mut self) { + self.0.increment().expect("exhausted payment addresses"); + } +} + impl From for DiversifierIndex { fn from(idx: masp_primitives::zip32::DiversifierIndex) -> Self { Self(idx) @@ -986,6 +993,131 @@ pub fn addr_taddr(addr: Address) -> TransparentAddress { TAddrData::Addr(addr).taddress() } +/// Unifies FMD and non-FMD payment address types. +#[derive( + Debug, + Clone, + BorshDeserialize, + BorshSerialize, + BorshDeserializer, + Hash, + Eq, + PartialEq, + Ord, + PartialOrd, +)] +pub enum UnifiedPaymentAddress { + /// First payment address format introduced by Namada. + /// + /// Identical to a Zcash sapling payment address. + V0(PaymentAddress), + /// Payment address similar to [`Self::V0`], augmented with + /// [`FmdPublicKeyBytes`]. + V1(FmdPaymentAddress), +} + +impl UnifiedPaymentAddress { + /// Create a v0 payment address. + pub fn v0_from_zip32( + vk: ExtendedViewingKey, + div_index: DiversifierIndex, + ) -> (DiversifierIndex, Self) { + let (div_index, pa) = + masp_primitives::zip32::ExtendedFullViewingKey::from(vk) + .find_address(div_index.into()) + .expect("exhausted payment address diversifier indices"); + + (DiversifierIndex(div_index), Self::V0(PaymentAddress(pa))) + } + + /// Create a v1 payment address. + pub fn v1_from_zip32( + vk: ExtendedViewingKey, + div_index: DiversifierIndex, + ) -> (DiversifierIndex, Self) { + let fmd_sk: FmdSecretKey = vk.0.fvk.vk.ivk().into(); + + let (div_index, pa) = + masp_primitives::zip32::ExtendedFullViewingKey::from(vk) + .find_address(div_index.into()) + .expect("exhausted payment address diversifier indices"); + + ( + DiversifierIndex(div_index), + Self::V1(PaymentAddress(pa).with_fmd_key(&fmd_sk)), + ) + } +} + +impl serde::Serialize for UnifiedPaymentAddress { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + serde::Serialize::serialize(&self.to_string(), serializer) + } +} + +impl<'de> serde::Deserialize<'de> for UnifiedPaymentAddress { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let encoded: String = serde::Deserialize::deserialize(deserializer)?; + Self::from_str(&encoded).map_err(D::Error::custom) + } +} + +impl From<&UnifiedPaymentAddress> for masp_primitives::sapling::PaymentAddress { + fn from(pa: &UnifiedPaymentAddress) -> Self { + match pa { + UnifiedPaymentAddress::V0(pa) => pa.0, + UnifiedPaymentAddress::V1(pa) => *pa.as_payment_address(), + } + } +} + +impl From for masp_primitives::sapling::PaymentAddress { + fn from(pa: UnifiedPaymentAddress) -> Self { + (&pa).into() + } +} + +impl From for UnifiedPaymentAddress { + fn from(pa: PaymentAddress) -> Self { + Self::V0(pa) + } +} + +impl From for UnifiedPaymentAddress { + fn from(pa: FmdPaymentAddress) -> Self { + Self::V1(pa) + } +} + +impl Display for UnifiedPaymentAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::V0(pa) => pa.fmt(f), + Self::V1(pa) => pa.fmt(f), + } + } +} + +impl FromStr for UnifiedPaymentAddress { + type Err = DecodeError; + + fn from_str(s: &str) -> Result { + PaymentAddress::from_str(s) + .map(Self::V0) + .or_else(|_err| FmdPaymentAddress::from_str(s).map(Self::V1)) + } +} + /// Represents a target for the funds of a transfer #[derive( Debug, @@ -1001,7 +1133,7 @@ pub enum TransferTarget { /// A transfer going to a transparent address Address(Address), /// A transfer going to a shielded address - PaymentAddress(PaymentAddress), + PaymentAddress(UnifiedPaymentAddress), /// A transfer going to an IBC address Ibc(String), } @@ -1021,9 +1153,9 @@ impl TransferTarget { } /// Get the contained PaymentAddress, if any - pub fn payment_address(&self) -> Option { + pub fn payment_address(&self) -> Option { match self { - Self::PaymentAddress(address) => Some(*address), + Self::PaymentAddress(address) => Some(address.clone()), _ => None, } } @@ -1050,7 +1182,12 @@ impl Display for TransferTarget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Address(x) => x.fmt(f), - Self::PaymentAddress(address) => address.fmt(f), + Self::PaymentAddress(UnifiedPaymentAddress::V0(address)) => { + address.fmt(f) + } + Self::PaymentAddress(UnifiedPaymentAddress::V1(address)) => { + address.fmt(f) + } Self::Ibc(x) => x.fmt(f), } } @@ -1109,7 +1246,7 @@ impl Display for BalanceOwner { #[derive(Debug, Clone)] pub enum MaspValue { /// A MASP PaymentAddress - PaymentAddress(PaymentAddress), + PaymentAddress(UnifiedPaymentAddress), /// A MASP ExtendedSpendingKey ExtendedSpendingKey(ExtendedSpendingKey), /// A MASP FullViewingKey @@ -1120,9 +1257,10 @@ impl FromStr for MaspValue { type Err = DecodeError; fn from_str(s: &str) -> Result { - // Try to decode this value first as a PaymentAddress, then as an - // ExtendedSpendingKey, then as FullViewingKey - PaymentAddress::from_str(s) + // Try to decode this value first as a UnifiedPaymentAddress, + // then as an ExtendedSpendingKey, then as a + // FullViewingKey + UnifiedPaymentAddress::from_str(s) .map(Self::PaymentAddress) .or_else(|_err| { ExtendedSpendingKey::from_str(s).map(Self::ExtendedSpendingKey) @@ -1262,7 +1400,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa); assert_eq!(target.effective_address(), MASP); @@ -1281,7 +1419,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa).address(); assert!(target.is_none()); @@ -1303,7 +1441,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa).t_addr_data(); assert!(target.is_none()); @@ -1319,7 +1457,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa: UnifiedPaymentAddress = PaymentAddress::from(pa).into(); const IBC_ADDR: &str = "noble18st0wqx84av8y6xdlss9d6m2nepyqwj6nfxxuv"; @@ -1404,7 +1542,7 @@ mod test { masp_primitives::zip32::ExtendedSpendingKey::master(&[0_u8]), ); let (_diversifier, pa) = sk.0.default_address(); - let pa = PaymentAddress::from(pa); + let pa = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa); let serialized = target.serialize_to_vec(); diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index b187c4f92f1..e23eeba47c9 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -116,7 +116,8 @@ pub use namada_sdk::tx::{ use namada_sdk::wallet::{DatedSpendingKey, Wallet}; use namada_sdk::{ FlagCiphertext, Namada, NamadaImpl, PaymentAddress, TransferSource, - TransferTarget, parameters, proof_of_stake, tendermint, + TransferTarget, UnifiedPaymentAddress, parameters, proof_of_stake, + tendermint, }; use namada_test_utils::tx_data::TxWriteData; use namada_vm::wasm::run; @@ -1188,7 +1189,9 @@ impl Default for BenchShieldedCtx { .wallet .insert_payment_addr( alias, - PaymentAddress::from(payment_addr), + UnifiedPaymentAddress::V0(PaymentAddress::from( + payment_addr, + )), true, ) .unwrap(); diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index 5d228b7c6fe..850e8dc571b 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -15,7 +15,7 @@ use namada_core::dec::Dec; use namada_core::ethereum_events::EthAddress; use namada_core::keccak::KeccakHash; use namada_core::key::{SchemeType, common}; -use namada_core::masp::{DiversifierIndex, MaspEpoch, PaymentAddress}; +use namada_core::masp::{DiversifierIndex, MaspEpoch}; use namada_core::string_encoding::StringEncoded; use namada_core::time::DateTimeUtc; use namada_core::token::Amount; @@ -131,7 +131,7 @@ impl NamadaTypes for SdkTypes { type EthereumAddress = (); type Keypair = namada_core::key::common::SecretKey; type MaspIndexerAddress = String; - type PaymentAddress = namada_core::masp::PaymentAddress; + type PaymentAddress = namada_core::masp::UnifiedPaymentAddress; type PublicKey = namada_core::key::common::PublicKey; type SpendingKey = PseudoExtendedKey; type TendermintAddress = tendermint_rpc::Url; @@ -2956,7 +2956,7 @@ pub struct KeyAddressFind { /// Public key hash to lookup keypair with pub public_key_hash: Option, /// Payment address to find - pub payment_address: Option, + pub payment_address: Option, /// Find keys only pub keys_only: bool, /// Find addresses only diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 3894644c8c7..d26a5b1f78e 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -52,8 +52,8 @@ use namada_core::ethereum_events::EthAddress; use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; use namada_core::key::*; pub use namada_core::masp::{ - ExtendedSpendingKey, ExtendedViewingKey, FlagCiphertext, PaymentAddress, - TransferSource, TransferTarget, + ExtendedSpendingKey, ExtendedViewingKey, FlagCiphertext, FmdPaymentAddress, + PaymentAddress, TransferSource, TransferTarget, UnifiedPaymentAddress, }; pub use namada_core::{control_flow, task_env}; use namada_io::{Client, Io, NamadaIo}; @@ -189,7 +189,7 @@ pub trait Namada: NamadaIo { /// arguments fn new_shielding_transfer( &self, - target: PaymentAddress, + target: UnifiedPaymentAddress, data: Vec, ) -> args::TxShieldingTransfer { args::TxShieldingTransfer { diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 09a554c22e2..54950430348 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -3397,7 +3397,7 @@ pub async fn build_shielding_transfer( transfer_data.push(MaspTransferData { source: TransferSource::Address(source.to_owned()), - target: TransferTarget::PaymentAddress(args.target), + target: TransferTarget::PaymentAddress(args.target.clone()), token: token.to_owned(), amount: validated_amount, }); @@ -4184,8 +4184,8 @@ async fn get_refund_target( match (source, refund_target) { (_, Some(TransferTarget::PaymentAddress(pa))) => { Err(Error::Other(format!( - "Supporting only a transparent address as a refund target: {}", - pa, + "Supporting only a transparent address as a refund target: \ + {pa}" ))) } ( diff --git a/crates/shielded_token/src/masp/shielded_wallet.rs b/crates/shielded_token/src/masp/shielded_wallet.rs index 4c7312697dd..645216d6f87 100644 --- a/crates/shielded_token/src/masp/shielded_wallet.rs +++ b/crates/shielded_token/src/masp/shielded_wallet.rs @@ -1724,7 +1724,7 @@ pub trait ShieldedApi: }); // Make transaction output tied to the current token, // denomination, and epoch. - if let Some(pa) = payment_address { + if let Some(ref pa) = payment_address { // If there is a shielded output builder .add_sapling_output( diff --git a/crates/wallet/src/lib.rs b/crates/wallet/src/lib.rs index d10f1d76075..983bdc2ca96 100644 --- a/crates/wallet/src/lib.rs +++ b/crates/wallet/src/lib.rs @@ -20,7 +20,8 @@ use namada_core::chain::BlockHeight; use namada_core::collections::{HashMap, HashSet}; use namada_core::key::*; use namada_core::masp::{ - DiversifierIndex, ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, + DiversifierIndex, ExtendedSpendingKey, ExtendedViewingKey, + UnifiedPaymentAddress, }; use namada_core::time::DateTimeUtc; use namada_ibc::trace::is_ibc_denom; @@ -463,14 +464,14 @@ impl Wallet { pub fn find_payment_addr( &self, alias: impl AsRef, - ) -> Option<&PaymentAddress> { + ) -> Option<&UnifiedPaymentAddress> { self.store.find_payment_addr(alias.as_ref()) } /// Find an alias by the payment address if it's in the wallet. pub fn find_alias_by_payment_addr( &self, - payment_address: &PaymentAddress, + payment_address: &UnifiedPaymentAddress, ) -> Option<&Alias> { self.store.find_alias_by_payment_addr(payment_address) } @@ -508,11 +509,11 @@ impl Wallet { } /// Get all known payment addresses by their alias - pub fn get_payment_addrs(&self) -> HashMap { + pub fn get_payment_addrs(&self) -> HashMap { self.store .get_payment_addrs() .iter() - .map(|(alias, value)| (alias.into(), *value)) + .map(|(alias, value)| (alias.into(), value.clone())) .collect() } @@ -1231,7 +1232,7 @@ impl Wallet { pub fn insert_payment_addr( &mut self, alias: String, - payment_addr: PaymentAddress, + payment_addr: UnifiedPaymentAddress, force_alias: bool, ) -> Option { self.store diff --git a/crates/wallet/src/store.rs b/crates/wallet/src/store.rs index a27bbe446cc..fa3598d5d3b 100644 --- a/crates/wallet/src/store.rs +++ b/crates/wallet/src/store.rs @@ -15,6 +15,7 @@ use namada_core::collections::HashSet; use namada_core::key::*; use namada_core::masp::{ DiversifierIndex, ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, + UnifiedPaymentAddress, }; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; @@ -69,7 +70,7 @@ pub struct Store { /// Known spending keys spend_keys: BTreeMap>, /// Payment address book - payment_addrs: BiBTreeMap, + payment_addrs: BiBTreeMap, /// Diverisifier index of the next payment address to be generated for a /// given key. diversifier_indices: BTreeMap, @@ -171,14 +172,14 @@ impl Store { pub fn find_payment_addr( &self, alias: impl AsRef, - ) -> Option<&PaymentAddress> { + ) -> Option<&UnifiedPaymentAddress> { self.payment_addrs.get_by_left(&alias.into()) } /// Find an alias by the address if it's in the wallet. pub fn find_alias_by_payment_addr( &self, - payment_address: &PaymentAddress, + payment_address: &UnifiedPaymentAddress, ) -> Option<&Alias> { self.payment_addrs.get_by_right(payment_address) } @@ -282,7 +283,9 @@ impl Store { } /// Get all known payment addresses by their alias. - pub fn get_payment_addrs(&self) -> &BiBTreeMap { + pub fn get_payment_addrs( + &self, + ) -> &BiBTreeMap { &self.payment_addrs } @@ -537,7 +540,7 @@ impl Store { pub fn insert_payment_addr( &mut self, alias: Alias, - payment_addr: PaymentAddress, + payment_addr: UnifiedPaymentAddress, force: bool, ) -> Option { // abort if the alias is reserved @@ -910,7 +913,11 @@ pub struct StoreV0 { impl From for Store { fn from(store: StoreV0) -> Self { let mut to = Store { - payment_addrs: store.payment_addrs, + payment_addrs: store + .payment_addrs + .into_iter() + .map(|(alias, pa)| (alias, UnifiedPaymentAddress::V0(pa))) + .collect(), secret_keys: store.secret_keys, public_keys: store.public_keys, derivation_paths: store.derivation_paths, @@ -962,7 +969,7 @@ pub struct StoreV1 { /// Known spending keys spend_keys: BTreeMap>, /// Payment address book - payment_addrs: BiBTreeMap, + payment_addrs: BiBTreeMap, /// Cryptographic keypairs secret_keys: BTreeMap>, /// Known public keys From 3a5c41f77f026341bb4735df600dc5a1dbbfb80d Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 6 May 2025 14:10:03 +0100 Subject: [PATCH 32/40] Generate v0 payment addrs --- crates/apps_lib/src/cli.rs | 8 ++++++++ crates/apps_lib/src/cli/wallet.rs | 8 ++++++-- crates/sdk/src/args.rs | 6 ++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index de7dfdaf7a7..129be0d0613 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -3631,6 +3631,7 @@ pub mod args { pub const PAYMENT_ADDRESS_TARGET: Arg = arg("target"); pub const PAYMENT_ADDRESS_TARGET_OPT: ArgOpt = arg_opt("target-pa"); + pub const PAYMENT_ADDRESS_V0: ArgFlag = flag("v0"); pub const PORT_ID: ArgDefault = arg_default( "port-id", DefaultFn(|| PortId::from_str("transfer").unwrap()), @@ -7980,6 +7981,7 @@ pub mod args { _ctx: &mut Context, ) -> Result { Ok(PayAddressGen { + v0: self.v0, alias: self.alias, alias_force: self.alias_force, viewing_key: self.viewing_key, @@ -7994,11 +7996,13 @@ pub mod args { let alias_force = ALIAS_FORCE.parse(matches); let diversifier_index = DIVERSIFIER_INDEX.parse(matches); let viewing_key = VIEWING_KEY_ALIAS.parse(matches); + let v0 = PAYMENT_ADDRESS_V0.parse(matches); Self { alias, alias_force, diversifier_index, viewing_key, + v0, } } @@ -8013,6 +8017,10 @@ pub mod args { "Set the viewing key's current diversifier index beforehand." ))) .arg(VIEWING_KEY.def().help(wrap!("The viewing key."))) + .arg(PAYMENT_ADDRESS_V0.def().help(wrap!( + "Force generating a v0 payment address. Not compatible with \ + FMD." + ))) } } diff --git a/crates/apps_lib/src/cli/wallet.rs b/crates/apps_lib/src/cli/wallet.rs index 07c94a22203..3d3cda402b4 100644 --- a/crates/apps_lib/src/cli/wallet.rs +++ b/crates/apps_lib/src/cli/wallet.rs @@ -403,6 +403,7 @@ fn payment_address_gen( alias_force, viewing_key: viewing_key_alias, diversifier_index, + v0, }: args::PayAddressGen, ) { let mut wallet = load_wallet(ctx); @@ -421,8 +422,11 @@ fn payment_address_gen( .copied() .unwrap_or_default() }); - let (diversifier_index, payment_addr) = - UnifiedPaymentAddress::v1_from_zip32(viewing_key, diversifier_index); + let (diversifier_index, payment_addr) = if v0 { + UnifiedPaymentAddress::v0_from_zip32(viewing_key, diversifier_index) + } else { + UnifiedPaymentAddress::v1_from_zip32(viewing_key, diversifier_index) + }; let mut next_div_idx = diversifier_index; next_div_idx.increment(); let alias = wallet diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index 850e8dc571b..ef6612f7c85 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -3021,6 +3021,12 @@ pub struct KeyAddressRemove { /// Generate payment address arguments #[derive(Clone, Debug)] pub struct PayAddressGen { + /// Force generating a v0 payment address. + /// + /// This does not include an FMD public key, therefore + /// should not be shared as a payment target if you + /// intend to use FMD to speed up shielded sync. + pub v0: bool, /// Payment address alias pub alias: String, /// Whether to force overwrite the alias From bfd957c10e460fbba53bfc74806509906a7cedf7 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 6 May 2025 15:10:53 +0100 Subject: [PATCH 33/40] Add flag ciphertexts to txs --- crates/node/src/bench_utils.rs | 1 + crates/sdk/src/lib.rs | 7 ++- crates/sdk/src/tx.rs | 53 ++++++++++++----- crates/shielded_token/src/masp.rs | 8 +++ .../src/masp/shielded_wallet.rs | 57 +++++++++++++++++-- crates/token/src/lib.rs | 17 +++++- 6 files changed, 122 insertions(+), 21 deletions(-) diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index e23eeba47c9..34d73c5627c 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -1280,6 +1280,7 @@ impl BenchShieldedCtx { masp_tx, metadata: _, epoch: _, + fmd_flags: _, }| masp_tx, ) .expect("MASP must have shielded part"); diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index d26a5b1f78e..a01067024e9 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -901,7 +901,7 @@ pub mod testing { arb_withdraw, }; use crate::tx::{ - Authorization, Code, Commitment, Header, MaspBuilder, Section, + Authorization, Code, Commitment, Data, Header, MaspBuilder, Section, TxCommitments, }; @@ -1146,6 +1146,11 @@ pub mod testing { if let Some((shielded_transfer, asset_types, build_params)) = aux { let shielded_section_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx).1; + tx.add_section( + Section::Data( + Data::from_borsh_encoded(&shielded_transfer.fmd_flags), + ), + ); tx.add_masp_builder(MaspBuilder { asset_types: asset_types.into_keys().collect(), // Store how the Info objects map to Descriptors/Outputs diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 54950430348..44241a81113 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -42,7 +42,8 @@ use namada_core::ibc::core::host::types::identifiers::{ChannelId, PortId}; use namada_core::ibc::primitives::{IntoTimestamp, Timestamp as IbcTimestamp}; use namada_core::key::{self, *}; use namada_core::masp::{ - AssetData, MaspEpoch, MaspTxData, TransferSource, TransferTarget, + AssetData, FlagCiphertext, MaspEpoch, MaspTxData, TransferSource, + TransferTarget, }; use namada_core::storage; use namada_core::time::DateTimeUtc; @@ -2821,19 +2822,25 @@ pub async fn build_ibc_transfer( .map(|(shielded_transfer, asset_types)| { let masp_tx_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx.clone()).1; + + let (fmd_section, flag_ciphertext_sechash) = + create_fmd_section(shielded_transfer.fmd_flags); + tx.add_section(fmd_section); + transfer.shielded_data = Some(MaspTxData { masp_tx_id: masp_tx_hash, - // TODO: change this to the actual sechash - // of the fmd flag - flag_ciphertext_sechash: Hash::zero(), + flag_ciphertext_sechash, }); + signing_data.shielded_hash = Some(masp_tx_hash); + tx.add_masp_builder(MaspBuilder { asset_types, metadata: shielded_transfer.metadata, builder: shielded_transfer.builder, target: masp_tx_hash, }); + Result::Ok(transfer) }) .transpose()?; @@ -3256,9 +3263,16 @@ pub async fn build_shielded_transfer( masp_tx, metadata, epoch: _, + fmd_flags, }, asset_types, ) = shielded_parts; + + // Create FMD flags section + let (fmd_section, flag_ciphertext_sechash) = + create_fmd_section(fmd_flags); + tx.add_section(fmd_section); + // Add a MASP Transaction section to the Tx and get the tx hash let section_hash = tx.add_masp_tx_section(masp_tx).1; @@ -3274,9 +3288,7 @@ pub async fn build_shielded_transfer( data.shielded_data = Some(MaspTxData { masp_tx_id: section_hash, - // TODO: change this to the actual sechash - // of the fmd flag - flag_ciphertext_sechash: Hash::zero(), + flag_ciphertext_sechash, }); signing_data.shielded_hash = Some(section_hash); tracing::debug!("Transfer data {data:?}"); @@ -3431,12 +3443,18 @@ pub async fn build_shielding_transfer( masp_tx, metadata, epoch: _, + fmd_flags, }, asset_types, ) = shielded_parts; // Add a MASP Transaction section to the Tx and get the tx hash let shielded_section_hash = tx.add_masp_tx_section(masp_tx).1; + // Create FMD flags section + let (fmd_section, flag_ciphertext_sechash) = + create_fmd_section(fmd_flags); + tx.add_section(fmd_section); + tx.add_masp_builder(MaspBuilder { asset_types, // Store how the Info objects map to Descriptors/Outputs @@ -3449,9 +3467,7 @@ pub async fn build_shielding_transfer( data.shielded_data = Some(MaspTxData { masp_tx_id: shielded_section_hash, - // TODO: change this to the actual sechash - // of the fmd flag - flag_ciphertext_sechash: Hash::zero(), + flag_ciphertext_sechash, }); signing_data.shielded_hash = Some(shielded_section_hash); tracing::debug!("Transfer data {data:?}"); @@ -3559,12 +3575,18 @@ pub async fn build_unshielding_transfer( masp_tx, metadata, epoch: _, + fmd_flags, }, asset_types, ) = shielded_parts; // Add a MASP Transaction section to the Tx and get the tx hash let shielded_section_hash = tx.add_masp_tx_section(masp_tx).1; + // Create FMD flags section + let (fmd_section, flag_ciphertext_sechash) = + create_fmd_section(fmd_flags); + tx.add_section(fmd_section); + tx.add_masp_builder(MaspBuilder { asset_types, // Store how the Info objects map to Descriptors/Outputs @@ -3577,9 +3599,7 @@ pub async fn build_unshielding_transfer( data.shielded_data = Some(MaspTxData { masp_tx_id: shielded_section_hash, - // TODO: change this to the actual sechash - // of the fmd flag - flag_ciphertext_sechash: Hash::zero(), + flag_ciphertext_sechash, }); signing_data.shielded_hash = Some(shielded_section_hash); tracing::debug!("Transfer data {data:?}"); @@ -4329,3 +4349,10 @@ fn proposal_to_vec(proposal: OnChainProposal) -> Result> { borsh::to_vec(&proposal.content) .map_err(|e| Error::from(EncodingError::Conversion(e.to_string()))) } + +fn create_fmd_section(fmd_flags: Vec) -> (Section, Hash) { + let fmd_section = Section::Data(Data::from_borsh_encoded(&fmd_flags)); + let fmd_sechash = fmd_section.get_hash(); + + (fmd_section, fmd_sechash) +} diff --git a/crates/shielded_token/src/masp.rs b/crates/shielded_token/src/masp.rs index b20b0ec5352..8399bdcc099 100644 --- a/crates/shielded_token/src/masp.rs +++ b/crates/shielded_token/src/masp.rs @@ -74,6 +74,11 @@ pub struct ShieldedTransfer { pub metadata: SaplingMetadata, /// Epoch in which the transaction was created pub epoch: MaspEpoch, + /// Vector of FMD flag ciphertexts. + /// + /// There must be a flag ciphertext per shielded output + /// in the `builder`. + pub fmd_flags: Vec, } /// The data for a masp fee payment @@ -191,6 +196,9 @@ pub enum TransferErr { /// Insufficient funds error #[error("Insufficient funds: {0}")] InsufficientFunds(MaspDataLog), + /// Invalid FMD public key + #[error("FMD public key included in the payment address is not valid")] + InvalidFmdPublicKey, /// Generic error #[error("{0}")] General(String), diff --git a/crates/shielded_token/src/masp/shielded_wallet.rs b/crates/shielded_token/src/masp/shielded_wallet.rs index 645216d6f87..0d1e9dccf31 100644 --- a/crates/shielded_token/src/masp/shielded_wallet.rs +++ b/crates/shielded_token/src/masp/shielded_wallet.rs @@ -31,7 +31,8 @@ use namada_core::chain::BlockHeight; use namada_core::collections::{HashMap, HashSet}; use namada_core::control_flow; use namada_core::masp::{ - AssetData, MaspEpoch, TransferSource, TransferTarget, encode_asset_type, + AssetData, FlagCiphertext, FmdSecretKey, MaspEpoch, TransferSource, + TransferTarget, UnifiedPaymentAddress, encode_asset_type, }; use namada_core::task_env::TaskEnvironment; use namada_core::time::{DateTimeUtc, DurationSecs}; @@ -45,7 +46,7 @@ use namada_io::{ }; use namada_wallet::{DatedKeypair, DatedSpendingKey}; use rand::prelude::StdRng; -use rand_core::{OsRng, SeedableRng}; +use rand_core::{CryptoRng, OsRng, RngCore, SeedableRng}; use super::utils::MaspIndexedTx; use crate::masp::utils::MaspClient; @@ -1215,9 +1216,12 @@ pub trait ShieldedApi: return Ok(None); }; + let mut fmd_flags = vec![]; + for (MaspSourceTransferData { source, token }, amount) in &source_data { self.add_inputs( context, + &mut rng, &mut builder, source, token, @@ -1225,6 +1229,7 @@ pub trait ShieldedApi: epoch, &mut denoms, &mut notes_tracker, + &mut fmd_flags, ) .await?; } @@ -1240,6 +1245,7 @@ pub trait ShieldedApi: { self.add_outputs( context, + &mut rng, &mut builder, source, &target, @@ -1247,6 +1253,7 @@ pub trait ShieldedApi: amount, epoch, &mut denoms, + &mut fmd_flags, ) .await?; } @@ -1312,11 +1319,31 @@ pub trait ShieldedApi: ) .map_err(|error| TransferErr::Build { error })?; + // Add remaining flag ciphertexts + fmd_flags.extend({ + let num_shielded_outputs = masp_tx + .sapling_bundle() + .map_or(0, |x| x.shielded_outputs.len()); + + let num_fmd_flags = fmd_flags.len(); + + let dummy_flag_ciphertexts = + checked!(num_shielded_outputs - num_fmd_flags).expect( + "number of shielded outputs in the masp bundle should be \ + greater than or equal to the number of flag ciphertexts \ + generated so far", + ); + + std::iter::repeat_with(|| FlagCiphertext::random(&mut rng)) + .take(dummy_flag_ciphertexts) + }); + Ok(Some(ShieldedTransfer { builder: builder_clone, masp_tx, metadata, epoch, + fmd_flags, })) } @@ -1509,9 +1536,10 @@ pub trait ShieldedApi: /// must be the current epoch. #[allow(async_fn_in_trait)] #[allow(clippy::too_many_arguments)] - async fn add_inputs( + async fn add_inputs( &mut self, context: &impl NamadaIo, + rng: &mut R, builder: &mut Builder, source: &TransferSource, token: &Address, @@ -1519,6 +1547,7 @@ pub trait ShieldedApi: epoch: MaspEpoch, denoms: &mut HashMap, notes_tracker: &mut SpentNotesTracker, + fmd_flags: &mut Vec, ) -> Result, TransferErr> { // We want to fund our transaction solely from supplied spending key let spending_key = source.spending_key(); @@ -1605,6 +1634,12 @@ pub trait ShieldedApi: .map_err(|e| TransferErr::Build { error: builder::Error::SaplingBuild(e), })?; + + fmd_flags.push({ + let fmd_sk: FmdSecretKey = + sk.to_viewing_key().fvk.vk.ivk().into(); + fmd_sk.default_public_key().flag(rng) + }); } // Commit the notes found to our transaction @@ -1669,9 +1704,10 @@ pub trait ShieldedApi: /// Add the necessary transaction outputs to the builder #[allow(clippy::too_many_arguments)] #[allow(async_fn_in_trait)] - async fn add_outputs( + async fn add_outputs( &mut self, context: &impl NamadaIo, + rng: &mut R, builder: &mut Builder, source: Option, target: &TransferTarget, @@ -1679,6 +1715,7 @@ pub trait ShieldedApi: amount: Amount, epoch: MaspEpoch, denoms: &mut HashMap, + fmd_flags: &mut Vec, ) -> Result<(), TransferErr> { // Anotate the asset type in the value balance with its decoding in // order to facilitate cross-epoch computations @@ -1737,6 +1774,18 @@ pub trait ShieldedApi: .map_err(|e| TransferErr::Build { error: builder::Error::SaplingBuild(e), })?; + + fmd_flags.push({ + match pa { + UnifiedPaymentAddress::V0(_) => { + FlagCiphertext::random(rng) + } + UnifiedPaymentAddress::V1(pa) => pa + .to_fmd_public_key() + .ok_or(TransferErr::InvalidFmdPublicKey)? + .flag(rng), + } + }); } else if let Some(t_addr_data) = target.t_addr_data() { // If there is a transparent output builder diff --git a/crates/token/src/lib.rs b/crates/token/src/lib.rs index 24a36d5d27d..d91173cbaca 100644 --- a/crates/token/src/lib.rs +++ b/crates/token/src/lib.rs @@ -357,9 +357,8 @@ pub mod testing { use namada_core::address::testing::arb_non_internal_address; use namada_core::address::{Address, MASP}; use namada_core::collections::HashMap; - use namada_core::hash::Hash; use namada_core::masp::{ - AssetData, MaspTxData, TAddrData, encode_asset_type, + AssetData, FlagCiphertext, MaspTxData, TAddrData, encode_asset_type, }; pub use namada_core::token::*; use namada_shielded_token::masp::testing::{ @@ -531,15 +530,27 @@ pub mod testing { &mut rng, &mut rng_build_params, ).unwrap(); + let fmd_flags = std::iter::repeat_with( + || FlagCiphertext::random(&mut rng) + ) + .take(builder.sapling_outputs().len()) + .collect(); + let fmd_sechash = { + let sec = namada_tx::Section::Data( + namada_tx::Data::from_borsh_encoded(&fmd_flags), + ); + sec.get_hash() + }; transfer.shielded_data = Some(MaspTxData { masp_tx_id: masp_tx.txid().into(), - flag_ciphertext_sechash: Hash::zero(), + flag_ciphertext_sechash: fmd_sechash, }); (transfer, ShieldedTransfer { builder: builder.map_builder(WalletMap), metadata, masp_tx, epoch, + fmd_flags, }, asset_types, rng_build_params.to_stored().unwrap()) } } From 70eaa07b5972474cfe89d5dd45c68759b042a2e6 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 8 May 2025 14:46:03 +0100 Subject: [PATCH 34/40] Validate flag ciphertexts in MASP VP --- crates/apps_lib/src/client/tx.rs | 9 +-- crates/core/src/masp.rs | 16 ++++ crates/ibc/src/lib.rs | 26 ++++--- crates/ibc/src/msg.rs | 27 +++++-- crates/sdk/src/args.rs | 5 +- crates/sdk/src/tx.rs | 4 +- .../src/masp/shielded_wallet.rs | 16 +--- crates/shielded_token/src/vp.rs | 73 +++++++++++++++++-- crates/systems/src/ibc.rs | 13 +++- crates/token/src/tx.rs | 6 ++ crates/tx/src/action.rs | 31 ++++++++ crates/tx/src/types.rs | 30 +++++++- wasm/tx_ibc/src/lib.rs | 25 ++++--- wasm/vp_implicit/src/lib.rs | 4 +- wasm/vp_user/src/lib.rs | 4 +- 15 files changed, 224 insertions(+), 65 deletions(-) diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 3f420cc0769..0b6ae83de22 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1935,7 +1935,8 @@ pub async fn gen_ibc_shielding_transfer( ) -> Result<(), error::Error> { let output_folder = args.output_folder.clone(); - if let Some(masp_tx) = tx::gen_ibc_shielding_transfer(context, args).await? + if let Some((masp_tx, fmd_flags)) = + tx::gen_ibc_shielding_transfer(context, args).await? { let tx_id = masp_tx.txid().to_string(); let filename = format!("ibc_masp_tx_{}.memo", tx_id); @@ -1945,11 +1946,7 @@ pub async fn gen_ibc_shielding_transfer( }; 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, - // TODO: add actual flag ciphertext - Default::default(), - ); + let bytes = convert_masp_tx_to_ibc_memo(masp_tx, fmd_flags); out.write_all(bytes.as_bytes()) .expect("Writing IBC MASP transaction file failed."); println!( diff --git a/crates/core/src/masp.rs b/crates/core/src/masp.rs index 3c7e70852aa..a7dc5323f37 100644 --- a/crates/core/src/masp.rs +++ b/crates/core/src/masp.rs @@ -1017,6 +1017,22 @@ pub enum UnifiedPaymentAddress { } impl UnifiedPaymentAddress { + /// Generate a [`FlagCiphertext`] from this payment address. + /// + /// Returns [`None`] if the FMD public key in the + /// [`UnifiedPaymentAddress::V1`] variant is invalid, and a random flag + /// ciphertext for the [`UnifiedPaymentAddress::V0`] variant. + #[cfg(feature = "rand")] + pub fn flag(&self, rng: &mut R) -> Option + where + R: rand_core::CryptoRng + rand_core::RngCore, + { + match self { + Self::V0(_) => Some(FlagCiphertext::random(rng)), + Self::V1(pa) => pa.to_fmd_public_key().map(|pk| pk.flag(rng)), + } + } + /// Create a v0 payment address. pub fn v0_from_zip32( vk: ExtendedViewingKey, diff --git a/crates/ibc/src/lib.rs b/crates/ibc/src/lib.rs index 4a98d76afc0..4aa52e5267c 100644 --- a/crates/ibc/src/lib.rs +++ b/crates/ibc/src/lib.rs @@ -92,7 +92,7 @@ use namada_core::ibc::core::channel::types::commitment::{ AcknowledgementCommitment, PacketCommitment, compute_packet_commitment, }; pub use namada_core::ibc::*; -use namada_core::masp::{TAddrData, addr_taddr, ibc_taddr}; +use namada_core::masp::{FlagCiphertext, TAddrData, addr_taddr, ibc_taddr}; use namada_core::masp_primitives::transaction::components::ValueSum; use namada_core::token::Amount; use namada_events::EmitEvents; @@ -237,18 +237,19 @@ impl namada_systems::ibc::Read for Store where S: StorageRead, { - fn try_extract_masp_tx_from_envelope( + fn try_extract_shielding_data_from_envelope( tx_data: &[u8], - ) -> StorageResult> { + ) -> StorageResult)>> { let msg = decode_message::(tx_data) .into_storage_result() .ok(); let tx = if let Some(IbcMessage::Envelope(ref envelope)) = msg { - Some(extract_masp_tx_from_envelope(envelope).ok_or_else(|| { - StorageError::new_const( - "Missing MASP transaction in IBC message", - ) - })?) + let shielding_data = extract_shielding_data_from_envelope(envelope) + .ok_or(StorageError::new_const( + "Missing MASP shielding data in IBC message", + ))?; + + Some((shielding_data.masp_tx, shielding_data.flag_ciphertexts)) } else { None }; @@ -578,7 +579,7 @@ pub struct InternalData { /// The transparent transfer that happens in parallel to IBC processes pub transparent: Option, /// The shielded transaction that happens in parallel to IBC processes - pub shielded: Option, + pub shielded: Option, /// IBC tokens that are credited/debited to internal accounts pub ibc_tokens: BTreeSet
, } @@ -760,13 +761,16 @@ where .map_err(Error::Storage)?; tokens.insert(token); } - (extract_masp_tx_from_packet(&msg.packet), tokens) + ( + extract_shielding_data_from_packet(&msg.packet), + tokens, + ) } #[cfg(is_apple_silicon)] MsgEnvelope::Packet(PacketMsg::Ack(msg)) => { // NOTE: This is unneeded but wasm compilation error // happened if deleted on macOS with Apple Silicon - let _ = extract_masp_tx_from_packet(&msg.packet); + let _ = extract_shielding_data_from_packet(&msg.packet); (None, BTreeSet::new()) } _ => (None, BTreeSet::new()), diff --git a/crates/ibc/src/msg.rs b/crates/ibc/src/msg.rs index b93fd17d373..67c44c6d8d7 100644 --- a/crates/ibc/src/msg.rs +++ b/crates/ibc/src/msg.rs @@ -242,8 +242,8 @@ impl BorshSchema for MsgNftTransfer { pub struct IbcShieldingData { /// MASP transaction forwarded over IBC. pub masp_tx: MaspTransaction, - /// Flag ciphertext to signal the owner of the new note(s). - pub flag_ciphertext: FlagCiphertext, + /// Flag ciphertexts to signal the owner(s) of the new note(s). + pub flag_ciphertexts: Vec, } impl From<&IbcShieldingData> for String { @@ -279,9 +279,17 @@ impl FromStr for IbcShieldingData { pub fn extract_masp_tx_from_envelope( envelope: &MsgEnvelope, ) -> Option { + extract_shielding_data_from_envelope(envelope) + .map(|IbcShieldingData { masp_tx, .. }| masp_tx) +} + +/// Extract IBC shielding data from IBC envelope +pub fn extract_shielding_data_from_envelope( + envelope: &MsgEnvelope, +) -> Option { match envelope { MsgEnvelope::Packet(PacketMsg::Recv(msg)) => { - extract_masp_tx_from_packet(&msg.packet) + extract_shielding_data_from_packet(&msg.packet) } _ => None, } @@ -306,10 +314,17 @@ pub fn decode_ibc_shielding_data( /// Extract MASP transaction from IBC packet memo pub fn extract_masp_tx_from_packet(packet: &Packet) -> Option { + extract_shielding_data_from_packet(packet) + .map(|IbcShieldingData { masp_tx, .. }| masp_tx) +} + +/// Extract IBC shielding data from IBC packet memo +pub fn extract_shielding_data_from_packet( + packet: &Packet, +) -> Option { let memo = extract_memo_from_packet(packet, &packet.port_id_on_b)?; decode_ibc_shielding_data(memo) - .map(|IbcShieldingData { masp_tx, .. }| masp_tx) } fn extract_memo_from_packet( @@ -376,11 +391,11 @@ pub fn extract_traces_from_recv_msg( /// Get IBC memo string from MASP transaction for receiving pub fn convert_masp_tx_to_ibc_memo( masp_tx: MaspTransaction, - flag_ciphertext: FlagCiphertext, + flag_ciphertexts: Vec, ) -> String { IbcShieldingData { masp_tx, - flag_ciphertext, + flag_ciphertexts, } .into() } diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index ef6612f7c85..fe6608ee710 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -718,7 +718,7 @@ impl TxOsmosisSwap { ), }; - let shielding_tx = tx::gen_ibc_shielding_transfer( + let (shielding_tx, fmd_flags) = tx::gen_ibc_shielding_transfer( ctx, GenIbcShieldingTransfer { query: Query { @@ -754,8 +754,7 @@ impl TxOsmosisSwap { shielding_data: StringEncoded::new( IbcShieldingData { masp_tx: shielding_tx, - // TODO: add actual flag ciphertext here - flag_ciphertext: Default::default(), + flag_ciphertexts: fmd_flags, }, ), shielded_amount: amount_to_shield, diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 44241a81113..edce82f7187 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -3981,7 +3981,7 @@ pub async fn build_custom( pub async fn gen_ibc_shielding_transfer( context: &N, args: args::GenIbcShieldingTransfer, -) -> Result> { +) -> Result)>> { let source = IBC; let token = match args.asset { @@ -4043,7 +4043,7 @@ pub async fn gen_ibc_shielding_transfer( .map_err(|err| TxSubmitError::MaspError(err.to_string()))? }; - Ok(shielded_transfer.map(|st| st.masp_tx)) + Ok(shielded_transfer.map(|st| (st.masp_tx, st.fmd_flags))) } pub(crate) async fn get_ibc_src_port_channel( diff --git a/crates/shielded_token/src/masp/shielded_wallet.rs b/crates/shielded_token/src/masp/shielded_wallet.rs index 0d1e9dccf31..987e34bb47c 100644 --- a/crates/shielded_token/src/masp/shielded_wallet.rs +++ b/crates/shielded_token/src/masp/shielded_wallet.rs @@ -32,7 +32,7 @@ use namada_core::collections::{HashMap, HashSet}; use namada_core::control_flow; use namada_core::masp::{ AssetData, FlagCiphertext, FmdSecretKey, MaspEpoch, TransferSource, - TransferTarget, UnifiedPaymentAddress, encode_asset_type, + TransferTarget, encode_asset_type, }; use namada_core::task_env::TaskEnvironment; use namada_core::time::{DateTimeUtc, DurationSecs}; @@ -1775,17 +1775,9 @@ pub trait ShieldedApi: error: builder::Error::SaplingBuild(e), })?; - fmd_flags.push({ - match pa { - UnifiedPaymentAddress::V0(_) => { - FlagCiphertext::random(rng) - } - UnifiedPaymentAddress::V1(pa) => pa - .to_fmd_public_key() - .ok_or(TransferErr::InvalidFmdPublicKey)? - .flag(rng), - } - }); + fmd_flags.push( + pa.flag(rng).ok_or(TransferErr::InvalidFmdPublicKey)?, + ); } else if let Some(t_addr_data) = target.t_addr_data() { // If there is a transparent output builder diff --git a/crates/shielded_token/src/vp.rs b/crates/shielded_token/src/vp.rs index 1875937c5c6..a79602e3961 100644 --- a/crates/shielded_token/src/vp.rs +++ b/crates/shielded_token/src/vp.rs @@ -17,7 +17,9 @@ use namada_core::address::{self, Address}; use namada_core::arith::{CheckedAdd, CheckedSub, checked}; use namada_core::booleans::BoolResultUnitExt; use namada_core::collections::HashSet; -use namada_core::masp::{MaspEpoch, TAddrData, addr_taddr, encode_asset_type}; +use namada_core::masp::{ + FlagCiphertext, MaspEpoch, TAddrData, addr_taddr, encode_asset_type, +}; use namada_core::storage::Key; use namada_core::token; use namada_core::token::{Amount, MaspDigitPos}; @@ -435,12 +437,13 @@ where .data(batched_tx.cmt) .ok_or_err_msg("No transaction data")?; let actions = ctx.read_actions()?; - // Try to get the Transaction object from the tx first (IBC) and from - // the actions afterwards - let shielded_tx = if let Some(tx) = - Ibc::try_extract_masp_tx_from_envelope::(&tx_data)? + + // Try to get the Transaction object and FMD flag ciphertexts + // from the tx first (IBC) and from the actions afterwards + let (shielded_tx, fmd_flags) = if let Some(shielding_data) = + Ibc::try_extract_shielding_data_from_envelope::(&tx_data)? { - tx + shielding_data } else { let masp_section_ref = namada_tx::action::get_masp_section_ref(&actions) @@ -450,14 +453,33 @@ where "Missing MASP section reference in action", ) })?; + let flag_ciphertexts_ref = + namada_tx::action::get_fmd_flag_ciphertexts_ref(&actions) + .map_err(Error::new_const)? + .ok_or_else(|| { + Error::new_const( + "Missing FMD flag ciphertexts reference in action", + ) + })?; - batched_tx + let masp_tx = batched_tx .tx .get_masp_section(&masp_section_ref) .cloned() .ok_or_else(|| { Error::new_const("Missing MASP section in transaction") - })? + })?; + let fmd_flags = batched_tx + .tx + .get_fmd_flag_ciphertexts(&flag_ciphertexts_ref) + .map_err(Error::new)? + .ok_or_else(|| { + Error::new_const( + "Missing FMD flag ciphertexts in transaction", + ) + })?; + + (masp_tx, fmd_flags) }; if u64::from(ctx.get_block_height()?) @@ -468,6 +490,8 @@ where return Err(error); } + validate_flag_ciphertexts(&shielded_tx, fmd_flags)?; + // Check the validity of the keys and get the transfer data let changed_balances = Self::validate_state_and_get_transfer_data( ctx, @@ -949,6 +973,39 @@ fn verify_sapling_balancing_value( } } +/// Check if the flag ciphertexts included in the tx are valid. +fn validate_flag_ciphertexts( + masp_tx: &Transaction, + fmd_flags: Vec, +) -> Result<()> { + let shielded_outputs_len = masp_tx + .sapling_bundle() + .map_or(0, |bundle| bundle.shielded_outputs.len()); + + if shielded_outputs_len != fmd_flags.len() { + let error = Error::new(format!( + "The number of shielded outputs in the MASP tx ({}) does not \ + match the number of FMD flag ciphertexts ({})", + shielded_outputs_len, + fmd_flags.len() + )); + tracing::debug!("{error}"); + return Err(error); + } + + fmd_flags + .iter() + .all(FlagCiphertext::is_valid) + .ok_or_else(|| { + let error = Error::new_const( + "Not all FMD flag ciphertexts in the MASP tx were considered \ + valid, either because of invalid gamma or tampered bits", + ); + tracing::debug!("{error}"); + error + }) +} + #[cfg(test)] mod shielded_token_tests { use std::cell::RefCell; diff --git a/crates/systems/src/ibc.rs b/crates/systems/src/ibc.rs index b492513b9e4..0fb0e5d31fd 100644 --- a/crates/systems/src/ibc.rs +++ b/crates/systems/src/ibc.rs @@ -6,16 +6,21 @@ use masp_primitives::transaction::TransparentAddress; use masp_primitives::transaction::components::ValueSum; use namada_core::address::Address; use namada_core::borsh::BorshDeserialize; -use namada_core::masp::TAddrData; +use namada_core::masp::{FlagCiphertext, TAddrData}; use namada_core::{masp_primitives, storage, token}; pub use namada_storage::Result; /// Abstract IBC storage read interface pub trait Read { - /// Extract MASP transaction from IBC envelope - fn try_extract_masp_tx_from_envelope( + /// Extract shielding data from IBC envelope + fn try_extract_shielding_data_from_envelope( tx_data: &[u8], - ) -> Result>; + ) -> Result< + Option<( + masp_primitives::transaction::Transaction, + Vec, + )>, + >; /// Apply relevant IBC packets to the changed balances structure fn apply_ibc_packet( diff --git a/crates/token/src/tx.rs b/crates/token/src/tx.rs index 0cc2488a63d..74146dc072f 100644 --- a/crates/token/src/tx.rs +++ b/crates/token/src/tx.rs @@ -5,6 +5,7 @@ use std::collections::{BTreeMap, BTreeSet}; use namada_core::arith::CheckedSub; use namada_core::collections::HashSet; +use namada_core::hash::Hash; use namada_core::masp::encode_asset_type; use namada_core::masp_primitives::transaction::Transaction; use namada_core::token::MaspDigitPos; @@ -46,6 +47,7 @@ where apply_shielded_transfer( env, shielded_data.masp_tx_id, + shielded_data.flag_ciphertext_sechash, debited_accounts, tokens, tx_data, @@ -157,6 +159,7 @@ where pub fn apply_shielded_transfer( env: &mut ENV, masp_section_ref: MaspTxId, + fmd_flags_section_ref: Hash, debited_accounts: HashSet
, tokens: HashSet
, tx_data: &BatchedTx, @@ -180,6 +183,9 @@ where env.push_action(Action::Masp(MaspAction::MaspSectionRef( masp_section_ref, )))?; + env.push_action(Action::Masp(MaspAction::FmdSectionRef( + fmd_flags_section_ref, + )))?; update_undated_balances(env, &shielded, tokens)?; // Extract the debited accounts for the masp part of the transfer and // push the relative actions diff --git a/crates/tx/src/action.rs b/crates/tx/src/action.rs index 4b477a2d2ce..c0f54a72a9d 100644 --- a/crates/tx/src/action.rs +++ b/crates/tx/src/action.rs @@ -10,6 +10,7 @@ use std::fmt; use namada_core::address::Address; use namada_core::borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::hash::Hash; use namada_core::masp::MaspTxId; use namada_core::storage::KeySeg; use namada_core::{address, storage}; @@ -71,6 +72,11 @@ pub enum PgfAction { pub enum MaspAction { /// The hash of the masp [`crate::Section`] MaspSectionRef(MaspTxId), + /// The hash of the fmd [`crate::Section`] + /// + /// The data section encodes a vector of flag ciphertexts, + /// one per shielded output + FmdSectionRef(Hash), /// A required authorizer for the transaction MaspAuthorizer(Address), } @@ -148,6 +154,31 @@ pub fn get_masp_section_ref( } } +/// Helper function to get the optional fmd section reference from the +/// [`Actions`]. If more than one [`MaspAction`] is found we return an error +pub fn get_fmd_flag_ciphertexts_ref( + actions: &Actions, +) -> Result, &'static str> { + let flag_ciphertext_refs: Vec<_> = actions + .iter() + .filter_map(|action| { + if let Action::Masp(MaspAction::FmdSectionRef(fmd_section_ref)) = + action + { + Some(*fmd_section_ref) + } else { + None + } + }) + .collect(); + + if flag_ciphertext_refs.len() > 1 { + Err("The transaction pushed multiple FMD flag ciphertext sections") + } else { + Ok(flag_ciphertext_refs.first().cloned()) + } +} + /// Helper function to check if the action is IBC shielding transfer pub fn is_ibc_shielding_transfer( reader: &T, diff --git a/crates/tx/src/types.rs b/crates/tx/src/types.rs index e2ab2aa2be7..cb5ff5bf2d0 100644 --- a/crates/tx/src/types.rs +++ b/crates/tx/src/types.rs @@ -6,6 +6,7 @@ use std::io; use std::ops::{Bound, RangeBounds}; use std::str::FromStr; +use data_encoding::HEXUPPER; use masp_primitives::transaction::Transaction; use namada_account::AccountPublicKeysMap; use namada_core::address::Address; @@ -15,7 +16,7 @@ use namada_core::borsh::{ use namada_core::chain::{BlockHeight, ChainId}; use namada_core::collections::{HashMap, HashSet}; use namada_core::key::*; -use namada_core::masp::MaspTxId; +use namada_core::masp::{FlagCiphertext, MaspTxId}; use namada_core::storage::TxIndex; use namada_core::time::DateTimeUtc; use namada_macros::BorshDeserializer; @@ -46,6 +47,8 @@ pub enum DecodeError { InvalidTimestamp(prost_types::TimestampError), #[error("Couldn't serialize transaction from JSON at {0}")] InvalidJSONDeserialization(String), + #[error("Could not decode FMD flag ciphertexts from tx section {0}")] + InvalidFlagCiphertexts(String), } #[allow(missing_docs)] @@ -287,6 +290,31 @@ impl Tx { None } + /// Get the FMD flag ciphertext with the given hash + pub fn get_fmd_flag_ciphertexts( + &self, + hash: &namada_core::hash::Hash, + ) -> Result>, DecodeError> { + let maybe_section = self.get_section(hash); + + let data = match maybe_section.as_ref().map(Cow::as_ref) { + Some(Section::Data(Data { data, .. })) => data, + Some(_) => { + return Err(DecodeError::InvalidFlagCiphertexts( + HEXUPPER.encode(&hash.0), + )); + } + None => return Ok(None), + }; + + let decoded = + BorshDeserialize::try_from_slice(data).map_err(|_err| { + DecodeError::InvalidFlagCiphertexts(HEXUPPER.encode(&hash.0)) + })?; + + Ok(Some(decoded)) + } + /// Remove the transaction section with the given hash pub fn remove_masp_section(&mut self, hash: &MaspTxId) { self.sections.retain(|section| { diff --git a/wasm/tx_ibc/src/lib.rs b/wasm/tx_ibc/src/lib.rs index ee2bcb60c28..3df5590f415 100644 --- a/wasm/tx_ibc/src/lib.rs +++ b/wasm/tx_ibc/src/lib.rs @@ -12,7 +12,7 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { .execute::(&data) .into_storage_result()?; - let (masp_section_ref, mut token_addrs) = + let (maybe_masp_refs, mut token_addrs) = if let Some(transfers) = data.transparent { let (_debited_accounts, tokens) = if let Some(transparent) = transfers.transparent_part() { @@ -22,18 +22,18 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { Default::default() }; - (transfers.masp_tx_id(), tokens) + (transfers.shielded_data, tokens) } else { (None, Default::default()) }; token_addrs.extend(data.ibc_tokens); - let shielded = if let Some(masp_section_ref) = masp_section_ref { + let maybe_masp_tx = if let Some(shielded) = maybe_masp_refs { Some( tx_data .tx - .get_masp_section(&masp_section_ref) + .get_masp_section(&shielded.masp_tx_id) .cloned() .ok_or_err_msg( "Unable to find required shielded section in tx data", @@ -44,20 +44,25 @@ fn apply_tx(ctx: &mut Ctx, tx_data: BatchedTx) -> TxResult { ) } else { data.shielded + .map(|ibc_shielding_data| ibc_shielding_data.masp_tx) }; - if let Some(shielded) = shielded { - token::utils::handle_masp_tx(ctx, &shielded) + + if let Some(masp_tx) = maybe_masp_tx { + token::utils::handle_masp_tx(ctx, &masp_tx) .wrap_err("Encountered error while handling MASP transaction")?; - update_masp_note_commitment_tree(&shielded) + update_masp_note_commitment_tree(&masp_tx) .wrap_err("Failed to update the MASP commitment tree")?; - if let Some(masp_section_ref) = masp_section_ref { + if let Some(masp_refs) = maybe_masp_refs { ctx.push_action(Action::Masp(MaspAction::MaspSectionRef( - masp_section_ref, + masp_refs.masp_tx_id, + )))?; + ctx.push_action(Action::Masp(MaspAction::FmdSectionRef( + masp_refs.flag_ciphertext_sechash, )))?; } else { ctx.push_action(Action::IbcShielding)?; } - token::update_undated_balances(ctx, &shielded, token_addrs)?; + token::update_undated_balances(ctx, &masp_tx, token_addrs)?; } Ok(()) diff --git a/wasm/vp_implicit/src/lib.rs b/wasm/vp_implicit/src/lib.rs index 8ad708c6de6..b9b1e9e8a86 100644 --- a/wasm/vp_implicit/src/lib.rs +++ b/wasm/vp_implicit/src/lib.rs @@ -113,7 +113,9 @@ fn validate_tx( cmt, &addr, )?, - Action::Masp(MaspAction::MaspSectionRef(_)) => (), + Action::Masp( + MaspAction::MaspSectionRef(_) | MaspAction::FmdSectionRef(_), + ) => (), Action::IbcShielding => (), } } diff --git a/wasm/vp_user/src/lib.rs b/wasm/vp_user/src/lib.rs index d53ea783044..6e71b7e65f9 100644 --- a/wasm/vp_user/src/lib.rs +++ b/wasm/vp_user/src/lib.rs @@ -112,7 +112,9 @@ fn validate_tx( cmt, &addr, )?, - Action::Masp(MaspAction::MaspSectionRef(_)) => (), + Action::Masp( + MaspAction::MaspSectionRef(_) | MaspAction::FmdSectionRef(_), + ) => (), Action::IbcShielding => (), } } From bf609fe7ff8fe16a9486fd18f2193f00fa45014f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 13 May 2025 16:23:45 +0100 Subject: [PATCH 35/40] Emit FMD flag ciphertext tx events --- crates/node/src/bench_utils.rs | 87 ++++++---- crates/node/src/protocol.rs | 152 +++++++++++------- crates/sdk/src/masp.rs | 12 +- crates/sdk/src/masp/utilities.rs | 10 +- .../src/masp/shielded_sync/utils.rs | 35 +--- crates/tx/src/event.rs | 125 +++++++++++--- 6 files changed, 268 insertions(+), 153 deletions(-) diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index 34d73c5627c..fc9900cf1d2 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -88,6 +88,7 @@ use namada_sdk::queries::{ use namada_sdk::state::StorageRead; use namada_sdk::state::write_log::StorageModification; use namada_sdk::storage::{Key, KeySeg, TxIndex}; +use namada_sdk::tendermint::abci::Event as AbciEvent; use namada_sdk::time::DateTimeUtc; use namada_sdk::token::{ self, Amount, DenominatedAmount, MaspTxData, Transfer, @@ -96,7 +97,9 @@ use namada_sdk::tx::data::pos::Bond; use namada_sdk::tx::data::{ BatchedTxResult, Fee, TxResult, VpsResult, compute_inner_tx_hash, }; -use namada_sdk::tx::event::{Batch, MaspEvent, MaspTxRef, new_tx_event}; +use namada_sdk::tx::event::{ + Batch, FmdSectionRef, MaspEvent, MaspTxKind, MaspTxRef, new_tx_event, +}; use namada_sdk::tx::{ Authorization, BatchedTx, BatchedTxRef, Code, Data, IndexedTx, Section, Tx, }; @@ -1062,43 +1065,67 @@ impl Client for BenchShell { let tx_event: Event = new_tx_event(tx, height.value()) .with(Batch(&tx_result)) .into(); - // Expect a single masp tx in the batch - let masp_ref = tx.sections.iter().find_map(|section| { - if let Section::MaspTx(transaction) = section { - Some(MaspTxRef::MaspSection(transaction.txid().into())) - } else { - None - } - }); let first_inner_tx_hash = compute_inner_tx_hash( tx.wrapper_hash().as_ref(), Either::Right(tx.first_commitments().unwrap()), ); - let masp_event = masp_ref.map(|data| { - let masp_event: Event = MaspEvent { - tx_index: IndexedTx { - block_height: namada_sdk::chain::BlockHeight( - u64::from(height), - ), - block_index: TxIndex::must_from_usize(idx), - batch_index: Some(0), - }, - kind: namada_sdk::tx::event::MaspEventKind::Transfer, - data, - } - .with(TxHash(tx.header_hash())) - .with(InnerTxHash(first_inner_tx_hash)) - .into(); - masp_event - }); - - res.push(namada_sdk::tendermint::abci::Event::from(tx_event)); + let wrapper_hash = tx.wrapper_hash().unwrap_or_default(); + let indexed_tx = IndexedTx { + block_height: namada_sdk::chain::BlockHeight(u64::from( + height, + )), + block_index: TxIndex::must_from_usize(idx), + batch_index: Some(0), + }; - if let Some(event) = masp_event { - res.push(namada_sdk::tendermint::abci::Event::from(event)); + let masp_tx_event = + tx.sections.iter().find_map(|section| match section { + Section::MaspTx(transaction) => { + Some(AbciEvent::from(Event::from( + MaspEvent::ShieldedOutput { + tx_index: indexed_tx, + kind: MaspTxKind::Transfer, + data: MaspTxRef::MaspSection( + transaction.txid().into(), + ), + } + .with(TxHash(wrapper_hash)) + .with(InnerTxHash(first_inner_tx_hash)), + ))) + } + _ => None, + }); + let masp_fmd_event = + tx.sections.iter().find_map(|section| match section { + sec @ Section::Data(Data { data, .. }) + if >::try_from_slice(data) + .is_ok() => + { + Some(AbciEvent::from(Event::from( + MaspEvent::FlagCiphertexts { + tx_index: indexed_tx, + section: FmdSectionRef::FmdSection( + sec.get_hash(), + ), + } + .with(TxHash(wrapper_hash)) + .with(InnerTxHash(first_inner_tx_hash)), + ))) + } + _ => None, + }); + + res.push(AbciEvent::from(tx_event)); + + if let Some((masp_tx_event, masp_fmd_event)) = + masp_tx_event.zip(masp_fmd_event) + { + res.push(masp_tx_event); + res.push(masp_fmd_event); } } + Some(res) } else { None diff --git a/crates/node/src/protocol.rs b/crates/node/src/protocol.rs index 63e0b748ea9..d53f7459f7f 100644 --- a/crates/node/src/protocol.rs +++ b/crates/node/src/protocol.rs @@ -30,7 +30,7 @@ use namada_sdk::tx::data::{ BatchedTxResult, TxResult, VpStatusFlags, VpsResult, WrapperTx, compute_inner_tx_hash, }; -use namada_sdk::tx::event::{MaspEvent, MaspEventKind, MaspTxRef}; +use namada_sdk::tx::event::{FmdSectionRef, MaspEvent, MaspTxKind, MaspTxRef}; use namada_sdk::tx::{BatchedTxRef, IndexedTx, Tx, TxCommitments}; use namada_sdk::validation::{ EthBridgeNutVp, EthBridgePoolVp, EthBridgeVp, GovernanceVp, IbcVp, MaspVp, @@ -395,38 +395,32 @@ where Ok(mut batched_tx_result) if batched_tx_result.is_accepted() => { // If the transaction was a masp one generate the // appropriate event - if let Some(masp_ref) = get_optional_masp_ref( + if let Some((masp_ref, fmd_ref)) = get_optional_masp_refs( state, cmt, Either::Right(&batched_tx_result), ) .map_err(|e| Box::new(DispatchError::from(e)))? { - let inner_tx_hash = - compute_inner_tx_hash(wrapper_hash, Either::Right(cmt)); - batched_tx_result.events.insert( - MaspEvent { - tx_index: IndexedTx { - block_height: height, - block_index: tx_index, - batch_index: tx - .header - .batch - .get_index_of(cmt) - .map(|idx| { - TxIndex::must_from_usize(idx).into() - }), - }, - kind: MaspEventKind::Transfer, - data: masp_ref, - } - .with(TxHashAttr( - // Zero hash if the wrapper is not provided - // (governance proposal) - wrapper_hash.cloned().unwrap_or_default(), - )) - .with(InnerTxHashAttr(inner_tx_hash)) - .into(), + insert_masp_events( + &mut batched_tx_result, + // Zero hash if the wrapper is not provided + // (governance proposal) + TxHashAttr(wrapper_hash.cloned().unwrap_or_default()), + InnerTxHashAttr(compute_inner_tx_hash( + wrapper_hash, + Either::Right(cmt), + )), + IndexedTx { + block_height: height, + block_index: tx_index, + batch_index: tx.header.batch.get_index_of(cmt).map( + |idx| TxIndex::must_from_usize(idx).into(), + ), + }, + MaspTxKind::Transfer, + masp_ref, + fmd_ref, ); } @@ -467,6 +461,7 @@ where pub struct MaspTxResult { tx_result: BatchedTxResult, masp_section_ref: MaspTxRef, + fmd_section_ref: FmdSectionRef, } /// Performs the required operation on a wrapper transaction: @@ -526,22 +521,21 @@ where let first_commitments = tx.first_commitments().unwrap(); let mut batch = TxResult::default(); // Generate Masp event if needed - masp_tx_result.tx_result.events.insert( - MaspEvent { - tx_index: IndexedTx { - block_height: height, - block_index: tx_index.to_owned(), - batch_index: Some(0), - }, - kind: MaspEventKind::FeePayment, - data: masp_tx_result.masp_section_ref, - } - .with(TxHashAttr(tx.header_hash())) - .with(InnerTxHashAttr(compute_inner_tx_hash( + insert_masp_events( + &mut masp_tx_result.tx_result, + TxHashAttr(tx.header_hash()), + InnerTxHashAttr(compute_inner_tx_hash( tx.wrapper_hash().as_ref(), Either::Right(first_commitments), - ))) - .into(), + )), + IndexedTx { + block_height: height, + block_index: tx_index.to_owned(), + batch_index: Some(0), + }, + MaspTxKind::FeePayment, + masp_tx_result.masp_section_ref, + masp_tx_result.fmd_section_ref, ); batch.insert_inner_tx_result( @@ -839,20 +833,22 @@ where // Ensure that the transaction is actually a masp one, otherwise // reject if is_masp_transfer && result.is_accepted() { - let masp_section_ref = get_optional_masp_ref( - *state, - first_tx.cmt, - Either::Left(true), - )? - .ok_or_else(|| { - Error::FeeError( - "Missing expected masp section reference" - .to_string(), - ) - })?; + let (masp_section_ref, fmd_section_ref) = + get_optional_masp_refs( + *state, + first_tx.cmt, + Either::Left(true), + )? + .ok_or_else(|| { + Error::FeeError( + "Missing expected masp section reference" + .to_string(), + ) + })?; MaspTxResult { tx_result: result, masp_section_ref, + fmd_section_ref, } } else { state.write_log_mut().drop_tx(); @@ -893,11 +889,11 @@ where // messing up with indexers/clients. Also a transaction can only be of one of // the two types, not both at the same time (the MASP VP accepts a single // Transaction) -fn get_optional_masp_ref>( +fn get_optional_masp_refs>( state: &S, cmt: &TxCommitments, is_masp_tx: Either, -) -> Result> { +) -> Result> { // Always check that the transaction was indeed a MASP one by looking at the // changed keys. A malicious tx could push a MASP Action without touching // any storage keys associated with the shielded pool @@ -912,14 +908,27 @@ fn get_optional_masp_ref>( let masp_ref = if action::is_ibc_shielding_transfer(state) .map_err(Error::StateError)? { - Some(MaspTxRef::IbcData(cmt.data_sechash().to_owned())) + let ibc_data = cmt.data_sechash().to_owned(); + + Some(( + MaspTxRef::IbcData(ibc_data), + FmdSectionRef::IbcData(ibc_data), + )) } else { let actions = state.read_actions().map_err(Error::StateError)?; - action::get_masp_section_ref(&actions) + + let masp_tx = action::get_masp_section_ref(&actions) + .map_err(|msg| { + Error::StateError(state::Error::new_alloc(msg.to_string())) + })? + .map(MaspTxRef::MaspSection); + let fmd_sechash = action::get_fmd_flag_ciphertexts_ref(&actions) .map_err(|msg| { Error::StateError(state::Error::new_alloc(msg.to_string())) })? - .map(MaspTxRef::MaspSection) + .map(FmdSectionRef::FmdSection); + + masp_tx.zip(fmd_sechash) }; Ok(masp_ref) @@ -1499,6 +1508,35 @@ fn merge_vp_results( )) } +/// Insert MASP event data into the provided [`BatchedTxResult`]. +fn insert_masp_events( + batched_tx_result: &mut BatchedTxResult, + wrapper_tx_hash: TxHashAttr, + inner_tx_hash: InnerTxHashAttr, + tx_index: IndexedTx, + masp_tx_kind: MaspTxKind, + masp_tx_ref: MaspTxRef, + fmd_ref: FmdSectionRef, +) { + batched_tx_result.events.extend([ + MaspEvent::ShieldedOutput { + tx_index, + kind: masp_tx_kind, + data: masp_tx_ref, + } + .with(TxHashAttr(wrapper_tx_hash.0)) + .with(InnerTxHashAttr(inner_tx_hash.0)) + .into(), + MaspEvent::FlagCiphertexts { + tx_index, + section: fmd_ref, + } + .with(TxHashAttr(wrapper_tx_hash.0)) + .with(InnerTxHashAttr(inner_tx_hash.0)) + .into(), + ]); +} + #[cfg(test)] mod tests { use eyre::Result; diff --git a/crates/sdk/src/masp.rs b/crates/sdk/src/masp.rs index 53f173c2bf0..7a148431c26 100644 --- a/crates/sdk/src/masp.rs +++ b/crates/sdk/src/masp.rs @@ -19,7 +19,7 @@ use namada_ibc::{IbcMessage, decode_message, extract_masp_tx_from_envelope}; use namada_io::client::Client; use namada_token::masp::shielded_wallet::ShieldedQueries; pub use namada_token::masp::{utils, *}; -use namada_tx::event::{MaspEvent, MaspEventKind, MaspTxRef}; +use namada_tx::event::{MaspTxEvent, MaspTxKind, MaspTxRef}; use namada_tx::{IndexedTx, Tx}; pub use utilities::{IndexerMaspClient, LedgerMaspClient}; @@ -92,10 +92,10 @@ fn extract_masp_tx( } // Retrieves all the masp events at the specified height. -async fn get_indexed_masp_events_at_height( +async fn get_indexed_masp_txs_at_height( client: &C, height: BlockHeight, -) -> Result, Error> { +) -> Result, Error> { let maybe_masp_events: Result, Error> = client .block_results(height.0) .await @@ -111,9 +111,9 @@ async fn get_indexed_masp_events_at_height( }; let kind = if kind == namada_tx::event::masp_types::TRANSFER { - MaspEventKind::Transfer + MaspTxKind::Transfer } else if kind == namada_tx::event::masp_types::FEE_PAYMENT { - MaspEventKind::FeePayment + MaspTxKind::FeePayment } else { return Ok(None); }; @@ -125,7 +125,7 @@ async fn get_indexed_masp_events_at_height( let tx_index = IndexedTx::read_from_event_attributes(&event.attributes)?; - Ok(Some(MaspEvent { + Ok(Some(MaspTxEvent { tx_index, kind, data, diff --git a/crates/sdk/src/masp/utilities.rs b/crates/sdk/src/masp/utilities.rs index 0a7c31052fd..fe1906f022e 100644 --- a/crates/sdk/src/masp/utilities.rs +++ b/crates/sdk/src/masp/utilities.rs @@ -18,12 +18,12 @@ use namada_token::masp::utils::{ IndexedNoteEntry, MaspClient, MaspClientCapabilities, MaspIndexedTx, MaspTxKind, }; -use namada_tx::event::MaspEvent; +use namada_tx::event::MaspTxEvent; use namada_tx::{IndexedTx, Tx}; use tokio::sync::Semaphore; use crate::error::{Error, QueryError}; -use crate::masp::{extract_masp_tx, get_indexed_masp_events_at_height}; +use crate::masp::{extract_masp_tx, get_indexed_masp_txs_at_height}; struct LedgerMaspClientInner { client: C, @@ -82,7 +82,7 @@ impl LedgerMaspClient { for height in from.0..=to.0 { let maybe_txs_results = async { - get_indexed_masp_events_at_height( + get_indexed_masp_txs_at_height( &self.inner.client, height.into(), ) @@ -109,7 +109,7 @@ impl LedgerMaspClient { // Cache the last tx seen to avoid multiple deserializations let mut last_tx: Option<(Tx, TxIndex)> = None; - for MaspEvent { + for MaspTxEvent { tx_index, kind, data, @@ -133,7 +133,7 @@ impl LedgerMaspClient { txs.push(( MaspIndexedTx { indexed_tx: tx_index, - kind: kind.into(), + kind, }, extracted_masp_tx, )); diff --git a/crates/shielded_token/src/masp/shielded_sync/utils.rs b/crates/shielded_token/src/masp/shielded_sync/utils.rs index f88e4c55546..b263bb8bab0 100644 --- a/crates/shielded_token/src/masp/shielded_sync/utils.rs +++ b/crates/shielded_token/src/masp/shielded_sync/utils.rs @@ -12,42 +12,9 @@ use namada_core::chain::BlockHeight; use namada_core::collections::HashMap; use namada_state::TxIndex; use namada_tx::IndexedTx; -use namada_tx::event::MaspEventKind; +pub use namada_tx::event::MaspTxKind; use serde::{Deserialize, Serialize}; -/// The type of a MASP transaction -#[derive( - Debug, - Default, - Clone, - Copy, - BorshSerialize, - BorshDeserialize, - PartialOrd, - PartialEq, - Eq, - Ord, - Serialize, - Deserialize, - Hash, -)] -pub enum MaspTxKind { - /// A MASP transaction used for fee payment - FeePayment, - /// A general MASP transfer - #[default] - Transfer, -} - -impl From for MaspTxKind { - fn from(value: MaspEventKind) -> Self { - match value { - MaspEventKind::FeePayment => Self::FeePayment, - MaspEventKind::Transfer => Self::Transfer, - } - } -} - /// An indexed masp tx carrying information on whether it was a fee paying tx or /// a normal transfer #[derive( diff --git a/crates/tx/src/event.rs b/crates/tx/src/event.rs index 4240076b4d3..7f756321871 100644 --- a/crates/tx/src/event.rs +++ b/crates/tx/src/event.rs @@ -4,6 +4,7 @@ use std::fmt::Display; use std::str::FromStr; use namada_core::borsh::{BorshDeserialize, BorshSerialize}; +use namada_core::hash::Hash; use namada_core::ibc::IbcTxDataHash; use namada_core::masp::MaspTxId; use namada_events::extend::{ @@ -112,9 +113,13 @@ pub mod masp_types { /// General MASP transfer event pub const TRANSFER: EventType = namada_events::event_type!(MaspEvent, "transfer"); + + /// FMD flag ciphertexts event + pub const FLAG_CIPHERTEXTS: EventType = + namada_events::event_type!(MaspEvent, "flag"); } -/// MASP event kind +/// The type of a MASP transaction #[derive( Debug, Default, @@ -130,7 +135,7 @@ pub mod masp_types { Deserialize, Hash, )] -pub enum MaspEventKind { +pub enum MaspTxKind { /// A MASP transaction used for fee payment FeePayment, /// A general MASP transfer @@ -138,24 +143,43 @@ pub enum MaspEventKind { Transfer, } -impl From<&MaspEventKind> for EventType { - fn from(masp_event_kind: &MaspEventKind) -> Self { - match masp_event_kind { - MaspEventKind::FeePayment => masp_types::FEE_PAYMENT, - MaspEventKind::Transfer => masp_types::TRANSFER, +impl From for EventType { + fn from(kind: MaspTxKind) -> Self { + match kind { + MaspTxKind::FeePayment => masp_types::FEE_PAYMENT, + MaspTxKind::Transfer => masp_types::TRANSFER, } } } -impl From for EventType { - fn from(masp_event_kind: MaspEventKind) -> Self { - (&masp_event_kind).into() +/// Represents a reference to an FMD flag ciphertext. +/// +/// Store either in an IBC packet memo, or a Namada tx section. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FmdSectionRef { + /// Reference to a flag ciphertext tx section. + FmdSection(Hash), + /// Reference to an IBC tx data section. + IbcData(IbcTxDataHash), +} + +impl Display for FmdSectionRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", serde_json::to_string(self).unwrap()) + } +} + +impl FromStr for FmdSectionRef { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) } } /// A type representing the possible reference to some MASP data, either a masp /// section or ibc tx data -#[derive(Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum MaspTxRef { /// Reference to a MASP section MaspSection(MaspTxId), @@ -177,30 +201,78 @@ impl FromStr for MaspTxRef { } } -/// A list of MASP tx references -#[derive(Default, Clone, Serialize, Deserialize)] -pub struct MaspTxRefs(pub Vec<(IndexedTx, MaspTxRef)>); +/// MASP transaction event +#[derive(Debug, Clone)] +pub enum MaspEvent { + /// Emit emitted upon generating a new shielded output + ShieldedOutput { + /// The indexed transaction that generated this event + tx_index: IndexedTx, + /// A flag signaling the type of the MASP transaction + kind: MaspTxKind, + /// The reference to the masp data + data: MaspTxRef, + }, + /// Emit emitted after flagging a new shielded output + /// + /// Generally follows the creation of [`Self::ShieldedOutput`] + FlagCiphertexts { + /// The indexed transaction that generated this event + tx_index: IndexedTx, + /// The tx section hash of the FMD flag ciphertext + section: FmdSectionRef, + }, +} /// MASP transaction event -pub struct MaspEvent { +#[derive(Debug, Clone)] +pub struct MaspTxEvent { /// The indexed transaction that generated this event pub tx_index: IndexedTx, - /// A flag signaling the type of the masp transaction - pub kind: MaspEventKind, + /// A flag signaling the type of the MASP transaction + pub kind: MaspTxKind, /// The reference to the masp data pub data: MaspTxRef, } +impl From for MaspEvent { + fn from( + MaspTxEvent { + tx_index, + kind, + data, + }: MaspTxEvent, + ) -> Self { + Self::ShieldedOutput { + tx_index, + kind, + data, + } + } +} + impl EventToEmit for MaspEvent { const DOMAIN: &'static str = "masp"; } impl From for Event { fn from(masp_event: MaspEvent) -> Self { - Self::new(masp_event.kind.into(), EventLevel::Tx) - .with(masp_event.data) - .with(masp_event.tx_index) - .into() + match masp_event { + MaspEvent::ShieldedOutput { + tx_index, + kind, + data, + } => Self::new(kind.into(), EventLevel::Tx) + .with(data) + .with(tx_index) + .into(), + MaspEvent::FlagCiphertexts { tx_index, section } => { + Self::new(masp_types::FLAG_CIPHERTEXTS, EventLevel::Tx) + .with(section) + .with(tx_index) + .into() + } + } } } @@ -225,3 +297,14 @@ impl EventAttributeEntry<'static> for IndexedTx { self } } + +impl EventAttributeEntry<'static> for FmdSectionRef { + type Value = Self; + type ValueOwned = Self; + + const KEY: &'static str = "ciphertext"; + + fn into_value(self) -> Self::Value { + self + } +} From d7d744cb6c404829969f36b6d46424aa682bcbb7 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 14 May 2025 15:35:20 +0100 Subject: [PATCH 36/40] Fix MASP integration test gas costs --- crates/tests/src/integration/masp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tests/src/integration/masp.rs b/crates/tests/src/integration/masp.rs index a252a605402..e5f1b8b8cb3 100644 --- a/crates/tests/src/integration/masp.rs +++ b/crates/tests/src/integration/masp.rs @@ -748,7 +748,7 @@ fn values_spanning_multiple_masp_digits() -> Result<()> { "--node", RPC, "--gas-limit", - "65000", + "75000", ]), ) }); @@ -867,7 +867,7 @@ fn values_spanning_multiple_masp_digits() -> Result<()> { "--gas-spending-key", C_SPENDING_KEY, "--gas-limit", - "65000", + "75000", ]), ) }); From 113d025b05f0abf989b060733ab99344573a3616 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 15 May 2025 10:30:02 +0100 Subject: [PATCH 37/40] Fix benchmarks --- crates/node/src/bench_utils.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index fc9900cf1d2..ef073d55814 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -1285,7 +1285,7 @@ impl BenchShieldedCtx { token: address::testing::nam(), amount: denominated_amount, }; - let shielded = async_runtime + let (shielded, fmd_flags) = async_runtime .block_on(async { let expiration = Namada::tx_builder(&namada).expiration.to_datetime(); @@ -1307,14 +1307,12 @@ impl BenchShieldedCtx { masp_tx, metadata: _, epoch: _, - fmd_flags: _, - }| masp_tx, + fmd_flags, + }| (masp_tx, fmd_flags), ) .expect("MASP must have shielded part"); - let fmd_section = Section::Data(Data::from_borsh_encoded(&vec![ - FlagCiphertext::default(), - ])); + let fmd_section = Section::Data(Data::from_borsh_encoded(&fmd_flags)); let shielded_data = MaspTxData { masp_tx_id: shielded.txid().into(), flag_ciphertext_sechash: fmd_section.get_hash(), @@ -1431,9 +1429,16 @@ impl BenchShieldedCtx { vec![vectorized_transfer.targets.into_iter().next().unwrap()] .into_iter() .collect(); - let fmd_section = Section::Data(Data::from_borsh_encoded(&vec![ - FlagCiphertext::default(), - ])); + let masp_tx = tx.tx.get_masp_section(&masp_tx_id).unwrap().clone(); + let fmd_section = Section::Data(Data::from_borsh_encoded( + &std::iter::repeat_with(FlagCiphertext::default) + .take( + masp_tx + .sapling_bundle() + .map_or(0, |x| x.shielded_outputs.len()), + ) + .collect::>(), + )); let transfer = Transfer { sources, targets, @@ -1442,7 +1447,6 @@ impl BenchShieldedCtx { flag_ciphertext_sechash: fmd_section.get_hash(), }), }; - let masp_tx = tx.tx.get_masp_section(&masp_tx_id).unwrap().clone(); let msg = MsgTransfer:: { message: msg, transfer: Some(transfer), From aef297b4dee97b34aa1c000a2fa7540b606182c8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 19 May 2025 11:05:10 +0100 Subject: [PATCH 38/40] Store FMD flag ciphertexts in extra data secs --- crates/node/src/bench_utils.rs | 17 +++++++++++------ crates/sdk/src/lib.rs | 8 +++----- crates/sdk/src/tx.rs | 2 +- crates/token/src/lib.rs | 4 ++-- crates/tx/src/section.rs | 17 ++++++++++++++++- crates/tx/src/types.rs | 21 +++++++++++++++++++-- 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/crates/node/src/bench_utils.rs b/crates/node/src/bench_utils.rs index ef073d55814..e3ec89f1056 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -192,7 +192,7 @@ impl BenchShellInner { if let Some(sections) = extra_sections { for section in sections { - if let Section::ExtraData(_) | Section::Data(_) = section { + if let Section::ExtraData(_) = section { tx.add_section(section); } } @@ -1098,9 +1098,13 @@ impl Client for BenchShell { }); let masp_fmd_event = tx.sections.iter().find_map(|section| match section { - sec @ Section::Data(Data { data, .. }) - if >::try_from_slice(data) - .is_ok() => + sec @ Section::ExtraData(extra_data) + if extra_data.id().is_some_and(|extra_data| { + >::try_from_slice( + extra_data, + ) + .is_ok() + }) => { Some(AbciEvent::from(Event::from( MaspEvent::FlagCiphertexts { @@ -1312,7 +1316,8 @@ impl BenchShieldedCtx { ) .expect("MASP must have shielded part"); - let fmd_section = Section::Data(Data::from_borsh_encoded(&fmd_flags)); + let fmd_section = + Section::ExtraData(Code::from_borsh_encoded(&fmd_flags)); let shielded_data = MaspTxData { masp_tx_id: shielded.txid().into(), flag_ciphertext_sechash: fmd_section.get_hash(), @@ -1430,7 +1435,7 @@ impl BenchShieldedCtx { .into_iter() .collect(); let masp_tx = tx.tx.get_masp_section(&masp_tx_id).unwrap().clone(); - let fmd_section = Section::Data(Data::from_borsh_encoded( + let fmd_section = Section::ExtraData(Code::from_borsh_encoded( &std::iter::repeat_with(FlagCiphertext::default) .take( masp_tx diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index a01067024e9..f63524f9d7c 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -901,7 +901,7 @@ pub mod testing { arb_withdraw, }; use crate::tx::{ - Authorization, Code, Commitment, Data, Header, MaspBuilder, Section, + Authorization, Code, Commitment, Header, MaspBuilder, Section, TxCommitments, }; @@ -1146,10 +1146,8 @@ pub mod testing { if let Some((shielded_transfer, asset_types, build_params)) = aux { let shielded_section_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx).1; - tx.add_section( - Section::Data( - Data::from_borsh_encoded(&shielded_transfer.fmd_flags), - ), + tx.add_fmd_flag_ciphertexts( + &shielded_transfer.fmd_flags, ); tx.add_masp_builder(MaspBuilder { asset_types: asset_types.into_keys().collect(), diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index edce82f7187..591ecd71618 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -4351,7 +4351,7 @@ fn proposal_to_vec(proposal: OnChainProposal) -> Result> { } fn create_fmd_section(fmd_flags: Vec) -> (Section, Hash) { - let fmd_section = Section::Data(Data::from_borsh_encoded(&fmd_flags)); + let fmd_section = Section::ExtraData(Code::from_borsh_encoded(&fmd_flags)); let fmd_sechash = fmd_section.get_hash(); (fmd_section, fmd_sechash) diff --git a/crates/token/src/lib.rs b/crates/token/src/lib.rs index d91173cbaca..70dc5b78a63 100644 --- a/crates/token/src/lib.rs +++ b/crates/token/src/lib.rs @@ -536,8 +536,8 @@ pub mod testing { .take(builder.sapling_outputs().len()) .collect(); let fmd_sechash = { - let sec = namada_tx::Section::Data( - namada_tx::Data::from_borsh_encoded(&fmd_flags), + let sec = namada_tx::Section::ExtraData( + namada_tx::Code::from_borsh_encoded(&fmd_flags), ); sec.get_hash() }; diff --git a/crates/tx/src/section.rs b/crates/tx/src/section.rs index d4aeef17597..a6649b17fcd 100644 --- a/crates/tx/src/section.rs +++ b/crates/tx/src/section.rs @@ -277,7 +277,7 @@ impl Data { /// Make a new data section with the given borsh encodable data #[inline] - pub fn from_borsh_encoded(data: &T) -> Self { + pub fn from_borsh_encoded(data: &T) -> Self { Self::new(data.serialize_to_vec()) } @@ -376,6 +376,21 @@ impl Code { } } + /// Return the code data, if it is present verbatim. + pub fn id(&self) -> Option<&[u8]> { + if let Commitment::Id(code) = &self.code { + Some(&code[..]) + } else { + None + } + } + + /// Make a new code section with the given borsh encodable data + #[inline] + pub fn from_borsh_encoded(data: &T) -> Self { + Self::new(data.serialize_to_vec(), None) + } + /// Make a new code section with the given hash pub fn from_hash( hash: namada_core::hash::Hash, diff --git a/crates/tx/src/types.rs b/crates/tx/src/types.rs index cb5ff5bf2d0..1fff8c148ae 100644 --- a/crates/tx/src/types.rs +++ b/crates/tx/src/types.rs @@ -290,6 +290,17 @@ impl Tx { None } + /// Add an FMD flag ciphertext section to the transaction + pub fn add_fmd_flag_ciphertexts( + &mut self, + flag_ciphertexts: &[FlagCiphertext], + ) -> &mut Self { + self.add_section(Section::ExtraData(Code::from_borsh_encoded( + flag_ciphertexts, + ))); + self + } + /// Get the FMD flag ciphertext with the given hash pub fn get_fmd_flag_ciphertexts( &self, @@ -297,8 +308,8 @@ impl Tx { ) -> Result>, DecodeError> { let maybe_section = self.get_section(hash); - let data = match maybe_section.as_ref().map(Cow::as_ref) { - Some(Section::Data(Data { data, .. })) => data, + let code = match maybe_section.as_ref().map(Cow::as_ref) { + Some(Section::ExtraData(code)) => code, Some(_) => { return Err(DecodeError::InvalidFlagCiphertexts( HEXUPPER.encode(&hash.0), @@ -307,6 +318,12 @@ impl Tx { None => return Ok(None), }; + let Some(data) = code.id() else { + return Err(DecodeError::InvalidFlagCiphertexts( + HEXUPPER.encode(&hash.0), + )); + }; + let decoded = BorshDeserialize::try_from_slice(data).map_err(|_err| { DecodeError::InvalidFlagCiphertexts(HEXUPPER.encode(&hash.0)) From a90febc1d32c6ff7300735cd4254409188b29e64 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 20 May 2025 13:20:21 +0100 Subject: [PATCH 39/40] Update tx test vector generation code --- crates/sdk/src/lib.rs | 22 ++++++++++++---------- crates/token/src/lib.rs | 39 ++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index f63524f9d7c..3c28ba8f81f 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1124,9 +1124,11 @@ pub mod testing { arb in prop_oneof![ arb_transparent_transfer(..5).prop_map(|xfer| (xfer, None)), arb_shielded_transfer(0..MAX_ASSETS) - .prop_map(|(w, x, y, z)| (w, Some((x, y, z)))) + .prop_map(|(xfer, masp_tx, asset_types, params, fmd_sec)| { + (xfer, Some((masp_tx, asset_types, params, fmd_sec))) + }) ], - ) -> (Transfer, Option<(ShieldedTransfer, HashMap, StoredBuildParams)>) { + ) -> (Transfer, Option<(ShieldedTransfer, HashMap, StoredBuildParams, Section)>) { arb } } @@ -1143,12 +1145,10 @@ pub mod testing { let mut tx = Tx { header, sections: vec![] }; tx.add_code_from_hash(code_hash, Some(TX_TRANSFER_WASM.to_owned())); tx.add_data(transfer.clone()); - if let Some((shielded_transfer, asset_types, build_params)) = aux { + if let Some((shielded_transfer, asset_types, build_params, fmd_sec)) = aux { let shielded_section_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx).1; - tx.add_fmd_flag_ciphertexts( - &shielded_transfer.fmd_flags, - ); + tx.add_section(fmd_sec); tx.add_masp_builder(MaspBuilder { asset_types: asset_types.into_keys().collect(), // Store how the Info objects map to Descriptors/Outputs @@ -1535,7 +1535,7 @@ pub mod testing { transfer_aux in option::of(arb_transfer()), ) -> ( MsgTransfer, - Option<(ShieldedTransfer, HashMap, StoredBuildParams)>, + Option<(ShieldedTransfer, HashMap, StoredBuildParams, Section)>, ) { if let Some((transfer, aux)) = transfer_aux { (MsgTransfer { message, transfer: Some(transfer) }, aux) @@ -1557,9 +1557,10 @@ pub mod testing { let mut tx = Tx { header, sections: vec![] }; tx.add_serialized_data(msg_transfer.serialize_to_vec()); tx.add_code_from_hash(code_hash, Some(TX_IBC_WASM.to_owned())); - if let Some((shielded_transfer, asset_types, build_params)) = aux { + if let Some((shielded_transfer, asset_types, build_params, fmd_sec)) = aux { let shielded_section_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx).1; + tx.add_section(fmd_sec); tx.add_masp_builder(MaspBuilder { asset_types: asset_types.into_keys().collect(), // Store how the Info objects map to Descriptors/Outputs @@ -1585,7 +1586,7 @@ pub mod testing { transfer_aux in option::of(arb_transfer()), ) -> ( MsgNftTransfer, - Option<(ShieldedTransfer, HashMap, StoredBuildParams)>, + Option<(ShieldedTransfer, HashMap, StoredBuildParams, Section)>, ) { if let Some((transfer, aux)) = transfer_aux { (MsgNftTransfer { message, transfer: Some(transfer) }, aux) @@ -1607,9 +1608,10 @@ pub mod testing { let mut tx = Tx { header, sections: vec![] }; tx.add_serialized_data(msg_transfer.serialize_to_vec()); tx.add_code_from_hash(code_hash, Some(TX_IBC_WASM.to_owned())); - if let Some((shielded_transfer, asset_types, build_params)) = aux { + if let Some((shielded_transfer, asset_types, build_params, fmd_sec)) = aux { let shielded_section_hash = tx.add_masp_tx_section(shielded_transfer.masp_tx).1; + tx.add_section(fmd_sec); tx.add_masp_builder(MaspBuilder { asset_types: asset_types.into_keys().collect(), // Store how the Info objects map to Descriptors/Outputs diff --git a/crates/token/src/lib.rs b/crates/token/src/lib.rs index 70dc5b78a63..e3c55d944d2 100644 --- a/crates/token/src/lib.rs +++ b/crates/token/src/lib.rs @@ -522,7 +522,13 @@ pub mod testing { prover_rng in arb_rng().prop_map(TestCsprng), mut rng in arb_rng().prop_map(TestCsprng), bparams_rng in arb_rng().prop_map(TestCsprng), - ) -> (Transfer, ShieldedTransfer, HashMap, StoredBuildParams) { + ) -> ( + Transfer, + ShieldedTransfer, + HashMap, + StoredBuildParams, + namada_tx::Section, + ) { let mut rng_build_params = RngBuildParams::new(bparams_rng); let (masp_tx, metadata) = builder.clone().build( &MockTxProver(Mutex::new(prover_rng)), @@ -535,23 +541,26 @@ pub mod testing { ) .take(builder.sapling_outputs().len()) .collect(); - let fmd_sechash = { - let sec = namada_tx::Section::ExtraData( - namada_tx::Code::from_borsh_encoded(&fmd_flags), - ); - sec.get_hash() - }; + let fmd_sec = namada_tx::Section::ExtraData( + namada_tx::Code::from_borsh_encoded(&fmd_flags), + ); transfer.shielded_data = Some(MaspTxData { masp_tx_id: masp_tx.txid().into(), - flag_ciphertext_sechash: fmd_sechash, + flag_ciphertext_sechash: fmd_sec.get_hash(), }); - (transfer, ShieldedTransfer { - builder: builder.map_builder(WalletMap), - metadata, - masp_tx, - epoch, - fmd_flags, - }, asset_types, rng_build_params.to_stored().unwrap()) + ( + transfer, + ShieldedTransfer { + builder: builder.map_builder(WalletMap), + metadata, + masp_tx, + epoch, + fmd_flags, + }, + asset_types, + rng_build_params.to_stored().unwrap(), + fmd_sec, + ) } } } From e8b27221a1a735d4dfd35216938fa9c283ada9ed Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 19 May 2025 13:47:26 +0100 Subject: [PATCH 40/40] Temporarily switch to forked ledger app --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4a475b4701..2008bb0fd60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ env: AWS_REGION: us-west-2 NIGHTLY: nightly-2025-03-27 NAMADA_MASP_PARAMS_DIR: /masp/.masp-params - LEDGER_APP_VERSION: "3.0.4" + LEDGER_APP_VERSION: "3.1.3.heliax" ROLE: arn:aws:iam::375643557360:role/github-runners-ci-shared SCCACHE_ERROR_LOG: /tmp/sccache_log.txt @@ -601,7 +601,7 @@ jobs: - name: Checkout ledger-namada run: | echo "Using Namada Ledger App version: v${LEDGER_APP_VERSION}" - git clone 'https://github.com/Zondax/ledger-namada' ../ledger-namada + git clone 'https://github.com/anoma/ledger-namada' ../ledger-namada cd ../ledger-namada git checkout "v$LEDGER_APP_VERSION" git submodule update --init --recursive