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 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 ddfb6bf3a3a..fb71a9976ef 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" @@ -204,6 +205,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" @@ -246,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/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index b918a1b34e3..129be0d0613 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), @@ -3401,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}; @@ -3628,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()), @@ -3652,8 +3656,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 = @@ -7976,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, @@ -7990,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, } } @@ -8009,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/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..3d3cda402b4 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}; @@ -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,23 +422,21 @@ 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) = 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() - .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 +446,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 +1094,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/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index 6689cd51191..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,7 +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); + 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/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/Cargo.toml b/crates/core/Cargo.toml index 9d3ce842959..5c9611d4730 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", "polyfuzzy/random-flag-ciphertexts"] +default-flag-ciphertext = ["rand"] 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", @@ -39,6 +40,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 +62,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/crates/core/src/masp.rs b/crates/core/src/masp.rs index fe8fecee579..a7dc5323f37 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,13 +22,18 @@ use ripemd::Digest as RipemdDigest; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sha2::Sha256; +pub use self::fmd::{ + FlagCiphertext, PublicKey as FmdPublicKey, + PublicKeyBytes as FmdPublicKeyBytes, SecretKey as FmdSecretKey, +}; 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, - MASP_PAYMENT_ADDRESS_HRP, + MASP_FMD_PAYMENT_ADDRESS_HRP, MASP_PAYMENT_ADDRESS_HRP, }; use crate::token::{Denomination, MaspDigitPos, NATIVE_MAX_DECIMAL_PLACES}; @@ -85,6 +92,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))] @@ -300,10 +345,21 @@ 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); +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) @@ -481,6 +537,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 { @@ -553,6 +657,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 { @@ -591,6 +709,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, @@ -805,6 +993,147 @@ 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 { + /// 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, + 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, @@ -820,7 +1149,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), } @@ -840,9 +1169,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, } } @@ -869,7 +1198,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), } } @@ -928,7 +1262,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 @@ -939,9 +1273,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) @@ -1081,7 +1416,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); @@ -1100,7 +1435,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()); @@ -1122,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 = PaymentAddress::from(pa).into(); let target = TransferTarget::PaymentAddress(pa).t_addr_data(); assert!(target.is_none()); @@ -1138,7 +1473,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"; @@ -1223,7 +1558,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/core/src/masp/fmd.rs b/crates/core/src/masp/fmd.rs new file mode 100644 index 00000000000..20d9efc9a94 --- /dev/null +++ b/crates/core/src/masp/fmd.rs @@ -0,0 +1,499 @@ +//! Fuzzy message detection MASP primitives. + +use std::collections::BTreeMap; +use std::io; +use std::ops::Deref; + +use borsh::schema::Definition; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use masp_primitives::sapling::SaplingIvk; +use masp_primitives::zip32::{ExtendedKey, PseudoExtendedKey}; +#[cfg(feature = "rand")] +use rand_core::{CryptoRng, RngCore}; +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::*; +} + +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; + + /// 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 { + // 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 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 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]; + + #[inline] + fn deref(&self) -> &[u8] { + self.0.as_slice() + } +} + +/// 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) -> 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 + /// 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)] +#[repr(transparent)] +pub struct SecretKey { + inner: polyfuzzy::CompactSecretKey, +} + +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 { + 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: polyfuzzy::CompactSecretKey::derive_from_xof_stream( + parameters::THRESHOLD, + |buf| { + xof_stream.squeeze(buf); + }, + ), + } + } +} + +impl From for SecretKey { + #[inline] + fn from(ivk: SaplingIvk) -> Self { + (&ivk).into() + } +} + +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)] +#[repr(transparent)] +pub struct FlagCiphertext { + inner: polyfuzzy::FlagCiphertexts, +} + +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()) + } + + /// Generate a random [`FlagCiphertext`]. + #[cfg(feature = "rand")] + pub fn random(rng: &mut R) -> Self + where + R: CryptoRng + RngCore, + { + Self { + inner: polyfuzzy::FlagCiphertexts::random(rng, parameters::GAMMA), + } + } +} + +impl From for FlagCiphertext { + fn from(flag_ciphertext: polyfuzzy::FlagCiphertexts) -> Self { + Self { + inner: flag_ciphertext, + } + } +} + +impl From for polyfuzzy::FlagCiphertexts { + fn from(flag_ciphertext: FlagCiphertext) -> Self { + flag_ciphertext.inner + } +} + +impl AsRef for FlagCiphertext { + fn as_ref(&self) -> &polyfuzzy::FlagCiphertexts { + &self.inner + } +} + +#[cfg(feature = "default-flag-ciphertext")] +impl Default for FlagCiphertext { + #[inline] + fn default() -> Self { + Self::random(&mut rand_core::OsRng) + } +} + +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(data) + .map_err(from_bincode_err)? + .try_into() + .map_err(io::Error::other)?; + writer.write_all(&size.to_le_bytes())?; + + bincode::serialize_into(writer, data).map_err(from_bincode_err) + } + + 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) + } + + #[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)] +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() { + 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() { + // 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 = { + 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"); + } + } +} 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 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 d38466bb5e5..67c44c6d8d7 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,7 +239,12 @@ impl BorshSchema for MsgNftTransfer { /// Shielding data in IBC packet memo #[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] -pub struct IbcShieldingData(pub MaspTransaction); +pub struct IbcShieldingData { + /// MASP transaction forwarded over IBC. + pub masp_tx: MaspTransaction, + /// Flag ciphertexts to signal the owner(s) of the new note(s). + pub flag_ciphertexts: Vec, +} impl From<&IbcShieldingData> for String { fn from(data: &IbcShieldingData) -> Self { @@ -273,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, } @@ -300,8 +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(|data| data.0) + + decode_ibc_shielding_data(memo) } fn extract_memo_from_packet( @@ -366,6 +389,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_ciphertexts: Vec, +) -> String { + IbcShieldingData { + masp_tx, + flag_ciphertexts, + } + .into() } 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..e3ec89f1056 100644 --- a/crates/node/src/bench_utils.rs +++ b/crates/node/src/bench_utils.rs @@ -88,13 +88,18 @@ 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, 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, }; -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, }; @@ -113,8 +118,9 @@ 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, UnifiedPaymentAddress, parameters, proof_of_stake, + tendermint, }; use namada_test_utils::tx_data::TxWriteData; use namada_vm::wasm::run; @@ -1059,43 +1065,71 @@ 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::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 { + 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 @@ -1186,7 +1220,9 @@ impl Default for BenchShieldedCtx { .wallet .insert_payment_addr( alias, - PaymentAddress::from(payment_addr), + UnifiedPaymentAddress::V0(PaymentAddress::from( + payment_addr, + )), true, ) .unwrap(); @@ -1253,7 +1289,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(); @@ -1275,25 +1311,32 @@ impl BenchShieldedCtx { masp_tx, metadata: _, epoch: _, - }| masp_tx, + fmd_flags, + }| (masp_tx, fmd_flags), ) .expect("MASP must have shielded part"); - let shielded_section_hash = shielded.txid().into(); + 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(), + }; + 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, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) } 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, @@ -1302,13 +1345,13 @@ impl BenchShieldedCtx { ) .unwrap(), Some(shielded), - None, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) } else { namada.client().read().generate_tx( TX_TRANSFER_WASM, - Transfer::masp(shielded_section_hash) + Transfer::masp(shielded_data) .transfer( MASP, target.effective_address(), @@ -1317,7 +1360,7 @@ impl BenchShieldedCtx { ) .unwrap(), Some(shielded), - None, + Some(vec![fmd_section]), vec![&defaults::albert_keypair()], ) }; @@ -1382,6 +1425,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() @@ -1390,18 +1434,24 @@ impl BenchShieldedCtx { vec![vectorized_transfer.targets.into_iter().next().unwrap()] .into_iter() .collect(); + let masp_tx = tx.tx.get_masp_section(&masp_tx_id).unwrap().clone(); + let fmd_section = Section::ExtraData(Code::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, - shielded_section_hash: Some( - vectorized_transfer.shielded_section_hash.unwrap(), - ), + shielded_data: Some(MaspTxData { + masp_tx_id, + flag_ciphertext_sechash: fmd_section.get_hash(), + }), }; - let masp_tx = tx - .tx - .get_masp_section(&transfer.shielded_section_hash.unwrap()) - .unwrap() - .clone(); let msg = MsgTransfer:: { message: msg, transfer: Some(transfer), @@ -1412,6 +1462,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) } 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/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"] } diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index b9ba33fc322..fe6608ee710 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; @@ -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 { @@ -752,7 +752,10 @@ impl TxOsmosisSwap { serde_json::to_value(&NamadaMemo { namada: NamadaMemoData::OsmosisSwap { shielding_data: StringEncoded::new( - IbcShieldingData(shielding_tx), + IbcShieldingData { + masp_tx: shielding_tx, + flag_ciphertexts: fmd_flags, + }, ), shielded_amount: amount_to_shield, overflow_receiver, @@ -2952,7 +2955,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 @@ -3017,6 +3020,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 diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 06c2dce010a..3c28ba8f81f 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, 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 { @@ -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,9 +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_section(fmd_sec); tx.add_masp_builder(MaspBuilder { asset_types: asset_types.into_keys().collect(), // Store how the Info objects map to Descriptors/Outputs @@ -1532,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) @@ -1554,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 @@ -1582,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) @@ -1604,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/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/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..591ecd71618 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -41,7 +41,10 @@ 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, FlagCiphertext, MaspEpoch, MaspTxData, TransferSource, + TransferTarget, +}; use namada_core::storage; use namada_core::time::DateTimeUtc; use namada_events::extend::EventAttributeEntry; @@ -2819,14 +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; - transfer.shielded_section_hash = Some(masp_tx_hash); + + 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, + 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()?; @@ -3249,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; @@ -3265,7 +3286,10 @@ 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, + flag_ciphertext_sechash, + }); signing_data.shielded_hash = Some(section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) @@ -3385,7 +3409,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, }); @@ -3419,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 @@ -3435,7 +3465,10 @@ 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, + flag_ciphertext_sechash, + }); signing_data.shielded_hash = Some(shielded_section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) @@ -3542,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 @@ -3558,7 +3597,10 @@ 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, + flag_ciphertext_sechash, + }); signing_data.shielded_hash = Some(shielded_section_hash); tracing::debug!("Transfer data {data:?}"); Ok(()) @@ -3939,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 { @@ -4001,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( @@ -4162,8 +4204,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}" ))) } ( @@ -4307,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::ExtraData(Code::from_borsh_encoded(&fmd_flags)); + let fmd_sechash = fmd_section.get_hash(); + + (fmd_section, fmd_sechash) +} 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/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_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/shielded_token/src/masp/shielded_wallet.rs b/crates/shielded_token/src/masp/shielded_wallet.rs index 4c7312697dd..987e34bb47c 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, 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 @@ -1724,7 +1761,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( @@ -1737,6 +1774,10 @@ pub trait ShieldedApi: .map_err(|e| TransferErr::Build { error: builder::Error::SaplingBuild(e), })?; + + 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/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", ]), ) }); diff --git a/crates/token/src/lib.rs b/crates/token/src/lib.rs index 361a1424ff6..e3c55d944d2 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,9 @@ 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::masp::{ + AssetData, FlagCiphertext, MaspTxData, TAddrData, encode_asset_type, + }; pub use namada_core::token::*; use namada_shielded_token::masp::testing::{ MockTxProver, TestCsprng, arb_masp_epoch, arb_output_descriptions, @@ -514,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)), @@ -522,13 +536,31 @@ pub mod testing { &mut rng, &mut rng_build_params, ).unwrap(); - transfer.shielded_section_hash = Some(masp_tx.txid().into()); - (transfer, ShieldedTransfer { - builder: builder.map_builder(WalletMap), - metadata, - masp_tx, - epoch, - }, asset_types, rng_build_params.to_stored().unwrap()) + let fmd_flags = std::iter::repeat_with( + || FlagCiphertext::random(&mut rng) + ) + .take(builder.sapling_outputs().len()) + .collect(); + 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_sec.get_hash(), + }); + ( + transfer, + ShieldedTransfer { + builder: builder.map_builder(WalletMap), + metadata, + masp_tx, + epoch, + fmd_flags, + }, + asset_types, + rng_build_params.to_stored().unwrap(), + fmd_sec, + ) } } } diff --git a/crates/token/src/tx.rs b/crates/token/src/tx.rs index d60eeb1a247..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; @@ -42,10 +43,11 @@ 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, + 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/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 + } +} diff --git a/crates/tx/src/section.rs b/crates/tx/src/section.rs index d288de3e058..a6649b17fcd 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()); @@ -370,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 e2ab2aa2be7..1fff8c148ae 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,48 @@ 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, + hash: &namada_core::hash::Hash, + ) -> Result>, DecodeError> { + let maybe_section = self.get_section(hash); + + 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), + )); + } + 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)) + })?; + + 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/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 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/tx_ibc/src/lib.rs b/wasm/tx_ibc/src/lib.rs index 01ca4f9733c..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.shielded_section_hash, 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 => (), } } 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",