From 17725eaca75baa6ed9427b9d92e6bfab2baeb26e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Thu, 19 Sep 2024 16:29:47 +0100 Subject: [PATCH] tx: split out tx sections into dedicated mod --- crates/tx/src/data/mod.rs | 2 +- crates/tx/src/lib.rs | 9 +- crates/tx/src/section.rs | 887 ++++++++++++++++++++++++++++++++++++++ crates/tx/src/types.rs | 874 +------------------------------------ 4 files changed, 900 insertions(+), 872 deletions(-) create mode 100644 crates/tx/src/section.rs diff --git a/crates/tx/src/data/mod.rs b/crates/tx/src/data/mod.rs index 364e7f4af9..a299ac9b2e 100644 --- a/crates/tx/src/data/mod.rs +++ b/crates/tx/src/data/mod.rs @@ -37,7 +37,7 @@ use sha2::{Digest, Sha256}; pub use wrapper::*; use crate::data::protocol::ProtocolTx; -use crate::types::TxCommitments; +use crate::TxCommitments; /// The different result codes that the ledger may send back to a client /// indicating the status of their submitted tx. diff --git a/crates/tx/src/lib.rs b/crates/tx/src/lib.rs index b399ab72f3..f699718808 100644 --- a/crates/tx/src/lib.rs +++ b/crates/tx/src/lib.rs @@ -21,6 +21,7 @@ pub mod action; pub mod data; pub mod event; pub mod proto; +mod section; mod sign; mod types; @@ -28,14 +29,16 @@ use data::TxType; pub use either; pub use event::new_tx_event; pub use namada_core::key::SignableEthMessage; +pub use section::{ + Authorization, Code, Commitment, CompressedAuthorization, Data, Header, + MaspBuilder, Memo, Section, Signer, TxCommitments, +}; pub use sign::{ standalone_signature, verify_standalone_sig, SignatureIndex, Signed, VerifySigError, }; pub use types::{ - Authorization, BatchedTx, BatchedTxRef, Code, Commitment, - CompressedAuthorization, Data, DecodeError, Header, IndexedTx, - IndexedTxRange, MaspBuilder, Memo, Section, Signer, Tx, TxCommitments, + BatchedTx, BatchedTxRef, DecodeError, IndexedTx, IndexedTxRange, Tx, TxError, }; diff --git a/crates/tx/src/section.rs b/crates/tx/src/section.rs new file mode 100644 index 0000000000..fb416d24c7 --- /dev/null +++ b/crates/tx/src/section.rs @@ -0,0 +1,887 @@ +use std::collections::BTreeMap; +use std::hash::Hash; + +use masp_primitives::transaction::builder::Builder; +use masp_primitives::transaction::components::sapling::builder::SaplingMetadata; +use masp_primitives::transaction::Transaction; +use masp_primitives::zip32::ExtendedFullViewingKey; +use namada_account::AccountPublicKeysMap; +use namada_core::address::Address; +use namada_core::borsh::{ + self, BorshDeserialize, BorshSchema, BorshSerialize, BorshSerializeExt, +}; +use namada_core::chain::ChainId; +use namada_core::collections::HashSet; +use namada_core::key::*; +use namada_core::masp::{AssetData, MaspTxId}; +use namada_core::time::DateTimeUtc; +use namada_macros::BorshDeserializer; +#[cfg(feature = "migrations")] +use namada_migrations::*; +use serde::de::Error as SerdeError; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::data::protocol::ProtocolTx; +use crate::data::{hash_tx, TxType, WrapperTx}; +use crate::sign::VerifySigError; +use crate::{hex_data_serde, hex_salt_serde, Tx, SALT_LENGTH}; + +/// A section of a transaction. Carries an independent piece of information +/// necessary for the processing of a transaction. +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, + PartialEq, +)] +pub enum Section { + /// Transaction data that needs to be sent to hardware wallets + Data(Data), + /// Transaction data that does not need to be sent to hardware wallets + ExtraData(Code), + /// Transaction code. Sending to hardware wallets optional + Code(Code), + /// A transaction header/protocol signature + Authorization(Authorization), + /// Embedded MASP transaction section + #[serde( + serialize_with = "borsh_serde::", + deserialize_with = "serde_borsh::" + )] + MaspTx(Transaction), + /// A section providing the auxiliary inputs used to construct a MASP + /// transaction. Only send to wallet, never send to protocol. + MaspBuilder(MaspBuilder), + /// Wrap a header with a section for the purposes of computing hashes + Header(Header), +} + +/// A Namada transaction header indicating where transaction subcomponents can +/// be found +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, + PartialEq, +)] +pub struct Header { + /// The chain which this transaction is being submitted to + pub chain_id: ChainId, + /// The time at which this transaction expires + pub expiration: Option, + /// A transaction timestamp + pub timestamp: DateTimeUtc, + /// The commitments to the transaction's sections + pub batch: HashSet, + /// Whether the inner txs should be executed atomically + pub atomic: bool, + /// The type of this transaction + pub tx_type: TxType, +} + +impl Header { + /// Make a new header of the given transaction type + pub fn new(tx_type: TxType) -> Self { + Self { + tx_type, + chain_id: ChainId::default(), + expiration: None, + #[allow(clippy::disallowed_methods)] + timestamp: DateTimeUtc::now(), + batch: Default::default(), + atomic: Default::default(), + } + } + + /// Get the hash of this transaction header. + pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { + hasher.update(self.serialize_to_vec()); + hasher + } + + /// Get the wrapper header if it is present + pub fn wrapper(&self) -> Option { + if let TxType::Wrapper(wrapper) = &self.tx_type { + Some(*wrapper.clone()) + } else { + None + } + } + + /// Get the protocol header if it is present + pub fn protocol(&self) -> Option { + if let TxType::Protocol(protocol) = &self.tx_type { + Some(*protocol.clone()) + } else { + None + } + } +} + +impl Section { + /// Hash this section. Section hashes are useful for signatures and also for + /// allowing transaction sections to cross reference. + pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { + // Get the index corresponding to this variant + let discriminant = self.serialize_to_vec()[0]; + // Use Borsh's discriminant in the Section's hash + hasher.update([discriminant]); + match self { + Self::Data(data) => data.hash(hasher), + Self::ExtraData(extra) => extra.hash(hasher), + Self::Code(code) => code.hash(hasher), + Self::Authorization(signature) => signature.hash(hasher), + Self::MaspBuilder(mb) => mb.hash(hasher), + Self::MaspTx(tx) => { + hasher.update(tx.serialize_to_vec()); + hasher + } + Self::Header(header) => header.hash(hasher), + } + } + + /// Get the hash of this section + pub fn get_hash(&self) -> namada_core::hash::Hash { + namada_core::hash::Hash( + self.hash(&mut Sha256::new()).finalize_reset().into(), + ) + } + + /// Extract the data from this section if possible + pub fn data(&self) -> Option { + if let Self::Data(data) = self { + Some(data.clone()) + } else { + None + } + } + + /// Extract the extra data from this section if possible + pub fn extra_data_sec(&self) -> Option { + if let Self::ExtraData(data) = self { + Some(data.clone()) + } else { + None + } + } + + /// Extract the extra data from this section if possible + pub fn extra_data(&self) -> Option> { + if let Self::ExtraData(data) = self { + data.code.id() + } else { + None + } + } + + /// Extract the code from this section is possible + pub fn code_sec(&self) -> Option { + if let Self::Code(data) = self { + Some(data.clone()) + } else { + None + } + } + + /// Extract the code from this section is possible + pub fn code(&self) -> Option> { + if let Self::Code(data) = self { + data.code.id() + } else { + None + } + } + + /// Extract the signature from this section if possible + pub fn signature(&self) -> Option { + if let Self::Authorization(data) = self { + Some(data.clone()) + } else { + None + } + } + + /// Extract the MASP transaction from this section if possible + pub fn masp_tx(&self) -> Option { + if let Self::MaspTx(data) = self { + Some(data.clone()) + } else { + None + } + } + + /// Extract the MASP builder from this section if possible + pub fn masp_builder(&self) -> Option { + if let Self::MaspBuilder(data) = self { + Some(data.clone()) + } else { + None + } + } +} + +/// A section representing transaction data +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, +)] +pub struct Data { + /// Salt with additional random data (usually a timestamp) + #[serde(with = "hex_salt_serde")] + pub salt: [u8; SALT_LENGTH], + /// Data bytes + #[serde(with = "hex_data_serde")] + pub data: Vec, +} + +impl PartialEq for Data { + fn eq(&self, other: &Self) -> bool { + self.data == other.data + } +} + +impl Data { + /// Make a new data section with the given bytes + pub fn new(data: Vec) -> Self { + use rand_core::{OsRng, RngCore}; + + Self { + salt: { + let mut buf = [0; SALT_LENGTH]; + OsRng.fill_bytes(&mut buf); + buf + }, + data, + } + } + + /// Hash this data section + pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { + hasher.update(self.serialize_to_vec()); + hasher + } +} + +/// Represents either some code bytes or their SHA-256 hash +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, +)] +pub enum Commitment { + /// Result of applying hash function to bytes + Hash(namada_core::hash::Hash), + /// Result of applying identity function to bytes + Id(Vec), +} + +impl PartialEq for Commitment { + fn eq(&self, other: &Self) -> bool { + self.hash() == other.hash() + } +} + +impl Commitment { + /// Return the contained hash commitment + pub fn hash(&self) -> namada_core::hash::Hash { + match self { + Self::Id(code) => hash_tx(code), + Self::Hash(hash) => *hash, + } + } + + /// Return the result of applying identity function if there is any + pub fn id(&self) -> Option> { + if let Self::Id(code) = self { + Some(code.clone()) + } else { + None + } + } +} + +/// A section representing transaction code +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, +)] +pub struct Code { + /// Additional random data + #[serde(with = "hex_salt_serde")] + pub salt: [u8; SALT_LENGTH], + /// Actual transaction code + pub code: Commitment, + /// The tag for the transaction code + pub tag: Option, +} + +impl PartialEq for Code { + fn eq(&self, other: &Self) -> bool { + self.code == other.code + } +} + +impl Code { + /// Make a new code section with the given bytes + pub fn new(code: Vec, tag: Option) -> Self { + use rand_core::{OsRng, RngCore}; + + Self { + salt: { + let mut buf = [0; SALT_LENGTH]; + OsRng.fill_bytes(&mut buf); + buf + }, + code: Commitment::Id(code), + tag, + } + } + + /// Make a new code section with the given hash + pub fn from_hash( + hash: namada_core::hash::Hash, + tag: Option, + ) -> Self { + use rand_core::{OsRng, RngCore}; + + Self { + salt: { + let mut buf = [0; SALT_LENGTH]; + OsRng.fill_bytes(&mut buf); + buf + }, + code: Commitment::Hash(hash), + tag, + } + } + + /// Hash this code section + pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { + hasher.update(self.salt); + hasher.update(self.code.hash()); + hasher.update(self.tag.serialize_to_vec()); + hasher + } +} + +/// A memo field (bytes). +pub type Memo = Vec; + +/// Indicates the list of public keys against which signatures will be verified +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, + PartialEq, +)] +pub enum Signer { + /// The address of a multisignature account + Address(Address), + /// The public keys that constitute a signer + PubKeys(Vec), +} + +/// A section representing a multisig over another section +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, + PartialEq, +)] +pub struct Authorization { + /// The hash of the section being signed + pub targets: Vec, + /// The public keys against which the signatures should be verified + pub signer: Signer, + /// The signature over the above hash + pub signatures: BTreeMap, +} + +impl Authorization { + /// Sign the given section hash with the given key and return a section + pub fn new( + targets: Vec, + secret_keys: BTreeMap, + signer: Option
, + ) -> Self { + // If no signer address is given, then derive the signer's public keys + // from the given secret keys. + let signer = if let Some(addr) = signer { + Signer::Address(addr) + } else { + // Make sure the corresponding public keys can be represented by a + // vector instead of a map + assert!( + secret_keys + .keys() + .cloned() + .eq(0..(u8::try_from(secret_keys.len()) + .expect("Number of SKs must not exceed `u8::MAX`"))), + "secret keys must be enumerated when signer address is absent" + ); + Signer::PubKeys(secret_keys.values().map(RefTo::ref_to).collect()) + }; + + // Commit to the given targets + let partial = Self { + targets, + signer, + signatures: BTreeMap::new(), + }; + let target = partial.get_raw_hash(); + // Turn the map of secret keys into a map of signatures over the + // commitment made above + let signatures = secret_keys + .iter() + .map(|(index, secret_key)| { + (*index, common::SigScheme::sign(secret_key, target)) + }) + .collect(); + Self { + signatures, + ..partial + } + } + + /// Hash this signature section + pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { + hasher.update(self.serialize_to_vec()); + hasher + } + + /// Get the hash of this section + pub fn get_hash(&self) -> namada_core::hash::Hash { + namada_core::hash::Hash( + self.hash(&mut Sha256::new()).finalize_reset().into(), + ) + } + + /// Get a hash of this section with its signer and signatures removed + pub fn get_raw_hash(&self) -> namada_core::hash::Hash { + Self { + signer: Signer::PubKeys(vec![]), + signatures: BTreeMap::new(), + ..self.clone() + } + .get_hash() + } + + /// Verify that the signature contained in this section is valid + pub fn verify_signature( + &self, + verified_pks: &mut HashSet, + public_keys_index_map: &AccountPublicKeysMap, + signer: &Option
, + consume_verify_sig_gas: &mut F, + ) -> std::result::Result + where + F: FnMut() -> std::result::Result<(), namada_gas::Error>, + { + // Records whether there are any successful verifications + let mut verifications = 0; + match &self.signer { + // Verify the signatures against the given public keys if the + // account addresses match + Signer::Address(addr) if Some(addr) == signer.as_ref() => { + for (idx, sig) in &self.signatures { + if let Some(pk) = + public_keys_index_map.get_public_key_from_index(*idx) + { + consume_verify_sig_gas()?; + common::SigScheme::verify_signature( + &pk, + &self.get_raw_hash(), + sig, + )?; + verified_pks.insert(*idx); + // Cannot overflow + #[allow(clippy::arithmetic_side_effects)] + { + verifications += 1; + } + } + } + } + // If the account addresses do not match, then there is no efficient + // way to map signatures to the given public keys + Signer::Address(_) => {} + // Verify the signatures against the subset of this section's public + // keys that are also in the given map + Signer::PubKeys(pks) => { + let hash = self.get_raw_hash(); + for (idx, pk) in pks.iter().enumerate() { + let map_idx = + public_keys_index_map.get_index_from_public_key(pk); + + // Use the first signature when fuzzing as the map is + // unlikely to contain matching PKs + #[cfg(fuzzing)] + let map_idx = map_idx.or(Some(0_u8)); + + if let Some(map_idx) = map_idx { + let sig_idx = u8::try_from(idx) + .map_err(|_| VerifySigError::PksOverflow)?; + consume_verify_sig_gas()?; + let sig = self + .signatures + .get(&sig_idx) + .ok_or(VerifySigError::MissingSignature)?; + common::SigScheme::verify_signature(pk, &hash, sig)?; + verified_pks.insert(map_idx); + // Cannot overflow + #[allow(clippy::arithmetic_side_effects)] + { + verifications += 1; + } + } + } + } + } + + // There's usually not enough signatures when fuzzing, this makes it + // more likely to pass authorization. + #[cfg(fuzzing)] + { + verifications = 1; + } + + Ok(verifications) + } +} + +/// A section representing a multisig over another section +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, +)] +pub struct CompressedAuthorization { + /// The hash of the section being signed + pub targets: Vec, + /// The public keys against which the signatures should be verified + pub signer: Signer, + /// The signature over the above hash + pub signatures: BTreeMap, +} + +impl CompressedAuthorization { + /// Decompress this signature object with respect to the given transaction + /// by looking up the necessary section hashes. Used by constrained hardware + /// wallets. + pub fn expand(self, tx: &Tx) -> Authorization { + let mut targets = Vec::new(); + for idx in self.targets { + if idx == 0 { + // The "zeroth" section is the header + targets.push(tx.header_hash()); + } else if idx == 255 { + // The 255th section is the raw header + targets.push(tx.raw_header_hash()); + } else { + targets.push( + tx.sections[(idx as usize) + .checked_sub(1) + .expect("cannot underflow")] + .get_hash(), + ); + } + } + Authorization { + targets, + signer: self.signer, + signatures: self.signatures, + } + } +} + +/// An inner transaction of the batch, represented by its commitments to the +/// [`Code`], [`Data`] and [`Memo`] sections +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive( + Clone, + Debug, + Default, + BorshSerialize, + BorshDeserialize, + BorshDeserializer, + BorshSchema, + Serialize, + Deserialize, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, +)] +pub struct TxCommitments { + /// The SHA-256 hash of the transaction's code section + pub code_hash: namada_core::hash::Hash, + /// The SHA-256 hash of the transaction's data section + pub data_hash: namada_core::hash::Hash, + /// The SHA-256 hash of the transaction's memo section + /// + /// In case a memo is not present in the transaction, a + /// byte array filled with zeroes is present instead + pub memo_hash: namada_core::hash::Hash, +} + +impl TxCommitments { + /// Get the hash of this transaction's code + pub fn code_sechash(&self) -> &namada_core::hash::Hash { + &self.code_hash + } + + /// Get the transaction data hash + pub fn data_sechash(&self) -> &namada_core::hash::Hash { + &self.data_hash + } + + /// Get the hash of this transaction's memo + pub fn memo_sechash(&self) -> &namada_core::hash::Hash { + &self.memo_hash + } + + /// Hash the commitments to the transaction's sections + pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { + hasher.update(self.serialize_to_vec()); + hasher + } + + /// Get the hash of this Commitments + pub fn get_hash(&self) -> namada_core::hash::Hash { + namada_core::hash::Hash( + self.hash(&mut Sha256::new()).finalize_reset().into(), + ) + } +} + +/// A section providing the auxiliary inputs used to construct a MASP +/// transaction +#[derive( + Clone, + Debug, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub struct MaspBuilder { + /// The MASP transaction that this section witnesses + pub target: MaspTxId, + /// The decoded set of asset types used by the transaction. Useful for + /// offline wallets trying to display AssetTypes. + pub asset_types: HashSet, + /// Track how Info objects map to descriptors and outputs + #[serde( + serialize_with = "borsh_serde::", + deserialize_with = "serde_borsh::" + )] + pub metadata: SaplingMetadata, + /// The data that was used to construct the target transaction + #[serde( + serialize_with = "borsh_serde::", + deserialize_with = "serde_borsh::" + )] + pub builder: Builder<(), ExtendedFullViewingKey, ()>, +} + +impl PartialEq for MaspBuilder { + fn eq(&self, other: &Self) -> bool { + self.target == other.target + } +} + +impl MaspBuilder { + /// Get the hash of this ciphertext section. This operation is done in such + /// a way it matches the hash of the type pun + pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { + hasher.update(self.serialize_to_vec()); + hasher + } +} + +#[cfg(feature = "arbitrary")] +impl arbitrary::Arbitrary<'_> for MaspBuilder { + fn arbitrary( + u: &mut arbitrary::Unstructured<'_>, + ) -> arbitrary::Result { + use masp_primitives::transaction::builder::MapBuilder; + use masp_primitives::transaction::components::sapling::builder::MapBuilder as SapMapBuilder; + use masp_primitives::zip32::ExtendedSpendingKey; + struct WalletMap; + + impl + SapMapBuilder + for WalletMap + { + fn map_params(&self, _s: P1) {} + + fn map_key( + &self, + s: ExtendedSpendingKey, + ) -> ExtendedFullViewingKey { + (&s).into() + } + } + impl + MapBuilder< + P1, + ExtendedSpendingKey, + N1, + (), + ExtendedFullViewingKey, + (), + > for WalletMap + { + fn map_notifier(&self, _s: N1) {} + } + + let target_height = masp_primitives::consensus::BlockHeight::from( + u.int_in_range(0_u32..=100_000_000)?, + ); + Ok(MaspBuilder { + target: arbitrary::Arbitrary::arbitrary(u)?, + asset_types: arbitrary::Arbitrary::arbitrary(u)?, + metadata: arbitrary::Arbitrary::arbitrary(u)?, + builder: Builder::new( + masp_primitives::consensus::TestNetwork, + target_height, + ) + .map_builder(WalletMap), + }) + } + + fn size_hint(depth: usize) -> (usize, Option) { + arbitrary::size_hint::and_all( + &[ + ::size_hint(depth), + ::size_hint(depth), + as arbitrary::Arbitrary>::size_hint(depth), + ::size_hint(depth), + ], + ) + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct TransactionSerde(Vec); + +impl From> for TransactionSerde { + fn from(tx: Vec) -> Self { + Self(tx) + } +} + +impl From for Vec { + fn from(tx: TransactionSerde) -> Vec { + tx.0 + } +} + +/// A structure to facilitate Serde (de)serializations of Builders +#[derive(serde::Serialize, serde::Deserialize)] +struct BuilderSerde(Vec); + +impl From> for BuilderSerde { + fn from(tx: Vec) -> Self { + Self(tx) + } +} + +impl From for Vec { + fn from(tx: BuilderSerde) -> Vec { + tx.0 + } +} + +/// A structure to facilitate Serde (de)serializations of SaplingMetadata +#[derive(serde::Serialize, serde::Deserialize)] +pub struct SaplingMetadataSerde(Vec); + +impl From> for SaplingMetadataSerde { + fn from(tx: Vec) -> Self { + Self(tx) + } +} + +impl From for Vec { + fn from(tx: SaplingMetadataSerde) -> Vec { + tx.0 + } +} + +fn borsh_serde( + obj: &impl BorshSerialize, + ser: S, +) -> std::result::Result +where + S: serde::Serializer, + T: From>, + T: serde::Serialize, +{ + Into::::into(obj.serialize_to_vec()).serialize(ser) +} + +fn serde_borsh<'de, T, S, U>(ser: S) -> std::result::Result +where + S: serde::Deserializer<'de>, + T: Into>, + T: serde::Deserialize<'de>, + U: BorshDeserialize, +{ + BorshDeserialize::try_from_slice(&Into::>::into(T::deserialize( + ser, + )?)) + .map_err(S::Error::custom) +} diff --git a/crates/tx/src/types.rs b/crates/tx/src/types.rs index cdf77f6ac6..6a0c14eea8 100644 --- a/crates/tx/src/types.rs +++ b/crates/tx/src/types.rs @@ -4,10 +4,7 @@ use std::hash::Hash; use std::ops::{Bound, RangeBounds}; use data_encoding::HEXUPPER; -use masp_primitives::transaction::builder::Builder; -use masp_primitives::transaction::components::sapling::builder::SaplingMetadata; use masp_primitives::transaction::Transaction; -use masp_primitives::zip32::ExtendedFullViewingKey; use namada_account::AccountPublicKeysMap; use namada_core::address::Address; use namada_core::borsh::{ @@ -16,21 +13,21 @@ use namada_core::borsh::{ use namada_core::chain::{BlockHeight, ChainId}; use namada_core::collections::{HashMap, HashSet}; use namada_core::key::*; -use namada_core::masp::{AssetData, MaspTxId}; +use namada_core::masp::MaspTxId; use namada_core::storage::TxIndex; use namada_core::time::DateTimeUtc; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; -use serde::de::Error as SerdeError; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use thiserror::Error; -use crate::data::protocol::ProtocolTx; -use crate::data::{hash_tx, Fee, GasLimit, TxType, WrapperTx}; +use crate::data::{Fee, GasLimit, TxType, WrapperTx}; use crate::sign::{SignatureIndex, VerifySigError}; -use crate::{hex_data_serde, hex_salt_serde, proto, SALT_LENGTH}; +use crate::{ + proto, Authorization, Code, Data, Header, MaspBuilder, Section, Signer, + TxCommitments, +}; #[allow(missing_docs)] #[derive(Error, Debug)] @@ -51,865 +48,6 @@ pub enum DecodeError { InvalidJSONDeserialization(String), } -/// A section representing transaction data -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, -)] -pub struct Data { - /// Salt with additional random data (usually a timestamp) - #[serde(with = "hex_salt_serde")] - pub salt: [u8; SALT_LENGTH], - /// Data bytes - #[serde(with = "hex_data_serde")] - pub data: Vec, -} - -impl PartialEq for Data { - fn eq(&self, other: &Self) -> bool { - self.data == other.data - } -} - -impl Data { - /// Make a new data section with the given bytes - pub fn new(data: Vec) -> Self { - use rand_core::{OsRng, RngCore}; - - Self { - salt: { - let mut buf = [0; SALT_LENGTH]; - OsRng.fill_bytes(&mut buf); - buf - }, - data, - } - } - - /// Hash this data section - pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { - hasher.update(self.serialize_to_vec()); - hasher - } -} - -/// Represents either some code bytes or their SHA-256 hash -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, -)] -pub enum Commitment { - /// Result of applying hash function to bytes - Hash(namada_core::hash::Hash), - /// Result of applying identity function to bytes - Id(Vec), -} - -impl PartialEq for Commitment { - fn eq(&self, other: &Self) -> bool { - self.hash() == other.hash() - } -} - -impl Commitment { - /// Return the contained hash commitment - pub fn hash(&self) -> namada_core::hash::Hash { - match self { - Self::Id(code) => hash_tx(code), - Self::Hash(hash) => *hash, - } - } - - /// Return the result of applying identity function if there is any - pub fn id(&self) -> Option> { - if let Self::Id(code) = self { - Some(code.clone()) - } else { - None - } - } -} - -/// A section representing transaction code -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, -)] -pub struct Code { - /// Additional random data - #[serde(with = "hex_salt_serde")] - pub salt: [u8; SALT_LENGTH], - /// Actual transaction code - pub code: Commitment, - /// The tag for the transaction code - pub tag: Option, -} - -impl PartialEq for Code { - fn eq(&self, other: &Self) -> bool { - self.code == other.code - } -} - -impl Code { - /// Make a new code section with the given bytes - pub fn new(code: Vec, tag: Option) -> Self { - use rand_core::{OsRng, RngCore}; - - Self { - salt: { - let mut buf = [0; SALT_LENGTH]; - OsRng.fill_bytes(&mut buf); - buf - }, - code: Commitment::Id(code), - tag, - } - } - - /// Make a new code section with the given hash - pub fn from_hash( - hash: namada_core::hash::Hash, - tag: Option, - ) -> Self { - use rand_core::{OsRng, RngCore}; - - Self { - salt: { - let mut buf = [0; SALT_LENGTH]; - OsRng.fill_bytes(&mut buf); - buf - }, - code: Commitment::Hash(hash), - tag, - } - } - - /// Hash this code section - pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { - hasher.update(self.salt); - hasher.update(self.code.hash()); - hasher.update(self.tag.serialize_to_vec()); - hasher - } -} - -/// A memo field (bytes). -pub type Memo = Vec; - -/// Indicates the list of public keys against which signatures will be verified -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, - PartialEq, -)] -pub enum Signer { - /// The address of a multisignature account - Address(Address), - /// The public keys that constitute a signer - PubKeys(Vec), -} - -/// A section representing a multisig over another section -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, - PartialEq, -)] -pub struct Authorization { - /// The hash of the section being signed - pub targets: Vec, - /// The public keys against which the signatures should be verified - pub signer: Signer, - /// The signature over the above hash - pub signatures: BTreeMap, -} - -impl Authorization { - /// Sign the given section hash with the given key and return a section - pub fn new( - targets: Vec, - secret_keys: BTreeMap, - signer: Option
, - ) -> Self { - // If no signer address is given, then derive the signer's public keys - // from the given secret keys. - let signer = if let Some(addr) = signer { - Signer::Address(addr) - } else { - // Make sure the corresponding public keys can be represented by a - // vector instead of a map - assert!( - secret_keys - .keys() - .cloned() - .eq(0..(u8::try_from(secret_keys.len()) - .expect("Number of SKs must not exceed `u8::MAX`"))), - "secret keys must be enumerated when signer address is absent" - ); - Signer::PubKeys(secret_keys.values().map(RefTo::ref_to).collect()) - }; - - // Commit to the given targets - let partial = Self { - targets, - signer, - signatures: BTreeMap::new(), - }; - let target = partial.get_raw_hash(); - // Turn the map of secret keys into a map of signatures over the - // commitment made above - let signatures = secret_keys - .iter() - .map(|(index, secret_key)| { - (*index, common::SigScheme::sign(secret_key, target)) - }) - .collect(); - Self { - signatures, - ..partial - } - } - - /// Hash this signature section - pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { - hasher.update(self.serialize_to_vec()); - hasher - } - - /// Get the hash of this section - pub fn get_hash(&self) -> namada_core::hash::Hash { - namada_core::hash::Hash( - self.hash(&mut Sha256::new()).finalize_reset().into(), - ) - } - - /// Get a hash of this section with its signer and signatures removed - pub fn get_raw_hash(&self) -> namada_core::hash::Hash { - Self { - signer: Signer::PubKeys(vec![]), - signatures: BTreeMap::new(), - ..self.clone() - } - .get_hash() - } - - /// Verify that the signature contained in this section is valid - pub fn verify_signature( - &self, - verified_pks: &mut HashSet, - public_keys_index_map: &AccountPublicKeysMap, - signer: &Option
, - consume_verify_sig_gas: &mut F, - ) -> std::result::Result - where - F: FnMut() -> std::result::Result<(), namada_gas::Error>, - { - // Records whether there are any successful verifications - let mut verifications = 0; - match &self.signer { - // Verify the signatures against the given public keys if the - // account addresses match - Signer::Address(addr) if Some(addr) == signer.as_ref() => { - for (idx, sig) in &self.signatures { - if let Some(pk) = - public_keys_index_map.get_public_key_from_index(*idx) - { - consume_verify_sig_gas()?; - common::SigScheme::verify_signature( - &pk, - &self.get_raw_hash(), - sig, - )?; - verified_pks.insert(*idx); - // Cannot overflow - #[allow(clippy::arithmetic_side_effects)] - { - verifications += 1; - } - } - } - } - // If the account addresses do not match, then there is no efficient - // way to map signatures to the given public keys - Signer::Address(_) => {} - // Verify the signatures against the subset of this section's public - // keys that are also in the given map - Signer::PubKeys(pks) => { - let hash = self.get_raw_hash(); - for (idx, pk) in pks.iter().enumerate() { - let map_idx = - public_keys_index_map.get_index_from_public_key(pk); - - // Use the first signature when fuzzing as the map is - // unlikely to contain matching PKs - #[cfg(fuzzing)] - let map_idx = map_idx.or(Some(0_u8)); - - if let Some(map_idx) = map_idx { - let sig_idx = u8::try_from(idx) - .map_err(|_| VerifySigError::PksOverflow)?; - consume_verify_sig_gas()?; - let sig = self - .signatures - .get(&sig_idx) - .ok_or(VerifySigError::MissingSignature)?; - common::SigScheme::verify_signature(pk, &hash, sig)?; - verified_pks.insert(map_idx); - // Cannot overflow - #[allow(clippy::arithmetic_side_effects)] - { - verifications += 1; - } - } - } - } - } - - // There's usually not enough signatures when fuzzing, this makes it - // more likely to pass authorization. - #[cfg(fuzzing)] - { - verifications = 1; - } - - Ok(verifications) - } -} - -/// A section representing a multisig over another section -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, -)] -pub struct CompressedAuthorization { - /// The hash of the section being signed - pub targets: Vec, - /// The public keys against which the signatures should be verified - pub signer: Signer, - /// The signature over the above hash - pub signatures: BTreeMap, -} - -impl CompressedAuthorization { - /// Decompress this signature object with respect to the given transaction - /// by looking up the necessary section hashes. Used by constrained hardware - /// wallets. - pub fn expand(self, tx: &Tx) -> Authorization { - let mut targets = Vec::new(); - for idx in self.targets { - if idx == 0 { - // The "zeroth" section is the header - targets.push(tx.header_hash()); - } else if idx == 255 { - // The 255th section is the raw header - targets.push(tx.raw_header_hash()); - } else { - targets.push( - tx.sections[(idx as usize) - .checked_sub(1) - .expect("cannot underflow")] - .get_hash(), - ); - } - } - Authorization { - targets, - signer: self.signer, - signatures: self.signatures, - } - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -struct TransactionSerde(Vec); - -impl From> for TransactionSerde { - fn from(tx: Vec) -> Self { - Self(tx) - } -} - -impl From for Vec { - fn from(tx: TransactionSerde) -> Vec { - tx.0 - } -} - -fn borsh_serde( - obj: &impl BorshSerialize, - ser: S, -) -> std::result::Result -where - S: serde::Serializer, - T: From>, - T: serde::Serialize, -{ - Into::::into(obj.serialize_to_vec()).serialize(ser) -} - -fn serde_borsh<'de, T, S, U>(ser: S) -> std::result::Result -where - S: serde::Deserializer<'de>, - T: Into>, - T: serde::Deserialize<'de>, - U: BorshDeserialize, -{ - BorshDeserialize::try_from_slice(&Into::>::into(T::deserialize( - ser, - )?)) - .map_err(S::Error::custom) -} - -/// A structure to facilitate Serde (de)serializations of Builders -#[derive(serde::Serialize, serde::Deserialize)] -struct BuilderSerde(Vec); - -impl From> for BuilderSerde { - fn from(tx: Vec) -> Self { - Self(tx) - } -} - -impl From for Vec { - fn from(tx: BuilderSerde) -> Vec { - tx.0 - } -} - -/// A structure to facilitate Serde (de)serializations of SaplingMetadata -#[derive(serde::Serialize, serde::Deserialize)] -pub struct SaplingMetadataSerde(Vec); - -impl From> for SaplingMetadataSerde { - fn from(tx: Vec) -> Self { - Self(tx) - } -} - -impl From for Vec { - fn from(tx: SaplingMetadataSerde) -> Vec { - tx.0 - } -} - -/// A section providing the auxiliary inputs used to construct a MASP -/// transaction -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Serialize, - Deserialize, -)] -pub struct MaspBuilder { - /// The MASP transaction that this section witnesses - pub target: MaspTxId, - /// The decoded set of asset types used by the transaction. Useful for - /// offline wallets trying to display AssetTypes. - pub asset_types: HashSet, - /// Track how Info objects map to descriptors and outputs - #[serde( - serialize_with = "borsh_serde::", - deserialize_with = "serde_borsh::" - )] - pub metadata: SaplingMetadata, - /// The data that was used to construct the target transaction - #[serde( - serialize_with = "borsh_serde::", - deserialize_with = "serde_borsh::" - )] - pub builder: Builder<(), ExtendedFullViewingKey, ()>, -} - -impl PartialEq for MaspBuilder { - fn eq(&self, other: &Self) -> bool { - self.target == other.target - } -} - -impl MaspBuilder { - /// Get the hash of this ciphertext section. This operation is done in such - /// a way it matches the hash of the type pun - pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { - hasher.update(self.serialize_to_vec()); - hasher - } -} - -#[cfg(feature = "arbitrary")] -impl arbitrary::Arbitrary<'_> for MaspBuilder { - fn arbitrary( - u: &mut arbitrary::Unstructured<'_>, - ) -> arbitrary::Result { - use masp_primitives::transaction::builder::MapBuilder; - use masp_primitives::transaction::components::sapling::builder::MapBuilder as SapMapBuilder; - use masp_primitives::zip32::ExtendedSpendingKey; - struct WalletMap; - - impl - SapMapBuilder - for WalletMap - { - fn map_params(&self, _s: P1) {} - - fn map_key( - &self, - s: ExtendedSpendingKey, - ) -> ExtendedFullViewingKey { - (&s).into() - } - } - impl - MapBuilder< - P1, - ExtendedSpendingKey, - N1, - (), - ExtendedFullViewingKey, - (), - > for WalletMap - { - fn map_notifier(&self, _s: N1) {} - } - - let target_height = masp_primitives::consensus::BlockHeight::from( - u.int_in_range(0_u32..=100_000_000)?, - ); - Ok(MaspBuilder { - target: arbitrary::Arbitrary::arbitrary(u)?, - asset_types: arbitrary::Arbitrary::arbitrary(u)?, - metadata: arbitrary::Arbitrary::arbitrary(u)?, - builder: Builder::new( - masp_primitives::consensus::TestNetwork, - target_height, - ) - .map_builder(WalletMap), - }) - } - - fn size_hint(depth: usize) -> (usize, Option) { - arbitrary::size_hint::and_all( - &[ - ::size_hint(depth), - ::size_hint(depth), - as arbitrary::Arbitrary>::size_hint(depth), - ::size_hint(depth), - ], - ) - } -} - -/// A section of a transaction. Carries an independent piece of information -/// necessary for the processing of a transaction. -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, - PartialEq, -)] -pub enum Section { - /// Transaction data that needs to be sent to hardware wallets - Data(Data), - /// Transaction data that does not need to be sent to hardware wallets - ExtraData(Code), - /// Transaction code. Sending to hardware wallets optional - Code(Code), - /// A transaction header/protocol signature - Authorization(Authorization), - /// Embedded MASP transaction section - #[serde( - serialize_with = "borsh_serde::", - deserialize_with = "serde_borsh::" - )] - MaspTx(Transaction), - /// A section providing the auxiliary inputs used to construct a MASP - /// transaction. Only send to wallet, never send to protocol. - MaspBuilder(MaspBuilder), - /// Wrap a header with a section for the purposes of computing hashes - Header(Header), -} - -impl Section { - /// Hash this section. Section hashes are useful for signatures and also for - /// allowing transaction sections to cross reference. - pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { - // Get the index corresponding to this variant - let discriminant = self.serialize_to_vec()[0]; - // Use Borsh's discriminant in the Section's hash - hasher.update([discriminant]); - match self { - Self::Data(data) => data.hash(hasher), - Self::ExtraData(extra) => extra.hash(hasher), - Self::Code(code) => code.hash(hasher), - Self::Authorization(signature) => signature.hash(hasher), - Self::MaspBuilder(mb) => mb.hash(hasher), - Self::MaspTx(tx) => { - hasher.update(tx.serialize_to_vec()); - hasher - } - Self::Header(header) => header.hash(hasher), - } - } - - /// Get the hash of this section - pub fn get_hash(&self) -> namada_core::hash::Hash { - namada_core::hash::Hash( - self.hash(&mut Sha256::new()).finalize_reset().into(), - ) - } - - /// Extract the data from this section if possible - pub fn data(&self) -> Option { - if let Self::Data(data) = self { - Some(data.clone()) - } else { - None - } - } - - /// Extract the extra data from this section if possible - pub fn extra_data_sec(&self) -> Option { - if let Self::ExtraData(data) = self { - Some(data.clone()) - } else { - None - } - } - - /// Extract the extra data from this section if possible - pub fn extra_data(&self) -> Option> { - if let Self::ExtraData(data) = self { - data.code.id() - } else { - None - } - } - - /// Extract the code from this section is possible - pub fn code_sec(&self) -> Option { - if let Self::Code(data) = self { - Some(data.clone()) - } else { - None - } - } - - /// Extract the code from this section is possible - pub fn code(&self) -> Option> { - if let Self::Code(data) = self { - data.code.id() - } else { - None - } - } - - /// Extract the signature from this section if possible - pub fn signature(&self) -> Option { - if let Self::Authorization(data) = self { - Some(data.clone()) - } else { - None - } - } - - /// Extract the MASP transaction from this section if possible - pub fn masp_tx(&self) -> Option { - if let Self::MaspTx(data) = self { - Some(data.clone()) - } else { - None - } - } - - /// Extract the MASP builder from this section if possible - pub fn masp_builder(&self) -> Option { - if let Self::MaspBuilder(data) = self { - Some(data.clone()) - } else { - None - } - } -} - -/// An inner transaction of the batch, represented by its commitments to the -/// [`Code`], [`Data`] and [`Memo`] sections -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - Default, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, - Eq, - PartialEq, - Ord, - PartialOrd, - Hash, -)] -pub struct TxCommitments { - /// The SHA-256 hash of the transaction's code section - pub code_hash: namada_core::hash::Hash, - /// The SHA-256 hash of the transaction's data section - pub data_hash: namada_core::hash::Hash, - /// The SHA-256 hash of the transaction's memo section - /// - /// In case a memo is not present in the transaction, a - /// byte array filled with zeroes is present instead - pub memo_hash: namada_core::hash::Hash, -} - -impl TxCommitments { - /// Get the hash of this transaction's code - pub fn code_sechash(&self) -> &namada_core::hash::Hash { - &self.code_hash - } - - /// Get the transaction data hash - pub fn data_sechash(&self) -> &namada_core::hash::Hash { - &self.data_hash - } - - /// Get the hash of this transaction's memo - pub fn memo_sechash(&self) -> &namada_core::hash::Hash { - &self.memo_hash - } - - /// Hash the commitments to the transaction's sections - pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { - hasher.update(self.serialize_to_vec()); - hasher - } - - /// Get the hash of this Commitments - pub fn get_hash(&self) -> namada_core::hash::Hash { - namada_core::hash::Hash( - self.hash(&mut Sha256::new()).finalize_reset().into(), - ) - } -} - -/// A Namada transaction header indicating where transaction subcomponents can -/// be found -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -#[derive( - Clone, - Debug, - BorshSerialize, - BorshDeserialize, - BorshDeserializer, - BorshSchema, - Serialize, - Deserialize, - PartialEq, -)] -pub struct Header { - /// The chain which this transaction is being submitted to - pub chain_id: ChainId, - /// The time at which this transaction expires - pub expiration: Option, - /// A transaction timestamp - pub timestamp: DateTimeUtc, - /// The commitments to the transaction's sections - pub batch: HashSet, - /// Whether the inner txs should be executed atomically - pub atomic: bool, - /// The type of this transaction - pub tx_type: TxType, -} - -impl Header { - /// Make a new header of the given transaction type - pub fn new(tx_type: TxType) -> Self { - Self { - tx_type, - chain_id: ChainId::default(), - expiration: None, - #[allow(clippy::disallowed_methods)] - timestamp: DateTimeUtc::now(), - batch: Default::default(), - atomic: Default::default(), - } - } - - /// Get the hash of this transaction header. - pub fn hash<'a>(&self, hasher: &'a mut Sha256) -> &'a mut Sha256 { - hasher.update(self.serialize_to_vec()); - hasher - } - - /// Get the wrapper header if it is present - pub fn wrapper(&self) -> Option { - if let TxType::Wrapper(wrapper) = &self.tx_type { - Some(*wrapper.clone()) - } else { - None - } - } - - /// Get the protocol header if it is present - pub fn protocol(&self) -> Option { - if let TxType::Protocol(protocol) = &self.tx_type { - Some(*protocol.clone()) - } else { - None - } - } -} - /// Errors relating to decrypting a wrapper tx and its /// encrypted payload from a Tx type #[allow(missing_docs)]