diff --git a/.circleci/config.yml b/.circleci/config.yml index eae26ab7e0..d14dbb87fd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -333,7 +333,7 @@ commands: parameters: test_command: type: string - default: cargo test --workspace --exclude "mc-fog-*" --frozen --target "$HOST_TARGET_TRIPLE" --no-fail-fast --tests -j 4 + default: cargo test --workspace --exclude "mc-fog-*" --exclude "mc-consensus-*" --frozen --target "$HOST_TARGET_TRIPLE" --no-fail-fast --tests -j 4 steps: - run: name: Run mobilecoin tests @@ -368,6 +368,22 @@ commands: - store_artifacts: path: /tmp/core_dumps + run-consensus-tests: + steps: + - run: + name: Run consensus tests + command: | + # tell the operating system to remove the file size limit on core dump files + ulimit -c unlimited + cargo test --package "mc-consensus-*" -j 4 --frozen --no-fail-fast + - run: + command: | + mkdir -p /tmp/core_dumps + cp core.* /tmp/core_dumps + when: on_fail + - store_artifacts: + path: /tmp/core_dumps + # FIXME: Figure out why the parallel tests stuff using cargo2junit isn't working in the cloud for fog, maybe a memory limit issue? run-fog-tests: steps: @@ -499,6 +515,20 @@ jobs: - post-build - post-mc-test + # Run consensus tests on a single container + run-consensus-tests: + executor: build-executor + environment: + <<: *default-build-environment + steps: + - prepare-for-build + - run-consensus-tests + - check-dirty-git + - when: + condition: { equal: [ << pipeline.git.branch >>, master ] } + steps: [ save-sccache-cache ] + - post-build + # Run fog tests on a single container run-fog-tests: executor: build-executor @@ -654,6 +684,10 @@ workflows: - run-mc-tests: filters: { branches: { ignore: /^deploy\/.*/ } } + # Run consensus tests on a single container + - run-consensus-tests: + filters: { branches: { ignore: /^deploy\/.*/ } } + # Run fog tests on a single container - run-fog-tests: filters: { branches: { ignore: /^deploy\/.*/ } } diff --git a/Cargo.lock b/Cargo.lock index 1251b26f10..2ce62862ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2908,6 +2908,7 @@ dependencies = [ "mc-util-build-script", "mc-util-from-random", "mc-util-serial", + "once_cell", "prost", "rand 0.8.5", "rand_core 0.6.3", diff --git a/android-bindings/src/bindings.rs b/android-bindings/src/bindings.rs index 885d3f1acf..245c7c206e 100644 --- a/android-bindings/src/bindings.rs +++ b/android-bindings/src/bindings.rs @@ -45,9 +45,9 @@ use mc_transaction_core::{ }, ring_signature::KeyImage, tx::{Tx, TxOut, TxOutConfirmationNumber, TxOutMembershipProof}, - Amount, CompressedCommitment, + Amount, BlockVersion, CompressedCommitment, }; -use mc_transaction_std::{InputCredentials, NoMemoBuilder, TransactionBuilder}; +use mc_transaction_std::{InputCredentials, RTHMemoBuilder, TransactionBuilder}; use mc_util_from_random::FromRandom; use mc_util_uri::FogUri; use protobuf::Message; @@ -1166,9 +1166,18 @@ pub unsafe extern "C" fn Java_com_mobilecoin_lib_TransactionBuilder_init_1jni( jni_ffi_call(&env, |env| { let fog_resolver: MutexGuard = env.get_rust_field(fog_resolver, RUST_OBJ_FIELD)?; - // TODO: After servers that support memos are deployed, use RTHMemoBuilder here - let memo_builder = NoMemoBuilder::default(); - let tx_builder = TransactionBuilder::new(fog_resolver.clone(), memo_builder); + // FIXME: block version should be a parameter, it should be the latest + // version that fog ledger told us about, or that we got from ledger-db + let block_version = BlockVersion::ONE; + // Note: RTHMemoBuilder can be selected here, but we will only actually + // write memos if block_version is large enough that memos are supported. + // If block version is < 2, then transaction builder will filter out memos. + let mut memo_builder = RTHMemoBuilder::default(); + // FIXME: we need to pass the source account key to build sender memo + // credentials memo_builder.set_sender_credential(SenderMemoCredential:: + // from(source_account_key)); + memo_builder.enable_destination_memo(); + let tx_builder = TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); Ok(env.set_rust_field(obj, RUST_OBJ_FIELD, tx_builder)?) }) } diff --git a/api/src/convert/archive_block.rs b/api/src/convert/archive_block.rs index 7f590bceb0..9ea5035598 100644 --- a/api/src/convert/archive_block.rs +++ b/api/src/convert/archive_block.rs @@ -122,7 +122,7 @@ mod tests { membership_proofs::Range, ring_signature::KeyImage, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Amount, Block, BlockContents, BlockData, BlockID, BlockSignature, + Amount, Block, BlockContents, BlockData, BlockID, BlockSignature, BlockVersion, }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -148,7 +148,7 @@ mod tests { let block_contents = BlockContents::new(vec![key_image.clone()], vec![tx_out.clone()]); let block = Block::new( - 1, + BlockVersion::ONE, &parent_block_id, 99 + block_idx, 400 + block_idx, diff --git a/api/src/convert/tx.rs b/api/src/convert/tx.rs index 1ea55d262c..6f79b310cb 100644 --- a/api/src/convert/tx.rs +++ b/api/src/convert/tx.rs @@ -33,6 +33,7 @@ mod tests { use mc_transaction_core::{ onetime_keys::recover_onetime_private_key, tx::{Tx, TxOut, TxOutMembershipProof}, + BlockVersion, }; use mc_transaction_core_test_utils::MockFogResolver; use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; @@ -46,87 +47,96 @@ mod tests { // transaction_builder.rs::test_simple_transaction let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let alice = AccountKey::random(&mut rng); - let bob = AccountKey::random(&mut rng); - let charlie = AccountKey::random(&mut rng); - - let minted_outputs: Vec = { - // Mint an initial collection of outputs, including one belonging to - // `sender_account`. - let mut recipient_and_amounts: Vec<(PublicAddress, u64)> = Vec::new(); - recipient_and_amounts.push((alice.default_subaddress(), 65536)); - - // Some outputs belonging to this account will be used as mix-ins. - recipient_and_amounts.push((charlie.default_subaddress(), 65536)); - recipient_and_amounts.push((charlie.default_subaddress(), 65536)); - mc_transaction_core_test_utils::get_outputs(&recipient_and_amounts, &mut rng) - }; - - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), EmptyMemoBuilder::default()); - - let ring: Vec = minted_outputs.clone(); - let public_key = RistrettoPublic::try_from(&minted_outputs[0].public_key).unwrap(); - let onetime_private_key = recover_onetime_private_key( - &public_key, - alice.view_private_key(), - &alice.default_subaddress_spend_private(), - ); - - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TransactionBuilder does not validate membership proofs, but does require one - // for each ring member. - TxOutMembershipProof::new(0, 0, Default::default()) - }) - .collect(); - - let input_credentials = InputCredentials::new( - ring.clone(), - membership_proofs, - 0, - onetime_private_key, - *alice.view_private_key(), - ) - .unwrap(); - - transaction_builder.add_input(input_credentials); - transaction_builder.set_fee(0).unwrap(); - transaction_builder - .add_output(65536, &bob.default_subaddress(), &mut rng) + for block_version in BlockVersion::iterator() { + let alice = AccountKey::random(&mut rng); + let bob = AccountKey::random(&mut rng); + let charlie = AccountKey::random(&mut rng); + + let minted_outputs: Vec = { + // Mint an initial collection of outputs, including one belonging to + // `sender_account`. + let mut recipient_and_amounts: Vec<(PublicAddress, u64)> = Vec::new(); + recipient_and_amounts.push((alice.default_subaddress(), 65536)); + + // Some outputs belonging to this account will be used as mix-ins. + recipient_and_amounts.push((charlie.default_subaddress(), 65536)); + recipient_and_amounts.push((charlie.default_subaddress(), 65536)); + mc_transaction_core_test_utils::get_outputs( + block_version, + &recipient_and_amounts, + &mut rng, + ) + }; + + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); + + let ring: Vec = minted_outputs.clone(); + let public_key = RistrettoPublic::try_from(&minted_outputs[0].public_key).unwrap(); + let onetime_private_key = recover_onetime_private_key( + &public_key, + alice.view_private_key(), + &alice.default_subaddress_spend_private(), + ); + + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TransactionBuilder does not validate membership proofs, but does require one + // for each ring member. + TxOutMembershipProof::new(0, 0, Default::default()) + }) + .collect(); + + let input_credentials = InputCredentials::new( + ring.clone(), + membership_proofs, + 0, + onetime_private_key, + *alice.view_private_key(), + ) .unwrap(); - let tx = transaction_builder.build(&mut rng).unwrap(); - - // decode(encode(tx)) should be the identity function. - { - let bytes = mc_util_serial::encode(&tx); - let recovered_tx = mc_util_serial::decode(&bytes).unwrap(); - assert_eq!(tx, recovered_tx); - } - - // Converting mc_transaction_core::Tx -> external::Tx -> mc_transaction_core::Tx - // should be the identity function. - { - let external_tx: external::Tx = external::Tx::from(&tx); - let recovered_tx: Tx = Tx::try_from(&external_tx).unwrap(); - assert_eq!(tx, recovered_tx); - } - - // Encoding with prost, decoding with protobuf should be the identity function. - { - let bytes = mc_util_serial::encode(&tx); - let recovered_tx = external::Tx::parse_from_bytes(&bytes).unwrap(); - assert_eq!(recovered_tx, external::Tx::from(&tx)); - } - - // Encoding with protobuf, decoding with prost should be the identity function. - { - let external_tx: external::Tx = external::Tx::from(&tx); - let bytes = external_tx.write_to_bytes().unwrap(); - let recovered_tx: Tx = mc_util_serial::decode(&bytes).unwrap(); - assert_eq!(tx, recovered_tx); + transaction_builder.add_input(input_credentials); + transaction_builder.set_fee(0).unwrap(); + transaction_builder + .add_output(65536, &bob.default_subaddress(), &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + + // decode(encode(tx)) should be the identity function. + { + let bytes = mc_util_serial::encode(&tx); + let recovered_tx = mc_util_serial::decode(&bytes).unwrap(); + assert_eq!(tx, recovered_tx); + } + + // Converting mc_transaction_core::Tx -> external::Tx -> mc_transaction_core::Tx + // should be the identity function. + { + let external_tx: external::Tx = external::Tx::from(&tx); + let recovered_tx: Tx = Tx::try_from(&external_tx).unwrap(); + assert_eq!(tx, recovered_tx); + } + + // Encoding with prost, decoding with protobuf should be the identity function. + { + let bytes = mc_util_serial::encode(&tx); + let recovered_tx = external::Tx::parse_from_bytes(&bytes).unwrap(); + assert_eq!(recovered_tx, external::Tx::from(&tx)); + } + + // Encoding with protobuf, decoding with prost should be the identity function. + { + let external_tx: external::Tx = external::Tx::from(&tx); + let bytes = external_tx.write_to_bytes().unwrap(); + let recovered_tx: Tx = mc_util_serial::decode(&bytes).unwrap(); + assert_eq!(tx, recovered_tx); + } } } } diff --git a/consensus/enclave/api/src/config.rs b/consensus/enclave/api/src/config.rs new file mode 100644 index 0000000000..d58ae4e4c5 --- /dev/null +++ b/consensus/enclave/api/src/config.rs @@ -0,0 +1,146 @@ +use crate::FeeMap; +use alloc::{format, string::String}; +use mc_common::ResponderId; +use mc_crypto_digestible::{Digestible, MerlinTranscript}; +use mc_transaction_core::BlockVersion; +use serde::{Deserialize, Serialize}; + +/// Configuration for the enclave which is used to help determine which +/// transactions are valid. +/// +/// (This can be contrasted with things like responder id and sealed block +/// signing key) +#[derive(Clone, Deserialize, Debug, Digestible, Eq, Hash, PartialEq, Serialize)] +pub struct BlockchainConfig { + /// The map from tokens to their minimum fees + pub fee_map: FeeMap, + /// The block version that this enclave will be applying rules for and + /// publishing + pub block_version: BlockVersion, +} + +impl Default for BlockchainConfig { + fn default() -> Self { + Self { + fee_map: FeeMap::default(), + block_version: BlockVersion::MAX, + } + } +} + +/// A blockchain config, together with a cache of its digest value. +/// This can be used to form responder id's in a fast and consistent way +/// based on the config. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct BlockchainConfigWithDigest { + config: BlockchainConfig, + cached_digest: String, +} + +impl From for BlockchainConfigWithDigest { + fn from(config: BlockchainConfig) -> Self { + let digest = config.digest32::(b"mc-blockchain-config"); + let cached_digest = hex::encode(digest); + Self { + config, + cached_digest, + } + } +} + +impl AsRef for BlockchainConfigWithDigest { + fn as_ref(&self) -> &BlockchainConfig { + &self.config + } +} + +impl Default for BlockchainConfigWithDigest { + fn default() -> Self { + Self::from(BlockchainConfig::default()) + } +} + +impl BlockchainConfigWithDigest { + /// Append the config digest to an existing responder id, producing a + /// responder id that is unique to the current fee configuration. + pub fn responder_id(&self, responder_id: &ResponderId) -> ResponderId { + ResponderId(format!("{}-{}", responder_id.0, self.cached_digest)) + } + + /// Get the config (non mutably) + pub fn get_config(&self) -> &BlockchainConfig { + &self.config + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::string::ToString; + use mc_transaction_core::{tokens::Mob, Token, TokenId}; + + /// Different block_version/fee maps/responder ids should result in + /// different responder ids over all + #[test] + fn different_fee_maps_result_in_different_responder_ids() { + let config1: BlockchainConfigWithDigest = BlockchainConfig { + fee_map: FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(2), 2000)]).unwrap(), + block_version: BlockVersion::ONE, + } + .into(); + let config2: BlockchainConfigWithDigest = BlockchainConfig { + fee_map: FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(2), 300)]).unwrap(), + block_version: BlockVersion::ONE, + } + .into(); + let config3: BlockchainConfigWithDigest = BlockchainConfig { + fee_map: FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(30), 300)]).unwrap(), + block_version: BlockVersion::ONE, + } + .into(); + + let responder_id1 = ResponderId("1.2.3.4:5".to_string()); + let responder_id2 = ResponderId("3.1.3.3:7".to_string()); + + assert_ne!( + config1.responder_id(&responder_id1), + config2.responder_id(&responder_id1) + ); + + assert_ne!( + config1.responder_id(&responder_id1), + config3.responder_id(&responder_id1) + ); + + assert_ne!( + config2.responder_id(&responder_id1), + config3.responder_id(&responder_id1) + ); + + assert_ne!( + config1.responder_id(&responder_id1), + config1.responder_id(&responder_id2) + ); + + assert_ne!( + config2.responder_id(&responder_id1), + config2.responder_id(&responder_id2) + ); + + let config4: BlockchainConfigWithDigest = BlockchainConfig { + fee_map: FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(30), 300)]).unwrap(), + block_version: BlockVersion::TWO, + } + .into(); + + assert_ne!( + config3.responder_id(&responder_id1), + config4.responder_id(&responder_id1) + ); + + assert_ne!( + config3.responder_id(&responder_id2), + config4.responder_id(&responder_id2) + ); + } +} diff --git a/consensus/enclave/api/src/error.rs b/consensus/enclave/api/src/error.rs index 441e6e1ebd..55aff5005d 100644 --- a/consensus/enclave/api/src/error.rs +++ b/consensus/enclave/api/src/error.rs @@ -55,6 +55,12 @@ pub enum Error { /// Invalid fee configuration: {0} FeeMap(FeeMapError), + + /// Enclave not initialized + NotInitialized, + + /// Block Version Error: {0} + BlockVersion(String), } impl From for Error { diff --git a/consensus/enclave/api/src/fee_map.rs b/consensus/enclave/api/src/fee_map.rs index c47731d835..fab74ec025 100644 --- a/consensus/enclave/api/src/fee_map.rs +++ b/consensus/enclave/api/src/fee_map.rs @@ -2,34 +2,28 @@ //! A helper object for maintaining a map of token id -> minimum fee. -use alloc::{collections::BTreeMap, format, string::String}; +use alloc::collections::BTreeMap; use core::{convert::TryFrom, iter::FromIterator}; use displaydoc::Display; -use mc_common::ResponderId; -use mc_crypto_digestible::{DigestTranscript, Digestible, MerlinTranscript}; +use mc_crypto_digestible::Digestible; use mc_transaction_core::{tokens::Mob, Token, TokenId}; use serde::{Deserialize, Serialize}; /// A thread-safe object that contains a map of fee value by token id. -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[derive(Clone, Debug, Deserialize, Digestible, Eq, Hash, PartialEq, Serialize)] pub struct FeeMap { /// The actual map of token_id to fee. /// Since we hash this map, it is important to use a BTreeMap as it /// guarantees iterating over the map is in sorted and predictable /// order. map: BTreeMap, - - /// Cached digest value, formatted as a string. - /// (Suitable for appending to responder id) - cached_digest: String, } impl Default for FeeMap { fn default() -> Self { let map = Self::default_map(); - let cached_digest = calc_digest_for_map(&map); - Self { map, cached_digest } + Self { map } } } @@ -39,9 +33,13 @@ impl TryFrom> for FeeMap { fn try_from(map: BTreeMap) -> Result { Self::is_valid_map(&map)?; - let cached_digest = calc_digest_for_map(&map); + Ok(Self { map }) + } +} - Ok(Self { map, cached_digest }) +impl AsRef> for FeeMap { + fn as_ref(&self) -> &BTreeMap { + &self.map } } @@ -52,12 +50,6 @@ impl FeeMap { Self::try_from(map) } - /// Append the fee map digest to an existing responder id, producing a - /// responder id that is unique to the current fee configuration. - pub fn responder_id(&self, responder_id: &ResponderId) -> ResponderId { - ResponderId(format!("{}-{}", responder_id.0, self.cached_digest)) - } - /// Get the fee for a given token id, or None if no fee is set for that /// token. pub fn get_fee_for_token(&self, token_id: &TokenId) -> Option { @@ -78,9 +70,6 @@ impl FeeMap { self.map = Self::default_map(); } - // Digest must be updated when the map is updated. - self.cached_digest = calc_digest_for_map(&self.map); - Ok(()) } @@ -113,19 +102,6 @@ impl FeeMap { } } -fn calc_digest_for_map(map: &BTreeMap) -> String { - let mut transcript = MerlinTranscript::new(b"fee_map"); - transcript.append_seq_header(b"fee_map", map.len() * 2); - for (token_id, fee) in map { - token_id.append_to_transcript(b"token_id", &mut transcript); - fee.append_to_transcript(b"fee", &mut transcript); - } - - let mut result = [0u8; 32]; - transcript.extract_digest(&mut result); - hex::encode(result) -} - /// Fee Map error type. #[derive(Clone, Debug, Deserialize, Display, PartialEq, PartialOrd, Serialize)] pub enum Error { @@ -139,38 +115,19 @@ pub enum Error { #[cfg(test)] mod test { use super::*; - use alloc::{string::ToString, vec}; + use alloc::vec; - /// Different fee maps/responder ids should result in different responder - /// ids. + /// Valid fee maps ids should be accepted #[test] - fn different_fee_maps_result_in_different_responder_ids() { + fn valid_fee_maps_accepted() { let fee_map1 = FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(2), 2000)]).unwrap(); - let fee_map2 = FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(2), 300)]).unwrap(); - let fee_map3 = FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(30), 300)]).unwrap(); - - let responder_id1 = ResponderId("1.2.3.4:5".to_string()); - let responder_id2 = ResponderId("3.1.3.3:7".to_string()); - - assert_ne!( - fee_map1.responder_id(&responder_id1), - fee_map2.responder_id(&responder_id1) - ); - - assert_ne!( - fee_map1.responder_id(&responder_id1), - fee_map3.responder_id(&responder_id1) - ); + assert!(fee_map1.get_fee_for_token(&Mob::ID).is_some()); - assert_ne!( - fee_map2.responder_id(&responder_id1), - fee_map3.responder_id(&responder_id1) - ); + let fee_map2 = FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(2), 300)]).unwrap(); + assert!(fee_map2.get_fee_for_token(&Mob::ID).is_some()); - assert_ne!( - fee_map1.responder_id(&responder_id1), - fee_map1.responder_id(&responder_id2) - ); + let fee_map3 = FeeMap::try_from_iter([(Mob::ID, 100), (TokenId::from(30), 300)]).unwrap(); + assert!(fee_map3.get_fee_for_token(&Mob::ID).is_some()); } /// Invalid fee maps are rejected. diff --git a/consensus/enclave/api/src/lib.rs b/consensus/enclave/api/src/lib.rs index 229ffb0766..1a96539523 100644 --- a/consensus/enclave/api/src/lib.rs +++ b/consensus/enclave/api/src/lib.rs @@ -3,14 +3,17 @@ //! APIs for MobileCoin Consensus Node Enclaves #![no_std] +#![deny(missing_docs)] extern crate alloc; +mod config; mod error; mod fee_map; mod messages; pub use crate::{ + config::{BlockchainConfig, BlockchainConfigWithDigest}, error::Error, fee_map::{Error as FeeMapError, FeeMap}, messages::EnclaveCall, @@ -87,26 +90,32 @@ impl WellFormedTxContext { } } + /// Get the tx_hash pub fn tx_hash(&self) -> &TxHash { &self.tx_hash } + /// Get the fee pub fn fee(&self) -> u64 { self.fee } + /// Get the tombstone block pub fn tombstone_block(&self) -> u64 { self.tombstone_block } + /// Get the key images pub fn key_images(&self) -> &Vec { &self.key_images } + /// Get the highest indices pub fn highest_indices(&self) -> &Vec { &self.highest_indices } + /// Get the output public keys pub fn output_public_keys(&self) -> &Vec { &self.output_public_keys } @@ -186,13 +195,20 @@ mod well_formed_tx_context_tests { /// place in `tx_is_well_formed`. #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] pub struct TxContext { + /// The Tx encrypted for the local enclave pub locally_encrypted_tx: LocallyEncryptedTx, + /// The hash of the (unencrypted) Tx pub tx_hash: TxHash, + /// The highest indices in the Tx merkle proof pub highest_indices: Vec, + /// The key images appearing in the Tx pub key_images: Vec, + /// The output public keys appearing in the Tx pub output_public_keys: Vec, } +/// A type alias for the SGX sealed version of the block signing key of the +/// local enclave pub type SealedBlockSigningKey = Vec; /// PublicAddress is not serializable with serde currently, and rather than @@ -200,7 +216,9 @@ pub type SealedBlockSigningKey = Vec; /// RistrettoPublic. #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] pub struct FeePublicKey { + /// The spend public key of the fee address pub spend_public_key: RistrettoPublic, + /// The view public key of the fee address pub view_public_key: RistrettoPublic, } @@ -214,7 +232,7 @@ pub trait ConsensusEnclave: ReportableEnclave { self_peer_id: &ResponderId, self_client_id: &ResponderId, sealed_key: &Option, - fee_map: &FeeMap, + blockchain_config: BlockchainConfig, ) -> Result<(SealedBlockSigningKey, Vec)>; /// Retrieve the current minimum fee for a given token id. diff --git a/consensus/enclave/api/src/messages.rs b/consensus/enclave/api/src/messages.rs index 04666eef6c..5e123b3ae9 100644 --- a/consensus/enclave/api/src/messages.rs +++ b/consensus/enclave/api/src/messages.rs @@ -3,7 +3,7 @@ //! The message types used by the consensus_enclave_api. use crate::{ - FeeMap, LocallyEncryptedTx, ResponderId, SealedBlockSigningKey, WellFormedEncryptedTx, + BlockchainConfig, LocallyEncryptedTx, ResponderId, SealedBlockSigningKey, WellFormedEncryptedTx, }; use alloc::vec::Vec; use mc_attest_core::{Quote, Report, TargetInfo, VerificationReport}; @@ -26,7 +26,7 @@ pub enum EnclaveCall { ResponderId, ResponderId, Option, - FeeMap, + BlockchainConfig, ), /// The [PeerableEnclave::peer_init()] method. diff --git a/consensus/enclave/impl/Cargo.toml b/consensus/enclave/impl/Cargo.toml index 8c18271e4b..14fa438151 100644 --- a/consensus/enclave/impl/Cargo.toml +++ b/consensus/enclave/impl/Cargo.toml @@ -43,6 +43,7 @@ mc-util-from-random = { path = "../../../util/from-random" } mc-util-serial = { path = "../../../util/serial" } mbedtls = { version = "0.8.1", default-features = false, features = ["no_std_deps"] } +once_cell = { version = "1.9", default-features = false, features = ["alloc", "race"] } prost = { version = "0.9", default-features = false, features = ["prost-derive"] } rand_core = { version = "0.6", default-features = false } diff --git a/consensus/enclave/impl/src/lib.rs b/consensus/enclave/impl/src/lib.rs index 85bf494ace..5c5a307285 100644 --- a/consensus/enclave/impl/src/lib.rs +++ b/consensus/enclave/impl/src/lib.rs @@ -17,7 +17,7 @@ mod identity; // Include autogenerated constants.rs include!(concat!(env!("OUT_DIR"), "/constants.rs")); -use alloc::{collections::BTreeSet, format, string::String, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeSet, format, string::String, vec::Vec}; use core::convert::TryFrom; use identity::Ed25519Identity; use mc_account_keys::PublicAddress; @@ -34,8 +34,9 @@ use mc_common::{ ResponderId, }; use mc_consensus_enclave_api::{ - ConsensusEnclave, Error, FeeMap, FeeMapError, FeePublicKey, LocallyEncryptedTx, Result, - SealedBlockSigningKey, TxContext, WellFormedEncryptedTx, WellFormedTxContext, + BlockchainConfig, BlockchainConfigWithDigest, ConsensusEnclave, Error, FeeMapError, + FeePublicKey, LocallyEncryptedTx, Result, SealedBlockSigningKey, TxContext, + WellFormedEncryptedTx, WellFormedTxContext, }; use mc_crypto_ake_enclave::AkeEnclaveState; use mc_crypto_digestible::{DigestTranscript, Digestible, MerlinTranscript}; @@ -49,8 +50,11 @@ use mc_transaction_core::{ ring_signature::{KeyImage, Scalar}, tx::{Tx, TxOut, TxOutMembershipElement, TxOutMembershipProof}, validation::TransactionValidationError, - Block, BlockContents, BlockSignature, TokenId, BLOCK_VERSION, + Block, BlockContents, BlockSignature, TokenId, }; +// Race here refers to, this is thread-safe, first-one-wins behavior, without +// blocking +use once_cell::race::OnceBox; use prost::Message; use rand_core::{CryptoRng, RngCore}; @@ -108,8 +112,12 @@ pub struct SgxConsensusEnclave { /// Logger. logger: Logger, - /// Fee map (for determining the minimum fee for a given token id). - fee_map: Mutex, + /// Blockchain Config + /// + /// This is configuration data that affects whether or not a transaction + /// is valid. To ensure that it is uniform across the network, it's hash + /// gets appended to responder id. + blockchain_config: OnceBox, } impl SgxConsensusEnclave { @@ -121,7 +129,7 @@ impl SgxConsensusEnclave { &mut McRng::default(), )), logger, - fee_map: Mutex::new(FeeMap::default()), + blockchain_config: Default::default(), } } @@ -172,10 +180,15 @@ impl ConsensusEnclave for SgxConsensusEnclave { peer_self_id: &ResponderId, client_self_id: &ResponderId, sealed_key: &Option, - fee_map: &FeeMap, + blockchain_config: BlockchainConfig, ) -> Result<(SealedBlockSigningKey, Vec)> { - // Inject the fee into the peer ResponderId. - let peer_self_id = fee_map.responder_id(peer_self_id); + let blockchain_config = BlockchainConfigWithDigest::from(blockchain_config); + // Inject the fee map and block version into the peer ResponderId. + let peer_self_id = blockchain_config.responder_id(peer_self_id); + + self.blockchain_config + .set(Box::new(blockchain_config)) + .expect("enclave already initialized"); // Init AKE. self.ake.init(peer_self_id, client_self_id.clone())?; @@ -197,8 +210,6 @@ impl ConsensusEnclave for SgxConsensusEnclave { let key = (*lock).private_key(); let sealed = IntelSealed::seal_raw(key.as_ref(), &[]).unwrap(); - *self.fee_map.lock().unwrap() = fee_map.clone(); - Ok(( sealed.as_ref().to_vec(), TARGET_FEATURES @@ -209,7 +220,13 @@ impl ConsensusEnclave for SgxConsensusEnclave { } fn get_minimum_fee(&self, token_id: &TokenId) -> Result> { - Ok(self.fee_map.lock()?.get_fee_for_token(token_id)) + Ok(self + .blockchain_config + .get() + .ok_or(Error::NotInitialized)? + .get_config() + .fee_map + .get_fee_for_token(token_id)) } fn get_identity(&self) -> Result { @@ -247,8 +264,12 @@ impl ConsensusEnclave for SgxConsensusEnclave { } fn peer_init(&self, peer_id: &ResponderId) -> Result { - // Inject the if fee map hash passing off to the AKE - let peer_id = self.fee_map.lock()?.responder_id(peer_id); + // Inject the blockchain config hash, passing off to the AKE + let peer_id = self + .blockchain_config + .get() + .ok_or(Error::NotInitialized)? + .responder_id(peer_id); Ok(self.ake.peer_init(&peer_id)?) } @@ -262,8 +283,12 @@ impl ConsensusEnclave for SgxConsensusEnclave { peer_id: &ResponderId, msg: PeerAuthResponse, ) -> Result<(PeerSession, VerificationReport)> { - // Inject the if fee map hash passing off to the AKE - let peer_id = self.fee_map.lock()?.responder_id(peer_id); + // Inject the blockchain config hash before passing off to the AKE + let peer_id = self + .blockchain_config + .get() + .ok_or(Error::NotInitialized)? + .responder_id(peer_id); Ok(self.ake.peer_connect(&peer_id, msg)?) } @@ -341,6 +366,12 @@ impl ConsensusEnclave for SgxConsensusEnclave { block_index: u64, proofs: Vec, ) -> Result<(WellFormedEncryptedTx, WellFormedTxContext)> { + let config = self + .blockchain_config + .get() + .ok_or(Error::NotInitialized)? + .get_config(); + // Enforce that all membership proofs provided by the untrusted system for // transaction validation came from the same ledger state. This can be // checked by requiring all proofs to have the same root hash. @@ -363,9 +394,8 @@ impl ConsensusEnclave for SgxConsensusEnclave { // Validate. let mut csprng = McRng::default(); - let minimum_fee = self + let minimum_fee = config .fee_map - .lock()? .get_fee_for_token(&TokenId::MOB) // This should actually never happen since the map enforces the existence of // MOB. @@ -373,6 +403,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { mc_transaction_core::validation::validate( &tx, block_index, + config.block_version, &proofs, minimum_fee, &mut csprng, @@ -424,6 +455,16 @@ impl ConsensusEnclave for SgxConsensusEnclave { encrypted_txs_with_proofs: &[(WellFormedEncryptedTx, Vec)], root_element: &TxOutMembershipElement, ) -> Result<(Block, BlockContents, BlockSignature)> { + let config = self + .blockchain_config + .get() + .ok_or(Error::NotInitialized)? + .get_config(); + + if parent_block.version > *config.block_version { + return Err(Error::BlockVersion(format!("Block version cannot decrease: parent_block.version = {}, config.block_version = {}", parent_block.version, config.block_version))); + } + // This implicitly converts Vec),_>> into // Result)>, _>, and terminates the // iteration when the first Error is encountered. @@ -441,9 +482,8 @@ impl ConsensusEnclave for SgxConsensusEnclave { // ledger that were used to validate the transactions. let mut root_elements = Vec::new(); let mut rng = McRng::default(); - let minimum_fee = self + let minimum_fee = config .fee_map - .lock()? .get_fee_for_token(&TokenId::MOB) // This should actually never happen since the map enforces the existence of // MOB. @@ -453,6 +493,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { mc_transaction_core::validation::validate( tx, parent_block.index + 1, + config.block_version, proofs, minimum_fee, &mut rng, @@ -564,7 +605,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { // Form the block. let block = Block::new_with_parent( - BLOCK_VERSION, + config.block_version, parent_block, &root_elements[0], &block_contents, @@ -629,6 +670,7 @@ mod tests { onetime_keys::{create_shared_secret, view_key_matches_output}, tx::TxOutMembershipHash, validation::TransactionValidationError, + BlockVersion, }; use mc_transaction_core_test_utils::{ create_ledger, create_transaction, initialize_ledger, AccountKey, ViewKey, @@ -646,637 +688,859 @@ mod tests { #[test_with_logger] fn test_tx_is_well_formed_works(logger: Logger) { - let enclave = SgxConsensusEnclave::new(logger); let mut rng = Hc128Rng::from_seed([1u8; 32]); - // Create a valid test transaction. - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); - - let mut ledger = create_ledger(); - let n_blocks = 3; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); - - // Choose a TxOut to spend. Only the TxOut in the last block is unspent. - let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap(); - let tx_out = block_contents.outputs[0].clone(); - - let tx = create_transaction( - &mut ledger, - &tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, - ); - - // Create a LocallyEncryptedTx that can be fed into `tx_is_well_formed`. - let tx_bytes = mc_util_serial::encode(&tx); - let locally_encrypted_tx = LocallyEncryptedTx( + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version, + ..Default::default() + }; enclave - .locally_encrypted_tx_cipher - .lock() - .unwrap() - .encrypt_bytes(&mut rng, tx_bytes.clone()), - ); + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); - // Call `tx_is_well_formed`. - let highest_indices = tx.get_membership_proof_highest_indices(); - let proofs = ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proofs"); - let block_index = ledger.num_blocks().unwrap(); - let (well_formed_encrypted_tx, well_formed_tx_context) = enclave - .tx_is_well_formed(locally_encrypted_tx.clone(), block_index, proofs) - .unwrap(); - - // Check that the context we got back is correct. - assert_eq!(well_formed_tx_context.tx_hash(), &tx.tx_hash()); - assert_eq!(well_formed_tx_context.fee(), tx.prefix.fee); - assert_eq!( - well_formed_tx_context.tombstone_block(), - tx.prefix.tombstone_block - ); - assert_eq!(*well_formed_tx_context.key_images(), tx.key_images()); - - // All three tx representations should be different. - assert_ne!(tx_bytes, locally_encrypted_tx.0); - assert_ne!(tx_bytes, well_formed_encrypted_tx.0); - assert_ne!(locally_encrypted_tx.0, well_formed_encrypted_tx.0); - - // Check that we can go back from the encrypted tx to the original tx. - let well_formed_tx = enclave - .decrypt_well_formed_tx(&well_formed_encrypted_tx) - .unwrap(); - assert_eq!(tx, well_formed_tx.tx); + // Create a valid test transaction. + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); + + let mut ledger = create_ledger(); + let n_blocks = 3; + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); + + // Choose a TxOut to spend. Only the TxOut in the last block is unspent. + let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap(); + let tx_out = block_contents.outputs[0].clone(); + + let tx = create_transaction( + block_version, + &mut ledger, + &tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); + + // Create a LocallyEncryptedTx that can be fed into `tx_is_well_formed`. + let tx_bytes = mc_util_serial::encode(&tx); + let locally_encrypted_tx = LocallyEncryptedTx( + enclave + .locally_encrypted_tx_cipher + .lock() + .unwrap() + .encrypt_bytes(&mut rng, tx_bytes.clone()), + ); + + // Call `tx_is_well_formed`. + let highest_indices = tx.get_membership_proof_highest_indices(); + let proofs = ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proofs"); + let block_index = ledger.num_blocks().unwrap(); + let (well_formed_encrypted_tx, well_formed_tx_context) = enclave + .tx_is_well_formed(locally_encrypted_tx.clone(), block_index, proofs) + .unwrap(); + + // Check that the context we got back is correct. + assert_eq!(well_formed_tx_context.tx_hash(), &tx.tx_hash()); + assert_eq!(well_formed_tx_context.fee(), tx.prefix.fee); + assert_eq!( + well_formed_tx_context.tombstone_block(), + tx.prefix.tombstone_block + ); + assert_eq!(*well_formed_tx_context.key_images(), tx.key_images()); + + // All three tx representations should be different. + assert_ne!(tx_bytes, locally_encrypted_tx.0); + assert_ne!(tx_bytes, well_formed_encrypted_tx.0); + assert_ne!(locally_encrypted_tx.0, well_formed_encrypted_tx.0); + + // Check that we can go back from the encrypted tx to the original tx. + let well_formed_tx = enclave + .decrypt_well_formed_tx(&well_formed_encrypted_tx) + .unwrap(); + assert_eq!(tx, well_formed_tx.tx); + } } #[test_with_logger] fn test_tx_is_well_formed_works_errors_on_bad_inputs(logger: Logger) { - let enclave = SgxConsensusEnclave::new(logger); let mut rng = Hc128Rng::from_seed([77u8; 32]); - // Create a valid test transaction. - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); - - let mut ledger = create_ledger(); - let n_blocks = 3; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); - - // Choose a TxOut to spend. Only the TxOut in the last block is unspent. - let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap(); - let tx_out = block_contents.outputs[0].clone(); - - let tx = create_transaction( - &mut ledger, - &tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, - ); - - // Create a LocallyEncryptedTx that can be fed into `tx_is_well_formed`. - let tx_bytes = mc_util_serial::encode(&tx); - let locally_encrypted_tx = LocallyEncryptedTx( + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version, + ..Default::default() + }; enclave - .locally_encrypted_tx_cipher - .lock() - .unwrap() - .encrypt_bytes(&mut rng, tx_bytes.clone()), - ); + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); - // Call `tx_is_well_formed` with a block index that puts us past the tombstone - // block. - let highest_indices = tx.get_membership_proof_highest_indices(); - let proofs = ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proofs"); - let block_index = ledger.num_blocks().unwrap(); - - assert_eq!( - enclave.tx_is_well_formed( - locally_encrypted_tx.clone(), - block_index + mc_transaction_core::constants::MAX_TOMBSTONE_BLOCKS, - proofs.clone(), - ), - Err(Error::MalformedTx( - TransactionValidationError::TombstoneBlockExceeded - )) - ); + // Create a valid test transaction. + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); - // Call `tx_is_well_formed` with a wrong proof. - let mut bad_proofs = proofs.clone(); - bad_proofs[0].elements[0].hash = TxOutMembershipHash::from([123; 32]); + let mut ledger = create_ledger(); + let n_blocks = 3; + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); - assert_eq!( - enclave.tx_is_well_formed(locally_encrypted_tx.clone(), block_index, bad_proofs,), - Err(Error::InvalidLocalMembershipProof) - ); + // Choose a TxOut to spend. Only the TxOut in the last block is unspent. + let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap(); + let tx_out = block_contents.outputs[0].clone(); + + let tx = create_transaction( + block_version, + &mut ledger, + &tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); - // Corrupt the encrypted data. - let mut corrputed_locally_encrypted_tx = locally_encrypted_tx.clone(); - corrputed_locally_encrypted_tx.0[0] = !corrputed_locally_encrypted_tx.0[0]; + // Create a LocallyEncryptedTx that can be fed into `tx_is_well_formed`. + let tx_bytes = mc_util_serial::encode(&tx); + let locally_encrypted_tx = LocallyEncryptedTx( + enclave + .locally_encrypted_tx_cipher + .lock() + .unwrap() + .encrypt_bytes(&mut rng, tx_bytes.clone()), + ); - assert_eq!( - enclave.tx_is_well_formed(corrputed_locally_encrypted_tx, block_index, proofs), - Err(Error::CacheCipher( - mc_crypto_message_cipher::CipherError::MacFailure - )) - ); + // Call `tx_is_well_formed` with a block index that puts us past the tombstone + // block. + let highest_indices = tx.get_membership_proof_highest_indices(); + let proofs = ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proofs"); + let block_index = ledger.num_blocks().unwrap(); + + assert_eq!( + enclave.tx_is_well_formed( + locally_encrypted_tx.clone(), + block_index + mc_transaction_core::constants::MAX_TOMBSTONE_BLOCKS, + proofs.clone(), + ), + Err(Error::MalformedTx( + TransactionValidationError::TombstoneBlockExceeded + )) + ); + + // Call `tx_is_well_formed` with a wrong proof. + let mut bad_proofs = proofs.clone(); + bad_proofs[0].elements[0].hash = TxOutMembershipHash::from([123; 32]); + + assert_eq!( + enclave.tx_is_well_formed(locally_encrypted_tx.clone(), block_index, bad_proofs,), + Err(Error::InvalidLocalMembershipProof) + ); + + // Corrupt the encrypted data. + let mut corrputed_locally_encrypted_tx = locally_encrypted_tx.clone(); + corrputed_locally_encrypted_tx.0[0] = !corrputed_locally_encrypted_tx.0[0]; + + assert_eq!( + enclave.tx_is_well_formed(corrputed_locally_encrypted_tx, block_index, proofs), + Err(Error::CacheCipher( + mc_crypto_message_cipher::CipherError::MacFailure + )) + ); + } } #[test_with_logger] // tx_is_well_formed rejects inconsistent root elements. fn test_tx_is_well_form_rejects_inconsistent_root_elements(logger: Logger) { - let enclave = SgxConsensusEnclave::new(logger); - // Construct TxOutMembershipProofs. - let mut ledger = create_ledger(); - let n_blocks = 16; let mut rng = Hc128Rng::from_seed([77u8; 32]); - let account_key = AccountKey::random(&mut rng); - initialize_ledger(&mut ledger, n_blocks, &account_key, &mut rng); - - let n_proofs = 10; - let indexes: Vec = (0..n_proofs as u64).into_iter().collect(); - let mut membership_proofs = ledger.get_tx_out_proof_of_memberships(&indexes).unwrap(); - // Modify one of the proofs to have a different root hash. - let inconsistent_proof = &mut membership_proofs[7]; - // TODO: check this - let root_element = inconsistent_proof.elements.last_mut().unwrap(); - root_element.hash = TxOutMembershipHash::from([33u8; 32]); - - // The membership proofs supplied by the server are checked before this is - // decrypted and validated, so it can just be constructed from an empty - // vector of bytes. - let locally_encrypted_tx = LocallyEncryptedTx(Vec::new()); - let block_index = 77; - let result = - enclave.tx_is_well_formed(locally_encrypted_tx, block_index, membership_proofs); - let expected = Err(Error::InvalidLocalMembershipProof); - assert_eq!(result, expected); + + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version, + ..Default::default() + }; + enclave + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); + + // Construct TxOutMembershipProofs. + let mut ledger = create_ledger(); + let n_blocks = 16; + let account_key = AccountKey::random(&mut rng); + initialize_ledger(block_version, &mut ledger, n_blocks, &account_key, &mut rng); + + let n_proofs = 10; + let indexes: Vec = (0..n_proofs as u64).into_iter().collect(); + let mut membership_proofs = ledger.get_tx_out_proof_of_memberships(&indexes).unwrap(); + // Modify one of the proofs to have a different root hash. + let inconsistent_proof = &mut membership_proofs[7]; + // TODO: check this + let root_element = inconsistent_proof.elements.last_mut().unwrap(); + root_element.hash = TxOutMembershipHash::from([33u8; 32]); + + // The membership proofs supplied by the server are checked before this is + // decrypted and validated, so it can just be constructed from an empty + // vector of bytes. + let locally_encrypted_tx = LocallyEncryptedTx(Vec::new()); + let block_index = 77; + let result = + enclave.tx_is_well_formed(locally_encrypted_tx, block_index, membership_proofs); + let expected = Err(Error::InvalidLocalMembershipProof); + assert_eq!(result, expected); + } } #[test_with_logger] fn test_form_block_works(logger: Logger) { - let enclave = SgxConsensusEnclave::new(logger); let mut rng = Hc128Rng::from_seed([77u8; 32]); - // Create a valid test transaction. - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version, + ..Default::default() + }; + enclave + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); + + // Create a valid test transaction. + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); + + let mut ledger = create_ledger(); + let n_blocks = 1; + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); + + // Spend outputs from the origin block. + let origin_block_contents = ledger.get_block_contents(0).unwrap(); + + let input_transactions: Vec = (0..3) + .map(|i| { + let tx_out = origin_block_contents.outputs[i].clone(); + + create_transaction( + block_version, + &mut ledger, + &tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ) + }) + .collect(); - let mut ledger = create_ledger(); - let n_blocks = 1; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + let total_fee: u64 = input_transactions.iter().map(|tx| tx.prefix.fee).sum(); - // Spend outputs from the origin block. - let origin_block_contents = ledger.get_block_contents(0).unwrap(); + // Create WellFormedEncryptedTxs + proofs + let well_formed_encrypted_txs_with_proofs: Vec<_> = input_transactions + .iter() + .map(|tx| { + let well_formed_tx = WellFormedTx::from(tx.clone()); + let encrypted_tx = enclave + .encrypt_well_formed_tx(&well_formed_tx, &mut rng) + .unwrap(); + + let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); + let membership_proofs = ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proof"); + (encrypted_tx, membership_proofs) + }) + .collect(); - let input_transactions: Vec = (0..3) - .map(|i| { - let tx_out = origin_block_contents.outputs[i].clone(); + // Form block + let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); + let root_element = ledger.get_root_tx_out_membership_element().unwrap(); - create_transaction( - &mut ledger, - &tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, + let (block, block_contents, signature) = enclave + .form_block( + &parent_block, + &well_formed_encrypted_txs_with_proofs, + &root_element, ) - }) - .collect(); - - let total_fee: u64 = input_transactions.iter().map(|tx| tx.prefix.fee).sum(); - - // Create WellFormedEncryptedTxs + proofs - let well_formed_encrypted_txs_with_proofs: Vec<_> = input_transactions - .iter() - .map(|tx| { - let well_formed_tx = WellFormedTx::from(tx.clone()); - let encrypted_tx = enclave - .encrypt_well_formed_tx(&well_formed_tx, &mut rng) - .unwrap(); - - let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); - let membership_proofs = ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proof"); - (encrypted_tx, membership_proofs) - }) - .collect(); + .unwrap(); + + // Verify signature. + { + assert_eq!( + signature.signer(), + &enclave + .ake + .get_identity() + .signing_keypair + .lock() + .unwrap() + .public_key() + ); + + assert!(signature.verify(&block).is_ok()); + } - // Form block - let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); - let root_element = ledger.get_root_tx_out_membership_element().unwrap(); + // `block_contents` should include the aggregate fee. - let (block, block_contents, signature) = enclave - .form_block( - &parent_block, - &well_formed_encrypted_txs_with_proofs, - &root_element, - ) - .unwrap(); + let num_outputs: usize = input_transactions + .iter() + .map(|tx| tx.prefix.outputs.len()) + .sum(); + assert_eq!(num_outputs + 1, block_contents.outputs.len()); + + // One of the outputs should be the aggregate fee. + let view_secret_key = RistrettoPrivate::try_from(&FEE_VIEW_PRIVATE_KEY).unwrap(); + + let fee_view_key = { + let fee_recipient_pubkeys = enclave.get_fee_recipient().unwrap(); + let public_address = PublicAddress::new( + &fee_recipient_pubkeys.spend_public_key, + &RistrettoPublic::from(&view_secret_key), + ); + ViewKey::new(view_secret_key, *public_address.spend_public_key()) + }; + + let fee_output = block_contents + .outputs + .iter() + .find(|output| { + let output_public_key = RistrettoPublic::try_from(&output.public_key).unwrap(); + let output_target_key = RistrettoPublic::try_from(&output.target_key).unwrap(); + view_key_matches_output(&fee_view_key, &output_target_key, &output_public_key) + }) + .unwrap(); - // Verify signature. - { - assert_eq!( - signature.signer(), - &enclave - .ake - .get_identity() - .signing_keypair - .lock() - .unwrap() - .public_key() - ); + let fee_output_public_key = RistrettoPublic::try_from(&fee_output.public_key).unwrap(); - assert!(signature.verify(&block).is_ok()); + // The value of the aggregate fee should equal the total value of fees in the + // input transaction. + let shared_secret = create_shared_secret(&fee_output_public_key, &view_secret_key); + let (value, _blinding) = fee_output.amount.get_value(&shared_secret).unwrap(); + assert_eq!(value, total_fee); } - - // `block_contents` should include the aggregate fee. - - let num_outputs: usize = input_transactions - .iter() - .map(|tx| tx.prefix.outputs.len()) - .sum(); - assert_eq!(num_outputs + 1, block_contents.outputs.len()); - - // One of the outputs should be the aggregate fee. - let view_secret_key = RistrettoPrivate::try_from(&FEE_VIEW_PRIVATE_KEY).unwrap(); - - let fee_view_key = { - let fee_recipient_pubkeys = enclave.get_fee_recipient().unwrap(); - let public_address = PublicAddress::new( - &fee_recipient_pubkeys.spend_public_key, - &RistrettoPublic::from(&view_secret_key), - ); - ViewKey::new(view_secret_key, *public_address.spend_public_key()) - }; - - let fee_output = block_contents - .outputs - .iter() - .find(|output| { - let output_public_key = RistrettoPublic::try_from(&output.public_key).unwrap(); - let output_target_key = RistrettoPublic::try_from(&output.target_key).unwrap(); - view_key_matches_output(&fee_view_key, &output_target_key, &output_public_key) - }) - .unwrap(); - - let fee_output_public_key = RistrettoPublic::try_from(&fee_output.public_key).unwrap(); - - // The value of the aggregate fee should equal the total value of fees in the - // input transaction. - let shared_secret = create_shared_secret(&fee_output_public_key, &view_secret_key); - let (value, _blinding) = fee_output.amount.get_value(&shared_secret).unwrap(); - assert_eq!(value, total_fee); } #[test_with_logger] /// form_block should return an error if the input transactions contain a /// double-spend. fn test_form_block_prevents_duplicate_spend(logger: Logger) { - let enclave = SgxConsensusEnclave::new(logger); let mut rng = Hc128Rng::from_seed([77u8; 32]); - // Initialize a ledger. `sender` is the owner of all outputs in the initial - // ledger. - let sender = AccountKey::random(&mut rng); - let mut ledger = create_ledger(); - let n_blocks = 3; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version, + ..Default::default() + }; + enclave + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); - // Create a few transactions from `sender` to `recipient`. - let num_transactions = 5; - let recipient = AccountKey::random(&mut rng); + // Initialize a ledger. `sender` is the owner of all outputs in the initial + // ledger. + let sender = AccountKey::random(&mut rng); + let mut ledger = create_ledger(); + let n_blocks = 3; + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); - // The first block contains RING_SIZE outputs. - let block_zero_contents = ledger.get_block_contents(0).unwrap(); + // Create a few transactions from `sender` to `recipient`. + let num_transactions = 5; + let recipient = AccountKey::random(&mut rng); - let mut new_transactions = Vec::new(); - for i in 0..num_transactions { - let tx_out = &block_zero_contents.outputs[i]; + // The first block contains RING_SIZE outputs. + let block_zero_contents = ledger.get_block_contents(0).unwrap(); - let tx = create_transaction( - &mut ledger, - tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, - ); - new_transactions.push(tx); - } + let mut new_transactions = Vec::new(); + for i in 0..num_transactions { + let tx_out = &block_zero_contents.outputs[i]; - // Create another transaction that spends the zero^th output in block zero. - let double_spend = { - let tx_out = &block_zero_contents.outputs[0]; + let tx = create_transaction( + block_version, + &mut ledger, + tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); + new_transactions.push(tx); + } - create_transaction( - &mut ledger, - tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, - ) - }; - new_transactions.push(double_spend); + // Create another transaction that spends the zero^th output in block zero. + let double_spend = { + let tx_out = &block_zero_contents.outputs[0]; - // Create WellFormedEncryptedTxs + proofs - let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions - .iter() - .map(|tx| { - let well_formed_tx = WellFormedTx::from(tx.clone()); - let encrypted_tx = enclave - .encrypt_well_formed_tx(&well_formed_tx, &mut rng) - .unwrap(); - - let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); - let membership_proofs = ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proof"); - (encrypted_tx, membership_proofs) - }) - .collect(); + create_transaction( + block_version, + &mut ledger, + tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ) + }; + new_transactions.push(double_spend); - // Form block - let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); - let root_element = ledger.get_root_tx_out_membership_element().unwrap(); + // Create WellFormedEncryptedTxs + proofs + let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions + .iter() + .map(|tx| { + let well_formed_tx = WellFormedTx::from(tx.clone()); + let encrypted_tx = enclave + .encrypt_well_formed_tx(&well_formed_tx, &mut rng) + .unwrap(); + + let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); + let membership_proofs = ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proof"); + (encrypted_tx, membership_proofs) + }) + .collect(); - let form_block_result = enclave.form_block( - &parent_block, - &well_formed_encrypted_txs_with_proofs, - &root_element, - ); - let expected_duplicate_key_image = new_transactions[0].key_images()[0]; + // Form block + let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); + let root_element = ledger.get_root_tx_out_membership_element().unwrap(); - // Check - let expected = Err(Error::FormBlock(format!( - "Duplicate key image: {:?}", - expected_duplicate_key_image - ))); + let form_block_result = enclave.form_block( + &parent_block, + &well_formed_encrypted_txs_with_proofs, + &root_element, + ); + let expected_duplicate_key_image = new_transactions[0].key_images()[0]; - assert_eq!(form_block_result, expected); + // Check + let expected = Err(Error::FormBlock(format!( + "Duplicate key image: {:?}", + expected_duplicate_key_image + ))); + + assert_eq!(form_block_result, expected); + } } #[test_with_logger] /// form_block should return an error if the input transactions contain a /// duplicate output public key. fn test_form_block_prevents_duplicate_output_public_key(logger: Logger) { - let enclave = SgxConsensusEnclave::new(logger); let mut rng = Hc128Rng::from_seed([77u8; 32]); - // Initialize a ledger. `sender` is the owner of all outputs in the initial - // ledger. - let sender = AccountKey::random(&mut rng); - let mut ledger = create_ledger(); - let n_blocks = 3; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version, + ..Default::default() + }; + enclave + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); - // Create a few transactions from `sender` to `recipient`. - let num_transactions = 5; - let recipient = AccountKey::random(&mut rng); + // Initialize a ledger. `sender` is the owner of all outputs in the initial + // ledger. + let sender = AccountKey::random(&mut rng); + let mut ledger = create_ledger(); + let n_blocks = 3; + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); - // The first block contains RING_SIZE outputs. - let block_zero_contents = ledger.get_block_contents(0).unwrap(); + // Create a few transactions from `sender` to `recipient`. + let num_transactions = 5; + let recipient = AccountKey::random(&mut rng); - // Re-create the rng so that we could more easily generate a duplicate output - // public key. - let mut rng = Hc128Rng::from_seed([77u8; 32]); + // The first block contains RING_SIZE outputs. + let block_zero_contents = ledger.get_block_contents(0).unwrap(); - let mut new_transactions = Vec::new(); - for i in 0..num_transactions - 1 { - let tx_out = &block_zero_contents.outputs[i]; + // Re-create the rng so that we could more easily generate a duplicate output + // public key. + let mut rng = Hc128Rng::from_seed([77u8; 32]); - let tx = create_transaction( - &mut ledger, - tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, - ); - new_transactions.push(tx); - } + let mut new_transactions = Vec::new(); + for i in 0..num_transactions - 1 { + let tx_out = &block_zero_contents.outputs[i]; - // Re-creating the rng here would result in a duplicate output public key. - { - let mut rng = Hc128Rng::from_seed([77u8; 32]); - let tx_out = &block_zero_contents.outputs[num_transactions - 1]; + let tx = create_transaction( + block_version, + &mut ledger, + tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); + new_transactions.push(tx); + } - let tx = create_transaction( - &mut ledger, - tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, - ); - new_transactions.push(tx); + // Re-creating the rng here would result in a duplicate output public key. + { + let mut rng = Hc128Rng::from_seed([77u8; 32]); + let tx_out = &block_zero_contents.outputs[num_transactions - 1]; - assert_eq!( - new_transactions[0].prefix.outputs[0].public_key, - new_transactions[num_transactions - 1].prefix.outputs[0].public_key, - ); - } + let tx = create_transaction( + block_version, + &mut ledger, + tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); + new_transactions.push(tx); - // Create WellFormedEncryptedTxs + proofs - let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions - .iter() - .map(|tx| { - let well_formed_tx = WellFormedTx::from(tx.clone()); - let encrypted_tx = enclave - .encrypt_well_formed_tx(&well_formed_tx, &mut rng) - .unwrap(); - - let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); - let membership_proofs = ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proof"); - (encrypted_tx, membership_proofs) - }) - .collect(); + assert_eq!( + new_transactions[0].prefix.outputs[0].public_key, + new_transactions[num_transactions - 1].prefix.outputs[0].public_key, + ); + } - // Form block - let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); - let root_element = ledger.get_root_tx_out_membership_element().unwrap(); + // Create WellFormedEncryptedTxs + proofs + let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions + .iter() + .map(|tx| { + let well_formed_tx = WellFormedTx::from(tx.clone()); + let encrypted_tx = enclave + .encrypt_well_formed_tx(&well_formed_tx, &mut rng) + .unwrap(); + + let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); + let membership_proofs = ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proof"); + (encrypted_tx, membership_proofs) + }) + .collect(); - let form_block_result = enclave.form_block( - &parent_block, - &well_formed_encrypted_txs_with_proofs, - &root_element, - ); - let expected_duplicate_output_public_key = new_transactions[0].output_public_keys()[0]; + // Form block + let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); + let root_element = ledger.get_root_tx_out_membership_element().unwrap(); + + let form_block_result = enclave.form_block( + &parent_block, + &well_formed_encrypted_txs_with_proofs, + &root_element, + ); + let expected_duplicate_output_public_key = new_transactions[0].output_public_keys()[0]; - // Check - let expected = Err(Error::FormBlock(format!( - "Duplicate output public key: {:?}", - expected_duplicate_output_public_key - ))); + // Check + let expected = Err(Error::FormBlock(format!( + "Duplicate output public key: {:?}", + expected_duplicate_output_public_key + ))); - assert_eq!(form_block_result, expected); + assert_eq!(form_block_result, expected); + } } #[test_with_logger] fn form_block_refuses_duplicate_root_elements(logger: Logger) { - let enclave = SgxConsensusEnclave::new(logger); let mut rng = Hc128Rng::from_seed([77u8; 32]); - // Initialize a ledger. `sender` is the owner of all outputs in the initial - // ledger. - let sender = AccountKey::random(&mut rng); - let mut ledger = create_ledger(); - let n_blocks = 3; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version, + ..Default::default() + }; + enclave + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); - let mut ledger2 = create_ledger(); - initialize_ledger(&mut ledger2, n_blocks + 1, &sender, &mut rng); + // Initialize a ledger. `sender` is the owner of all outputs in the initial + // ledger. + let sender = AccountKey::random(&mut rng); + let mut ledger = create_ledger(); + let n_blocks = 3; + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); - // Create a few transactions from `sender` to `recipient`. - let num_transactions = 6; - let recipient = AccountKey::random(&mut rng); + let mut ledger2 = create_ledger(); + initialize_ledger(block_version, &mut ledger2, n_blocks + 1, &sender, &mut rng); - // The first block contains a single transaction with RING_SIZE outputs. - let block_zero_contents = ledger.get_block_contents(0).unwrap(); + // Create a few transactions from `sender` to `recipient`. + let num_transactions = 6; + let recipient = AccountKey::random(&mut rng); - let mut new_transactions = Vec::new(); - for i in 0..num_transactions { - let tx_out = &block_zero_contents.outputs[i]; + // The first block contains a single transaction with RING_SIZE outputs. + let block_zero_contents = ledger.get_block_contents(0).unwrap(); - let tx = create_transaction( - &mut ledger, - tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, - ); - new_transactions.push(tx); - } + let mut new_transactions = Vec::new(); + for i in 0..num_transactions { + let tx_out = &block_zero_contents.outputs[i]; - // Create WellFormedEncryptedTxs + proofs - let well_formed_encrypted_txs_with_proofs: Vec<( - WellFormedEncryptedTx, - Vec, - )> = new_transactions - .iter() - .enumerate() - .map(|(tx_idx, tx)| { - let well_formed_tx = WellFormedTx::from(tx.clone()); - let encrypted_tx = enclave - .encrypt_well_formed_tx(&well_formed_tx, &mut rng) - .unwrap(); - - let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); - let membership_proofs = highest_indices - .iter() - .map(|index| { - // Make one of the proofs have a different root element by creating it from - // a different - if tx_idx == 0 { - ledger2 - .get_tx_out_proof_of_memberships(&[*index]) - .expect("failed getting proof")[0] - .clone() - } else { - ledger - .get_tx_out_proof_of_memberships(&[*index]) - .expect("failed getting proof")[0] - .clone() - } - }) - .collect(); - (encrypted_tx, membership_proofs) - }) - .collect(); + let tx = create_transaction( + block_version, + &mut ledger, + tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); + new_transactions.push(tx); + } - // Form block - let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); - let root_element = ledger.get_root_tx_out_membership_element().unwrap(); + // Create WellFormedEncryptedTxs + proofs + let well_formed_encrypted_txs_with_proofs: Vec<( + WellFormedEncryptedTx, + Vec, + )> = new_transactions + .iter() + .enumerate() + .map(|(tx_idx, tx)| { + let well_formed_tx = WellFormedTx::from(tx.clone()); + let encrypted_tx = enclave + .encrypt_well_formed_tx(&well_formed_tx, &mut rng) + .unwrap(); + + let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); + let membership_proofs = highest_indices + .iter() + .map(|index| { + // Make one of the proofs have a different root element by creating it + // from a different + if tx_idx == 0 { + ledger2 + .get_tx_out_proof_of_memberships(&[*index]) + .expect("failed getting proof")[0] + .clone() + } else { + ledger + .get_tx_out_proof_of_memberships(&[*index]) + .expect("failed getting proof")[0] + .clone() + } + }) + .collect(); + (encrypted_tx, membership_proofs) + }) + .collect(); - let form_block_result = enclave.form_block( - &parent_block, - &well_formed_encrypted_txs_with_proofs, - &root_element, - ); + // Form block + let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); + let root_element = ledger.get_root_tx_out_membership_element().unwrap(); + + let form_block_result = enclave.form_block( + &parent_block, + &well_formed_encrypted_txs_with_proofs, + &root_element, + ); - // Check - let expected = Err(Error::MalformedTx( - TransactionValidationError::InvalidTxOutMembershipProof, - )); - assert_eq!(form_block_result, expected); + // Check + let expected = Err(Error::MalformedTx( + TransactionValidationError::InvalidTxOutMembershipProof, + )); + assert_eq!(form_block_result, expected); + } } #[test_with_logger] fn form_block_refuses_incorrect_root_element(logger: Logger) { - let enclave = SgxConsensusEnclave::new(logger); let mut rng = Hc128Rng::from_seed([77u8; 32]); - // Initialize a ledger. `sender` is the owner of all outputs in the initial - // ledger. - let sender = AccountKey::random(&mut rng); - let mut ledger = create_ledger(); - let n_blocks = 3; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version, + ..Default::default() + }; + enclave + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); - // Create a few transactions from `sender` to `recipient`. - let num_transactions = 6; - let recipient = AccountKey::random(&mut rng); + // Initialize a ledger. `sender` is the owner of all outputs in the initial + // ledger. + let sender = AccountKey::random(&mut rng); + let mut ledger = create_ledger(); + let n_blocks = 3; + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); - // The first block contains a single transaction with RING_SIZE outputs. - let block_zero_contents = ledger.get_block_contents(0).unwrap(); + // Create a few transactions from `sender` to `recipient`. + let num_transactions = 6; + let recipient = AccountKey::random(&mut rng); - let mut new_transactions = Vec::new(); - for i in 0..num_transactions { - let tx_out = &block_zero_contents.outputs[i]; + // The first block contains a single transaction with RING_SIZE outputs. + let block_zero_contents = ledger.get_block_contents(0).unwrap(); - let tx = create_transaction( - &mut ledger, - tx_out, - &sender, - &recipient.default_subaddress(), - n_blocks + 1, - &mut rng, + let mut new_transactions = Vec::new(); + for i in 0..num_transactions { + let tx_out = &block_zero_contents.outputs[i]; + + let tx = create_transaction( + block_version, + &mut ledger, + tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); + new_transactions.push(tx); + } + + // Create WellFormedEncryptedTxs + proofs + let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions + .iter() + .map(|tx| { + let well_formed_tx = WellFormedTx::from(tx.clone()); + let encrypted_tx = enclave + .encrypt_well_formed_tx(&well_formed_tx, &mut rng) + .unwrap(); + + let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); + let membership_proofs = ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proof"); + (encrypted_tx, membership_proofs) + }) + .collect(); + + // Form block + let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); + let mut root_element = ledger.get_root_tx_out_membership_element().unwrap(); + + // Alter the root element so that it is inconsistent with the proofs. + root_element.hash.0[0] = !root_element.hash.0[0]; + + let form_block_result = enclave.form_block( + &parent_block, + &well_formed_encrypted_txs_with_proofs, + &root_element, ); - new_transactions.push(tx); + + // Check + let expected = Err(Error::InvalidLocalMembershipRootElement); + assert_eq!(form_block_result, expected); } + } - // Create WellFormedEncryptedTxs + proofs - let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions - .iter() - .map(|tx| { - let well_formed_tx = WellFormedTx::from(tx.clone()); - let encrypted_tx = enclave - .encrypt_well_formed_tx(&well_formed_tx, &mut rng) - .unwrap(); - - let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); - let membership_proofs = ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proof"); - (encrypted_tx, membership_proofs) - }) - .collect(); + #[test_with_logger] + fn form_block_refuses_decreasing_block_version(logger: Logger) { + let mut rng = Hc128Rng::from_seed([77u8; 32]); - // Form block - let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); - let mut root_element = ledger.get_root_tx_out_membership_element().unwrap(); + for block_version in BlockVersion::iterator() { + let enclave = SgxConsensusEnclave::new(logger.clone()); + let blockchain_config = BlockchainConfig { + block_version: BlockVersion::try_from(*block_version - 1).unwrap(), + ..Default::default() + }; + enclave + .enclave_init( + &Default::default(), + &Default::default(), + &None, + blockchain_config, + ) + .unwrap(); - // Alter the root element so that it is inconsistent with the proofs. - root_element.hash.0[0] = !root_element.hash.0[0]; + // Initialize a ledger. `sender` is the owner of all outputs in the initial + // ledger. + let sender = AccountKey::random(&mut rng); + let mut ledger = create_ledger(); + let n_blocks = 3; + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); - let form_block_result = enclave.form_block( - &parent_block, - &well_formed_encrypted_txs_with_proofs, - &root_element, - ); + // Create a few transactions from `sender` to `recipient`. + let num_transactions = 6; + let recipient = AccountKey::random(&mut rng); + + // The first block contains a single transaction with RING_SIZE outputs. + let block_zero_contents = ledger.get_block_contents(0).unwrap(); - // Check - let expected = Err(Error::InvalidLocalMembershipRootElement); - assert_eq!(form_block_result, expected); + let mut new_transactions = Vec::new(); + for i in 0..num_transactions { + let tx_out = &block_zero_contents.outputs[i]; + + let tx = create_transaction( + block_version, + &mut ledger, + tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); + new_transactions.push(tx); + } + + // Create WellFormedEncryptedTxs + proofs + let well_formed_encrypted_txs_with_proofs: Vec<( + WellFormedEncryptedTx, + Vec, + )> = new_transactions + .iter() + .map(|tx| { + let well_formed_tx = WellFormedTx::from(tx.clone()); + let encrypted_tx = enclave + .encrypt_well_formed_tx(&well_formed_tx, &mut rng) + .unwrap(); + + let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices(); + let membership_proofs = ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proof"); + (encrypted_tx, membership_proofs) + }) + .collect(); + + // Form block + let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap(); + + let root_element = ledger.get_root_tx_out_membership_element().unwrap(); + + let form_block_result = enclave.form_block( + &parent_block, + &well_formed_encrypted_txs_with_proofs, + &root_element, + ); + + log::info!(logger, "got form block result: {:?}", form_block_result); + + // Check if we get a form block error as expected + match form_block_result { + Err(Error::BlockVersion(_)) => {} + _ => panic!( + "Expected a BlockVersion error due to config.block_version being less than parent" + ), + } + } } } diff --git a/consensus/enclave/mock/src/lib.rs b/consensus/enclave/mock/src/lib.rs index 5cb4a35453..b934d406b0 100644 --- a/consensus/enclave/mock/src/lib.rs +++ b/consensus/enclave/mock/src/lib.rs @@ -7,8 +7,9 @@ mod mock_consensus_enclave; pub use mock_consensus_enclave::MockConsensusEnclave; pub use mc_consensus_enclave_api::{ - ConsensusEnclave, ConsensusEnclaveProxy, Error, FeeMap, FeePublicKey, LocallyEncryptedTx, - Result, SealedBlockSigningKey, TxContext, WellFormedEncryptedTx, WellFormedTxContext, + BlockchainConfig, ConsensusEnclave, ConsensusEnclaveProxy, Error, FeePublicKey, + LocallyEncryptedTx, Result, SealedBlockSigningKey, TxContext, WellFormedEncryptedTx, + WellFormedTxContext, }; use mc_attest_core::{IasNonce, Quote, QuoteNonce, Report, TargetInfo, VerificationReport}; @@ -28,7 +29,7 @@ use mc_transaction_core::{ tokens::Mob, tx::{Tx, TxOut, TxOutMembershipElement, TxOutMembershipProof}, validation::TransactionValidationError, - Block, BlockContents, BlockSignature, Token, TokenId, BLOCK_VERSION, + Block, BlockContents, BlockSignature, Token, TokenId, }; use mc_util_from_random::FromRandom; use rand_core::SeedableRng; @@ -41,18 +42,18 @@ use std::{ #[derive(Clone)] pub struct ConsensusServiceMockEnclave { pub signing_keypair: Arc, - pub fee_map: Arc>, + pub blockchain_config: Arc>, } impl Default for ConsensusServiceMockEnclave { fn default() -> Self { let mut csprng = Hc128Rng::seed_from_u64(0); let signing_keypair = Arc::new(Ed25519Pair::from_random(&mut csprng)); - let fee_map = Arc::new(Mutex::new(FeeMap::default())); + let blockchain_config = Arc::new(Mutex::new(BlockchainConfig::default())); Self { signing_keypair, - fee_map, + blockchain_config, } } } @@ -99,15 +100,20 @@ impl ConsensusEnclave for ConsensusServiceMockEnclave { _self_peer_id: &ResponderId, _self_client_id: &ResponderId, _sealed_key: &Option, - fee_map: &FeeMap, + blockchain_config: BlockchainConfig, ) -> Result<(SealedBlockSigningKey, Vec)> { - *self.fee_map.lock().unwrap() = fee_map.clone(); + *self.blockchain_config.lock().unwrap() = blockchain_config; Ok((vec![], vec![])) } fn get_minimum_fee(&self, token_id: &TokenId) -> Result> { - Ok(self.fee_map.lock().unwrap().get_fee_for_token(token_id)) + Ok(self + .blockchain_config + .lock() + .unwrap() + .fee_map + .get_fee_for_token(token_id)) } fn get_identity(&self) -> Result { @@ -212,6 +218,7 @@ impl ConsensusEnclave for ConsensusServiceMockEnclave { encrypted_txs_with_proofs: &[(WellFormedEncryptedTx, Vec)], _root_element: &TxOutMembershipElement, ) -> Result<(Block, BlockContents, BlockSignature)> { + let block_version = self.blockchain_config.lock().unwrap().block_version; let transactions_with_proofs: Vec<(Tx, Vec)> = encrypted_txs_with_proofs .iter() @@ -233,6 +240,7 @@ impl ConsensusEnclave for ConsensusServiceMockEnclave { mc_transaction_core::validation::validate( tx, parent_block.index + 1, + block_version, proofs, Mob::MINIMUM_FEE, &mut rng, @@ -262,7 +270,7 @@ impl ConsensusEnclave for ConsensusServiceMockEnclave { let block_contents = BlockContents::new(key_images, outputs); let block = Block::new_with_parent( - BLOCK_VERSION, + block_version, parent_block, &root_elements[0], &block_contents, diff --git a/consensus/enclave/mock/src/mock_consensus_enclave.rs b/consensus/enclave/mock/src/mock_consensus_enclave.rs index e28bf0a644..f7f5329976 100644 --- a/consensus/enclave/mock/src/mock_consensus_enclave.rs +++ b/consensus/enclave/mock/src/mock_consensus_enclave.rs @@ -8,8 +8,9 @@ use mc_attest_core::{IasNonce, Quote, QuoteNonce, Report, TargetInfo, Verificati use mc_attest_enclave_api::*; use mc_common::ResponderId; use mc_consensus_enclave_api::{ - ConsensusEnclave, FeeMap, FeePublicKey, LocallyEncryptedTx, Result as ConsensusEnclaveResult, - SealedBlockSigningKey, TxContext, WellFormedEncryptedTx, WellFormedTxContext, + BlockchainConfig, ConsensusEnclave, FeePublicKey, LocallyEncryptedTx, + Result as ConsensusEnclaveResult, SealedBlockSigningKey, TxContext, WellFormedEncryptedTx, + WellFormedTxContext, }; use mc_crypto_keys::{Ed25519Public, X25519Public}; use mc_sgx_report_cache_api::{ReportableEnclave, Result as SgxReportResult}; @@ -32,7 +33,7 @@ mock! { self_peer_id: &ResponderId, self_client_id: &ResponderId, sealed_key: &Option, - fee_map: &FeeMap, + blockchain_config: BlockchainConfig, ) -> ConsensusEnclaveResult<(SealedBlockSigningKey, Vec)>; fn get_minimum_fee(&self, token_id: &TokenId) -> ConsensusEnclaveResult>; diff --git a/consensus/enclave/src/lib.rs b/consensus/enclave/src/lib.rs index 06bc1581b5..36d75ccd8b 100644 --- a/consensus/enclave/src/lib.rs +++ b/consensus/enclave/src/lib.rs @@ -3,8 +3,9 @@ //! The Consensus Service SGX Enclave Proxy pub use mc_consensus_enclave_api::{ - ConsensusEnclave, ConsensusEnclaveProxy, EnclaveCall, Error, FeeMap, FeeMapError, FeePublicKey, - LocallyEncryptedTx, Result, TxContext, WellFormedEncryptedTx, WellFormedTxContext, + BlockchainConfig, ConsensusEnclave, ConsensusEnclaveProxy, EnclaveCall, Error, FeeMap, + FeeMapError, FeePublicKey, LocallyEncryptedTx, Result, TxContext, WellFormedEncryptedTx, + WellFormedTxContext, }; use mc_attest_core::{ @@ -44,7 +45,7 @@ impl ConsensusServiceSgxEnclave { self_peer_id: &ResponderId, self_client_id: &ResponderId, sealed_key: &Option, - fee_map: &FeeMap, + blockchain_config: BlockchainConfig, ) -> ( ConsensusServiceSgxEnclave, SealedBlockSigningKey, @@ -71,7 +72,7 @@ impl ConsensusServiceSgxEnclave { }; let (sealed_key, features) = sgx_enclave - .enclave_init(self_peer_id, self_client_id, sealed_key, fee_map) + .enclave_init(self_peer_id, self_client_id, sealed_key, blockchain_config) .expect("enclave_init failed"); (sgx_enclave, sealed_key, features) @@ -123,13 +124,13 @@ impl ConsensusEnclave for ConsensusServiceSgxEnclave { self_peer_id: &ResponderId, self_client_id: &ResponderId, sealed_key: &Option, - fee_map: &FeeMap, + blockchain_config: BlockchainConfig, ) -> Result<(SealedBlockSigningKey, Vec)> { let inbuf = mc_util_serial::serialize(&EnclaveCall::EnclaveInit( self_peer_id.clone(), self_client_id.clone(), sealed_key.clone(), - fee_map.clone(), + blockchain_config, ))?; let outbuf = self.enclave_call(&inbuf)?; mc_util_serial::deserialize(&outbuf[..])? diff --git a/consensus/enclave/trusted/Cargo.lock b/consensus/enclave/trusted/Cargo.lock index b019baacfa..b96b6cc7d6 100644 --- a/consensus/enclave/trusted/Cargo.lock +++ b/consensus/enclave/trusted/Cargo.lock @@ -828,6 +828,7 @@ dependencies = [ "mc-util-build-script", "mc-util-from-random", "mc-util-serial", + "once_cell", "prost", "rand_core", ] @@ -1294,6 +1295,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + [[package]] name = "opaque-debug" version = "0.3.0" diff --git a/consensus/enclave/trusted/src/lib.rs b/consensus/enclave/trusted/src/lib.rs index 4863077128..88e2d3de5f 100644 --- a/consensus/enclave/trusted/src/lib.rs +++ b/consensus/enclave/trusted/src/lib.rs @@ -34,9 +34,14 @@ pub fn ecall_dispatcher(inbuf: &[u8]) -> Result, sgx_status_t> { // And actually do it let outdata = match call_details { // Utility methods - EnclaveCall::EnclaveInit(peer_self_id, client_self_id, sealed_key, fee_map) => { - serialize(&ENCLAVE.enclave_init(&peer_self_id, &client_self_id, &sealed_key, &fee_map)) - .or(Err(sgx_status_t::SGX_ERROR_UNEXPECTED))? + EnclaveCall::EnclaveInit(peer_self_id, client_self_id, sealed_key, blockchain_config) => { + serialize(&ENCLAVE.enclave_init( + &peer_self_id, + &client_self_id, + &sealed_key, + blockchain_config, + )) + .or(Err(sgx_status_t::SGX_ERROR_UNEXPECTED))? } EnclaveCall::GetMinimumFee(token_id) => serialize(&ENCLAVE.get_minimum_fee(&token_id)) .or(Err(sgx_status_t::SGX_ERROR_UNEXPECTED))?, diff --git a/consensus/service/src/api/blockchain_api_service.rs b/consensus/service/src/api/blockchain_api_service.rs index dd756861e9..4b8a88ab2e 100644 --- a/consensus/service/src/api/blockchain_api_service.rs +++ b/consensus/service/src/api/blockchain_api_service.rs @@ -179,7 +179,7 @@ mod tests { use grpcio::{ChannelBuilder, Environment, Error as GrpcError, Server, ServerBuilder}; use mc_common::{logger::test_with_logger, time::SystemTimeProvider}; use mc_consensus_api::consensus_common_grpc::{self, BlockchainApiClient}; - use mc_transaction_core::TokenId; + use mc_transaction_core::{BlockVersion, TokenId}; use mc_transaction_core_test_utils::{create_ledger, initialize_ledger, AccountKey}; use mc_util_grpc::{AnonymousAuthenticator, TokenAuthenticator}; use rand::{rngs::StdRng, SeedableRng}; @@ -223,7 +223,13 @@ mod tests { let authenticator = Arc::new(AnonymousAuthenticator::default()); let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); let account_key = AccountKey::random(&mut rng); - let block_entities = initialize_ledger(&mut ledger_db, 10, &account_key, &mut rng); + let block_entities = initialize_ledger( + BlockVersion::MAX, + &mut ledger_db, + 10, + &account_key, + &mut rng, + ); let mut expected_response = LastBlockInfoResponse::new(); expected_response.set_index(block_entities.last().unwrap().index); @@ -277,7 +283,13 @@ mod tests { let authenticator = Arc::new(AnonymousAuthenticator::default()); let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); let account_key = AccountKey::random(&mut rng); - let block_entities = initialize_ledger(&mut ledger_db, 10, &account_key, &mut rng); + let block_entities = initialize_ledger( + BlockVersion::MAX, + &mut ledger_db, + 10, + &account_key, + &mut rng, + ); let expected_blocks: Vec = block_entities .into_iter() @@ -320,7 +332,13 @@ mod tests { let authenticator = Arc::new(AnonymousAuthenticator::default()); let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); let account_key = AccountKey::random(&mut rng); - let _blocks = initialize_ledger(&mut ledger_db, 10, &account_key, &mut rng); + let _blocks = initialize_ledger( + BlockVersion::MAX, + &mut ledger_db, + 10, + &account_key, + &mut rng, + ); let mut blockchain_api_service = BlockchainApiService::new(ledger_db, authenticator, FeeMap::default(), logger); @@ -341,7 +359,13 @@ mod tests { let authenticator = Arc::new(AnonymousAuthenticator::default()); let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); let account_key = AccountKey::random(&mut rng); - let block_entities = initialize_ledger(&mut ledger_db, 10, &account_key, &mut rng); + let block_entities = initialize_ledger( + BlockVersion::MAX, + &mut ledger_db, + 10, + &account_key, + &mut rng, + ); let expected_blocks: Vec = block_entities .into_iter() diff --git a/consensus/service/src/bin/main.rs b/consensus/service/src/bin/main.rs index aa19ff8059..4f85bbb73a 100644 --- a/consensus/service/src/bin/main.rs +++ b/consensus/service/src/bin/main.rs @@ -8,7 +8,7 @@ use mc_common::{ logger::{create_app_logger, log, o}, time::SystemTimeProvider, }; -use mc_consensus_enclave::{ConsensusServiceSgxEnclave, ENCLAVE_FILE}; +use mc_consensus_enclave::{BlockchainConfig, ConsensusServiceSgxEnclave, ENCLAVE_FILE}; use mc_consensus_service::{ config::Config, consensus_service::{ConsensusService, ConsensusServiceError}, @@ -59,6 +59,11 @@ fn main() -> Result<(), ConsensusServiceError> { scope.set_tag("local_node_id", local_node_id.responder_id.to_string()); }); + let blockchain_config = BlockchainConfig { + fee_map: fee_map.clone(), + block_version: config.block_version, + }; + let enclave_path = env::current_exe() .expect("Could not get the path of our executable") .with_file_name(ENCLAVE_FILE); @@ -67,11 +72,7 @@ fn main() -> Result<(), ConsensusServiceError> { &config.peer_responder_id, &config.client_responder_id, &cached_key, - // Note/TODO: Right now the fee map is optionally provided by the tokens configuration - // file, and that is the only configurtable parameter in that file. Once the configuration - // is extended, we will likely need to pass parts of it (or all of it) to the enclave in - // order to include it in the responder id. - &fee_map, + blockchain_config, ); log::info!(logger, "Enclave target features: {}", features.join(", ")); diff --git a/consensus/service/src/byzantine_ledger/mod.rs b/consensus/service/src/byzantine_ledger/mod.rs index 48d200de33..766a628e59 100644 --- a/consensus/service/src/byzantine_ledger/mod.rs +++ b/consensus/service/src/byzantine_ledger/mod.rs @@ -255,6 +255,7 @@ mod tests { use mc_ledger_db::Ledger; use mc_peers::{MockBroadcast, ThreadedBroadcaster}; use mc_peers_test_utils::MockPeerConnection; + use mc_transaction_core::BlockVersion; use mc_transaction_core_test_utils::{ create_ledger, create_transaction, initialize_ledger, AccountKey, }; @@ -270,6 +271,9 @@ mod tests { time::Instant, }; + // Run these tests with a particular block version + const BLOCK_VERSION: BlockVersion = BlockVersion::ONE; + fn test_peer_uri(node_id: u32, pubkey: String) -> PeerUri { PeerUri::from_str(&format!( "mcp://node{}.test.mobilecoin.com/?consensus-msg-key={}", @@ -356,7 +360,7 @@ mod tests { let mut ledger = create_ledger(); let sender = AccountKey::random(&mut rng); let num_blocks = 1; - initialize_ledger(&mut ledger, num_blocks, &sender, &mut rng); + initialize_ledger(BLOCK_VERSION, &mut ledger, num_blocks, &sender, &mut rng); // Mock peer_manager let peer_manager = ConnectionManager::new( @@ -423,7 +427,7 @@ mod tests { let mut ledger = create_ledger(); let sender = AccountKey::random(&mut rng); let num_blocks = 1; - initialize_ledger(&mut ledger, num_blocks, &sender, &mut rng); + initialize_ledger(BLOCK_VERSION, &mut ledger, num_blocks, &sender, &mut rng); // Mock peer_manager let mock_peer = MockPeerConnection::new( @@ -457,6 +461,8 @@ mod tests { ))); let enclave = ConsensusServiceMockEnclave::default(); + enclave.blockchain_config.lock().unwrap().block_version = BLOCK_VERSION; + let tx_manager = Arc::new(TxManagerImpl::new( enclave.clone(), DefaultTxManagerUntrustedInterfaces::new(ledger.clone()), @@ -494,6 +500,7 @@ mod tests { let recipient = AccountKey::random(&mut rng); let tx1 = create_transaction( + BLOCK_VERSION, &mut ledger, &block_contents.outputs[0], &sender, @@ -504,6 +511,7 @@ mod tests { let recipient = AccountKey::random(&mut rng); let tx2 = create_transaction( + BLOCK_VERSION, &mut ledger, &block_contents.outputs[1], &sender, @@ -514,6 +522,7 @@ mod tests { let recipient = AccountKey::random(&mut rng); let tx3 = create_transaction( + BLOCK_VERSION, &mut ledger, &block_contents.outputs[2], &sender, diff --git a/consensus/service/src/config/mod.rs b/consensus/service/src/config/mod.rs index 594dc43ec2..d1bc89c9c4 100644 --- a/consensus/service/src/config/mod.rs +++ b/consensus/service/src/config/mod.rs @@ -9,6 +9,7 @@ use crate::config::{network::NetworkConfig, tokens::TokensConfig}; use mc_attest_core::ProviderId; use mc_common::{NodeID, ResponderId}; use mc_crypto_keys::{DistinguishedEncoding, Ed25519Pair, Ed25519Private}; +use mc_transaction_core::BlockVersion; use mc_util_parse::parse_duration_in_seconds; use mc_util_uri::{AdminUri, ConsensusClientUri as ClientUri, ConsensusPeerUri as PeerUri}; use std::{fmt::Debug, path::PathBuf, string::String, sync::Arc, time::Duration}; @@ -96,6 +97,10 @@ pub struct Config { /// The location for the network.toml/json configuration file. #[structopt(long = "tokens", parse(from_os_str))] pub tokens_path: Option, + + /// The configured block version + #[structopt(long, env = "MC_BLOCK_VERSION", default_value = "1")] + pub block_version: BlockVersion, } /// Decodes an Ed25519 private key. @@ -175,6 +180,7 @@ mod tests { client_auth_token_secret: None, client_auth_token_max_lifetime: Duration::from_secs(60), tokens_path: None, + block_version: BlockVersion::ONE, }; assert_eq!( @@ -241,6 +247,7 @@ mod tests { client_auth_token_secret: None, client_auth_token_max_lifetime: Duration::from_secs(60), tokens_path: None, + block_version: BlockVersion::ONE, }; assert_eq!( diff --git a/consensus/service/src/tx_manager/mod.rs b/consensus/service/src/tx_manager/mod.rs index 716c645d05..e29234ba7a 100644 --- a/consensus/service/src/tx_manager/mod.rs +++ b/consensus/service/src/tx_manager/mod.rs @@ -370,7 +370,7 @@ mod tests { use mc_ledger_db::Ledger; use mc_transaction_core::{ membership_proofs::Range, tx::TxOutMembershipElement, - validation::TransactionValidationError, + validation::TransactionValidationError, BlockVersion, }; use mc_transaction_core_test_utils::{ create_ledger, create_transaction, initialize_ledger, AccountKey, @@ -870,16 +870,20 @@ mod tests { #[test_with_logger] fn test_hashes_to_block(logger: Logger) { let mut rng: StdRng = SeedableRng::from_seed([77u8; 32]); + let block_version = BlockVersion::ONE; let sender = AccountKey::random(&mut rng); let mut ledger = create_ledger(); let n_blocks = 3; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); let num_blocks = ledger.num_blocks().expect("Ledger must contain a block."); let parent_block = ledger.get_block(num_blocks - 1).unwrap(); + let enclave = ConsensusServiceMockEnclave::default(); + enclave.blockchain_config.lock().unwrap().block_version = block_version; + let tx_manager = TxManagerImpl::new( - ConsensusServiceMockEnclave::default(), + enclave, DefaultTxManagerUntrustedInterfaces::new(ledger.clone()), logger.clone(), ); @@ -891,12 +895,13 @@ mod tests { let sender = AccountKey::random(&mut rng); let mut ledger = create_ledger(); let n_blocks = 3; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng); let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap(); let tx_out = block_contents.outputs[0].clone(); let recipient = AccountKey::random(&mut rng); let tx1 = create_transaction( + block_version, &mut ledger, &tx_out, &sender, @@ -907,6 +912,7 @@ mod tests { let recipient = AccountKey::random(&mut rng); let tx2 = create_transaction( + block_version, &mut ledger, &tx_out, &sender, @@ -917,6 +923,7 @@ mod tests { let recipient = AccountKey::random(&mut rng); let tx3 = create_transaction( + block_version, &mut ledger, &tx_out, &sender, @@ -927,6 +934,7 @@ mod tests { let recipient = AccountKey::random(&mut rng); let tx4 = create_transaction( + block_version, &mut ledger, &tx_out, &sender, diff --git a/consensus/service/src/validators.rs b/consensus/service/src/validators.rs index 50a2b74328..f9519ffe29 100644 --- a/consensus/service/src/validators.rs +++ b/consensus/service/src/validators.rs @@ -500,6 +500,7 @@ mod combine_tests { use mc_transaction_core::{ onetime_keys::recover_onetime_private_key, tx::{TxOut, TxOutMembershipProof}, + BlockVersion, }; use mc_transaction_core_test_utils::{AccountKey, MockFogResolver}; use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; @@ -529,105 +530,275 @@ mod combine_tests { fn combine_single_transaction() { let mut rng = Hc128Rng::from_seed([1u8; 32]); - let alice = AccountKey::random(&mut rng); - let bob = AccountKey::random(&mut rng); + for block_version in BlockVersion::iterator() { + let alice = AccountKey::random(&mut rng); + let bob = AccountKey::random(&mut rng); - // Step 1: create a TxOut and the keys for its enclosing transaction. This TxOut - // will be used as the input for a transaction used in the test. + // Step 1: create a TxOut and the keys for its enclosing transaction. This TxOut + // will be used as the input for a transaction used in the test. - // The transaction secret key r and its public key R. - let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); + // The transaction secret key r and its public key R. + let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); - let tx_out = TxOut::new( - 123, - &alice.default_subaddress(), - &tx_secret_key_for_txo, - Default::default(), - ) - .unwrap(); + let tx_out = TxOut::new( + 123, + &alice.default_subaddress(), + &tx_secret_key_for_txo, + Default::default(), + ) + .unwrap(); - let tx_public_key_for_txo = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); + let tx_public_key_for_txo = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); - // Step 2: Alice creates a transaction that sends the full value of `tx_out` to - // Bob. + // Step 2: Alice creates a transaction that sends the full value of `tx_out` to + // Bob. - // Create InputCredentials to spend the TxOut. - let onetime_private_key = recover_onetime_private_key( - &tx_public_key_for_txo, - alice.view_private_key(), - &alice.default_subaddress_spend_private(), - ); + // Create InputCredentials to spend the TxOut. + let onetime_private_key = recover_onetime_private_key( + &tx_public_key_for_txo, + alice.view_private_key(), + &alice.default_subaddress_spend_private(), + ); - let ring: Vec = vec![tx_out]; - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TODO: provide valid proofs for each tx_out. - TxOutMembershipProof::new(0, 0, Default::default()) - }) - .collect(); - - let input_credentials = InputCredentials::new( - ring, - membership_proofs, - 0, - onetime_private_key, - *alice.view_private_key(), - ) - .unwrap(); - - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), EmptyMemoBuilder::default()); - transaction_builder.add_input(input_credentials); - transaction_builder.set_fee(0).unwrap(); - transaction_builder - .add_output(123, &bob.default_subaddress(), &mut rng) + let ring: Vec = vec![tx_out]; + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TODO: provide valid proofs for each tx_out. + TxOutMembershipProof::new(0, 0, Default::default()) + }) + .collect(); + + let input_credentials = InputCredentials::new( + ring, + membership_proofs, + 0, + onetime_private_key, + *alice.view_private_key(), + ) .unwrap(); - let tx = transaction_builder.build(&mut rng).unwrap(); - let client_tx = WellFormedTxContext::from(&tx); + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); + transaction_builder.add_input(input_credentials); + transaction_builder.set_fee(0).unwrap(); + transaction_builder + .add_output(123, &bob.default_subaddress(), &mut rng) + .unwrap(); - // "Combining" a singleton set should return a vec containing the single - // element. - let combined_transactions = combine(vec![client_tx], 100); - assert_eq!(combined_transactions.len(), 1); + let tx = transaction_builder.build(&mut rng).unwrap(); + let client_tx = WellFormedTxContext::from(&tx); + + // "Combining" a singleton set should return a vec containing the single + // element. + let combined_transactions = combine(vec![client_tx], 100); + assert_eq!(combined_transactions.len(), 1); + } } #[test] // `combine` should enforce a maximum limit on the number of returned items. fn combine_max_size() { let mut rng = Hc128Rng::from_seed([1u8; 32]); - let mut transaction_set: Vec = Vec::new(); - let alice = AccountKey::random(&mut rng); - let bob = AccountKey::random(&mut rng); + for block_version in BlockVersion::iterator() { + let mut transaction_set: Vec = Vec::new(); + + let alice = AccountKey::random(&mut rng); + let bob = AccountKey::random(&mut rng); + + for _i in 0..10 { + let client_tx: WellFormedTxContext = { + // Step 1: create a TxOut and the keys for its enclosing transaction. This TxOut + // will be used as the input for a transaction used in the test. + + // The transaction keys. + let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); + + let tx_out = TxOut::new( + 88, + &alice.default_subaddress(), + &tx_secret_key_for_txo, + Default::default(), + ) + .unwrap(); + + let tx_public_key_for_txo = + RistrettoPublic::try_from(&tx_out.public_key).unwrap(); + + // Step 2: Create a transaction that sends the full value of `tx_out` to + // `recipient_account`. + + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); + + // Create InputCredentials to spend the TxOut. + let onetime_private_key = recover_onetime_private_key( + &tx_public_key_for_txo, + alice.view_private_key(), + &alice.default_subaddress_spend_private(), + ); + + // Create InputCredentials to spend the TxOut. + let ring: Vec = vec![tx_out.clone()]; + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TODO: provide valid proofs for each tx_out. + TxOutMembershipProof::new(0, 0, Default::default()) + }) + .collect(); + + let input_credentials = InputCredentials::new( + ring, + membership_proofs, + 0, + onetime_private_key, + *alice.view_private_key(), + ) + .unwrap(); + transaction_builder.add_input(input_credentials); + transaction_builder.set_fee(0).unwrap(); + transaction_builder + .add_output(88, &bob.default_subaddress(), &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + WellFormedTxContext::from(&tx) + }; + transaction_set.push(client_tx); + } + + let max_elements: usize = 7; + let combined_transactions = combine(transaction_set, max_elements); + + // The combined list of transactions should contain no more than `max_elements`. + assert_eq!(combined_transactions.len(), max_elements); + } + } + + #[test] + // `combine` should omit transactions that would cause a key image to be used + // twice. + fn combine_reject_reused_key_images() { + let mut rng = Hc128Rng::from_seed([1u8; 32]); + + for block_version in BlockVersion::iterator() { + let alice = AccountKey::random(&mut rng); + let bob = AccountKey::random(&mut rng); + + // Create a TxOut that was sent to Alice. + let tx_out = TxOut::new( + 123, + &alice.default_subaddress(), + &RistrettoPrivate::from_random(&mut rng), + Default::default(), + ) + .unwrap(); + + // Alice creates InputCredentials to spend her tx_out. + let onetime_private_key = recover_onetime_private_key( + &RistrettoPublic::try_from(&tx_out.public_key).unwrap(), + alice.view_private_key(), + &alice.default_subaddress_spend_private(), + ); + + // Create a transaction that sends the full value of `tx_out` to bob. + let first_client_tx: WellFormedTxContext = { + let ring = vec![tx_out.clone()]; + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TODO: provide valid proofs for each tx_out. + TxOutMembershipProof::new(0, 0, Default::default()) + }) + .collect(); + + let input_credentials = InputCredentials::new( + ring, + membership_proofs, + 0, + onetime_private_key, + *alice.view_private_key(), + ) + .unwrap(); - for _i in 0..10 { - let client_tx: WellFormedTxContext = { - // Step 1: create a TxOut and the keys for its enclosing transaction. This TxOut - // will be used as the input for a transaction used in the test. + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); + transaction_builder.add_input(input_credentials); + transaction_builder.set_fee(0).unwrap(); + transaction_builder + .add_output(123, &bob.default_subaddress(), &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + WellFormedTxContext::from(&tx) + }; + + // Create another transaction that attempts to spend `tx_out`. + let second_client_tx: WellFormedTxContext = { + let recipient_account = AccountKey::random(&mut rng); + let ring: Vec = vec![tx_out]; + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TODO: provide valid proofs for each tx_out. + TxOutMembershipProof::new(0, 0, Default::default()) + }) + .collect(); + + let input_credentials = InputCredentials::new( + ring, + membership_proofs, + 0, + onetime_private_key, + *alice.view_private_key(), + ) + .unwrap(); + + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); + transaction_builder.add_input(input_credentials); + transaction_builder.set_fee(0).unwrap(); + transaction_builder + .add_output(123, &recipient_account.default_subaddress(), &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + WellFormedTxContext::from(&tx) + }; + + // This transaction spends a different TxOut, unrelated to `first_client_tx` and + // `second_client_tx`. + let third_client_tx: WellFormedTxContext = { + let recipient_account = AccountKey::random(&mut rng); // The transaction keys. let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); - let tx_out = TxOut::new( - 88, + 123, &alice.default_subaddress(), &tx_secret_key_for_txo, Default::default(), ) .unwrap(); - let tx_public_key_for_txo = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); // Step 2: Create a transaction that sends the full value of `tx_out` to // `recipient_account`. - let mut transaction_builder = TransactionBuilder::new( - MockFogResolver::default(), - EmptyMemoBuilder::default(), - ); - // Create InputCredentials to spend the TxOut. let onetime_private_key = recover_onetime_private_key( &tx_public_key_for_txo, @@ -635,8 +806,7 @@ mod combine_tests { &alice.default_subaddress_spend_private(), ); - // Create InputCredentials to spend the TxOut. - let ring: Vec = vec![tx_out.clone()]; + let ring: Vec = vec![tx_out]; let membership_proofs: Vec = ring .iter() .map(|_tx_out| { @@ -653,351 +823,214 @@ mod combine_tests { *alice.view_private_key(), ) .unwrap(); + + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); transaction_builder.add_input(input_credentials); transaction_builder.set_fee(0).unwrap(); transaction_builder - .add_output(88, &bob.default_subaddress(), &mut rng) + .add_output(123, &recipient_account.default_subaddress(), &mut rng) .unwrap(); let tx = transaction_builder.build(&mut rng).unwrap(); WellFormedTxContext::from(&tx) }; - transaction_set.push(client_tx); - } - let max_elements: usize = 7; - let combined_transactions = combine(transaction_set, max_elements); + // `combine` the set of transactions. + let transaction_set = vec![first_client_tx, second_client_tx, third_client_tx.clone()]; - // The combined list of transactions should contain no more than `max_elements`. - assert_eq!(combined_transactions.len(), max_elements); + let combined_transactions = combine(transaction_set, 10); + // `combine` should only allow one of the transactions that attempts to use the + // same key image. + assert_eq!(combined_transactions.len(), 2); + assert!(combined_transactions.contains(third_client_tx.tx_hash())); + } } #[test] - // `combine` should omit transactions that would cause a key image to be used - // twice. - fn combine_reject_reused_key_images() { + // `combine` should omit transactions that would cause an output public key to + // appear twice. + fn combine_reject_duplicate_output_public_key() { let mut rng = Hc128Rng::from_seed([1u8; 32]); - let alice = AccountKey::random(&mut rng); - let bob = AccountKey::random(&mut rng); + for block_version in BlockVersion::iterator() { + let alice = AccountKey::random(&mut rng); + let bob = AccountKey::random(&mut rng); - // Create a TxOut that was sent to Alice. - let tx_out = TxOut::new( - 123, - &alice.default_subaddress(), - &RistrettoPrivate::from_random(&mut rng), - Default::default(), - ) - .unwrap(); - - // Alice creates InputCredentials to spend her tx_out. - let onetime_private_key = recover_onetime_private_key( - &RistrettoPublic::try_from(&tx_out.public_key).unwrap(), - alice.view_private_key(), - &alice.default_subaddress_spend_private(), - ); - - // Create a transaction that sends the full value of `tx_out` to bob. - let first_client_tx: WellFormedTxContext = { - let ring = vec![tx_out.clone()]; - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TODO: provide valid proofs for each tx_out. - TxOutMembershipProof::new(0, 0, Default::default()) - }) - .collect(); - - let input_credentials = InputCredentials::new( - ring, - membership_proofs, - 0, - onetime_private_key, - *alice.view_private_key(), - ) - .unwrap(); - - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), EmptyMemoBuilder::default()); - transaction_builder.add_input(input_credentials); - transaction_builder.set_fee(0).unwrap(); - transaction_builder - .add_output(123, &bob.default_subaddress(), &mut rng) - .unwrap(); - - let tx = transaction_builder.build(&mut rng).unwrap(); - WellFormedTxContext::from(&tx) - }; - - // Create another transaction that attempts to spend `tx_out`. - let second_client_tx: WellFormedTxContext = { - let recipient_account = AccountKey::random(&mut rng); - let ring: Vec = vec![tx_out]; - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TODO: provide valid proofs for each tx_out. - TxOutMembershipProof::new(0, 0, Default::default()) - }) - .collect(); - - let input_credentials = InputCredentials::new( - ring, - membership_proofs, - 0, - onetime_private_key, - *alice.view_private_key(), + // Create two TxOuts that were sent to Alice. + let tx_out1 = TxOut::new( + 123, + &alice.default_subaddress(), + &RistrettoPrivate::from_random(&mut rng), + Default::default(), ) .unwrap(); - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), EmptyMemoBuilder::default()); - transaction_builder.add_input(input_credentials); - transaction_builder.set_fee(0).unwrap(); - transaction_builder - .add_output(123, &recipient_account.default_subaddress(), &mut rng) - .unwrap(); - - let tx = transaction_builder.build(&mut rng).unwrap(); - WellFormedTxContext::from(&tx) - }; - - // This transaction spends a different TxOut, unrelated to `first_client_tx` and - // `second_client_tx`. - let third_client_tx: WellFormedTxContext = { - let recipient_account = AccountKey::random(&mut rng); - - // The transaction keys. - let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); - let tx_out = TxOut::new( + let tx_out2 = TxOut::new( 123, &alice.default_subaddress(), - &tx_secret_key_for_txo, + &RistrettoPrivate::from_random(&mut rng), Default::default(), ) .unwrap(); - let tx_public_key_for_txo = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); - // Step 2: Create a transaction that sends the full value of `tx_out` to - // `recipient_account`. - - // Create InputCredentials to spend the TxOut. - let onetime_private_key = recover_onetime_private_key( - &tx_public_key_for_txo, + // Alice creates InputCredentials to spend her tx_outs. + let onetime_private_key1 = recover_onetime_private_key( + &RistrettoPublic::try_from(&tx_out1.public_key).unwrap(), alice.view_private_key(), &alice.default_subaddress_spend_private(), ); - let ring: Vec = vec![tx_out]; - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TODO: provide valid proofs for each tx_out. - TxOutMembershipProof::new(0, 0, Default::default()) - }) - .collect(); + let onetime_private_key2 = recover_onetime_private_key( + &RistrettoPublic::try_from(&tx_out2.public_key).unwrap(), + alice.view_private_key(), + &alice.default_subaddress_spend_private(), + ); - let input_credentials = InputCredentials::new( - ring, - membership_proofs, - 0, - onetime_private_key, - *alice.view_private_key(), - ) - .unwrap(); + // Create a transaction that sends the full value of `tx_out1` to bob. + let first_client_tx: WellFormedTxContext = { + let ring = vec![tx_out1.clone()]; + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TODO: provide valid proofs for each tx_out. + TxOutMembershipProof::new(0, 0, Default::default()) + }) + .collect(); - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), EmptyMemoBuilder::default()); - transaction_builder.add_input(input_credentials); - transaction_builder.set_fee(0).unwrap(); - transaction_builder - .add_output(123, &recipient_account.default_subaddress(), &mut rng) + let input_credentials = InputCredentials::new( + ring, + membership_proofs, + 0, + onetime_private_key1, + *alice.view_private_key(), + ) .unwrap(); - let tx = transaction_builder.build(&mut rng).unwrap(); - WellFormedTxContext::from(&tx) - }; - - // `combine` the set of transactions. - let transaction_set = vec![first_client_tx, second_client_tx, third_client_tx.clone()]; - - let combined_transactions = combine(transaction_set, 10); - // `combine` should only allow one of the transactions that attempts to use the - // same key image. - assert_eq!(combined_transactions.len(), 2); - assert!(combined_transactions.contains(third_client_tx.tx_hash())); - } - - #[test] - // `combine` should omit transactions that would cause an output public key to - // appear twice. - fn combine_reject_duplicate_output_public_key() { - let mut rng = Hc128Rng::from_seed([1u8; 32]); - - let alice = AccountKey::random(&mut rng); - let bob = AccountKey::random(&mut rng); - - // Create two TxOuts that were sent to Alice. - let tx_out1 = TxOut::new( - 123, - &alice.default_subaddress(), - &RistrettoPrivate::from_random(&mut rng), - Default::default(), - ) - .unwrap(); - - let tx_out2 = TxOut::new( - 123, - &alice.default_subaddress(), - &RistrettoPrivate::from_random(&mut rng), - Default::default(), - ) - .unwrap(); - - // Alice creates InputCredentials to spend her tx_outs. - let onetime_private_key1 = recover_onetime_private_key( - &RistrettoPublic::try_from(&tx_out1.public_key).unwrap(), - alice.view_private_key(), - &alice.default_subaddress_spend_private(), - ); - - let onetime_private_key2 = recover_onetime_private_key( - &RistrettoPublic::try_from(&tx_out2.public_key).unwrap(), - alice.view_private_key(), - &alice.default_subaddress_spend_private(), - ); + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); + transaction_builder.add_input(input_credentials); + transaction_builder.set_fee(0).unwrap(); + transaction_builder + .add_output(123, &bob.default_subaddress(), &mut rng) + .unwrap(); - // Create a transaction that sends the full value of `tx_out1` to bob. - let first_client_tx: WellFormedTxContext = { - let ring = vec![tx_out1.clone()]; - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TODO: provide valid proofs for each tx_out. - TxOutMembershipProof::new(0, 0, Default::default()) - }) - .collect(); + let tx = transaction_builder.build(&mut rng).unwrap(); + WellFormedTxContext::from(&tx) + }; - let input_credentials = InputCredentials::new( - ring, - membership_proofs, - 0, - onetime_private_key1, - *alice.view_private_key(), - ) - .unwrap(); + // Create another transaction that attempts to spend `tx_out2` but has the same + // output public key. + let second_client_tx: WellFormedTxContext = { + let recipient_account = AccountKey::random(&mut rng); + let ring: Vec = vec![tx_out2]; + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TODO: provide valid proofs for each tx_out. + TxOutMembershipProof::new(0, 0, Default::default()) + }) + .collect(); - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), EmptyMemoBuilder::default()); - transaction_builder.add_input(input_credentials); - transaction_builder.set_fee(0).unwrap(); - transaction_builder - .add_output(123, &bob.default_subaddress(), &mut rng) + let input_credentials = InputCredentials::new( + ring, + membership_proofs, + 0, + onetime_private_key2, + *alice.view_private_key(), + ) .unwrap(); - let tx = transaction_builder.build(&mut rng).unwrap(); - WellFormedTxContext::from(&tx) - }; + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); + transaction_builder.add_input(input_credentials); + transaction_builder.set_fee(0).unwrap(); + transaction_builder + .add_output(123, &recipient_account.default_subaddress(), &mut rng) + .unwrap(); - // Create another transaction that attempts to spend `tx_out2` but has the same - // output public key. - let second_client_tx: WellFormedTxContext = { - let recipient_account = AccountKey::random(&mut rng); - let ring: Vec = vec![tx_out2]; - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TODO: provide valid proofs for each tx_out. - TxOutMembershipProof::new(0, 0, Default::default()) - }) - .collect(); + let mut tx = transaction_builder.build(&mut rng).unwrap(); + tx.prefix.outputs[0].public_key = first_client_tx.output_public_keys()[0].clone(); + WellFormedTxContext::from(&tx) + }; - let input_credentials = InputCredentials::new( - ring, - membership_proofs, - 0, - onetime_private_key2, - *alice.view_private_key(), - ) - .unwrap(); + // This transaction spends a different TxOut, unrelated to `first_client_tx` and + // `second_client_tx`. + let third_client_tx: WellFormedTxContext = { + let recipient_account = AccountKey::random(&mut rng); - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), EmptyMemoBuilder::default()); - transaction_builder.add_input(input_credentials); - transaction_builder.set_fee(0).unwrap(); - transaction_builder - .add_output(123, &recipient_account.default_subaddress(), &mut rng) + // The transaction keys. + let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); + let tx_out = TxOut::new( + 123, + &alice.default_subaddress(), + &tx_secret_key_for_txo, + Default::default(), + ) .unwrap(); + let tx_public_key_for_txo = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); - let mut tx = transaction_builder.build(&mut rng).unwrap(); - tx.prefix.outputs[0].public_key = first_client_tx.output_public_keys()[0].clone(); - WellFormedTxContext::from(&tx) - }; - - // This transaction spends a different TxOut, unrelated to `first_client_tx` and - // `second_client_tx`. - let third_client_tx: WellFormedTxContext = { - let recipient_account = AccountKey::random(&mut rng); - - // The transaction keys. - let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); - let tx_out = TxOut::new( - 123, - &alice.default_subaddress(), - &tx_secret_key_for_txo, - Default::default(), - ) - .unwrap(); - let tx_public_key_for_txo = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); - - // Step 2: Create a transaction that sends the full value of `tx_out` to - // `recipient_account`. - - // Create InputCredentials to spend the TxOut. - let onetime_private_key = recover_onetime_private_key( - &tx_public_key_for_txo, - alice.view_private_key(), - &alice.default_subaddress_spend_private(), - ); + // Step 2: Create a transaction that sends the full value of `tx_out` to + // `recipient_account`. - let ring: Vec = vec![tx_out]; - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TODO: provide valid proofs for each tx_out. - TxOutMembershipProof::new(0, 0, Default::default()) - }) - .collect(); + // Create InputCredentials to spend the TxOut. + let onetime_private_key = recover_onetime_private_key( + &tx_public_key_for_txo, + alice.view_private_key(), + &alice.default_subaddress_spend_private(), + ); - let input_credentials = InputCredentials::new( - ring, - membership_proofs, - 0, - onetime_private_key, - *alice.view_private_key(), - ) - .unwrap(); + let ring: Vec = vec![tx_out]; + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TODO: provide valid proofs for each tx_out. + TxOutMembershipProof::new(0, 0, Default::default()) + }) + .collect(); - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), EmptyMemoBuilder::default()); - transaction_builder.add_input(input_credentials); - transaction_builder.set_fee(0).unwrap(); - transaction_builder - .add_output(123, &recipient_account.default_subaddress(), &mut rng) + let input_credentials = InputCredentials::new( + ring, + membership_proofs, + 0, + onetime_private_key, + *alice.view_private_key(), + ) .unwrap(); - let tx = transaction_builder.build(&mut rng).unwrap(); - WellFormedTxContext::from(&tx) - }; + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); + transaction_builder.add_input(input_credentials); + transaction_builder.set_fee(0).unwrap(); + transaction_builder + .add_output(123, &recipient_account.default_subaddress(), &mut rng) + .unwrap(); - // `combine` the set of transactions. - let transaction_set = vec![first_client_tx, second_client_tx, third_client_tx.clone()]; + let tx = transaction_builder.build(&mut rng).unwrap(); + WellFormedTxContext::from(&tx) + }; - let combined_transactions = combine(transaction_set, 10); - // `combine` should only allow one of the transactions that attempts to use the - // same output public key. - assert_eq!(combined_transactions.len(), 2); - assert!(combined_transactions.contains(third_client_tx.tx_hash())); + // `combine` the set of transactions. + let transaction_set = vec![first_client_tx, second_client_tx, third_client_tx.clone()]; + + let combined_transactions = combine(transaction_set, 10); + // `combine` should only allow one of the transactions that attempts to use the + // same output public key. + assert_eq!(combined_transactions.len(), 2); + assert!(combined_transactions.contains(third_client_tx.tx_hash())); + } } #[test] diff --git a/crypto/digestible/src/lib.rs b/crypto/digestible/src/lib.rs index 41079e79ae..c99616e7b2 100644 --- a/crypto/digestible/src/lib.rs +++ b/crypto/digestible/src/lib.rs @@ -446,6 +446,28 @@ impl DigestibleAsBytes for [u8; 32] {} impl> DigestibleAsBytes for GenericArray {} +// Implementation for tuples of Digestible +// This is treated as an Agg in the abstract structure hashing schema, +// because that is how digestible-derive handles tuple structs and enums in +// tuples. +// +// Note: It would be nice to be able to implement this for (T, U) instead, +// and have a blanket impl for &T where T is digestible. That doesn't seem to +// work right now. +impl Digestible for (&T, &U) { + #[inline] + fn append_to_transcript( + &self, + context: &'static [u8], + transcript: &mut DT, + ) { + transcript.append_agg_header(context, b"Tuple"); + self.0.append_to_transcript(b"0", transcript); + self.1.append_to_transcript(b"1", transcript); + transcript.append_agg_closer(context, b"Tuple"); + } +} + // Implementation for slices of Digestible // This is treated as a Seq in the abstract structure hashing schema // @@ -529,7 +551,7 @@ cfg_if! { extern crate alloc; use alloc::vec::Vec; use alloc::string::String; - use alloc::collections::BTreeSet; + use alloc::collections::{BTreeSet, BTreeMap}; // Forward from Vec to &[T] impl impl Digestible for Vec { @@ -608,6 +630,29 @@ cfg_if! { } } } + + // Treat a BTreeMap as a (sorted) sequence + // This implementation should match that for &[(T, U)] + impl Digestible for BTreeMap { + #[inline] + fn append_to_transcript(&self, context: &'static [u8], transcript: &mut DT) { + if self.is_empty() { + // This allows for schema evolution in variant types, it means Vec can be added to a fieldless enum + transcript.append_none(context); + } else { + transcript.append_seq_header(context, self.len()); + for elem in self.iter() { + elem.append_to_transcript(b"", transcript); + } + } + } + #[inline] + fn append_to_transcript_allow_omit(&self, context: &'static [u8], transcript: &mut DT) { + if !self.is_empty() { + self.append_to_transcript(context, transcript); + } + } + } } } diff --git a/crypto/digestible/tests/basic.rs b/crypto/digestible/tests/basic.rs index 70fde71b7b..0aee46ca25 100644 --- a/crypto/digestible/tests/basic.rs +++ b/crypto/digestible/tests/basic.rs @@ -5,6 +5,7 @@ use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT, scalar::Scalar}; use mc_crypto_digestible::{Digestible, MerlinTranscript}; +use std::collections::{BTreeMap, BTreeSet}; // Test merlin transcript hash values for various primitives // @@ -466,6 +467,46 @@ fn test_btree_set() { ); } +// Test digesting of BTreeMap +#[test] +fn test_btree_map() { + let mut temp: std::collections::BTreeMap = Default::default(); + assert_eq!( + temp.digest32::(b"test"), + [ + 35, 213, 109, 195, 226, 235, 162, 166, 228, 183, 30, 23, 226, 184, 19, 8, 12, 166, 24, + 194, 247, 84, 216, 45, 122, 19, 75, 140, 159, 233, 85, 6 + ] + ); + + temp.insert(19, "woot".to_string()); + assert_eq!( + temp.digest32::(b"test"), + [ + 157, 91, 97, 183, 53, 162, 127, 203, 78, 224, 250, 181, 7, 233, 137, 34, 155, 253, 11, + 218, 205, 131, 249, 193, 147, 122, 147, 210, 206, 120, 146, 30 + ] + ); + + temp.insert(17, "megapile".to_string()); + assert_eq!( + temp.digest32::(b"test"), + [ + 86, 233, 76, 166, 1, 234, 207, 241, 211, 139, 180, 111, 129, 50, 124, 103, 204, 156, + 111, 108, 68, 189, 26, 150, 99, 129, 229, 137, 135, 254, 15, 187 + ] + ); + + temp.insert(49, "electric".to_string()); + assert_eq!( + temp.digest32::(b"test"), + [ + 238, 88, 122, 55, 171, 144, 7, 202, 32, 204, 179, 33, 203, 2, 43, 166, 92, 208, 16, + 179, 0, 119, 188, 71, 38, 184, 254, 237, 90, 176, 177, 213 + ] + ); +} + // Test digesting of Generic Array // // Particularly, check that it is hashing the same way as a regular array @@ -480,3 +521,32 @@ fn test_generic_array() { garray.digest32::(b"test"), ); } + +// Test that hashing BTreeSet of strings is the same as hashing Vec of (sorted) +// strings +#[test] +fn test_btree_set_vs_vec() { + let vec1 = vec!["a".to_string(), "b".to_string(), "c".to_string()]; + let set1 = vec1.iter().cloned().collect::>(); + + assert_eq!( + vec1.digest32::(b"test"), + set1.digest32::(b"test"), + ) +} + +// Test that hashing BTreeMap of int is the same as hashing Vec of (sorted) +// pairs +#[test] +fn test_btree_map_vs_vec() { + let vec1: Vec<(&u64, &u64)> = vec![(&9, &11), (&14, &25), (&19, &1)]; + let map1 = vec1 + .iter() + .map(|(a, b)| (**a, **b)) + .collect::>(); + + assert_eq!( + vec1.digest32::(b"test"), + map1.digest32::(b"test"), + ) +} diff --git a/fog/distribution/src/main.rs b/fog/distribution/src/main.rs index 693f6e29dd..2197be2817 100755 --- a/fog/distribution/src/main.rs +++ b/fog/distribution/src/main.rs @@ -24,7 +24,7 @@ use mc_transaction_core::{ tokens::Mob, tx::{Tx, TxOut, TxOutMembershipProof}, validation::TransactionValidationError, - Token, + BlockVersion, Token, }; use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; use mc_util_uri::FogUri; @@ -37,7 +37,7 @@ use std::{ path::Path, str::FromStr, sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicU32, AtomicU64, Ordering}, Arc, Mutex, }, thread, @@ -71,6 +71,7 @@ fn get_conns( lazy_static! { pub static ref BLOCK_HEIGHT: AtomicU64 = AtomicU64::default(); + pub static ref BLOCK_VERSION: AtomicU32 = AtomicU32::new(1); pub static ref FEE: AtomicU64 = AtomicU64::default(); @@ -125,6 +126,10 @@ fn main() { let ledger_db = LedgerDB::open(ledger_dir.path()).expect("Could not open ledger_db"); BLOCK_HEIGHT.store(ledger_db.num_blocks().unwrap(), Ordering::SeqCst); + BLOCK_VERSION.store( + ledger_db.get_latest_block().unwrap().version, + Ordering::SeqCst, + ); // Use the maximum fee of all configured consensus nodes FEE.store( @@ -563,8 +568,14 @@ fn build_tx( // Sanity assert_eq!(utxos_with_proofs.len(), rings.len()); + // This max occurs because the bootstrapped ledger has block version 0, + // but non-bootstrap blocks always have block version >= 1 + let block_version = BlockVersion::try_from(BLOCK_VERSION.load(Ordering::SeqCst)) + .expect("Unsupported block version"); + // Create tx_builder. - let mut tx_builder = TransactionBuilder::new(fog_resolver, EmptyMemoBuilder::default()); + let mut tx_builder = + TransactionBuilder::new(block_version, fog_resolver, EmptyMemoBuilder::default()); tx_builder.set_fee(FEE.load(Ordering::SeqCst)).unwrap(); diff --git a/fog/ingest/server/tests/three_node_cluster.rs b/fog/ingest/server/tests/three_node_cluster.rs index fc78b13092..bd65ab31d6 100644 --- a/fog/ingest/server/tests/three_node_cluster.rs +++ b/fog/ingest/server/tests/three_node_cluster.rs @@ -23,7 +23,7 @@ use mc_transaction_core::{ membership_proofs::Range, ring_signature::KeyImage, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Amount, Block, BlockContents, BlockData, BlockSignature, BLOCK_VERSION, + Amount, Block, BlockContents, BlockData, BlockSignature, BlockVersion, }; use mc_util_from_random::FromRandom; use mc_watcher::watcher_db::WatcherDB; @@ -125,7 +125,12 @@ fn add_test_block(ledger: &mut LedgerDB, watcher: &Watch hash: TxOutMembershipHash::from([0u8; 32]), }; - let block = Block::new_with_parent(BLOCK_VERSION, &last_block, &root_element, &block_contents); + let block = Block::new_with_parent( + BlockVersion::ONE, + &last_block, + &root_element, + &block_contents, + ); let signer = Ed25519Pair::from_random(rng); diff --git a/fog/ledger/server/src/key_image_service.rs b/fog/ledger/server/src/key_image_service.rs index 825a654d8a..324cdcbffc 100644 --- a/fog/ledger/server/src/key_image_service.rs +++ b/fog/ledger/server/src/key_image_service.rs @@ -91,7 +91,7 @@ impl KeyImageService { latest_block_version, max_block_version: core::cmp::max( latest_block_version, - mc_transaction_core::BLOCK_VERSION, + *mc_transaction_core::MAX_BLOCK_VERSION, ), }; diff --git a/fog/ledger/server/src/merkle_proof_service.rs b/fog/ledger/server/src/merkle_proof_service.rs index 4d2393e065..da0dad69f9 100644 --- a/fog/ledger/server/src/merkle_proof_service.rs +++ b/fog/ledger/server/src/merkle_proof_service.rs @@ -143,7 +143,7 @@ impl MerkleProofService { latest_block_version, max_block_version: core::cmp::max( latest_block_version, - mc_transaction_core::BLOCK_VERSION, + *mc_transaction_core::MAX_BLOCK_VERSION, ), }) } diff --git a/fog/ledger/server/tests/connection.rs b/fog/ledger/server/tests/connection.rs index 29fa58ffb0..326f32df38 100644 --- a/fog/ledger/server/tests/connection.rs +++ b/fog/ledger/server/tests/connection.rs @@ -24,7 +24,7 @@ use mc_fog_test_infra::get_enclave_path; use mc_fog_uri::{ConnectionUri, FogLedgerUri}; use mc_ledger_db::{Ledger, LedgerDB}; use mc_transaction_core::{ - ring_signature::KeyImage, tx::TxOut, Block, BlockContents, BlockSignature, BLOCK_VERSION, + ring_signature::KeyImage, tx::TxOut, Block, BlockContents, BlockSignature, BlockVersion, }; use mc_util_from_random::FromRandom; use mc_util_grpc::GrpcRetryConfig; @@ -66,155 +66,166 @@ fn fog_ledger_merkle_proofs_test(logger: Logger) { let mut rng = RngType::from_seed([0u8; 32]); - let alice = AccountKey::random_with_fog(&mut rng); - let bob = AccountKey::random_with_fog(&mut rng); - let charlie = AccountKey::random_with_fog(&mut rng); - - let recipients = vec![ - alice.default_subaddress(), - bob.default_subaddress(), - charlie.default_subaddress(), - ]; - - // Make LedgerDB - let ledger_dir = TempDir::new("fog-ledger").expect("Could not get test_ledger tempdir"); - let db_full_path = ledger_dir.path(); - let mut ledger = generate_ledger_db(db_full_path); - - let (mut watcher, watcher_dir) = setup_watcher_db(logger.clone()); - - // Populate ledger with some data - add_block_to_ledger_db(&mut ledger, &recipients, &[], &mut rng, &mut watcher); - add_block_to_ledger_db( - &mut ledger, - &recipients, - &[KeyImage::from(1)], - &mut rng, - &mut watcher, - ); - let num_blocks = add_block_to_ledger_db( - &mut ledger, - &recipients, - &[KeyImage::from(2)], - &mut rng, - &mut watcher, - ); + for block_version in BlockVersion::iterator() { + let alice = AccountKey::random_with_fog(&mut rng); + let bob = AccountKey::random_with_fog(&mut rng); + let charlie = AccountKey::random_with_fog(&mut rng); - { - // Make LedgerServer - let client_uri = FogLedgerUri::from_str(&format!( - "insecure-fog-ledger://127.0.0.1:{}", - base_port + 7 - )) - .unwrap(); - let config = LedgerServerConfig { - ledger_db: db_full_path.to_path_buf(), - watcher_db: watcher_dir, - admin_listen_uri: Default::default(), - client_listen_uri: client_uri.clone(), - client_responder_id: ResponderId::from_str(&client_uri.addr()).unwrap(), - ias_spid: Default::default(), - ias_api_key: Default::default(), - client_auth_token_secret: None, - client_auth_token_max_lifetime: Default::default(), - omap_capacity: OMAP_CAPACITY, - }; - - let enclave = LedgerSgxEnclave::new( - get_enclave_path(mc_fog_ledger_enclave::ENCLAVE_FILE), - &config.client_responder_id, - OMAP_CAPACITY, - logger.clone(), + let recipients = vec![ + alice.default_subaddress(), + bob.default_subaddress(), + charlie.default_subaddress(), + ]; + + // Make LedgerDB + let ledger_dir = TempDir::new("fog-ledger").expect("Could not get test_ledger tempdir"); + let db_full_path = ledger_dir.path(); + let mut ledger = generate_ledger_db(db_full_path); + + let (mut watcher, watcher_dir) = setup_watcher_db(logger.clone()); + + // Populate ledger with some data + add_block_to_ledger_db( + block_version, + &mut ledger, + &recipients, + &[], + &mut rng, + &mut watcher, ); - - let ra_client = - AttestClient::new(&config.ias_api_key).expect("Could not create IAS client"); - - let grpc_env = Arc::new(grpcio::EnvBuilder::new().build()); - - let mut ledger_server = LedgerServer::new( - config, - enclave, - ledger.clone(), - watcher.clone(), - ra_client, - SystemTimeProvider::default(), - logger.clone(), + add_block_to_ledger_db( + block_version, + &mut ledger, + &recipients, + &[KeyImage::from(1)], + &mut rng, + &mut watcher, + ); + let num_blocks = add_block_to_ledger_db( + block_version, + &mut ledger, + &recipients, + &[KeyImage::from(2)], + &mut rng, + &mut watcher, ); - ledger_server - .start() - .expect("Failed starting ledger server"); - - // Make ledger enclave client - let mut mr_signer_verifier = - MrSignerVerifier::from(mc_fog_ledger_enclave_measurement::sigstruct()); - mr_signer_verifier.allow_hardening_advisory("INTEL-SA-00334"); - - let mut verifier = Verifier::default(); - verifier.mr_signer(mr_signer_verifier).debug(DEBUG_ENCLAVE); + { + // Make LedgerServer + let client_uri = FogLedgerUri::from_str(&format!( + "insecure-fog-ledger://127.0.0.1:{}", + base_port + 7 + )) + .unwrap(); + let config = LedgerServerConfig { + ledger_db: db_full_path.to_path_buf(), + watcher_db: watcher_dir, + admin_listen_uri: Default::default(), + client_listen_uri: client_uri.clone(), + client_responder_id: ResponderId::from_str(&client_uri.addr()).unwrap(), + ias_spid: Default::default(), + ias_api_key: Default::default(), + client_auth_token_secret: None, + client_auth_token_max_lifetime: Default::default(), + omap_capacity: OMAP_CAPACITY, + }; - let mut client = FogMerkleProofGrpcClient::new( - client_uri, - GRPC_RETRY_CONFIG, - verifier, - grpc_env, - logger, - ); + let enclave = LedgerSgxEnclave::new( + get_enclave_path(mc_fog_ledger_enclave::ENCLAVE_FILE), + &config.client_responder_id, + OMAP_CAPACITY, + logger.clone(), + ); + + let ra_client = + AttestClient::new(&config.ias_api_key).expect("Could not create IAS client"); + + let grpc_env = Arc::new(grpcio::EnvBuilder::new().build()); + + let mut ledger_server = LedgerServer::new( + config, + enclave, + ledger.clone(), + watcher.clone(), + ra_client, + SystemTimeProvider::default(), + logger.clone(), + ); + + ledger_server + .start() + .expect("Failed starting ledger server"); + + // Make ledger enclave client + let mut mr_signer_verifier = + MrSignerVerifier::from(mc_fog_ledger_enclave_measurement::sigstruct()); + mr_signer_verifier.allow_hardening_advisory("INTEL-SA-00334"); + + let mut verifier = Verifier::default(); + verifier.mr_signer(mr_signer_verifier).debug(DEBUG_ENCLAVE); + + let mut client = FogMerkleProofGrpcClient::new( + client_uri, + GRPC_RETRY_CONFIG, + verifier, + grpc_env, + logger.clone(), + ); + + // Get merkle root of num_blocks - 1 + let merkle_root = { + let temp = ledger.get_tx_out_proof_of_memberships(&[0u64]).unwrap(); + let merkle_proof = &temp[0]; + mc_transaction_core::membership_proofs::compute_implied_merkle_root(merkle_proof) + .unwrap() + }; - // Get merkle root of num_blocks - 1 - let merkle_root = { - let temp = ledger.get_tx_out_proof_of_memberships(&[0u64]).unwrap(); - let merkle_proof = &temp[0]; - mc_transaction_core::membership_proofs::compute_implied_merkle_root(merkle_proof) - .unwrap() - }; + // Get some tx outs and merkle proofs + let response = client + .get_outputs( + vec![0u64, 1u64, 2u64, 3u64, 4u64, 5u64, 6u64, 7u64, 8u64], + num_blocks - 1, + ) + .expect("get outputs failed"); + + // Test the basic fields + assert_eq!(response.num_blocks, num_blocks); + assert_eq!(response.global_txo_count, ledger.num_txos().unwrap()); + + // Validate merkle proofs + for res in response.results.iter() { + let (tx_out, proof) = res.status().unwrap().unwrap(); + let result = mc_transaction_core::membership_proofs::is_membership_proof_valid( + &tx_out, + &proof, + merkle_root.hash.as_ref(), + ) + .expect("membership proof structure failed!"); + assert!(result, "membership proof was invalid! idx = {}, output = {:?}, proof = {:?}, merkle_root = {:?}", res.index, tx_out, proof, merkle_root); + } - // Get some tx outs and merkle proofs - let response = client - .get_outputs( - vec![0u64, 1u64, 2u64, 3u64, 4u64, 5u64, 6u64, 7u64, 8u64], - num_blocks - 1, - ) - .expect("get outputs failed"); - - // Test the basic fields - assert_eq!(response.num_blocks, num_blocks); - assert_eq!(response.global_txo_count, ledger.num_txos().unwrap()); - - // Validate merkle proofs - for res in response.results.iter() { - let (tx_out, proof) = res.status().unwrap().unwrap(); - let result = mc_transaction_core::membership_proofs::is_membership_proof_valid( - &tx_out, - &proof, - merkle_root.hash.as_ref(), - ) - .expect("membership proof structure failed!"); - assert!(result, "membership proof was invalid! idx = {}, output = {:?}, proof = {:?}, merkle_root = {:?}", res.index, tx_out, proof, merkle_root); + // Make some queries that are out of bounds + let response = client + .get_outputs(vec![1u64, 6u64, 9u64, 14u64], num_blocks - 1) + .expect("get outputs failed"); + + // Test the basic fields + assert_eq!(response.num_blocks, num_blocks); + assert_eq!(response.global_txo_count, ledger.num_txos().unwrap()); + assert_eq!(response.results.len(), 4); + assert!(response.results[0].status().as_ref().unwrap().is_some()); + assert!(response.results[1].status().as_ref().unwrap().is_some()); + assert!(response.results[2].status().as_ref().unwrap().is_none()); + assert!(response.results[3].status().as_ref().unwrap().is_none()); } - // Make some queries that are out of bounds - let response = client - .get_outputs(vec![1u64, 6u64, 9u64, 14u64], num_blocks - 1) - .expect("get outputs failed"); - - // Test the basic fields - assert_eq!(response.num_blocks, num_blocks); - assert_eq!(response.global_txo_count, ledger.num_txos().unwrap()); - assert_eq!(response.results.len(), 4); - assert!(response.results[0].status().as_ref().unwrap().is_some()); - assert!(response.results[1].status().as_ref().unwrap().is_some()); - assert!(response.results[2].status().as_ref().unwrap().is_none()); - assert!(response.results[3].status().as_ref().unwrap().is_none()); + // grpcio detaches all its threads and does not join them :( + // we opened a PR here: https://github.com/tikv/grpc-rs/pull/455 + // in the meantime we can just sleep after grpcio env and all related + // objects have been destroyed, and hope that those 6 threads see the + // shutdown requests within 1 second. + std::thread::sleep(std::time::Duration::from_millis(1000)); } - - // grpcio detaches all its threads and does not join them :( - // we opened a PR here: https://github.com/tikv/grpc-rs/pull/455 - // in the meantime we can just sleep after grpcio env and all related - // objects have been destroyed, and hope that those 6 threads see the - // shutdown requests within 1 second. - std::thread::sleep(std::time::Duration::from_millis(1000)); } // Test that a fog ledger connection is able to check key images by hitting @@ -225,190 +236,207 @@ fn fog_ledger_key_images_test(logger: Logger) { let mut rng = RngType::from_seed([0u8; 32]); - let alice = AccountKey::random_with_fog(&mut rng); - - let recipients = vec![alice.default_subaddress()]; - - let keys: Vec = (0..20).map(|x| KeyImage::from(x as u64)).collect(); - - // Make LedgerDB - let ledger_dir = TempDir::new("fog-ledger").expect("Could not get test_ledger tempdir"); - let db_full_path = ledger_dir.path(); - let mut ledger = generate_ledger_db(db_full_path); - - // Make WatcherDB - let (mut watcher, watcher_dir) = setup_watcher_db(logger.clone()); + for block_version in BlockVersion::iterator() { + let alice = AccountKey::random_with_fog(&mut rng); - // Populate ledger with some data - // Origin block cannot have key images - add_block_to_ledger_db(&mut ledger, &recipients, &[], &mut rng, &mut watcher); - add_block_to_ledger_db( - &mut ledger, - &recipients, - &keys[0..2], - &mut rng, - &mut watcher, - ); - add_block_to_ledger_db( - &mut ledger, - &recipients, - &keys[3..6], - &mut rng, - &mut watcher, - ); - let num_blocks = add_block_to_ledger_db( - &mut ledger, - &recipients, - &keys[6..9], - &mut rng, - &mut watcher, - ); + let recipients = vec![alice.default_subaddress()]; - // Populate watcher with Signature and Timestamp for block 1 - let url1 = Url::parse(TEST_URL).unwrap(); - let block1 = ledger.get_block(1).unwrap(); - let signing_key_a = Ed25519Pair::from_random(&mut rng); - let filename = String::from("00/00"); - let mut signed_block_a1 = - BlockSignature::from_block_and_keypair(&block1, &signing_key_a).unwrap(); - signed_block_a1.set_signed_at(1593798844); - watcher - .add_block_signature(&url1, 1, signed_block_a1, filename.clone()) - .unwrap(); + let keys: Vec = (0..20).map(|x| KeyImage::from(x as u64)).collect(); - // Update last synced to block 2, to indicate that this URL did not participate - // in consensus for block 2. - watcher.update_last_synced(&url1, 2).unwrap(); + // Make LedgerDB + let ledger_dir = TempDir::new("fog-ledger").expect("Could not get test_ledger tempdir"); + let db_full_path = ledger_dir.path(); + let mut ledger = generate_ledger_db(db_full_path); - { - // Make LedgerServer - let client_uri = FogLedgerUri::from_str(&format!( - "insecure-fog-ledger://127.0.0.1:{}", - base_port + 7 - )) - .unwrap(); - let config = LedgerServerConfig { - ledger_db: db_full_path.to_path_buf(), - watcher_db: watcher_dir, - admin_listen_uri: Default::default(), - client_listen_uri: client_uri.clone(), - client_responder_id: ResponderId::from_str(&client_uri.addr()).unwrap(), - ias_spid: Default::default(), - ias_api_key: Default::default(), - client_auth_token_secret: None, - client_auth_token_max_lifetime: Default::default(), - omap_capacity: OMAP_CAPACITY, - }; + // Make WatcherDB + let (mut watcher, watcher_dir) = setup_watcher_db(logger.clone()); - let enclave = LedgerSgxEnclave::new( - get_enclave_path(mc_fog_ledger_enclave::ENCLAVE_FILE), - &config.client_responder_id, - OMAP_CAPACITY, - logger.clone(), + // Populate ledger with some data + // Origin block cannot have key images + add_block_to_ledger_db( + block_version, + &mut ledger, + &recipients, + &[], + &mut rng, + &mut watcher, ); - - let ra_client = - AttestClient::new(&config.ias_api_key).expect("Could not create IAS client"); - - let grpc_env = Arc::new(grpcio::EnvBuilder::new().build()); - - let mut ledger_server = LedgerServer::new( - config, - enclave, - ledger.clone(), - watcher, - ra_client, - SystemTimeProvider::default(), - logger.clone(), + add_block_to_ledger_db( + block_version, + &mut ledger, + &recipients, + &keys[0..2], + &mut rng, + &mut watcher, + ); + add_block_to_ledger_db( + block_version, + &mut ledger, + &recipients, + &keys[3..6], + &mut rng, + &mut watcher, + ); + let num_blocks = add_block_to_ledger_db( + block_version, + &mut ledger, + &recipients, + &keys[6..9], + &mut rng, + &mut watcher, ); - ledger_server - .start() - .expect("Failed starting ledger server"); - - // Make ledger enclave client - let mut mr_signer_verifier = - MrSignerVerifier::from(mc_fog_ledger_enclave_measurement::sigstruct()); - mr_signer_verifier.allow_hardening_advisory("INTEL-SA-00334"); - - let mut verifier = Verifier::default(); - verifier.mr_signer(mr_signer_verifier).debug(DEBUG_ENCLAVE); - - let mut client = - FogKeyImageGrpcClient::new(client_uri, GRPC_RETRY_CONFIG, verifier, grpc_env, logger); - - // Check on key images - let mut response = client - .check_key_images(&[keys[0], keys[1], keys[3], keys[7], keys[19]]) - .expect("check_key_images failed"); + // Populate watcher with Signature and Timestamp for block 1 + let url1 = Url::parse(TEST_URL).unwrap(); + let block1 = ledger.get_block(1).unwrap(); + let signing_key_a = Ed25519Pair::from_random(&mut rng); + let filename = String::from("00/00"); + let mut signed_block_a1 = + BlockSignature::from_block_and_keypair(&block1, &signing_key_a).unwrap(); + signed_block_a1.set_signed_at(1593798844); + watcher + .add_block_signature(&url1, 1, signed_block_a1, filename.clone()) + .unwrap(); + + // Update last synced to block 2, to indicate that this URL did not participate + // in consensus for block 2. + watcher.update_last_synced(&url1, 2).unwrap(); + + { + // Make LedgerServer + let client_uri = FogLedgerUri::from_str(&format!( + "insecure-fog-ledger://127.0.0.1:{}", + base_port + 7 + )) + .unwrap(); + let config = LedgerServerConfig { + ledger_db: db_full_path.to_path_buf(), + watcher_db: watcher_dir, + admin_listen_uri: Default::default(), + client_listen_uri: client_uri.clone(), + client_responder_id: ResponderId::from_str(&client_uri.addr()).unwrap(), + ias_spid: Default::default(), + ias_api_key: Default::default(), + client_auth_token_secret: None, + client_auth_token_max_lifetime: Default::default(), + omap_capacity: OMAP_CAPACITY, + }; - let mut n = 1; - // adding a delay to give fog ledger time to fully initialize - while response.num_blocks != num_blocks { - response = client + let enclave = LedgerSgxEnclave::new( + get_enclave_path(mc_fog_ledger_enclave::ENCLAVE_FILE), + &config.client_responder_id, + OMAP_CAPACITY, + logger.clone(), + ); + + let ra_client = + AttestClient::new(&config.ias_api_key).expect("Could not create IAS client"); + + let grpc_env = Arc::new(grpcio::EnvBuilder::new().build()); + + let mut ledger_server = LedgerServer::new( + config, + enclave, + ledger.clone(), + watcher, + ra_client, + SystemTimeProvider::default(), + logger.clone(), + ); + + ledger_server + .start() + .expect("Failed starting ledger server"); + + // Make ledger enclave client + let mut mr_signer_verifier = + MrSignerVerifier::from(mc_fog_ledger_enclave_measurement::sigstruct()); + mr_signer_verifier.allow_hardening_advisory("INTEL-SA-00334"); + + let mut verifier = Verifier::default(); + verifier.mr_signer(mr_signer_verifier).debug(DEBUG_ENCLAVE); + + let mut client = FogKeyImageGrpcClient::new( + client_uri, + GRPC_RETRY_CONFIG, + verifier, + grpc_env, + logger.clone(), + ); + + // Check on key images + let mut response = client .check_key_images(&[keys[0], keys[1], keys[3], keys[7], keys[19]]) .expect("check_key_images failed"); - thread::sleep(time::Duration::from_secs(10)); - // panic on the 20th time - n += 1; // - if n > 20 { - panic!("Fog ledger not fully initialized"); + let mut n = 1; + // adding a delay to give fog ledger time to fully initialize + while response.num_blocks != num_blocks { + response = client + .check_key_images(&[keys[0], keys[1], keys[3], keys[7], keys[19]]) + .expect("check_key_images failed"); + + thread::sleep(time::Duration::from_secs(10)); + // panic on the 20th time + n += 1; // + if n > 20 { + panic!("Fog ledger not fully initialized"); + } } - } - // FIXME assert_eq!(response.num_txos, ...); - assert_eq!(response.results[0].key_image, keys[0]); - assert_eq!(response.results[0].status(), Ok(Some(1))); - assert_eq!(response.results[0].timestamp, 100); - assert_eq!( - response.results[0].timestamp_result_code, - TimestampResultCode::TimestampFound as u32 - ); - assert_eq!(response.results[1].key_image, keys[1]); - assert_eq!(response.results[1].status(), Ok(Some(1))); - assert_eq!(response.results[1].timestamp, 100); - assert_eq!( - response.results[1].timestamp_result_code, - TimestampResultCode::TimestampFound as u32 - ); - - // Check a key_image for a block which will never have signatures & timestamps - assert_eq!(response.results[2].key_image, keys[3]); - assert_eq!(response.results[2].status(), Ok(Some(2))); // Spent in block 2 - assert_eq!(response.results[2].timestamp, 200); - assert_eq!( - response.results[2].timestamp_result_code, - TimestampResultCode::TimestampFound as u32 - ); + // FIXME assert_eq!(response.num_txos, ...); + assert_eq!(response.results[0].key_image, keys[0]); + assert_eq!(response.results[0].status(), Ok(Some(1))); + assert_eq!(response.results[0].timestamp, 100); + assert_eq!( + response.results[0].timestamp_result_code, + TimestampResultCode::TimestampFound as u32 + ); + assert_eq!(response.results[1].key_image, keys[1]); + assert_eq!(response.results[1].status(), Ok(Some(1))); + assert_eq!(response.results[1].timestamp, 100); + assert_eq!( + response.results[1].timestamp_result_code, + TimestampResultCode::TimestampFound as u32 + ); + + // Check a key_image for a block which will never have signatures & timestamps + assert_eq!(response.results[2].key_image, keys[3]); + assert_eq!(response.results[2].status(), Ok(Some(2))); // Spent in block 2 + assert_eq!(response.results[2].timestamp, 200); + assert_eq!( + response.results[2].timestamp_result_code, + TimestampResultCode::TimestampFound as u32 + ); + + // Watcher has only synced 1 block, so timestamp should be behind + assert_eq!(response.results[3].key_image, keys[7]); + assert_eq!(response.results[3].status(), Ok(Some(3))); // Spent in block 3 + assert_eq!(response.results[3].timestamp, 300); + assert_eq!( + response.results[3].timestamp_result_code, + TimestampResultCode::TimestampFound as u32 + ); + + // Check a key_image that has not been spent + assert_eq!(response.results[4].key_image, keys[19]); + assert_eq!(response.results[4].status(), Ok(None)); // Not spent + assert_eq!(response.results[4].timestamp, u64::MAX); + assert_eq!( + response.results[4].timestamp_result_code, + TimestampResultCode::TimestampFound as u32 + ); + } - // Watcher has only synced 1 block, so timestamp should be behind - assert_eq!(response.results[3].key_image, keys[7]); - assert_eq!(response.results[3].status(), Ok(Some(3))); // Spent in block 3 - assert_eq!(response.results[3].timestamp, 300); - assert_eq!( - response.results[3].timestamp_result_code, - TimestampResultCode::TimestampFound as u32 - ); + // FIXME: Check a key_image that generates a DatabaseError - tough to generate - // Check a key_image that has not been spent - assert_eq!(response.results[4].key_image, keys[19]); - assert_eq!(response.results[4].status(), Ok(None)); // Not spent - assert_eq!(response.results[4].timestamp, u64::MAX); - assert_eq!( - response.results[4].timestamp_result_code, - TimestampResultCode::TimestampFound as u32 - ); + // grpcio detaches all its threads and does not join them :( + // we opened a PR here: https://github.com/tikv/grpc-rs/pull/455 + // in the meantime we can just sleep after grpcio env and all related + // objects have been destroyed, and hope that those 6 threads see the + // shutdown requests within 1 second. + std::thread::sleep(std::time::Duration::from_millis(1000)); } - - // FIXME: Check a key_image that generates a DatabaseError - tough to generate - - // grpcio detaches all its threads and does not join them :( - // we opened a PR here: https://github.com/tikv/grpc-rs/pull/455 - // in the meantime we can just sleep after grpcio env and all related - // objects have been destroyed, and hope that those 6 threads see the - // shutdown requests within 1 second. - std::thread::sleep(std::time::Duration::from_millis(1000)); } // Test that a fog ledger connection is able to check key images by hitting @@ -435,6 +463,7 @@ fn fog_ledger_blocks_api_test(logger: Logger) { // Populate ledger with some data // Origin block cannot have key images add_block_to_ledger_db( + BlockVersion::MAX, &mut ledger, &[alice.default_subaddress()], &[], @@ -442,6 +471,7 @@ fn fog_ledger_blocks_api_test(logger: Logger) { &mut watcher, ); add_block_to_ledger_db( + BlockVersion::MAX, &mut ledger, &[alice.default_subaddress(), bob.default_subaddress()], &[KeyImage::from(1)], @@ -449,6 +479,7 @@ fn fog_ledger_blocks_api_test(logger: Logger) { &mut watcher, ); add_block_to_ledger_db( + BlockVersion::MAX, &mut ledger, &[ alice.default_subaddress(), @@ -460,6 +491,7 @@ fn fog_ledger_blocks_api_test(logger: Logger) { &mut watcher, ); let num_blocks = add_block_to_ledger_db( + BlockVersion::MAX, &mut ledger, &recipients, &[KeyImage::from(3)], @@ -592,6 +624,7 @@ fn fog_ledger_untrusted_tx_out_api_test(logger: Logger) { // Populate ledger with some data // Origin block cannot have key images add_block_to_ledger_db( + BlockVersion::MAX, &mut ledger, &[alice.default_subaddress()], &[], @@ -599,6 +632,7 @@ fn fog_ledger_untrusted_tx_out_api_test(logger: Logger) { &mut watcher, ); add_block_to_ledger_db( + BlockVersion::MAX, &mut ledger, &[alice.default_subaddress(), bob.default_subaddress()], &[KeyImage::from(1)], @@ -606,6 +640,7 @@ fn fog_ledger_untrusted_tx_out_api_test(logger: Logger) { &mut watcher, ); add_block_to_ledger_db( + BlockVersion::MAX, &mut ledger, &[ alice.default_subaddress(), @@ -617,6 +652,7 @@ fn fog_ledger_untrusted_tx_out_api_test(logger: Logger) { &mut watcher, ); let _num_blocks = add_block_to_ledger_db( + BlockVersion::MAX, &mut ledger, &recipients, &[KeyImage::from(3)], @@ -737,6 +773,7 @@ fn generate_ledger_db(path: &Path) -> LedgerDB { /// * `recipients` - Recipients of outputs. /// * `rng` fn add_block_to_ledger_db( + block_version: BlockVersion, ledger_db: &mut LedgerDB, recipients: &[PublicAddress], key_images: &[KeyImage], @@ -798,7 +835,7 @@ fn add_block_to_ledger_db( .get_block(num_blocks - 1) .expect("failed to get parent block"); new_block = - Block::new_with_parent(BLOCK_VERSION, &parent, &Default::default(), &block_contents); + Block::new_with_parent(block_version, &parent, &Default::default(), &block_contents); } else { new_block = Block::new_origin_block(&outputs); } diff --git a/fog/load_testing/src/bin/ingest.rs b/fog/load_testing/src/bin/ingest.rs index e7d6b56fdc..4ef603b941 100644 --- a/fog/load_testing/src/bin/ingest.rs +++ b/fog/load_testing/src/bin/ingest.rs @@ -22,7 +22,7 @@ use mc_fog_recovery_db_iface::RecoveryDb; use mc_fog_sql_recovery_db::test_utils::SqlRecoveryDbTestContext; use mc_fog_uri::{ConnectionUri, FogIngestUri, IngestPeerUri}; use mc_ledger_db::{Ledger, LedgerDB}; -use mc_transaction_core::{Block, BlockContents, BlockSignature}; +use mc_transaction_core::{Block, BlockContents, BlockSignature, BlockVersion}; use mc_util_from_random::FromRandom; use mc_util_grpc::{admin_grpc::AdminApiClient, ConnectionUriGrpcioChannel, Empty}; use mc_util_uri::AdminUri; @@ -197,7 +197,10 @@ fn load_test(ingest_server_binary: &Path, test_params: TestParams, logger: Logge LedgerDB::create(ledger_db_path.path()).unwrap(); let mut ledger_db = LedgerDB::open(ledger_db_path.path()).unwrap(); + let block_version = BlockVersion::ONE; + mc_transaction_core_test_utils::initialize_ledger( + block_version, &mut ledger_db, 1u64, &AccountKey::random(&mut McRng {}), @@ -301,6 +304,7 @@ fn load_test(ingest_server_binary: &Path, test_params: TestParams, logger: Logge .collect::>(); let results: Vec<(Block, BlockContents)> = mc_transaction_core_test_utils::get_blocks( + block_version, &recipient_pub_keys[..], REPETITIONS, CHUNK_SIZE, diff --git a/fog/overseer/server/tests/utils/mod.rs b/fog/overseer/server/tests/utils/mod.rs index 10080fc00f..4428134e92 100644 --- a/fog/overseer/server/tests/utils/mod.rs +++ b/fog/overseer/server/tests/utils/mod.rs @@ -19,7 +19,7 @@ use mc_transaction_core::{ membership_proofs::Range, ring_signature::KeyImage, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Amount, Block, BlockContents, BlockData, BlockSignature, BLOCK_VERSION, + Amount, Block, BlockContents, BlockData, BlockSignature, BlockVersion, }; use mc_util_from_random::FromRandom; use mc_watcher::watcher_db::WatcherDB; @@ -130,7 +130,12 @@ pub fn add_test_block( hash: TxOutMembershipHash::from([0u8; 32]), }; - let block = Block::new_with_parent(BLOCK_VERSION, &last_block, &root_element, &block_contents); + let block = Block::new_with_parent( + BlockVersion::ONE, + &last_block, + &root_element, + &block_contents, + ); let signer = Ed25519Pair::from_random(rng); diff --git a/fog/sample-paykit/src/cached_tx_data/mod.rs b/fog/sample-paykit/src/cached_tx_data/mod.rs index de6d67818a..267064b055 100644 --- a/fog/sample-paykit/src/cached_tx_data/mod.rs +++ b/fog/sample-paykit/src/cached_tx_data/mod.rs @@ -105,6 +105,10 @@ pub struct CachedTxData { /// but might ideally take into account fog view server responses as well. /// However, that would require a change that would conflict with SQL PR. latest_global_txo_count: u64, + /// The latest block version that we have heard about. + /// This is used by the transaction builder to target the correct block + /// version. + latest_block_version: u32, /// A memo handler which attempts to decrypt memos and validate them memo_handler: MemoHandler, /// A pre-calculated map of subaddress public spend key to subaddress index. @@ -129,6 +133,7 @@ impl CachedTxData { owned_tx_outs: Default::default(), key_image_data_completeness: BlockCount::MAX, latest_global_txo_count: 0, + latest_block_version: 1, memo_handler: MemoHandler::new(address_book, logger.clone()), spsk_to_index, missed_block_ranges: Vec::new(), @@ -176,6 +181,15 @@ impl CachedTxData { self.latest_global_txo_count } + /// Get the latest_block_version + /// + /// This is the latest value of block_version known to be in the blockchain. + /// Note that this may not be a valid block version according to our copy + /// of mc-transaction-core. + pub fn get_latest_block_version(&self) -> u32 { + self.latest_block_version + } + /// Helper function: Compute the set of Txos contributing to the balance, /// not known to be spent at all. /// These can be used creating transaction input sets. @@ -661,7 +675,16 @@ impl CachedTxData { match key_image_client.check_key_images(key_images) { Ok(response) => { self.latest_global_txo_count = - core::cmp::max(self.latest_global_txo_count, response.global_txo_count); + max(self.latest_global_txo_count, response.global_txo_count); + // Note: latest_block_version is only increasing on the block chain, since + // the network enforces that each block version is at least as large as its + // parent. However, the client could talk to ledger servers + // that are ahead and then to ledger servers that are + // behind. Putting a max here on the client side helps + // protect the client from being "poisoned" by talking to a ledger server that + // is behind, and having a subsequent Tx fail validation. + self.latest_block_version = + max(self.latest_block_version, response.latest_block_version); for result in response.results.iter() { if let Some(global_index) = key_image_to_global_index.get(&result.key_image) { diff --git a/fog/sample-paykit/src/client.rs b/fog/sample-paykit/src/client.rs index 0f65bdfe6f..3e70e55186 100644 --- a/fog/sample-paykit/src/client.rs +++ b/fog/sample-paykit/src/client.rs @@ -33,11 +33,11 @@ use mc_transaction_core::{ ring_signature::KeyImage, tokens::Mob, tx::{Tx, TxOut, TxOutMembershipProof}, - BlockIndex, Token, + BlockIndex, BlockVersion, Token, }; use mc_transaction_std::{ - ChangeDestination, InputCredentials, MemoType, NoMemoBuilder, RTHMemoBuilder, - SenderMemoCredential, TransactionBuilder, + ChangeDestination, InputCredentials, MemoType, RTHMemoBuilder, SenderMemoCredential, + TransactionBuilder, }; use mc_util_telemetry::{block_span_builder, telemetry_static_key, tracer, Key, Span}; use mc_util_uri::{ConnectionUri, FogUri}; @@ -70,9 +70,6 @@ pub struct Client { /// tombstone block when generating a new transaction. new_tx_block_attempts: u16, - /// Whether to use RTH memos. For backwards compat, we can turn memos off. - use_rth_memos: bool, - logger: Logger, } @@ -90,7 +87,6 @@ impl Client { ring_size: usize, account_key: AccountKey, address_book: Vec, - use_rth_memos: bool, logger: Logger, ) -> Self { let tx_data = CachedTxData::new(account_key.clone(), address_book, logger.clone()); @@ -108,7 +104,6 @@ impl Client { account_key, tx_data, new_tx_block_attempts: DEFAULT_NEW_TX_BLOCK_ATTEMPTS, - use_rth_memos, logger, } } @@ -159,7 +154,7 @@ impl Client { /// * Balance (in picomob) /// * Number of blocks in the chain at the time that this was the correct /// balance - pub fn compute_balance(&mut self) -> (u64, BlockCount) { + pub fn compute_balance(&self) -> (u64, BlockCount) { self.tx_data.get_balance() } @@ -169,10 +164,16 @@ impl Client { } /// Get the last memo (or validation error) that we recieved from a TxOut - pub fn get_last_memo(&mut self) -> &StdResult, MemoHandlerError> { + pub fn get_last_memo(&self) -> &StdResult, MemoHandlerError> { self.tx_data.get_last_memo() } + /// Get the latest block version that we heard about from fog + /// Note that this may not be a "valid" block version if our software is old + pub fn get_latest_block_version(&self) -> u32 { + self.tx_data.get_latest_block_version() + } + /// Submits a transaction to the MobileCoin network. /// /// To get a transaction, call build_transaction. @@ -327,6 +328,8 @@ impl Client { let tombstone_block = self.compute_tombstone_block()?; + let block_version = BlockVersion::try_from(self.tx_data.get_latest_block_version())?; + // Make fog resolver // TODO: This should be the change subaddress, not the default subaddress, for // self.account_key @@ -341,6 +344,7 @@ impl Client { let fog_resolver = FogResolver::new(fog_responses, &self.fog_verifier)?; build_transaction_helper( + block_version, inputs, rings, amount, @@ -348,7 +352,6 @@ impl Client { target_address, tombstone_block, fog_resolver, - self.use_rth_memos, rng, &self.logger, fee, @@ -541,6 +544,7 @@ impl Client { /// and returns the remainder to the sender minus the transaction fee. /// /// # Arguments +/// * `block_version` - The block version to target /// * `inputs` - Inputs that will be spent by the transaction. /// * `rings` - A ring of TxOuts and membership proofs for each input. /// * `amount` - The amount that will be sent. @@ -552,6 +556,7 @@ impl Client { /// longer valid. /// * `rng` - fn build_transaction_helper( + block_version: BlockVersion, inputs: Vec<(OwnedTxOut, TxOutMembershipProof)>, rings: Vec>, amount: u64, @@ -559,7 +564,6 @@ fn build_transaction_helper( target_address: &PublicAddress, tombstone_block: BlockIndex, fog_resolver: FPR, - use_rth_memos: bool, rng: &mut T, logger: &Logger, fee: u64, @@ -574,16 +578,14 @@ fn build_transaction_helper( return Err(Error::RingsForInput(rings.len(), inputs.len())); } - // Use the RTHMemoBuilder if memos are enabled, NoMemoBuilder otherwise - let mut tx_builder = if use_rth_memos { + // Use the RTHMemoBuilder + // Note: Memos are disabled if we target an older block version + let mut tx_builder = { let mut memo_builder = RTHMemoBuilder::default(); memo_builder.set_sender_credential(SenderMemoCredential::from(source_account_key)); memo_builder.enable_destination_memo(); - TransactionBuilder::new(fog_resolver, memo_builder) - } else { - let memo_builder = NoMemoBuilder::default(); - TransactionBuilder::new(fog_resolver, memo_builder) + TransactionBuilder::new(block_version, fog_resolver, memo_builder) }; tx_builder.set_fee(fee)?; @@ -728,115 +730,120 @@ mod test_build_transaction_helper { fn test_build_transaction_helper_rings_disjoint_from_inputs(logger: Logger) { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender_account_key = AccountKey::random(&mut rng); - let sender_public_address = sender_account_key.default_subaddress(); + for block_version in BlockVersion::iterator() { + let sender_account_key = AccountKey::random(&mut rng); + let sender_public_address = sender_account_key.default_subaddress(); - // Amount per input. - let initial_amount = 300 * MILLIMOB_TO_PICOMOB; - let amount_to_send = 457 * MILLIMOB_TO_PICOMOB; - let num_inputs = 3; - let ring_size = 1; - - // Create inputs. - let inputs = { - let mut recipient_and_amount: Vec<(PublicAddress, u64)> = Vec::new(); - for _i in 0..num_inputs { - recipient_and_amount.push((sender_public_address.clone(), initial_amount)); - } - let outputs = get_outputs(&recipient_and_amount, &mut rng); + // Amount per input. + let initial_amount = 300 * MILLIMOB_TO_PICOMOB; + let amount_to_send = 457 * MILLIMOB_TO_PICOMOB; + let num_inputs = 3; + let ring_size = 1; - let cached_inputs: Vec<(OwnedTxOut, TxOutMembershipProof)> = outputs - .into_iter() - .map(|tx_out| { - let fog_tx_out = FogTxOut::from(&tx_out); - let meta = FogTxOutMetadata::default(); - let txo_record = TxOutRecord::new(fog_tx_out, meta); + // Create inputs. + let inputs = { + let mut recipient_and_amount: Vec<(PublicAddress, u64)> = Vec::new(); + for _i in 0..num_inputs { + recipient_and_amount.push((sender_public_address.clone(), initial_amount)); + } + let outputs = get_outputs(block_version, &recipient_and_amount, &mut rng); + + let cached_inputs: Vec<(OwnedTxOut, TxOutMembershipProof)> = outputs + .into_iter() + .map(|tx_out| { + let fog_tx_out = FogTxOut::from(&tx_out); + let meta = FogTxOutMetadata::default(); + let txo_record = TxOutRecord::new(fog_tx_out, meta); + + let tx_out_target_key = + RistrettoPublic::try_from(&tx_out.target_key).unwrap(); + let tx_public_key = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); + + let subaddress_spk = recover_public_subaddress_spend_key( + sender_account_key.view_private_key(), + &tx_out_target_key, + &tx_public_key, + ); + let spsk_to_index = + HashMap::from_iter(vec![(subaddress_spk, DEFAULT_SUBADDRESS_INDEX)]); - let tx_out_target_key = RistrettoPublic::try_from(&tx_out.target_key).unwrap(); - let tx_public_key = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); + let owned_tx_out = + OwnedTxOut::new(txo_record, &sender_account_key, &spsk_to_index) + .unwrap(); - let subaddress_spk = recover_public_subaddress_spend_key( - sender_account_key.view_private_key(), - &tx_out_target_key, - &tx_public_key, - ); - let spsk_to_index = - HashMap::from_iter(vec![(subaddress_spk, DEFAULT_SUBADDRESS_INDEX)]); + let proof = TxOutMembershipProof::new(0, 0, Default::default()); - let owned_tx_out = - OwnedTxOut::new(txo_record, &sender_account_key, &spsk_to_index).unwrap(); + (owned_tx_out, proof) + }) + .collect(); - let proof = TxOutMembershipProof::new(0, 0, Default::default()); + cached_inputs + }; - (owned_tx_out, proof) - }) - .collect(); + assert_eq!(inputs.len(), num_inputs); - cached_inputs - }; + // Create rings. + let mut rings: Vec> = Vec::new(); + for _i in 0..num_inputs { + let ring: Vec = { + let mut recipient_and_amount: Vec<(PublicAddress, u64)> = Vec::new(); + for _i in 0..ring_size { + recipient_and_amount.push((sender_public_address.clone(), 33)); + } + get_outputs(block_version, &recipient_and_amount, &mut rng) + }; + assert_eq!(ring.len(), ring_size); + rings.push(ring); + } - assert_eq!(inputs.len(), num_inputs); + assert_eq!(inputs.len(), rings.len()); + + let mut rings_and_membership_proofs: Vec> = + Vec::new(); + for ring in rings.into_iter() { + let ring_with_proofs = ring + .into_iter() + .map(|tx_out| { + let membership_proof = TxOutMembershipProof::new(0, 0, Default::default()); + (tx_out, membership_proof) + }) + .collect(); + rings_and_membership_proofs.push(ring_with_proofs); + } - // Create rings. - let mut rings: Vec> = Vec::new(); - for _i in 0..num_inputs { - let ring: Vec = { - let mut recipient_and_amount: Vec<(PublicAddress, u64)> = Vec::new(); - for _i in 0..ring_size { - recipient_and_amount.push((sender_public_address.clone(), 33)); - } - get_outputs(&recipient_and_amount, &mut rng) - }; - assert_eq!(ring.len(), ring_size); - rings.push(ring); - } + let recipient_account_key = AccountKey::random(&mut rng); + + let fake_acct_resolver = FakeAcctResolver {}; + let tx = build_transaction_helper( + block_version, + inputs, + rings_and_membership_proofs, + amount_to_send, + &sender_account_key, + &recipient_account_key.default_subaddress(), + super::BlockIndex::max_value(), + fake_acct_resolver, + &mut rng, + &logger, + Mob::MINIMUM_FEE, + ) + .unwrap(); - assert_eq!(inputs.len(), rings.len()); - - let mut rings_and_membership_proofs: Vec> = Vec::new(); - for ring in rings.into_iter() { - let ring_with_proofs = ring - .into_iter() - .map(|tx_out| { - let membership_proof = TxOutMembershipProof::new(0, 0, Default::default()); - (tx_out, membership_proof) - }) - .collect(); - rings_and_membership_proofs.push(ring_with_proofs); - } + // The transaction should contain the correct number of inputs. + assert_eq!(tx.prefix.inputs.len(), num_inputs); - let recipient_account_key = AccountKey::random(&mut rng); + // Each TxIn should contain a ring of `ring_size` elements. If `ring_size` is + // zero, the ring will have size 1 after the input is included. + for tx_in in tx.prefix.inputs { + assert_eq!(tx_in.ring.len(), ring_size); + } - let fake_acct_resolver = FakeAcctResolver {}; - let tx = build_transaction_helper( - inputs, - rings_and_membership_proofs, - amount_to_send, - &sender_account_key, - &recipient_account_key.default_subaddress(), - super::BlockIndex::max_value(), - fake_acct_resolver, - true, - &mut rng, - &logger, - Mob::MINIMUM_FEE, - ) - .unwrap(); + // TODO: `tx` should contain the correct number of outputs. - // The transaction should contain the correct number of inputs. - assert_eq!(tx.prefix.inputs.len(), num_inputs); + // TODO: `tx` should send the correct amount to the recipient. - // Each TxIn should contain a ring of `ring_size` elements. If `ring_size` is - // zero, the ring will have size 1 after the input is included. - for tx_in in tx.prefix.inputs { - assert_eq!(tx_in.ring.len(), ring_size); + // TODO: `tx` should return the correct "change" to the sender. } - - // TODO: `tx` should contain the correct number of outputs. - - // TODO: `tx` should send the correct amount to the recipient. - - // TODO: `tx` should return the correct "change" to the sender. } #[test] diff --git a/fog/sample-paykit/src/client_builder.rs b/fog/sample-paykit/src/client_builder.rs index e86af72531..0fc6f581a7 100644 --- a/fog/sample-paykit/src/client_builder.rs +++ b/fog/sample-paykit/src/client_builder.rs @@ -34,9 +34,6 @@ pub struct ClientBuilder { // Optional, has sane defaults ring_size: usize, - // Whether to use memos. For backwards compat, turn this off - use_rth_memos: bool, - // Uris to fog services fog_view_address: FogViewUri, ledger_server_address: FogLedgerUri, @@ -66,7 +63,6 @@ impl ClientBuilder { logger, grpc_retry_config: Default::default(), ring_size: RING_SIZE, - use_rth_memos: true, fog_view_address, ledger_server_address, address_book: Default::default(), @@ -91,13 +87,6 @@ impl ClientBuilder { retval } - /// Sets whether or not to use memos - pub fn use_rth_memos(self, flag: bool) -> Self { - let mut retval = self; - retval.use_rth_memos = flag; - retval - } - /// Sets the address book for the client, used with memos pub fn address_book(self, address_book: Vec) -> Self { let mut retval = self; @@ -190,7 +179,6 @@ impl ClientBuilder { self.ring_size, self.key.clone(), self.address_book.clone(), - self.use_rth_memos, self.logger.clone(), ) } diff --git a/fog/sample-paykit/src/error.rs b/fog/sample-paykit/src/error.rs index e9b2284089..cf5e3caa73 100644 --- a/fog/sample-paykit/src/error.rs +++ b/fog/sample-paykit/src/error.rs @@ -11,7 +11,7 @@ use mc_fog_ledger_connection::{Error as LedgerConnectionError, KeyImageQueryErro use mc_fog_report_connection::Error as FogResolutionError; use mc_fog_types::view::FogTxOutError; use mc_fog_view_protocol::TxOutPollingError; -use mc_transaction_core::{validation::TransactionValidationError, AmountError}; +use mc_transaction_core::{validation::TransactionValidationError, AmountError, BlockVersionError}; use mc_transaction_std::TxBuilderError; use mc_util_uri::UriParseError; use std::result::Result as StdResult; @@ -111,6 +111,9 @@ pub enum Error { /// Could not parse uri: {0} Uri(UriParseError), + + /// Block version error: {0} + BlockVersion(BlockVersionError), } impl From for Error { @@ -175,3 +178,9 @@ impl From for Error { Self::Uri(src) } } + +impl From for Error { + fn from(src: BlockVersionError) -> Self { + Self::BlockVersion(src) + } +} diff --git a/fog/test-client/src/error.rs b/fog/test-client/src/error.rs index bcaace54db..0b7c8122c2 100644 --- a/fog/test-client/src/error.rs +++ b/fog/test-client/src/error.rs @@ -4,6 +4,7 @@ use displaydoc::Display; use mc_fog_sample_paykit::Error as SamplePaykitError; +use mc_transaction_core::BlockVersionError; /// Error that can occur when running a test transfer #[derive(Display, Debug)] @@ -32,4 +33,12 @@ pub enum TestClientError { SubmitTx(SamplePaykitError), /// Client error while confirming a transaction: {0} ConfirmTx(SamplePaykitError), + /// Block version error: {0} + BlockVersion(BlockVersionError), +} + +impl From for TestClientError { + fn from(src: BlockVersionError) -> Self { + Self::BlockVersion(src) + } } diff --git a/fog/test-client/src/test_client.rs b/fog/test-client/src/test_client.rs index b61170960d..e9de71886c 100644 --- a/fog/test-client/src/test_client.rs +++ b/fog/test-client/src/test_client.rs @@ -14,7 +14,7 @@ use mc_crypto_rand::McRng; use mc_fog_sample_paykit::{AccountKey, Client, ClientBuilder, TransactionStatus, Tx}; use mc_fog_uri::{FogLedgerUri, FogViewUri}; use mc_sgx_css::Signature; -use mc_transaction_core::{constants::RING_SIZE, tokens::Mob, BlockIndex, Token}; +use mc_transaction_core::{constants::RING_SIZE, tokens::Mob, BlockIndex, BlockVersion, Token}; use mc_transaction_std::MemoType; use mc_util_grpc::GrpcRetryConfig; use mc_util_telemetry::{ @@ -26,6 +26,7 @@ use more_asserts::assert_gt; use once_cell::sync::OnceCell; use serde::Serialize; use std::{ + convert::TryFrom, ops::Sub, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, @@ -203,7 +204,6 @@ impl TestClient { ) .grpc_retry_config(self.grpc_retry_config) .ring_size(RING_SIZE) - .use_rth_memos(self.policy.test_rth_memos) .address_book(address_book.clone()) .consensus_sig(self.consensus_sig.clone()) .fog_ingest_sig(self.fog_ingest_sig.clone()) @@ -546,59 +546,63 @@ impl TestClient { receive_tx_worker.join()?; if self.policy.test_rth_memos { - // Ensure source client got a destination memo, as expected for recoverable - // transcation history - match source_client_lk.get_last_memo() { - Ok(Some(memo)) => match memo { - MemoType::Destination(memo) => { - if memo.get_total_outlay() != self.policy.transfer_amount + fee { - log::error!(self.logger, "Destination memo had wrong total outlay, found {}, expected {}. Tx Info: {}", memo.get_total_outlay(), self.policy.transfer_amount + fee, self.tx_info); - return Err(TestClientError::UnexpectedMemo); - } - if memo.get_fee() != fee { - log::error!( + let block_version = + BlockVersion::try_from(source_client_lk.get_latest_block_version())?; + if block_version.e_memo_feature_is_supported() { + // Ensure source client got a destination memo, as expected for recoverable + // transcation history + match source_client_lk.get_last_memo() { + Ok(Some(memo)) => match memo { + MemoType::Destination(memo) => { + if memo.get_total_outlay() != self.policy.transfer_amount + fee { + log::error!(self.logger, "Destination memo had wrong total outlay, found {}, expected {}. Tx Info: {}", memo.get_total_outlay(), self.policy.transfer_amount + fee, self.tx_info); + return Err(TestClientError::UnexpectedMemo); + } + if memo.get_fee() != fee { + log::error!( self.logger, "Destination memo had wrong fee, found {}, expected {}. Tx Info: {}", memo.get_fee(), fee, self.tx_info ); - return Err(TestClientError::UnexpectedMemo); - } - if memo.get_num_recipients() != 1 { - log::error!(self.logger, "Destination memo had wrong num_recipients, found {}, expected 1. TxInfo: {}", memo.get_num_recipients(), self.tx_info); - return Err(TestClientError::UnexpectedMemo); + return Err(TestClientError::UnexpectedMemo); + } + if memo.get_num_recipients() != 1 { + log::error!(self.logger, "Destination memo had wrong num_recipients, found {}, expected 1. TxInfo: {}", memo.get_num_recipients(), self.tx_info); + return Err(TestClientError::UnexpectedMemo); + } + if *memo.get_address_hash() != tgt_address_hash { + log::error!(self.logger, "Destination memo had wrong address hash, found {:?}, expected {:?}. Tx Info: {}", memo.get_address_hash(), tgt_address_hash, self.tx_info); + return Err(TestClientError::UnexpectedMemo); + } } - if *memo.get_address_hash() != tgt_address_hash { - log::error!(self.logger, "Destination memo had wrong address hash, found {:?}, expected {:?}. Tx Info: {}", memo.get_address_hash(), tgt_address_hash, self.tx_info); + _ => { + log::error!( + self.logger, + "Source Client: Unexpected memo type. Tx Info: {}", + self.tx_info + ); return Err(TestClientError::UnexpectedMemo); } - } - _ => { + }, + Ok(None) => { log::error!( self.logger, - "Source Client: Unexpected memo type. Tx Info: {}", + "Source Client: Missing memo. Tx Info: {}", self.tx_info ); return Err(TestClientError::UnexpectedMemo); } - }, - Ok(None) => { - log::error!( - self.logger, - "Source Client: Missing memo. Tx Info: {}", - self.tx_info - ); - return Err(TestClientError::UnexpectedMemo); - } - Err(err) => { - log::error!( - self.logger, - "Source Client: Memo parse error: {}. TxInfo: {}", - err, - self.tx_info - ); - return Err(TestClientError::InvalidMemo); + Err(err) => { + log::error!( + self.logger, + "Source Client: Memo parse error: {}. TxInfo: {}", + err, + self.tx_info + ); + return Err(TestClientError::InvalidMemo); + } } } } @@ -730,6 +734,9 @@ impl TestClient { TestClientError::ConfirmTx(_) => { counters::CONFIRM_TX_ERROR_COUNT.inc(); } + TestClientError::BlockVersion(_) => { + counters::BUILD_TX_ERROR_COUNT.inc(); + } } } } @@ -823,43 +830,47 @@ impl ReceiveTxWorker { counters::TX_RECEIVED_TIME.observe(start.elapsed().as_secs_f64()); if policy.test_rth_memos { - // Ensure target client got a sender memo, as expected for recoverable - // transcation history - match client.get_last_memo() { - Ok(Some(memo)) => match memo { - MemoType::AuthenticatedSender(memo) => { - if let Some(hash) = expected_memo_contents { - if memo.sender_address_hash() != hash { - log::error!(logger, "Target Client: Unexpected address hash: {:?} != {:?}. TxInfo: {}", memo.sender_address_hash(), hash, tx_info); - return Err(TestClientError::UnexpectedMemo); + let block_version = + BlockVersion::try_from(client.get_latest_block_version())?; + if block_version.e_memo_feature_is_supported() { + // Ensure target client got a sender memo, as expected for + // recoverable transcation history + match client.get_last_memo() { + Ok(Some(memo)) => match memo { + MemoType::AuthenticatedSender(memo) => { + if let Some(hash) = expected_memo_contents { + if memo.sender_address_hash() != hash { + log::error!(logger, "Target Client: Unexpected address hash: {:?} != {:?}. TxInfo: {}", memo.sender_address_hash(), hash, tx_info); + return Err(TestClientError::UnexpectedMemo); + } } } - } - _ => { + _ => { + log::error!( + logger, + "Target Client: Unexpected memo type. TxInfo: {}", + tx_info + ); + return Err(TestClientError::UnexpectedMemo); + } + }, + Ok(None) => { log::error!( logger, - "Target Client: Unexpected memo type. TxInfo: {}", + "Target Client: Missing memo. TxInfo: {}", tx_info ); return Err(TestClientError::UnexpectedMemo); } - }, - Ok(None) => { - log::error!( - logger, - "Target Client: Missing memo. TxInfo: {}", - tx_info - ); - return Err(TestClientError::UnexpectedMemo); - } - Err(err) => { - log::error!( - logger, - "Target Client: Memo parse error: {}. TxInfo: {}", - err, - tx_info - ); - return Err(TestClientError::InvalidMemo); + Err(err) => { + log::error!( + logger, + "Target Client: Memo parse error: {}. TxInfo: {}", + err, + tx_info + ); + return Err(TestClientError::InvalidMemo); + } } } } diff --git a/fog/test_infra/src/bin/add_test_block.rs b/fog/test_infra/src/bin/add_test_block.rs index 34de9d7c74..e53aea0bfc 100644 --- a/fog/test_infra/src/bin/add_test_block.rs +++ b/fog/test_infra/src/bin/add_test_block.rs @@ -37,7 +37,7 @@ use mc_transaction_core::{ onetime_keys::recover_onetime_private_key, ring_signature::KeyImage, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Block, BlockContents, BlockData, BlockSignature, BLOCK_VERSION, + Block, BlockContents, BlockData, BlockSignature, BlockVersion, }; use mc_util_from_random::FromRandom; use rand_core::SeedableRng; @@ -197,8 +197,11 @@ fn main() { hash: TxOutMembershipHash::from([0u8; 32]), }; + // Use the same block version as the previous block + let block_version = BlockVersion::try_from(last_block.version).unwrap(); + let block = - Block::new_with_parent(BLOCK_VERSION, &last_block, &root_element, &block_contents); + Block::new_with_parent(block_version, &last_block, &root_element, &block_contents); let signer = Ed25519Pair::from_random(&mut rng); diff --git a/fog/test_infra/src/db_tests.rs b/fog/test_infra/src/db_tests.rs index fd1fa49065..1a289ae83e 100644 --- a/fog/test_infra/src/db_tests.rs +++ b/fog/test_infra/src/db_tests.rs @@ -7,7 +7,7 @@ use mc_fog_recovery_db_iface::{ ReportDb, }; use mc_fog_types::view::{RngRecord, TxOutSearchResultCode}; -use mc_transaction_core::{Block, BlockID, BLOCK_VERSION}; +use mc_transaction_core::{Block, BlockID, BlockVersion}; use mc_util_from_random::FromRandom; use rand_core::{CryptoRng, RngCore}; @@ -661,7 +661,7 @@ pub fn random_block( num_txs: usize, ) -> (Block, Vec) { let block = Block::new( - BLOCK_VERSION, + BlockVersion::ONE, &BlockID::default(), block_index, 0, diff --git a/fog/test_infra/src/lib.rs b/fog/test_infra/src/lib.rs index 6c4a9619f3..b5bbc3de3a 100644 --- a/fog/test_infra/src/lib.rs +++ b/fog/test_infra/src/lib.rs @@ -11,7 +11,7 @@ use mc_fog_ingest_client::FogIngestGrpcClient; use mc_fog_view_protocol::FogViewConnection; use mc_ledger_db::{Ledger, LedgerDB}; use mc_transaction_core::{ - ring_signature::KeyImage, Block, BlockContents, BlockSignature, BLOCK_VERSION, + ring_signature::KeyImage, Block, BlockContents, BlockSignature, BlockVersion, }; use mc_util_from_random::FromRandom; use mc_watcher::watcher_db::WatcherDB; @@ -149,7 +149,7 @@ pub fn test_block( .get_block(block_index - 1) .unwrap_or_else(|err| panic!("Failed getting block {}: {:?}", block_index - 1, err)); let block = Block::new_with_parent( - BLOCK_VERSION, + BlockVersion::ONE, &parent_block, &Default::default(), &block_contents, diff --git a/fog/view/server/tests/smoke_tests.rs b/fog/view/server/tests/smoke_tests.rs index 42014bc4d2..93c6be6325 100644 --- a/fog/view/server/tests/smoke_tests.rs +++ b/fog/view/server/tests/smoke_tests.rs @@ -30,7 +30,7 @@ use mc_fog_view_connection::FogViewGrpcClient; use mc_fog_view_enclave::SgxViewEnclave; use mc_fog_view_protocol::FogViewConnection; use mc_fog_view_server::{config::MobileAcctViewConfig as ViewConfig, server::ViewServer}; -use mc_transaction_core::{Block, BlockID, BLOCK_VERSION}; +use mc_transaction_core::{Block, BlockID, BlockVersion}; use mc_util_from_random::FromRandom; use mc_util_grpc::GrpcRetryConfig; use rand::{rngs::StdRng, SeedableRng}; @@ -146,7 +146,7 @@ fn test_view_integration(view_omap_capacity: u64, logger: Logger) { db.add_block_data( &invoc_id1, &Block::new( - BLOCK_VERSION, + BlockVersion::ONE, &BlockID::default(), 0, 2, @@ -161,7 +161,7 @@ fn test_view_integration(view_omap_capacity: u64, logger: Logger) { db.add_block_data( &invoc_id1, &Block::new( - BLOCK_VERSION, + BlockVersion::ONE, &BlockID::default(), 1, 6, @@ -184,7 +184,7 @@ fn test_view_integration(view_omap_capacity: u64, logger: Logger) { db.add_block_data( &invoc_id2, &Block::new( - BLOCK_VERSION, + BlockVersion::ONE, &BlockID::default(), 2, 12, @@ -219,7 +219,7 @@ fn test_view_integration(view_omap_capacity: u64, logger: Logger) { db.add_block_data( &invoc_id2, &Block::new( - BLOCK_VERSION, + BlockVersion::ONE, &BlockID::default(), 3, 12, @@ -234,7 +234,7 @@ fn test_view_integration(view_omap_capacity: u64, logger: Logger) { db.add_block_data( &invoc_id2, &Block::new( - BLOCK_VERSION, + BlockVersion::ONE, &BlockID::default(), 4, 16, @@ -251,7 +251,7 @@ fn test_view_integration(view_omap_capacity: u64, logger: Logger) { db.add_block_data( &invoc_id2, &Block::new( - BLOCK_VERSION, + BlockVersion::ONE, &BlockID::default(), 5, 20, diff --git a/ledger/db/src/lib.rs b/ledger/db/src/lib.rs index 274be3ee5c..538e905dae 100644 --- a/ledger/db/src/lib.rs +++ b/ledger/db/src/lib.rs @@ -27,7 +27,7 @@ use mc_transaction_core::{ membership_proofs::Range, ring_signature::KeyImage, tx::{TxOut, TxOutMembershipElement, TxOutMembershipProof}, - Block, BlockContents, BlockData, BlockID, BlockSignature, BLOCK_VERSION, + Block, BlockContents, BlockData, BlockID, BlockSignature, MAX_BLOCK_VERSION, }; use mc_util_lmdb::MetadataStoreSettings; use mc_util_serial::{decode, encode, Message}; @@ -609,7 +609,7 @@ impl LedgerDB { // The block's version should be bounded by // [prev block version, max block version] - if block.version < last_block.version || block.version > BLOCK_VERSION { + if block.version < last_block.version || block.version > *MAX_BLOCK_VERSION { return Err(Error::InvalidBlockVersion(block.version)); } @@ -743,13 +743,18 @@ mod ledger_db_test { use core::convert::TryFrom; use mc_account_keys::AccountKey; use mc_crypto_keys::RistrettoPrivate; - use mc_transaction_core::{compute_block_id, membership_proofs::compute_implied_merkle_root}; + use mc_transaction_core::{ + compute_block_id, membership_proofs::compute_implied_merkle_root, BlockVersion, + }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; use rand_core::RngCore; use tempdir::TempDir; use test::Bencher; + // TODO: Should these tests run over several block versions? + const BLOCK_VERSION: BlockVersion = BlockVersion::ONE; + /// Creates a LedgerDB instance. fn create_db() -> LedgerDB { let temp_dir = TempDir::new("test").unwrap(); @@ -1153,7 +1158,7 @@ mod ledger_db_test { let block_contents = BlockContents::new(key_images.clone(), outputs); let block = Block::new_with_parent( - BLOCK_VERSION, + BlockVersion::ONE, &origin_block, &Default::default(), &block_contents, @@ -1198,8 +1203,12 @@ mod ledger_db_test { let block_contents = BlockContents::new(key_images.clone(), outputs); let parent = ledger_db.get_block(n_blocks - 1).unwrap(); - let block = - Block::new_with_parent(BLOCK_VERSION, &parent, &Default::default(), &block_contents); + let block = Block::new_with_parent( + BlockVersion::ONE, + &parent, + &Default::default(), + &block_contents, + ); ledger_db .append_block(&block, &block_contents, None) @@ -1233,7 +1242,7 @@ mod ledger_db_test { let block_contents = BlockContents::new(key_images.clone(), outputs); let block = Block::new_with_parent( - BLOCK_VERSION, + BlockVersion::ONE, &origin_block, &Default::default(), &block_contents, @@ -1275,9 +1284,9 @@ mod ledger_db_test { #[test] /// Appending blocks that have ever-increasing and continous version numbers - /// should work as long as it is <= BLOCK_VERSION. - /// Appending a block > BLOCK_VERSION should fail even if it is after a - /// block with version == BLOCK_VERSION. + /// should work as long as it is <= MAX_BLOCK_VERSION. + /// Appending a block > MAX_BLOCK_VERSION should fail even if it is after a + /// block with version == MAX_BLOCK_VERSION. /// Appending a block with a version < last block's version should fail. fn test_append_block_with_version_bumps() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); @@ -1293,8 +1302,8 @@ mod ledger_db_test { let mut last_block = origin_block; - // BLOCK_VERSION sets the current version, which is the max version. - for version in 0..=BLOCK_VERSION { + // MAX_BLOCK_VERSION sets the current max block version + for block_version in BlockVersion::iterator() { // In each iteration we add a few blocks with the same version. for _ in 0..3 { let recipient_account_key = AccountKey::random(&mut rng); @@ -1315,7 +1324,7 @@ mod ledger_db_test { let block_contents = BlockContents::new(key_images.clone(), outputs); last_block = Block::new_with_parent( - version, + block_version, &last_block, &Default::default(), &block_contents, @@ -1325,21 +1334,21 @@ mod ledger_db_test { .append_block(&last_block, &block_contents, None) .unwrap(); } - } - // All blocks should've been written (+ origin block). - assert_eq!( - ledger_db.num_blocks().unwrap(), - 1 + (3 * (BLOCK_VERSION + 1)) as u64 - ); + // All blocks should've been written (+ origin block). + assert_eq!( + ledger_db.num_blocks().unwrap(), + 1 + (3 * (*block_version)) as u64 + ); + } // Last block version should be what we expect. let last_block = ledger_db .get_block(ledger_db.num_blocks().unwrap() - 1) .unwrap(); - assert_eq!(last_block.version, BLOCK_VERSION); + assert_eq!(last_block.version, *MAX_BLOCK_VERSION); - // Appending a block with version > BLOCK_VERSION should fail. + // Appending a block with version < previous block version should fail. { let recipient_account_key = AccountKey::random(&mut rng); let outputs: Vec = (0..4) @@ -1358,10 +1367,12 @@ mod ledger_db_test { (0..5).map(|_i| KeyImage::from(rng.next_u64())).collect(); let block_contents = BlockContents::new(key_images.clone(), outputs); - assert_eq!(last_block.version, BLOCK_VERSION); + assert_eq!(last_block.version, *MAX_BLOCK_VERSION); + // Note: unsafe transmute is being used to skirt the invariant that BlockVersion + // does not exceed MAX_BLOCK_VERSION let invalid_block = Block::new_with_parent( - last_block.version + 1, + unsafe { core::mem::transmute(last_block.version + 1) }, &last_block, &Default::default(), &block_contents, @@ -1373,7 +1384,7 @@ mod ledger_db_test { if last_block.version > 0 { let invalid_block = Block::new_with_parent( - last_block.version - 1, + BlockVersion::try_from(last_block.version - 1).unwrap(), &last_block, &Default::default(), &block_contents, @@ -1653,6 +1664,7 @@ mod ledger_db_test { // Get some random blocks let results: Vec<(Block, BlockContents)> = mc_transaction_core_test_utils::get_blocks( + BLOCK_VERSION, &recipient_pub_keys[..], 20, 20, @@ -1690,6 +1702,7 @@ mod ledger_db_test { // Get some random blocks let results: Vec<(Block, BlockContents)> = mc_transaction_core_test_utils::get_blocks( + BLOCK_VERSION, &recipient_pub_keys[..], 20, 20, diff --git a/ledger/db/src/test_utils/mock_ledger.rs b/ledger/db/src/test_utils/mock_ledger.rs index 9985413add..98fb954ded 100644 --- a/ledger/db/src/test_utils/mock_ledger.rs +++ b/ledger/db/src/test_utils/mock_ledger.rs @@ -7,7 +7,7 @@ use mc_crypto_keys::{CompressedRistrettoPublic, RistrettoPrivate}; use mc_transaction_core::{ ring_signature::KeyImage, tx::{TxOut, TxOutMembershipElement, TxOutMembershipProof}, - Block, BlockContents, BlockData, BlockID, BlockSignature, BLOCK_VERSION, + Block, BlockContents, BlockData, BlockID, BlockSignature, BlockVersion, }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -249,7 +249,7 @@ pub fn get_test_ledger_blocks(n_blocks: usize) -> Vec<(Block, BlockContents)> { let block_contents = BlockContents::new(key_images, outputs); let block = Block::new_with_parent( - BLOCK_VERSION, + BlockVersion::ONE, &blocks_and_contents[block_index - 1].0, &TxOutMembershipElement::default(), &block_contents, diff --git a/ledger/sync/src/test_app/main.rs b/ledger/sync/src/test_app/main.rs index aba5442501..1c9fb4ed54 100644 --- a/ledger/sync/src/test_app/main.rs +++ b/ledger/sync/src/test_app/main.rs @@ -9,7 +9,7 @@ use mc_connection::{ConnectionManager, HardcodedCredentialsProvider, ThickClient use mc_consensus_scp::{test_utils::test_node_id, QuorumSet}; use mc_ledger_db::{Ledger, LedgerDB}; use mc_ledger_sync::{LedgerSync, LedgerSyncService, PollingNetworkState}; -use mc_transaction_core::{Block, BlockContents}; +use mc_transaction_core::{Block, BlockContents, BlockVersion}; use mc_util_uri::ConsensusClientUri as ClientUri; use std::{path::PathBuf, str::FromStr, sync::Arc}; use tempdir::TempDir; @@ -32,6 +32,7 @@ fn _make_ledger_long(ledger: &mut LedgerDB) { .collect::>(); let results: Vec<(Block, BlockContents)> = mc_transaction_core_test_utils::get_blocks( + BlockVersion::ONE, &recipient_pub_keys[..], 1, 1000, diff --git a/libmobilecoin/src/transaction.rs b/libmobilecoin/src/transaction.rs index 31f32d3135..1bbbf99d6a 100644 --- a/libmobilecoin/src/transaction.rs +++ b/libmobilecoin/src/transaction.rs @@ -11,9 +11,9 @@ use mc_transaction_core::{ onetime_keys::{recover_onetime_private_key, recover_public_subaddress_spend_key}, ring_signature::KeyImage, tx::{TxOut, TxOutConfirmationNumber, TxOutMembershipProof}, - Amount, CompressedCommitment, + Amount, BlockVersion, CompressedCommitment, }; -use mc_transaction_std::{InputCredentials, NoMemoBuilder, TransactionBuilder}; +use mc_transaction_std::{InputCredentials, RTHMemoBuilder, TransactionBuilder}; use mc_util_ffi::*; /* ==== TxOut ==== */ @@ -342,10 +342,19 @@ pub extern "C" fn mc_transaction_builder_create( FogResolver::new(fog_resolver.0.clone(), &fog_resolver.1) .expect("FogResolver could not be constructed from the provided materials") }); - // TODO: After servers are deployed that are supporting the memos, - // Enable recoverable transaction history by configuring an RTHMemoBuilder - let memo_builder = NoMemoBuilder::default(); - let mut transaction_builder = TransactionBuilder::new(fog_resolver, memo_builder); + // FIXME: block version should be a parameter, it should be the latest + // version that fog ledger told us about, or that we got from ledger-db + let block_version = BlockVersion::ONE; + // Note: RTHMemoBuilder can be selected here, but we will only actually + // write memos if block_version is large enough that memos are supported. + // If block version is < 2, then transaction builder will filter out memos. + let mut memo_builder = RTHMemoBuilder::default(); + // FIXME: we need to pass the source account key to build sender memo + // credentials memo_builder.set_sender_credential(SenderMemoCredential:: + // from(source_account_key)); + memo_builder.enable_destination_memo(); + let mut transaction_builder = + TransactionBuilder::new(block_version, fog_resolver, memo_builder); transaction_builder .set_fee(fee) .expect("failure not expected"); diff --git a/mobilecoind-json/src/data_types.rs b/mobilecoind-json/src/data_types.rs index d518939597..67aa6376f9 100644 --- a/mobilecoind-json/src/data_types.rs +++ b/mobilecoind-json/src/data_types.rs @@ -1300,7 +1300,7 @@ mod test { use super::*; use mc_crypto_keys::RistrettoPublic; use mc_ledger_db::Ledger; - use mc_transaction_core::{encrypted_fog_hint::ENCRYPTED_FOG_HINT_LEN, Amount}; + use mc_transaction_core::{encrypted_fog_hint::ENCRYPTED_FOG_HINT_LEN, Amount, BlockVersion}; use mc_transaction_core_test_utils::{ create_ledger, create_transaction, initialize_ledger, AccountKey, }; @@ -1318,12 +1318,13 @@ mod test { let mut ledger = create_ledger(); let sender = AccountKey::random(&mut rng); let recipient = AccountKey::random(&mut rng); - initialize_ledger(&mut ledger, 1, &sender, &mut rng); + initialize_ledger(BlockVersion::MAX, &mut ledger, 1, &sender, &mut rng); let block_contents = ledger.get_block_contents(0).unwrap(); let tx_out = block_contents.outputs[0].clone(); create_transaction( + BlockVersion::MAX, &mut ledger, &tx_out, &sender, diff --git a/mobilecoind/src/conversions.rs b/mobilecoind/src/conversions.rs index fca33cb87a..b91206941d 100644 --- a/mobilecoind/src/conversions.rs +++ b/mobilecoind/src/conversions.rs @@ -175,7 +175,7 @@ mod test { use mc_ledger_db::Ledger; use mc_transaction_core::{encrypted_fog_hint::ENCRYPTED_FOG_HINT_LEN, Amount}; use mc_transaction_core_test_utils::{ - create_ledger, create_transaction, initialize_ledger, AccountKey, + create_ledger, create_transaction, initialize_ledger, AccountKey, BlockVersion, }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -255,12 +255,13 @@ mod test { let mut ledger = create_ledger(); let sender = AccountKey::random(&mut rng); let recipient = AccountKey::random(&mut rng); - initialize_ledger(&mut ledger, 1, &sender, &mut rng); + initialize_ledger(BlockVersion::MAX, &mut ledger, 1, &sender, &mut rng); let block_contents = ledger.get_block_contents(0).unwrap(); let tx_out = block_contents.outputs[0].clone(); create_transaction( + BlockVersion::MAX, &mut ledger, &tx_out, &sender, diff --git a/mobilecoind/src/database.rs b/mobilecoind/src/database.rs index 64d7ea950b..6fd7b5db70 100644 --- a/mobilecoind/src/database.rs +++ b/mobilecoind/src/database.rs @@ -354,6 +354,7 @@ mod test { use crate::{error::Error, test_utils::get_test_databases}; use mc_account_keys::AccountKey; use mc_common::logger::{test_with_logger, Logger}; + use mc_transaction_core::BlockVersion; use rand::{rngs::StdRng, SeedableRng}; use std::iter::FromIterator; use tempdir::TempDir; @@ -530,7 +531,7 @@ mod test { // Set up a db with 3 random recipients and 10 blocks. let (_ledger_db, mobilecoind_db) = - get_test_databases(3, &vec![], 10, logger.clone(), &mut rng); + get_test_databases(BlockVersion::ONE, 3, &vec![], 10, logger.clone(), &mut rng); // A test accouunt. let account_key = AccountKey::random(&mut rng); diff --git a/mobilecoind/src/monitor_store.rs b/mobilecoind/src/monitor_store.rs index 56faa65eb1..6c1bc3a972 100644 --- a/mobilecoind/src/monitor_store.rs +++ b/mobilecoind/src/monitor_store.rs @@ -331,7 +331,7 @@ mod test { use super::*; use crate::{ error::Error, - test_utils::{get_test_databases, get_test_monitor_data_and_id}, + test_utils::{get_test_databases, get_test_monitor_data_and_id, BlockVersion}, }; use mc_account_keys::RootIdentity; use mc_common::logger::{test_with_logger, Logger}; @@ -407,7 +407,7 @@ pKZkdp8MQU5TLFOE9qjNeVsCAwEAAQ== // Set up a db with 3 random recipients and 10 blocks. let (_ledger_db, mobilecoind_db) = - get_test_databases(3, &vec![], 10, logger.clone(), &mut rng); + get_test_databases(BlockVersion::MAX, 3, &vec![], 10, logger.clone(), &mut rng); // Check that there are no monitors yet. assert_eq!( diff --git a/mobilecoind/src/payments.rs b/mobilecoind/src/payments.rs index aed31780f7..b812051814 100644 --- a/mobilecoind/src/payments.rs +++ b/mobilecoind/src/payments.rs @@ -22,9 +22,11 @@ use mc_transaction_core::{ ring_signature::KeyImage, tokens::Mob, tx::{Tx, TxOut, TxOutConfirmationNumber, TxOutMembershipProof}, - BlockIndex, Token, + BlockIndex, BlockVersion, Token, +}; +use mc_transaction_std::{ + ChangeDestination, EmptyMemoBuilder, InputCredentials, TransactionBuilder, }; -use mc_transaction_std::{ChangeDestination, InputCredentials, NoMemoBuilder, TransactionBuilder}; use mc_util_uri::FogUri; use rand::Rng; use rayon::prelude::*; @@ -259,11 +261,16 @@ impl>, + block_version: BlockVersion, fee: u64, from_account_key: &AccountKey, change_subaddress: u64, @@ -763,8 +781,11 @@ impl LedgerDB { /// * `key_images` - Key images to include in the block. /// * `rng` - Random number generator. pub fn add_block_to_ledger_db( + block_version: BlockVersion, ledger_db: &mut LedgerDB, recipients: &[PublicAddress], output_value: u64, @@ -185,7 +188,7 @@ pub fn add_block_to_ledger_db( .get_block(num_blocks - 1) .expect("failed to get parent block"); new_block = - Block::new_with_parent(BLOCK_VERSION, &parent, &Default::default(), &block_contents); + Block::new_with_parent(block_version, &parent, &Default::default(), &block_contents); } else { new_block = Block::new_origin_block(&outputs); } @@ -203,6 +206,7 @@ pub fn add_block_to_ledger_db( /// * `ledger_db` /// * `outputs` - TXOs to add to ledger. pub fn add_txos_to_ledger_db( + block_version: BlockVersion, ledger_db: &mut LedgerDB, outputs: &Vec, rng: &mut (impl CryptoRng + RngCore), @@ -217,7 +221,7 @@ pub fn add_txos_to_ledger_db( .get_block(num_blocks - 1) .expect("failed to get parent block"); new_block = - Block::new_with_parent(BLOCK_VERSION, &parent, &Default::default(), &block_contents); + Block::new_with_parent(block_version, &parent, &Default::default(), &block_contents); } else { new_block = Block::new_origin_block(&outputs); } @@ -316,6 +320,7 @@ pub fn setup_client(uri: &MobilecoindUri, logger: &Logger) -> MobilecoindApiClie /// * `rng` pub fn get_testing_environment( + block_version: BlockVersion, num_random_recipients: u32, recipients: &[PublicAddress], monitors: &[MonitorData], @@ -329,6 +334,7 @@ pub fn get_testing_environment( ConnectionManager>, ) { let (ledger_db, mobilecoind_db) = get_test_databases( + block_version, num_random_recipients, recipients, GET_TESTING_ENVIRONMENT_NUM_BLOCKS, diff --git a/mobilecoind/src/utxo_store.rs b/mobilecoind/src/utxo_store.rs index 01014359c8..7b5e396a88 100644 --- a/mobilecoind/src/utxo_store.rs +++ b/mobilecoind/src/utxo_store.rs @@ -419,7 +419,7 @@ impl UtxoStore { #[cfg(test)] mod test { use super::*; - use crate::test_utils::{get_test_databases, get_test_monitor_data_and_id}; + use crate::test_utils::{get_test_databases, get_test_monitor_data_and_id, BlockVersion}; use mc_common::{ logger::{test_with_logger, Logger}, HashSet, @@ -436,7 +436,7 @@ mod test { ) -> (LedgerDB, UtxoStore, Vec) { // Set up a db with 3 random recipients and 10 blocks. let (ledger_db, _mobilecoind_db) = - get_test_databases(3, &vec![], 10, logger.clone(), &mut rng); + get_test_databases(BlockVersion::ONE, 3, &vec![], 10, logger.clone(), &mut rng); // Get a few TxOuts to play with, and use them to construct UnspentTxOuts. let utxos: Vec = (0..5) diff --git a/slam/src/main.rs b/slam/src/main.rs index 213b0dbe71..7df72170e6 100755 --- a/slam/src/main.rs +++ b/slam/src/main.rs @@ -25,9 +25,9 @@ use mc_transaction_core::{ ring_signature::KeyImage, tokens::Mob, tx::{Tx, TxOut, TxOutMembershipProof}, - Token, + BlockVersion, Token, }; -use mc_transaction_std::{InputCredentials, NoMemoBuilder, TransactionBuilder}; +use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; use mc_util_uri::ConnectionUri; use rand::{seq::SliceRandom, thread_rng, Rng}; use rayon::prelude::*; @@ -35,7 +35,7 @@ use std::{ iter::empty, path::Path, sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicU32, AtomicU64, Ordering}, Arc, Mutex, RwLock, }, thread, @@ -71,6 +71,7 @@ fn get_conns( lazy_static! { pub static ref BLOCK_HEIGHT: AtomicU64 = AtomicU64::default(); + pub static ref BLOCK_VERSION: AtomicU32 = AtomicU32::new(1); pub static ref FEE: AtomicU64 = AtomicU64::default(); @@ -128,6 +129,10 @@ fn main() { let ledger_db = LedgerDB::open(ledger_dir.path()).expect("Could not open ledger_db"); BLOCK_HEIGHT.store(ledger_db.num_blocks().unwrap(), Ordering::SeqCst); + BLOCK_VERSION.store( + ledger_db.get_latest_block().unwrap().version, + Ordering::SeqCst, + ); // Use the maximum fee of all configured consensus nodes FEE.store( @@ -491,8 +496,17 @@ fn build_tx( // Sanity assert_eq!(utxos_with_proofs.len(), rings.len()); + // This max occurs because the bootstrapped ledger has block version 0, + // but non-bootstrap blocks always have block version >= 1 + let block_version = BlockVersion::try_from(BLOCK_VERSION.load(Ordering::SeqCst)) + .expect("Unsupported block version"); + // Create tx_builder. No fog reports. - let mut tx_builder = TransactionBuilder::new(FogResolver::default(), NoMemoBuilder::default()); + let mut tx_builder = TransactionBuilder::new( + block_version, + FogResolver::default(), + EmptyMemoBuilder::default(), + ); tx_builder .set_fee(FEE.load(Ordering::SeqCst)) diff --git a/transaction/core/src/blockchain/block.rs b/transaction/core/src/blockchain/block.rs index dde3664a01..24f160f725 100644 --- a/transaction/core/src/blockchain/block.rs +++ b/transaction/core/src/blockchain/block.rs @@ -2,15 +2,16 @@ use crate::{ tx::{TxOut, TxOutMembershipElement}, - BlockContents, BlockContentsHash, BlockID, + BlockContents, BlockContentsHash, BlockID, BlockVersion, }; use alloc::vec::Vec; use mc_crypto_digestible::{DigestTranscript, Digestible, MerlinTranscript}; use prost::Message; use serde::{Deserialize, Serialize}; -/// The current block format version. -pub const BLOCK_VERSION: u32 = 1; +/// The maximum supported block format version for this build of +/// mc-transaction-core +pub const MAX_BLOCK_VERSION: BlockVersion = BlockVersion::MAX; /// The index of a block in the blockchain. pub type BlockIndex = u64; @@ -94,7 +95,7 @@ impl Block { /// * `root_element` - The root element for membership proofs /// * `block_contents - The Contents of the block. pub fn new_with_parent( - version: u32, + version: BlockVersion, parent: &Block, root_element: &TxOutMembershipElement, block_contents: &BlockContents, @@ -122,7 +123,7 @@ impl Block { /// * `root_element` - The root element for membership proofs /// * `block_contents` - Contents of the block. pub fn new( - version: u32, + version: BlockVersion, parent_id: &BlockID, index: BlockIndex, cumulative_txo_count: u64, @@ -131,7 +132,7 @@ impl Block { ) -> Self { let contents_hash = block_contents.hash(); let id = compute_block_id( - version, + *version, parent_id, index, cumulative_txo_count, @@ -141,7 +142,7 @@ impl Block { Self { id, - version, + version: *version, parent_id: parent_id.clone(), index, cumulative_txo_count, @@ -203,7 +204,7 @@ mod block_tests { membership_proofs::Range, ring_signature::KeyImage, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Block, BlockContents, BlockContentsHash, BlockID, BLOCK_VERSION, + Block, BlockContents, BlockContentsHash, BlockID, BlockVersion, }; use alloc::vec::Vec; use core::convert::TryFrom; @@ -212,6 +213,8 @@ mod block_tests { use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, CryptoRng, RngCore, SeedableRng}; + const BLOCK_VERSION: BlockVersion = BlockVersion::ONE; + fn get_block_contents(rng: &mut RNG) -> BlockContents { let (key_images, outputs) = get_key_images_and_outputs(rng); BlockContents::new(key_images, outputs) diff --git a/transaction/core/src/blockchain/block_version.rs b/transaction/core/src/blockchain/block_version.rs new file mode 100644 index 0000000000..cf6d4b1ee5 --- /dev/null +++ b/transaction/core/src/blockchain/block_version.rs @@ -0,0 +1,147 @@ +// Copyright (c) 2018-2022 The MobileCoin Foundation + +use core::{convert::TryFrom, fmt, hash::Hash, ops::Deref, str::FromStr}; +use displaydoc::Display; +use mc_crypto_digestible::Digestible; +use serde::{Deserialize, Serialize}; + +/// A block version number that is known to be less or equal to +/// BlockVersion::MAX +/// +/// Note: BlockVersion::MAX may vary from client to client as we roll out +/// network upgrades. Software should handle errors where the block version of +/// the network is not supported, generally by requesting users to upgrade their +/// software. +/// +/// If you need to manipulate block versions that cannot be understood by your +/// version of `mc-transaction-core`, then you should use u32 to represent +/// block version numbers. +#[derive( + Clone, + Copy, + Debug, + Default, + Deserialize, + Digestible, + Eq, + Hash, + Ord, + PartialOrd, + PartialEq, + Serialize, +)] +#[serde(try_from = "u32")] +pub struct BlockVersion(u32); + +impl TryFrom for BlockVersion { + type Error = BlockVersionError; + + fn try_from(src: u32) -> Result { + if src <= *Self::MAX { + Ok(Self(src)) + } else { + Err(BlockVersionError::UnsupportedBlockVersion(src, *Self::MAX)) + } + } +} + +impl FromStr for BlockVersion { + type Err = BlockVersionError; + + fn from_str(src: &str) -> Result { + let src = u32::from_str(src).map_err(|_| BlockVersionError::Parse)?; + Self::try_from(src) + } +} + +impl BlockVersion { + /// The maximum value of block_version that this build of + /// mc-transaction-core has support for + pub const MAX: Self = Self(2); + + /// Refers to the block version number at network launch. + /// Note: The origin blocks use block version zero. + pub const ONE: Self = Self(1); + + /// Constant for block version two + pub const TWO: Self = Self(2); + + /// Iterator over block versions from one up to max, inclusive. For use in + /// tests. + pub fn iterator() -> BlockVersionIterator { + BlockVersionIterator(1) + } + + /// The encrypted memos [MCIP #3](https://github.com/mobilecoinfoundation/mcips/pull/3) + /// feature is introduced in block version 2. + pub fn e_memo_feature_is_supported(&self) -> bool { + self.0 >= 2 + } +} + +impl Deref for BlockVersion { + type Target = u32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for BlockVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone)] +pub struct BlockVersionIterator(u32); + +impl Iterator for BlockVersionIterator { + type Item = BlockVersion; + fn next(&mut self) -> Option { + if self.0 <= *BlockVersion::MAX { + let result = self.0; + self.0 += 1; + Some(BlockVersion(result)) + } else { + None + } + } +} + +#[derive(Clone, Display, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub enum BlockVersionError { + /// Unsupported block version: {0} > {1}. Try upgrading your software + UnsupportedBlockVersion(u32, u32), + /// Could not parse block version + Parse, +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + + // Test that block_version::iterator is working as expected + #[test] + fn test_block_version_iterator() { + let observed = BlockVersion::iterator().map(|x| *x).collect::>(); + let expected = (1..=*BlockVersion::MAX).collect::>(); + assert_eq!(observed, expected); + } + + // Test that block_version::try_from is working as expected + #[test] + fn test_block_version_parsing() { + BlockVersion::try_from(0).unwrap(); + for block_version in BlockVersion::iterator() { + assert_eq!( + block_version, + BlockVersion::try_from(*block_version) + .expect("Could not parse *block version as block version") + ); + } + assert!(BlockVersion::try_from(*BlockVersion::MAX + 1).is_err()); + assert!(BlockVersion::try_from(*BlockVersion::MAX + 2).is_err()); + } +} diff --git a/transaction/core/src/blockchain/mod.rs b/transaction/core/src/blockchain/mod.rs index b19ae80b82..5bbf059ff1 100644 --- a/transaction/core/src/blockchain/mod.rs +++ b/transaction/core/src/blockchain/mod.rs @@ -7,12 +7,14 @@ mod block_contents; mod block_data; mod block_id; mod block_signature; +mod block_version; pub use block::*; pub use block_contents::*; pub use block_data::*; pub use block_id::*; pub use block_signature::*; +pub use block_version::{BlockVersion, BlockVersionError}; use displaydoc::Display; diff --git a/transaction/core/src/token.rs b/transaction/core/src/token.rs index dfb02dcef1..f8d70b622b 100644 --- a/transaction/core/src/token.rs +++ b/transaction/core/src/token.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; /// Token Id, used to identify different assets on on the blockchain. #[derive( - Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize, Digestible, Hash, + Clone, Copy, Debug, Deserialize, Digestible, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, )] pub struct TokenId(u32); @@ -18,7 +18,7 @@ impl From for TokenId { impl fmt::Display for TokenId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) + write!(f, "{}", self.0) } } diff --git a/transaction/core/src/validation/validate.rs b/transaction/core/src/validation/validate.rs index 343f2f4d47..1075e03ccb 100644 --- a/transaction/core/src/validation/validate.rs +++ b/transaction/core/src/validation/validate.rs @@ -4,14 +4,14 @@ extern crate alloc; -use alloc::vec::Vec; +use alloc::{format, vec::Vec}; use super::error::{TransactionValidationError, TransactionValidationResult}; use crate::{ constants::*, membership_proofs::{derive_proof_at_index, is_membership_proof_valid}, tx::{Tx, TxOut, TxOutMembershipProof, TxPrefix}, - CompressedCommitment, + BlockVersion, CompressedCommitment, }; use mc_common::HashSet; use mc_crypto_keys::CompressedRistrettoPublic; @@ -30,10 +30,18 @@ use rand_core::{CryptoRng, RngCore}; pub fn validate( tx: &Tx, current_block_index: u64, + block_version: BlockVersion, root_proofs: &[TxOutMembershipProof], minimum_fee: u64, csprng: &mut R, ) -> TransactionValidationResult<()> { + if block_version < BlockVersion::ONE || BlockVersion::MAX < block_version { + return Err(TransactionValidationError::Ledger(format!( + "Invalid block version: {}", + block_version + ))); + } + validate_number_of_inputs(&tx.prefix, MAX_INPUTS)?; validate_number_of_outputs(&tx.prefix, MAX_OUTPUTS)?; @@ -61,12 +69,17 @@ pub fn validate( // Note: The transaction must not contain a Key Image that has previously been // spent. This must be checked outside the enclave. - // In the 1.2 release, it is planned that clients will know to read memos, - // but memos will not be allowed to exist in the chain until the next release. - // If we implement "block-version-based protocol evolution" (MCIP 26), then this - // function would become block-version aware and this could become a branch. - validate_no_memos_exist(tx)?; - // validate_memos_exist(tx)?; + //// + // Validate rules which depend on block version (see MCIP #26) + //// + + // If memos are supported, then all outputs must have memo fields. + // If memos are not yet supported, then no outputs may have memo fields. + if block_version.e_memo_feature_is_supported() { + validate_memos_exist(tx)?; + } else { + validate_no_memos_exist(tx)?; + } Ok(()) } @@ -212,12 +225,6 @@ fn validate_no_memos_exist(tx: &Tx) -> TransactionValidationResult<()> { } /// All outputs have a memo (old-style TxOuts (Pre MCIP #3) are rejected) -/// -/// Note: This is only under test for now, and can become live -/// at the time that we address mobilecoinfoundation/mobilecoin/issues/905 -/// "make memos mandatory". See MCIP #0003 for discussion of interim period -/// during which memos are optional. -#[cfg(test)] fn validate_memos_exist(tx: &Tx) -> TransactionValidationResult<()> { if tx .prefix @@ -430,7 +437,7 @@ mod tests { use mc_ledger_db::{Ledger, LedgerDB}; use mc_transaction_core_test_utils::{ create_ledger, create_transaction, create_transaction_with_amount, initialize_ledger, - INITIALIZE_LEDGER_AMOUNT, + AccountKey, INITIALIZE_LEDGER_AMOUNT, }; use rand::{rngs::StdRng, SeedableRng}; use serde::{de::DeserializeOwned, ser::Serialize}; @@ -450,19 +457,26 @@ mod tests { mc_util_serial::deserialize(&bytes).unwrap() } - fn create_test_tx() -> (Tx, LedgerDB) { + fn create_test_tx(block_version: BlockVersion) -> (Tx, LedgerDB) { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender = mc_transaction_core_test_utils::AccountKey::random(&mut rng); + let sender = AccountKey::random(&mut rng); let mut ledger = create_ledger(); let n_blocks = 1; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + initialize_ledger( + adapt_hack(&block_version), + &mut ledger, + n_blocks, + &sender, + &mut rng, + ); // Spend an output from the last block. let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap(); let tx_out = block_contents.outputs[0].clone(); - let recipient = mc_transaction_core_test_utils::AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); let tx = create_transaction( + adapt_hack(&block_version), &mut ledger, &tx_out, &sender, @@ -474,19 +488,30 @@ mod tests { (adapt_hack(&tx), ledger) } - fn create_test_tx_with_amount(amount: u64, fee: u64) -> (Tx, LedgerDB) { + fn create_test_tx_with_amount( + block_version: BlockVersion, + amount: u64, + fee: u64, + ) -> (Tx, LedgerDB) { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender = mc_transaction_core_test_utils::AccountKey::random(&mut rng); + let sender = AccountKey::random(&mut rng); let mut ledger = create_ledger(); let n_blocks = 1; - initialize_ledger(&mut ledger, n_blocks, &sender, &mut rng); + initialize_ledger( + adapt_hack(&block_version), + &mut ledger, + n_blocks, + &sender, + &mut rng, + ); // Spend an output from the last block. let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap(); let tx_out = block_contents.outputs[0].clone(); - let recipient = mc_transaction_core_test_utils::AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); let tx = create_transaction_with_amount( + adapt_hack(&block_version), &mut ledger, &tx_out, &sender, @@ -503,40 +528,31 @@ mod tests { #[test] // Should return MissingMemo when memos are missing in any the outputs fn test_validate_memos_exist() { - let (mut tx, _) = create_test_tx(); + let (tx, _) = create_test_tx(BlockVersion::ONE); - assert!(tx.prefix.outputs.first_mut().unwrap().e_memo.is_none()); + assert!(tx.prefix.outputs.first().unwrap().e_memo.is_none()); assert_eq!( validate_memos_exist(&tx), Err(TransactionValidationError::MissingMemo) ); - for ref mut output in tx.prefix.outputs.iter_mut() { - output.e_memo = Some(Default::default()); - } + let (tx, _) = create_test_tx(BlockVersion::TWO); + assert!(tx.prefix.outputs.first().unwrap().e_memo.is_some()); assert_eq!(validate_memos_exist(&tx), Ok(())); } #[test] // Should return MemosNotAllowed when memos are present in any of the outputs fn test_validate_no_memos_exist() { - let (mut tx, _) = create_test_tx(); + let (tx, _) = create_test_tx(BlockVersion::ONE); - assert!(tx.prefix.outputs.first_mut().unwrap().e_memo.is_none()); + assert!(tx.prefix.outputs.first().unwrap().e_memo.is_none()); assert_eq!(validate_no_memos_exist(&tx), Ok(())); - tx.prefix.outputs.first_mut().unwrap().e_memo = Some(Default::default()); - - assert_eq!( - validate_no_memos_exist(&tx), - Err(TransactionValidationError::MemosNotAllowed) - ); - - for ref mut output in tx.prefix.outputs.iter_mut() { - output.e_memo = Some(Default::default()); - } + let (tx, _) = create_test_tx(BlockVersion::TWO); + assert!(tx.prefix.outputs.first().unwrap().e_memo.is_some()); assert_eq!( validate_no_memos_exist(&tx), Err(TransactionValidationError::MemosNotAllowed) @@ -547,34 +563,36 @@ mod tests { // Should return Ok(()) when the Tx's membership proofs are correct and agree // with ledger. fn test_validate_membership_proofs() { - let (tx, ledger) = create_test_tx(); - - let highest_indices = tx.get_membership_proof_highest_indices(); - let root_proofs: Vec = adapt_hack( - &ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proofs"), - ); + for block_version in BlockVersion::iterator() { + let (tx, ledger) = create_test_tx(block_version); - // Validate the transaction prefix without providing the correct ledger context. - { - let mut broken_proofs = root_proofs.clone(); - broken_proofs[0].elements[0].hash = TxOutMembershipHash::from([1u8; 32]); - assert_eq!( - validate_membership_proofs(&tx.prefix, &broken_proofs), - Err(TransactionValidationError::InvalidTxOutMembershipProof) - ); - } - - // Validate the transaction prefix with the correct root proofs. - { let highest_indices = tx.get_membership_proof_highest_indices(); let root_proofs: Vec = adapt_hack( &ledger .get_tx_out_proof_of_memberships(&highest_indices) .expect("failed getting proofs"), ); - assert_eq!(validate_membership_proofs(&tx.prefix, &root_proofs), Ok(())); + + // Validate the transaction prefix without providing the correct ledger context. + { + let mut broken_proofs = root_proofs.clone(); + broken_proofs[0].elements[0].hash = TxOutMembershipHash::from([1u8; 32]); + assert_eq!( + validate_membership_proofs(&tx.prefix, &broken_proofs), + Err(TransactionValidationError::InvalidTxOutMembershipProof) + ); + } + + // Validate the transaction prefix with the correct root proofs. + { + let highest_indices = tx.get_membership_proof_highest_indices(); + let root_proofs: Vec = adapt_hack( + &ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proofs"), + ); + assert_eq!(validate_membership_proofs(&tx.prefix, &root_proofs), Ok(())); + } } } @@ -582,328 +600,358 @@ mod tests { // Should return InvalidRangeProof if a membership proof containing an invalid // Range. fn test_validate_membership_proofs_invalid_range_in_tx() { - let (mut tx, ledger) = create_test_tx(); + for block_version in BlockVersion::iterator() { + let (mut tx, ledger) = create_test_tx(block_version); - let highest_indices = tx.get_membership_proof_highest_indices(); - let root_proofs: Vec = adapt_hack( - &ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proofs"), - ); + let highest_indices = tx.get_membership_proof_highest_indices(); + let root_proofs: Vec = adapt_hack( + &ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proofs"), + ); - // Modify tx to include an invalid Range. - let mut proof = tx.prefix.inputs[0].proofs[0].clone(); - let mut first_element = proof.elements[0].clone(); - first_element.range = Range { from: 7, to: 3 }; - proof.elements[0] = first_element; - tx.prefix.inputs[0].proofs[0] = proof; + // Modify tx to include an invalid Range. + let mut proof = tx.prefix.inputs[0].proofs[0].clone(); + let mut first_element = proof.elements[0].clone(); + first_element.range = Range { from: 7, to: 3 }; + proof.elements[0] = first_element; + tx.prefix.inputs[0].proofs[0] = proof; - assert_eq!( - validate_membership_proofs(&tx.prefix, &root_proofs), - Err(TransactionValidationError::MembershipProofValidationError) - ); + assert_eq!( + validate_membership_proofs(&tx.prefix, &root_proofs), + Err(TransactionValidationError::MembershipProofValidationError) + ); + } } #[test] // Should return InvalidRangeProof if a root proof containing an invalid Range. fn test_validate_membership_proofs_invalid_range_in_root_proof() { - let (tx, ledger) = create_test_tx(); + for block_version in BlockVersion::iterator() { + let (tx, ledger) = create_test_tx(block_version); - let highest_indices = tx.get_membership_proof_highest_indices(); - let mut root_proofs: Vec = adapt_hack( - &ledger - .get_tx_out_proof_of_memberships(&highest_indices) - .expect("failed getting proofs"), - ); + let highest_indices = tx.get_membership_proof_highest_indices(); + let mut root_proofs: Vec = adapt_hack( + &ledger + .get_tx_out_proof_of_memberships(&highest_indices) + .expect("failed getting proofs"), + ); - // Modify a root proof to include an invalid Range. - let mut proof = root_proofs[0].clone(); - let mut first_element = proof.elements[0].clone(); - first_element.range = Range { from: 7, to: 3 }; - proof.elements[0] = first_element; - root_proofs[0] = proof; + // Modify a root proof to include an invalid Range. + let mut proof = root_proofs[0].clone(); + let mut first_element = proof.elements[0].clone(); + first_element.range = Range { from: 7, to: 3 }; + proof.elements[0] = first_element; + root_proofs[0] = proof; - assert_eq!( - validate_membership_proofs(&tx.prefix, &root_proofs), - Err(TransactionValidationError::MembershipProofValidationError) - ); + assert_eq!( + validate_membership_proofs(&tx.prefix, &root_proofs), + Err(TransactionValidationError::MembershipProofValidationError) + ); + } } #[test] fn test_validate_number_of_inputs() { - let (orig_tx, _ledger) = create_test_tx(); - let max_inputs = 25; - - for num_inputs in 0..100 { - let mut tx_prefix = orig_tx.prefix.clone(); - tx_prefix.inputs.clear(); - for _i in 0..num_inputs { - tx_prefix.inputs.push(orig_tx.prefix.inputs[0].clone()); - } - - let expected_result = if num_inputs == 0 { - Err(TransactionValidationError::NoInputs) - } else if num_inputs > max_inputs { - Err(TransactionValidationError::TooManyInputs) - } else { - Ok(()) - }; + for block_version in BlockVersion::iterator() { + let (orig_tx, _ledger) = create_test_tx(block_version); + let max_inputs = 25; + + for num_inputs in 0..100 { + let mut tx_prefix = orig_tx.prefix.clone(); + tx_prefix.inputs.clear(); + for _i in 0..num_inputs { + tx_prefix.inputs.push(orig_tx.prefix.inputs[0].clone()); + } - assert_eq!( - validate_number_of_inputs(&tx_prefix, max_inputs), - expected_result, - ); + let expected_result = if num_inputs == 0 { + Err(TransactionValidationError::NoInputs) + } else if num_inputs > max_inputs { + Err(TransactionValidationError::TooManyInputs) + } else { + Ok(()) + }; + + assert_eq!( + validate_number_of_inputs(&tx_prefix, max_inputs), + expected_result, + ); + } } } #[test] fn test_validate_number_of_outputs() { - let (orig_tx, _ledger) = create_test_tx(); - let max_outputs = 25; - - for num_outputs in 0..100 { - let mut tx_prefix = orig_tx.prefix.clone(); - tx_prefix.outputs.clear(); - for _i in 0..num_outputs { - tx_prefix.outputs.push(orig_tx.prefix.outputs[0].clone()); - } - - let expected_result = if num_outputs == 0 { - Err(TransactionValidationError::NoOutputs) - } else if num_outputs > max_outputs { - Err(TransactionValidationError::TooManyOutputs) - } else { - Ok(()) - }; + for block_version in BlockVersion::iterator() { + let (orig_tx, _ledger) = create_test_tx(block_version); + let max_outputs = 25; + + for num_outputs in 0..100 { + let mut tx_prefix = orig_tx.prefix.clone(); + tx_prefix.outputs.clear(); + for _i in 0..num_outputs { + tx_prefix.outputs.push(orig_tx.prefix.outputs[0].clone()); + } - assert_eq!( - validate_number_of_outputs(&tx_prefix, max_outputs), - expected_result, - ); + let expected_result = if num_outputs == 0 { + Err(TransactionValidationError::NoOutputs) + } else if num_outputs > max_outputs { + Err(TransactionValidationError::TooManyOutputs) + } else { + Ok(()) + }; + + assert_eq!( + validate_number_of_outputs(&tx_prefix, max_outputs), + expected_result, + ); + } } } #[test] fn test_validate_ring_sizes() { - let (tx, _ledger) = create_test_tx(); - assert_eq!(tx.prefix.inputs.len(), 1); - assert_eq!(tx.prefix.inputs[0].ring.len(), RING_SIZE); - - // A transaction with a single input containing RING_SIZE elements. - assert_eq!(validate_ring_sizes(&tx.prefix, RING_SIZE), Ok(())); - - // A single input containing zero elements. - { - let mut tx_prefix = tx.prefix.clone(); - tx_prefix.inputs[0].ring.clear(); - - assert_eq!( - validate_ring_sizes(&tx_prefix, RING_SIZE), - Err(TransactionValidationError::InsufficientRingSize), - ); - } - - // A single input containing too few elements. - { - let mut tx_prefix = tx.prefix.clone(); - tx_prefix.inputs[0].ring.pop(); + for block_version in BlockVersion::iterator() { + let (tx, _ledger) = create_test_tx(block_version); + assert_eq!(tx.prefix.inputs.len(), 1); + assert_eq!(tx.prefix.inputs[0].ring.len(), RING_SIZE); + + // A transaction with a single input containing RING_SIZE elements. + assert_eq!(validate_ring_sizes(&tx.prefix, RING_SIZE), Ok(())); + + // A single input containing zero elements. + { + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs[0].ring.clear(); + + assert_eq!( + validate_ring_sizes(&tx_prefix, RING_SIZE), + Err(TransactionValidationError::InsufficientRingSize), + ); + } - assert_eq!( - validate_ring_sizes(&tx_prefix, RING_SIZE), - Err(TransactionValidationError::InsufficientRingSize), - ); - } + // A single input containing too few elements. + { + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs[0].ring.pop(); - // A single input containing too many elements. - { - let mut tx_prefix = tx.prefix.clone(); - let element = tx_prefix.inputs[0].ring[0].clone(); - tx_prefix.inputs[0].ring.push(element); + assert_eq!( + validate_ring_sizes(&tx_prefix, RING_SIZE), + Err(TransactionValidationError::InsufficientRingSize), + ); + } - assert_eq!( - validate_ring_sizes(&tx_prefix, RING_SIZE), - Err(TransactionValidationError::ExcessiveRingSize), - ); - } + // A single input containing too many elements. + { + let mut tx_prefix = tx.prefix.clone(); + let element = tx_prefix.inputs[0].ring[0].clone(); + tx_prefix.inputs[0].ring.push(element); - // Two inputs each containing RING_SIZE elements. - { - let mut tx_prefix = tx.prefix.clone(); - let input = tx_prefix.inputs[0].clone(); - tx_prefix.inputs.push(input); + assert_eq!( + validate_ring_sizes(&tx_prefix, RING_SIZE), + Err(TransactionValidationError::ExcessiveRingSize), + ); + } - assert_eq!(validate_ring_sizes(&tx_prefix, RING_SIZE), Ok(())); - } + // Two inputs each containing RING_SIZE elements. + { + let mut tx_prefix = tx.prefix.clone(); + let input = tx_prefix.inputs[0].clone(); + tx_prefix.inputs.push(input); - // The second input contains too few elements. - { - let mut tx_prefix = tx.prefix.clone(); - let mut input = tx_prefix.inputs[0].clone(); - input.ring.pop(); - tx_prefix.inputs.push(input); + assert_eq!(validate_ring_sizes(&tx_prefix, RING_SIZE), Ok(())); + } - assert_eq!( - validate_ring_sizes(&tx_prefix, RING_SIZE), - Err(TransactionValidationError::InsufficientRingSize), - ); + // The second input contains too few elements. + { + let mut tx_prefix = tx.prefix.clone(); + let mut input = tx_prefix.inputs[0].clone(); + input.ring.pop(); + tx_prefix.inputs.push(input); + + assert_eq!( + validate_ring_sizes(&tx_prefix, RING_SIZE), + Err(TransactionValidationError::InsufficientRingSize), + ); + } } } #[test] fn test_validate_ring_elements_are_unique() { - let (tx, _ledger) = create_test_tx(); - assert_eq!(tx.prefix.inputs.len(), 1); - - // A transaction with a single input and unique ring elements. - assert_eq!(validate_ring_elements_are_unique(&tx.prefix), Ok(())); - - // A transaction with a single input and duplicate ring elements. - { - let mut tx_prefix = tx.prefix.clone(); - tx_prefix.inputs[0] - .ring - .push(tx.prefix.inputs[0].ring[0].clone()); + for block_version in BlockVersion::iterator() { + let (tx, _ledger) = create_test_tx(block_version); + assert_eq!(tx.prefix.inputs.len(), 1); + + // A transaction with a single input and unique ring elements. + assert_eq!(validate_ring_elements_are_unique(&tx.prefix), Ok(())); + + // A transaction with a single input and duplicate ring elements. + { + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs[0] + .ring + .push(tx.prefix.inputs[0].ring[0].clone()); + + assert_eq!( + validate_ring_elements_are_unique(&tx_prefix), + Err(TransactionValidationError::DuplicateRingElements) + ); + } - assert_eq!( - validate_ring_elements_are_unique(&tx_prefix), - Err(TransactionValidationError::DuplicateRingElements) - ); - } + // A transaction with a multiple inputs and unique ring elements. + { + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs.push(tx.prefix.inputs[0].clone()); - // A transaction with a multiple inputs and unique ring elements. - { - let mut tx_prefix = tx.prefix.clone(); - tx_prefix.inputs.push(tx.prefix.inputs[0].clone()); + for mut tx_out in tx_prefix.inputs[1].ring.iter_mut() { + let mut bytes = tx_out.target_key.to_bytes(); + bytes[0] = !bytes[0]; + tx_out.target_key = CompressedRistrettoPublic::from_bytes(&bytes).unwrap(); + } - for mut tx_out in tx_prefix.inputs[1].ring.iter_mut() { - let mut bytes = tx_out.target_key.to_bytes(); - bytes[0] = !bytes[0]; - tx_out.target_key = CompressedRistrettoPublic::from_bytes(&bytes).unwrap(); + assert_eq!(validate_ring_elements_are_unique(&tx_prefix), Ok(())); } - assert_eq!(validate_ring_elements_are_unique(&tx_prefix), Ok(())); - } - - // A transaction with a multiple inputs and duplicate ring elements in different - // rings. - { - let mut tx_prefix = tx.prefix.clone(); - tx_prefix.inputs.push(tx.prefix.inputs[0].clone()); + // A transaction with a multiple inputs and duplicate ring elements in different + // rings. + { + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs.push(tx.prefix.inputs[0].clone()); - assert_eq!( - validate_ring_elements_are_unique(&tx_prefix), - Err(TransactionValidationError::DuplicateRingElements) - ); + assert_eq!( + validate_ring_elements_are_unique(&tx_prefix), + Err(TransactionValidationError::DuplicateRingElements) + ); + } } } #[test] /// validate_ring_elements_are_sorted should reject an unsorted ring. fn test_validate_ring_elements_are_sorted() { - let (mut tx, _ledger) = create_test_tx(); - assert_eq!(validate_ring_elements_are_sorted(&tx.prefix), Ok(())); + for block_version in BlockVersion::iterator() { + let (mut tx, _ledger) = create_test_tx(block_version); + assert_eq!(validate_ring_elements_are_sorted(&tx.prefix), Ok(())); - // Change the ordering of a ring. - tx.prefix.inputs[0].ring.swap(0, 3); - assert_eq!( - validate_ring_elements_are_sorted(&tx.prefix), - Err(TransactionValidationError::UnsortedRingElements) - ); + // Change the ordering of a ring. + tx.prefix.inputs[0].ring.swap(0, 3); + assert_eq!( + validate_ring_elements_are_sorted(&tx.prefix), + Err(TransactionValidationError::UnsortedRingElements) + ); + } } #[test] /// validate_inputs_are_sorted should reject unsorted inputs. fn test_validate_inputs_are_sorted() { - let (tx, _ledger) = create_test_tx(); + for block_version in BlockVersion::iterator() { + let (tx, _ledger) = create_test_tx(block_version); - // Add a second input to the transaction. - let mut tx_prefix = tx.prefix.clone(); - tx_prefix.inputs.push(tx.prefix.inputs[0].clone()); + // Add a second input to the transaction. + let mut tx_prefix = tx.prefix.clone(); + tx_prefix.inputs.push(tx.prefix.inputs[0].clone()); - // By removing the first ring element of the second input we ensure the inputs - // are different, but remain sorted (since the ring elements are - // sorted). - tx_prefix.inputs[1].ring.remove(0); + // By removing the first ring element of the second input we ensure the inputs + // are different, but remain sorted (since the ring elements are + // sorted). + tx_prefix.inputs[1].ring.remove(0); - assert_eq!(validate_inputs_are_sorted(&tx_prefix), Ok(())); + assert_eq!(validate_inputs_are_sorted(&tx_prefix), Ok(())); - // Change the ordering of inputs. - tx_prefix.inputs.swap(0, 1); - assert_eq!( - validate_inputs_are_sorted(&tx_prefix), - Err(TransactionValidationError::UnsortedInputs) - ); + // Change the ordering of inputs. + tx_prefix.inputs.swap(0, 1); + assert_eq!( + validate_inputs_are_sorted(&tx_prefix), + Err(TransactionValidationError::UnsortedInputs) + ); + } } #[test] /// validate_key_images_are_unique rejects duplicate key image. fn test_validate_key_images_are_unique_rejects_duplicate() { - let (mut tx, _ledger) = create_test_tx(); - // Tx only contains a single ring signature, which contains the key image. - // Duplicate the ring signature so that tx.key_images() returns a - // duplicate key image. - let ring_signature = tx.signature.ring_signatures[0].clone(); - tx.signature.ring_signatures.push(ring_signature); + for block_version in BlockVersion::iterator() { + let (mut tx, _ledger) = create_test_tx(block_version); + // Tx only contains a single ring signature, which contains the key image. + // Duplicate the ring signature so that tx.key_images() returns a + // duplicate key image. + let ring_signature = tx.signature.ring_signatures[0].clone(); + tx.signature.ring_signatures.push(ring_signature); - assert_eq!( - validate_key_images_are_unique(&tx), - Err(TransactionValidationError::DuplicateKeyImages) - ); + assert_eq!( + validate_key_images_are_unique(&tx), + Err(TransactionValidationError::DuplicateKeyImages) + ); + } } #[test] /// validate_key_images_are_unique returns Ok if all key images are unique. fn test_validate_key_images_are_unique_ok() { - let (tx, _ledger) = create_test_tx(); - assert_eq!(validate_key_images_are_unique(&tx), Ok(()),); + for block_version in BlockVersion::iterator() { + let (tx, _ledger) = create_test_tx(block_version); + assert_eq!(validate_key_images_are_unique(&tx), Ok(()),); + } } #[test] /// validate_outputs_public_keys_are_unique rejects duplicate public key. fn test_validate_output_public_keys_are_unique_rejects_duplicate() { - let (mut tx, _ledger) = create_test_tx(); - // Tx only contains a single output. Duplicate the - // output so that tx.output_public_keys() returns a duplicate public key. - let tx_out = tx.prefix.outputs[0].clone(); - tx.prefix.outputs.push(tx_out); + for block_version in BlockVersion::iterator() { + let (mut tx, _ledger) = create_test_tx(block_version); + // Tx only contains a single output. Duplicate the + // output so that tx.output_public_keys() returns a duplicate public key. + let tx_out = tx.prefix.outputs[0].clone(); + tx.prefix.outputs.push(tx_out); - assert_eq!( - validate_outputs_public_keys_are_unique(&tx), - Err(TransactionValidationError::DuplicateOutputPublicKey) - ); + assert_eq!( + validate_outputs_public_keys_are_unique(&tx), + Err(TransactionValidationError::DuplicateOutputPublicKey) + ); + } } #[test] /// validate_outputs_public_keys_are_unique returns Ok if all public keys /// are unique. fn test_validate_output_public_keys_are_unique_ok() { - let (tx, _ledger) = create_test_tx(); - assert_eq!(validate_outputs_public_keys_are_unique(&tx), Ok(()),); + for block_version in BlockVersion::iterator() { + let (tx, _ledger) = create_test_tx(block_version); + assert_eq!(validate_outputs_public_keys_are_unique(&tx), Ok(()),); + } } #[test] // `validate_signature` return OK for a valid transaction. fn test_validate_signature_ok() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let (tx, _ledger) = create_test_tx(); - assert_eq!(validate_signature(&tx, &mut rng), Ok(())); + + for block_version in BlockVersion::iterator() { + let (tx, _ledger) = create_test_tx(block_version); + assert_eq!(validate_signature(&tx, &mut rng), Ok(())); + } } #[test] // Should return InvalidTransactionSignature if an input is modified. fn test_transaction_signature_err_modified_input() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let (mut tx, _ledger) = create_test_tx(); - // Remove an input. - tx.prefix.inputs[0].ring.pop(); + for block_version in BlockVersion::iterator() { + let (mut tx, _ledger) = create_test_tx(block_version); - match validate_signature(&tx, &mut rng) { - Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. - Err(e) => { - panic!("Unexpected error {}", e); + // Remove an input. + tx.prefix.inputs[0].ring.pop(); + + match validate_signature(&tx, &mut rng) { + Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. + Err(e) => { + panic!("Unexpected error {}", e); + } + Ok(()) => panic!("Unexpected success"), } - Ok(()) => panic!("Unexpected success"), } } @@ -911,18 +959,21 @@ mod tests { // Should return InvalidTransactionSignature if an output is modified. fn test_transaction_signature_err_modified_output() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let (mut tx, _ledger) = create_test_tx(); - // Add an output. - let output = tx.prefix.outputs.get(0).unwrap().clone(); - tx.prefix.outputs.push(output); + for block_version in BlockVersion::iterator() { + let (mut tx, _ledger) = create_test_tx(block_version); + + // Add an output. + let output = tx.prefix.outputs.get(0).unwrap().clone(); + tx.prefix.outputs.push(output); - match validate_signature(&tx, &mut rng) { - Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. - Err(e) => { - panic!("Unexpected error {}", e); + match validate_signature(&tx, &mut rng) { + Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. + Err(e) => { + panic!("Unexpected error {}", e); + } + Ok(()) => panic!("Unexpected success"), } - Ok(()) => panic!("Unexpected success"), } } @@ -930,54 +981,63 @@ mod tests { // Should return InvalidTransactionSignature if the fee is modified. fn test_transaction_signature_err_modified_fee() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let (mut tx, _ledger) = create_test_tx(); - tx.prefix.fee = tx.prefix.fee + 1; + for block_version in BlockVersion::iterator() { + let (mut tx, _ledger) = create_test_tx(block_version); + + tx.prefix.fee = tx.prefix.fee + 1; - match validate_signature(&tx, &mut rng) { - Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. - Err(e) => { - panic!("Unexpected error {}", e); + match validate_signature(&tx, &mut rng) { + Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. + Err(e) => { + panic!("Unexpected error {}", e); + } + Ok(()) => panic!("Unexpected success"), } - Ok(()) => panic!("Unexpected success"), } } #[test] fn test_validate_transaction_fee() { - { - // Zero fees gets rejected - let (tx, _ledger) = create_test_tx_with_amount(INITIALIZE_LEDGER_AMOUNT, 0); - assert_eq!( - validate_transaction_fee(&tx, 1000), - Err(TransactionValidationError::TxFeeError) - ); - } + for block_version in BlockVersion::iterator() { + { + // Zero fees gets rejected + let (tx, _ledger) = + create_test_tx_with_amount(block_version, INITIALIZE_LEDGER_AMOUNT, 0); + assert_eq!( + validate_transaction_fee(&tx, 1000), + Err(TransactionValidationError::TxFeeError) + ); + } - { - // Off by one fee gets rejected - let fee = Mob::MINIMUM_FEE - 1; - let (tx, _ledger) = create_test_tx_with_amount(INITIALIZE_LEDGER_AMOUNT - fee, fee); - assert_eq!( - validate_transaction_fee(&tx, Mob::MINIMUM_FEE), - Err(TransactionValidationError::TxFeeError) - ); - } + { + // Off by one fee gets rejected + let fee = Mob::MINIMUM_FEE - 1; + let (tx, _ledger) = + create_test_tx_with_amount(block_version, INITIALIZE_LEDGER_AMOUNT - fee, fee); + assert_eq!( + validate_transaction_fee(&tx, Mob::MINIMUM_FEE), + Err(TransactionValidationError::TxFeeError) + ); + } - { - // Exact fee amount is okay - let (tx, _ledger) = create_test_tx_with_amount( - INITIALIZE_LEDGER_AMOUNT - Mob::MINIMUM_FEE, - Mob::MINIMUM_FEE, - ); - assert_eq!(validate_transaction_fee(&tx, Mob::MINIMUM_FEE), Ok(())); - } + { + // Exact fee amount is okay + let (tx, _ledger) = create_test_tx_with_amount( + block_version, + INITIALIZE_LEDGER_AMOUNT - Mob::MINIMUM_FEE, + Mob::MINIMUM_FEE, + ); + assert_eq!(validate_transaction_fee(&tx, Mob::MINIMUM_FEE), Ok(())); + } - { - // Overpaying fees is okay - let fee = Mob::MINIMUM_FEE + 1; - let (tx, _ledger) = create_test_tx_with_amount(INITIALIZE_LEDGER_AMOUNT - fee, fee); - assert_eq!(validate_transaction_fee(&tx, Mob::MINIMUM_FEE), Ok(())); + { + // Overpaying fees is okay + let fee = Mob::MINIMUM_FEE + 1; + let (tx, _ledger) = + create_test_tx_with_amount(block_version, INITIALIZE_LEDGER_AMOUNT - fee, fee); + assert_eq!(validate_transaction_fee(&tx, Mob::MINIMUM_FEE), Ok(())); + } } } diff --git a/transaction/core/test-utils/src/lib.rs b/transaction/core/test-utils/src/lib.rs index 97ebd8348b..d6066ebaab 100644 --- a/transaction/core/test-utils/src/lib.rs +++ b/transaction/core/test-utils/src/lib.rs @@ -13,9 +13,9 @@ pub use mc_transaction_core::{ ring_signature::KeyImage, tokens::Mob, tx::{Tx, TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Block, BlockID, BlockIndex, Token, BLOCK_VERSION, + Block, BlockID, BlockIndex, BlockVersion, Token, }; -use mc_transaction_std::{InputCredentials, NoMemoBuilder, TransactionBuilder}; +use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; use mc_util_from_random::FromRandom; use rand::{seq::SliceRandom, Rng}; use tempdir::TempDir; @@ -42,6 +42,7 @@ pub fn create_ledger() -> LedgerDB { /// * `tombstone_block` - The tombstone block for the new transaction. /// * `rng` - The randomness used by this function pub fn create_transaction( + block_version: BlockVersion, ledger: &mut L, tx_out: &TxOut, sender: &AccountKey, @@ -56,6 +57,7 @@ pub fn create_transaction( assert!(value >= Mob::MINIMUM_FEE); create_transaction_with_amount( + block_version, ledger, tx_out, sender, @@ -78,6 +80,7 @@ pub fn create_transaction( /// * `tombstone_block` - The tombstone block for the new transaction. /// * `rng` - The randomness used by this function pub fn create_transaction_with_amount( + block_version: BlockVersion, ledger: &mut L, tx_out: &TxOut, sender: &AccountKey, @@ -87,8 +90,11 @@ pub fn create_transaction_with_amount( tombstone_block: BlockIndex, rng: &mut R, ) -> Tx { - let mut transaction_builder = - TransactionBuilder::new(MockFogResolver::default(), NoMemoBuilder::default()); + let mut transaction_builder = TransactionBuilder::new( + block_version, + MockFogResolver::default(), + EmptyMemoBuilder::default(), + ); // The first transaction in the origin block should contain enough outputs to // use as mixins. @@ -159,6 +165,7 @@ pub fn create_transaction_with_amount( /// /// Returns the blocks that were created. pub fn initialize_ledger( + block_version: BlockVersion, ledger: &mut L, n_blocks: u64, account_key: &AccountKey, @@ -176,6 +183,7 @@ pub fn initialize_ledger( let (block, block_contents) = match to_spend { Some(tx_out) => { let tx = create_transaction( + block_version, ledger, &tx_out, account_key, @@ -190,7 +198,7 @@ pub fn initialize_ledger( let block_contents = BlockContents::new(key_images, outputs); let block = Block::new( - 0, + block_version, &parent.as_ref().unwrap().id, block_index, parent.as_ref().unwrap().cumulative_txo_count, @@ -241,6 +249,7 @@ pub fn initialize_ledger( /// Generate a list of blocks, each with a random number of transactions. pub fn get_blocks( + block_version: BlockVersion, recipients: &[PublicAddress], n_blocks: usize, min_txs_per_block: usize, @@ -264,7 +273,7 @@ pub fn get_blocks( ) }) .collect(); - let outputs = get_outputs(&recipient_and_amount, rng); + let outputs = get_outputs(block_version, &recipient_and_amount, rng); // Non-origin blocks must have at least one key image. let key_images = vec![KeyImage::from(block_index as u64)]; @@ -278,7 +287,7 @@ pub fn get_blocks( }; let block = - Block::new_with_parent(BLOCK_VERSION, &last_block, &root_element, &block_contents); + Block::new_with_parent(block_version, &last_block, &root_element, &block_contents); last_block = block.clone(); @@ -290,19 +299,24 @@ pub fn get_blocks( /// Generate a set of outputs that "mint" coins for each recipient. pub fn get_outputs( + block_version: BlockVersion, recipient_and_amount: &[(PublicAddress, u64)], rng: &mut T, ) -> Vec { recipient_and_amount .iter() .map(|(recipient, value)| { - TxOut::new( + let mut result = TxOut::new( *value, recipient, &RistrettoPrivate::from_random(rng), Default::default(), ) - .unwrap() + .unwrap(); + if !block_version.e_memo_feature_is_supported() { + result.e_memo = None; + } + result }) .collect() } diff --git a/transaction/core/tests/blockchain.rs b/transaction/core/tests/blockchain.rs index da2cc14f9a..d0d2b51ab2 100644 --- a/transaction/core/tests/blockchain.rs +++ b/transaction/core/tests/blockchain.rs @@ -1,34 +1,38 @@ use mc_account_keys::AccountKey; -use mc_transaction_core::{Block, BlockContents}; +use mc_transaction_core::{Block, BlockContents, BlockVersion}; #[test] fn test_cumulative_txo_counts() { mc_util_test_helper::run_with_several_seeds(|mut rng| { - let origin = Block::new_origin_block(&[]); + for block_version in BlockVersion::iterator() { + let origin = Block::new_origin_block(&[]); - let accounts: Vec = (0..20).map(|_i| AccountKey::random(&mut rng)).collect(); - let recipient_pub_keys = accounts - .iter() - .map(|account| account.default_subaddress()) - .collect::>(); + let accounts: Vec = + (0..20).map(|_i| AccountKey::random(&mut rng)).collect(); + let recipient_pub_keys = accounts + .iter() + .map(|account| account.default_subaddress()) + .collect::>(); - let results: Vec<(Block, BlockContents)> = mc_transaction_core_test_utils::get_blocks( - &recipient_pub_keys[..], - 1, - 50, - 50, - &origin, - &mut rng, - ); - - let mut prev = origin.clone(); - for (block, block_contents) in &results { - assert_eq!( - block.cumulative_txo_count, - prev.cumulative_txo_count + block_contents.outputs.len() as u64 + let results: Vec<(Block, BlockContents)> = mc_transaction_core_test_utils::get_blocks( + block_version, + &recipient_pub_keys[..], + 1, + 50, + 50, + &origin, + &mut rng, ); - assert_eq!(block.parent_id, prev.id); - prev = block.clone(); + + let mut prev = origin.clone(); + for (block, block_contents) in &results { + assert_eq!( + block.cumulative_txo_count, + prev.cumulative_txo_count + block_contents.outputs.len() as u64 + ); + assert_eq!(block.parent_id, prev.id); + prev = block.clone(); + } } }) } diff --git a/transaction/core/tests/digest-test-vectors.rs b/transaction/core/tests/digest-test-vectors.rs index 544257be84..7edbd2640c 100644 --- a/transaction/core/tests/digest-test-vectors.rs +++ b/transaction/core/tests/digest-test-vectors.rs @@ -1,7 +1,9 @@ use mc_account_keys::AccountKey; use mc_crypto_digestible_test_utils::*; use mc_crypto_keys::RistrettoPrivate; -use mc_transaction_core::{encrypted_fog_hint::EncryptedFogHint, tx::TxOut, Block, BlockContents}; +use mc_transaction_core::{ + encrypted_fog_hint::EncryptedFogHint, tx::TxOut, Block, BlockContents, BlockVersion, +}; use mc_util_from_random::FromRandom; use rand_core::{RngCore, SeedableRng}; use rand_hc::Hc128Rng as FixedRng; @@ -33,7 +35,7 @@ fn test_origin_tx_outs() -> Vec { .collect() } -fn test_blockchain() -> Vec<(Block, BlockContents)> { +fn test_blockchain(block_version: BlockVersion) -> Vec<(Block, BlockContents)> { let mut rng: FixedRng = SeedableRng::from_seed([10u8; 32]); let origin_tx_outs = test_origin_tx_outs(); @@ -45,6 +47,7 @@ fn test_blockchain() -> Vec<(Block, BlockContents)> { .collect::>(); mc_transaction_core_test_utils::get_blocks( + block_version, &recipient_pub_keys[..], 3, 50, @@ -211,7 +214,7 @@ fn origin_block_digestible_ast() { #[test] fn block_contents_digestible_test_vectors() { - let mut results = test_blockchain(); + let results = test_blockchain(BlockVersion::TWO); // Test digest of block contents assert_eq!( @@ -236,12 +239,8 @@ fn block_contents_digestible_test_vectors() { ] ); - // Now remove all memos and run the old test vectors - for (_, ref mut block_contents) in results.iter_mut() { - for ref mut output in block_contents.outputs.iter_mut() { - output.e_memo = None; - } - } + // Now set block version 1 and run the old test vectors + let results = test_blockchain(BlockVersion::ONE); // Test digest of block contents assert_eq!( diff --git a/transaction/std/src/error.rs b/transaction/std/src/error.rs index c3f782ad10..879e47d857 100644 --- a/transaction/std/src/error.rs +++ b/transaction/std/src/error.rs @@ -44,6 +44,12 @@ pub enum TxBuilderError { /// Memo: {0} Memo(NewMemoError), + + /// Block version ({0} < {1}) is too old to be supported + BlockVersionTooOld(u32, u32), + + /// Block version ({0} > {1}) is too new to be supported + BlockVersionTooNew(u32, u32), } impl From for TxBuilderError { diff --git a/transaction/std/src/lib.rs b/transaction/std/src/lib.rs index f4f673948f..0f0fe30794 100644 --- a/transaction/std/src/lib.rs +++ b/transaction/std/src/lib.rs @@ -20,7 +20,7 @@ pub use memo::{ DestinationMemoError, MemoDecodingError, MemoType, RegisteredMemoType, SenderMemoCredential, UnusedMemo, }; -pub use memo_builder::{EmptyMemoBuilder, MemoBuilder, NoMemoBuilder, RTHMemoBuilder}; +pub use memo_builder::{EmptyMemoBuilder, MemoBuilder, RTHMemoBuilder}; pub use transaction_builder::TransactionBuilder; // Re-export this to help the exported macros work diff --git a/transaction/std/src/memo_builder/mod.rs b/transaction/std/src/memo_builder/mod.rs index b243d36ddb..d23dc20195 100644 --- a/transaction/std/src/memo_builder/mod.rs +++ b/transaction/std/src/memo_builder/mod.rs @@ -20,6 +20,10 @@ pub use rth_memo_builder::RTHMemoBuilder; /// installed in the transaction builder when that is constructed. /// This way low-level handing of memo payloads with TxOuts is not needed, /// and just invoking the TransactionBuilder as before will do the right thing. +/// +/// Note: Even if the memo builder creates memo paylaods, they will be filtered +/// out by the transaction builder if the block version is too low for memos +/// to be supported. pub trait MemoBuilder: Debug { /// Set the fee. /// The memo builder is in the loop when the fee is set and changed, @@ -34,7 +38,7 @@ pub trait MemoBuilder: Debug { value: u64, recipient: &PublicAddress, memo_context: MemoContext, - ) -> Result, NewMemoError>; + ) -> Result; /// Build a memo for a change output (to ourselves). fn make_memo_for_change_output( @@ -42,7 +46,7 @@ pub trait MemoBuilder: Debug { value: u64, change_destination: &ChangeDestination, memo_context: MemoContext, - ) -> Result, NewMemoError>; + ) -> Result; } /// The empty memo builder always builds UnusedMemo. @@ -60,42 +64,8 @@ impl MemoBuilder for EmptyMemoBuilder { _value: u64, _recipient: &PublicAddress, _memo_context: MemoContext, - ) -> Result, NewMemoError> { - Ok(Some(memo::UnusedMemo {}.into())) - } - - fn make_memo_for_change_output( - &mut self, - _value: u64, - _change_destination: &ChangeDestination, - _memo_context: MemoContext, - ) -> Result, NewMemoError> { - Ok(Some(memo::UnusedMemo {}.into())) - } -} - -/// The NoMemoBuilder always selects None for the memo. -/// This can be used in the transitional period when the servers transition from -/// not expecting or accepting memos, to allowing memos to be optional. -/// In a future release, memos will become mandatory and this memo builder will -/// be removed in favor of the EmptyMemoBuilder. (The EmptyMemoBuilder won't -/// work in the period of time before the servers that know about memos have -/// been deployed) -#[derive(Default, Clone, Debug)] -pub struct NoMemoBuilder; - -impl MemoBuilder for NoMemoBuilder { - fn set_fee(&mut self, _fee: u64) -> Result<(), NewMemoError> { - Ok(()) - } - - fn make_memo_for_output( - &mut self, - _value: u64, - _recipient: &PublicAddress, - _memo_context: MemoContext, - ) -> Result, NewMemoError> { - Ok(None) + ) -> Result { + Ok(memo::UnusedMemo {}.into()) } fn make_memo_for_change_output( @@ -103,7 +73,7 @@ impl MemoBuilder for NoMemoBuilder { _value: u64, _change_destination: &ChangeDestination, _memo_context: MemoContext, - ) -> Result, NewMemoError> { - Ok(None) + ) -> Result { + Ok(memo::UnusedMemo {}.into()) } } diff --git a/transaction/std/src/memo_builder/rth_memo_builder.rs b/transaction/std/src/memo_builder/rth_memo_builder.rs index 4e99d5b767..a2cb09d66a 100644 --- a/transaction/std/src/memo_builder/rth_memo_builder.rs +++ b/transaction/std/src/memo_builder/rth_memo_builder.rs @@ -146,7 +146,7 @@ impl MemoBuilder for RTHMemoBuilder { value: u64, recipient: &PublicAddress, memo_context: MemoContext, - ) -> Result, NewMemoError> { + ) -> Result { if self.wrote_destination_memo { return Err(NewMemoError::OutputsAfterChange); } @@ -179,7 +179,7 @@ impl MemoBuilder for RTHMemoBuilder { } else { UnusedMemo {}.into() }; - Ok(Some(payload)) + Ok(payload) } /// Build a memo for a change output (to ourselves). @@ -188,9 +188,9 @@ impl MemoBuilder for RTHMemoBuilder { _value: u64, _change_destination: &ChangeDestination, _memo_context: MemoContext, - ) -> Result, NewMemoError> { + ) -> Result { if !self.destination_memo_enabled { - return Ok(Some(UnusedMemo {}.into())); + return Ok(UnusedMemo {}.into()); } if self.wrote_destination_memo { return Err(NewMemoError::MultipleChangeOutputs); @@ -203,7 +203,7 @@ impl MemoBuilder for RTHMemoBuilder { Ok(mut d_memo) => { self.wrote_destination_memo = true; d_memo.set_num_recipients(self.num_recipients); - Ok(Some(d_memo.into())) + Ok(d_memo.into()) } Err(err) => match err { DestinationMemoError::FeeTooLarge => Err(NewMemoError::LimitsExceeded("fee")), diff --git a/transaction/std/src/transaction_builder.rs b/transaction/std/src/transaction_builder.rs index a3f04bd41c..6b18195221 100644 --- a/transaction/std/src/transaction_builder.rs +++ b/transaction/std/src/transaction_builder.rs @@ -17,7 +17,7 @@ use mc_transaction_core::{ ring_signature::SignatureRctBulletproofs, tokens::Mob, tx::{Tx, TxIn, TxOut, TxOutConfirmationNumber, TxPrefix}, - CompressedCommitment, MemoContext, MemoPayload, NewMemoError, Token, + BlockVersion, CompressedCommitment, MemoContext, MemoPayload, NewMemoError, Token, }; use mc_util_from_random::FromRandom; use rand_core::{CryptoRng, RngCore}; @@ -32,6 +32,8 @@ use rand_core::{CryptoRng, RngCore}; /// use the memos in the TxOuts. #[derive(Debug)] pub struct TransactionBuilder { + /// The block version that we are targetting for this transaction + block_version: BlockVersion, /// The input credentials used to form the transaction input_credentials: Vec, /// The outputs created by the transaction, and associated shared secrets @@ -65,10 +67,11 @@ impl TransactionBuilder { /// * `memo_builder` - An object which creates memos for the TxOuts in this /// transaction pub fn new( + block_version: BlockVersion, fog_resolver: FPR, memo_builder: MB, ) -> Self { - TransactionBuilder::new_with_box(fog_resolver, Box::new(memo_builder)) + TransactionBuilder::new_with_box(block_version, fog_resolver, Box::new(memo_builder)) } /// Initializes a new TransactionBuilder, using a Box @@ -80,10 +83,12 @@ impl TransactionBuilder { /// * `memo_builder` - An object which creates memos for the TxOuts in this /// transaction pub fn new_with_box( + block_version: BlockVersion, fog_resolver: FPR, memo_builder: Box, ) -> Self { TransactionBuilder { + block_version, input_credentials: Vec::new(), outputs_and_shared_secrets: Vec::new(), tombstone_block: u64::max_value(), @@ -126,11 +131,18 @@ impl TransactionBuilder { .memo_builder .take() .expect("memo builder is missing, this is a logic error"); + let block_version = self.block_version; let result = self.add_output_with_fog_hint_address( value, recipient, recipient, - |memo_ctxt| mb.make_memo_for_output(value, recipient, memo_ctxt), + |memo_ctxt| { + if block_version.e_memo_feature_is_supported() { + Some(mb.make_memo_for_output(value, recipient, memo_ctxt)).transpose() + } else { + Ok(None) + } + }, rng, ); // Put the memo builder back @@ -178,11 +190,19 @@ impl TransactionBuilder { .memo_builder .take() .expect("memo builder is missing, this is a logic error"); + let block_version = self.block_version; let result = self.add_output_with_fog_hint_address( value, &change_destination.change_subaddress, &change_destination.primary_address, - |memo_ctxt| mb.make_memo_for_change_output(value, change_destination, memo_ctxt), + |memo_ctxt| { + if block_version.e_memo_feature_is_supported() { + Some(mb.make_memo_for_change_output(value, change_destination, memo_ctxt)) + .transpose() + } else { + Ok(None) + } + }, rng, ); // Put the memo builder back @@ -270,6 +290,25 @@ impl TransactionBuilder { /// Consume the builder and return the transaction. pub fn build(mut self, rng: &mut RNG) -> Result { + // Note: Origin block has block version zero, so some clients like slam that + // start with a bootstrapped ledger will target block version 0. However, + // block version zero has no special rules and so targetting block version 0 + // should be the same as targetting block version 1, for the transaction + // builder. This test is mainly here in case we decide that the + // transaction builder should stop supporting sufficiently old block + // versions in the future, then we can replace the zero here with + // something else. + if self.block_version < BlockVersion::default() { + return Err(TxBuilderError::BlockVersionTooOld(*self.block_version, 0)); + } + + if self.block_version > BlockVersion::MAX { + return Err(TxBuilderError::BlockVersionTooNew( + *self.block_version, + *BlockVersion::MAX, + )); + } + if self.input_credentials.is_empty() { return Err(TxBuilderError::NoInputs); } @@ -434,6 +473,7 @@ pub mod transaction_builder_tests { subaddress_matches_tx_out, tx::TxOutMembershipProof, validation::validate_signature, + TokenId, }; use rand::{rngs::StdRng, SeedableRng}; use std::convert::TryFrom; @@ -451,6 +491,7 @@ pub mod transaction_builder_tests { /// # Returns /// * A transaction output, and the shared secret for this TxOut. fn create_output( + block_version: BlockVersion, value: u64, recipient: &PublicAddress, fog_resolver: &FPR, @@ -461,7 +502,13 @@ pub mod transaction_builder_tests { value, recipient, hint, - |_| Ok(Some(MemoPayload::default())), + |_| { + Ok(if block_version.e_memo_feature_is_supported() { + Some(MemoPayload::default()) + } else { + None + }) + }, rng, ) } @@ -476,6 +523,7 @@ pub mod transaction_builder_tests { /// /// Returns (ring, real_index) fn get_ring( + block_version: BlockVersion, ring_size: usize, account: &AccountKey, value: u64, @@ -487,14 +535,21 @@ pub mod transaction_builder_tests { // Create ring_size - 1 mixins. for _i in 0..ring_size - 1 { let address = AccountKey::random(rng).default_subaddress(); - let (tx_out, _) = create_output(value, &address, fog_resolver, rng).unwrap(); + let (tx_out, _) = + create_output(block_version, value, &address, fog_resolver, rng).unwrap(); ring.push(tx_out); } // Insert the real element. let real_index = (rng.next_u64() % ring_size as u64) as usize; - let (tx_out, _) = - create_output(value, &account.default_subaddress(), fog_resolver, rng).unwrap(); + let (tx_out, _) = create_output( + block_version, + value, + &account.default_subaddress(), + fog_resolver, + rng, + ) + .unwrap(); ring.insert(real_index, tx_out); assert_eq!(ring.len(), ring_size); @@ -510,12 +565,13 @@ pub mod transaction_builder_tests { /// /// Returns (input_credentials) fn get_input_credentials( + block_version: BlockVersion, account: &AccountKey, value: u64, fog_resolver: &FPR, rng: &mut RNG, ) -> InputCredentials { - let (ring, real_index) = get_ring(3, account, value, fog_resolver, rng); + let (ring, real_index) = get_ring(block_version, 3, account, value, fog_resolver, rng); let real_output = ring[real_index].clone(); let onetime_private_key = recover_onetime_private_key( @@ -545,6 +601,7 @@ pub mod transaction_builder_tests { // Uses TransactionBuilder to build a transaction. fn get_transaction( + block_version: BlockVersion, num_inputs: usize, num_outputs: usize, sender: &AccountKey, @@ -552,14 +609,18 @@ pub mod transaction_builder_tests { fog_resolver: FPR, rng: &mut RNG, ) -> Result { - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), EmptyMemoBuilder::default()); + let mut transaction_builder = TransactionBuilder::new( + block_version, + fog_resolver.clone(), + EmptyMemoBuilder::default(), + ); let input_value = 1000; let output_value = 10; // Inputs for _i in 0..num_inputs { - let input_credentials = get_input_credentials(sender, input_value, &fog_resolver, rng); + let input_credentials = + get_input_credentials(block_version, sender, input_value, &fog_resolver, rng); transaction_builder.add_input(input_credentials); } @@ -577,372 +638,223 @@ pub mod transaction_builder_tests { transaction_builder.build(rng) } + // Helper which produces a list of block_version, TokenId pairs to iterate over + // in tests + fn get_block_version_token_id_pairs() -> Vec<(BlockVersion, TokenId)> { + vec![ + (BlockVersion::try_from(1).unwrap(), TokenId::from(0)), + (BlockVersion::try_from(2).unwrap(), TokenId::from(0)), + ] + } + #[test] // Spend a single input and send its full value to a single recipient. fn test_simple_transaction() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let fpr = MockFogResolver::default(); - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); - let value = 1475 * MILLIMOB_TO_PICOMOB; - - // Mint an initial collection of outputs, including one belonging to Alice. - let input_credentials = get_input_credentials(&sender, value, &fpr, &mut rng); - let membership_proofs = input_credentials.membership_proofs.clone(); - let key_image = KeyImage::from(&input_credentials.onetime_private_key); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let fpr = MockFogResolver::default(); + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); + let value = 1475 * MILLIMOB_TO_PICOMOB; - let mut transaction_builder = TransactionBuilder::new(fpr, EmptyMemoBuilder::default()); + // Mint an initial collection of outputs, including one belonging to Alice. + let input_credentials = + get_input_credentials(block_version, &sender, value, &fpr, &mut rng); - transaction_builder.add_input(input_credentials); - let (_txout, confirmation) = transaction_builder - .add_output( - value - Mob::MINIMUM_FEE, - &recipient.default_subaddress(), - &mut rng, - ) - .unwrap(); - - let tx = transaction_builder.build(&mut rng).unwrap(); - - // The transaction should have a single input. - assert_eq!(tx.prefix.inputs.len(), 1); - - assert_eq!(tx.prefix.inputs[0].proofs.len(), membership_proofs.len()); - - let expected_key_images = vec![key_image]; - assert_eq!(tx.key_images(), expected_key_images); - - // The transaction should have one output. - assert_eq!(tx.prefix.outputs.len(), 1); + let membership_proofs = input_credentials.membership_proofs.clone(); + let key_image = KeyImage::from(&input_credentials.onetime_private_key); - let output: &TxOut = tx.prefix.outputs.get(0).unwrap(); - - // The output should belong to the correct recipient. - assert!(subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).unwrap()); - - // The output should have the correct value and confirmation number - { - let public_key = RistrettoPublic::try_from(&output.public_key).unwrap(); - assert!(confirmation.validate(&public_key, &recipient.view_private_key())); - } - - // The transaction should have a valid signature. - assert!(validate_signature(&tx, &mut rng).is_ok()); - } + let mut transaction_builder = + TransactionBuilder::new(block_version, fpr, EmptyMemoBuilder::default()); - #[test] - // Spend a single input and send its full value to a single fog recipient. - fn test_simple_fog_transaction() { - let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random_with_fog(&mut rng); - let ingest_private_key = RistrettoPrivate::from_random(&mut rng); - - let fog_resolver = MockFogResolver(btreemap! { - recipient - .default_subaddress() - .fog_report_url() - .unwrap() - .to_string() - => - FullyValidatedFogPubkey { - pubkey: RistrettoPublic::from(&ingest_private_key), - pubkey_expiry: 1000, - }, - }); - - let value = 1475 * MILLIMOB_TO_PICOMOB; - - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - - let membership_proofs = input_credentials.membership_proofs.clone(); - let key_image = KeyImage::from(&input_credentials.onetime_private_key); - - let mut transaction_builder = - TransactionBuilder::new(fog_resolver, EmptyMemoBuilder::default()); - - transaction_builder.add_input(input_credentials); - let (_txout, confirmation) = transaction_builder - .add_output( - value - Mob::MINIMUM_FEE, - &recipient.default_subaddress(), - &mut rng, - ) - .unwrap(); + transaction_builder.add_input(input_credentials); + let (_txout, confirmation) = transaction_builder + .add_output( + value - Mob::MINIMUM_FEE, + &recipient.default_subaddress(), + &mut rng, + ) + .unwrap(); - let tx = transaction_builder.build(&mut rng).unwrap(); + let tx = transaction_builder.build(&mut rng).unwrap(); - // The transaction should have a single input. - assert_eq!(tx.prefix.inputs.len(), 1); + // The transaction should have a single input. + assert_eq!(tx.prefix.inputs.len(), 1); - assert_eq!(tx.prefix.inputs[0].proofs.len(), membership_proofs.len()); + assert_eq!(tx.prefix.inputs[0].proofs.len(), membership_proofs.len()); - let expected_key_images = vec![key_image]; - assert_eq!(tx.key_images(), expected_key_images); + let expected_key_images = vec![key_image]; + assert_eq!(tx.key_images(), expected_key_images); - // The transaction should have one output. - assert_eq!(tx.prefix.outputs.len(), 1); + // The transaction should have one output. + assert_eq!(tx.prefix.outputs.len(), 1); - let output: &TxOut = tx.prefix.outputs.get(0).unwrap(); + let output: &TxOut = tx.prefix.outputs.get(0).unwrap(); - // The output should belong to the correct recipient. - assert!(subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).unwrap()); + // The output should belong to the correct recipient. + assert!( + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).unwrap() + ); - // The output should have the correct value and confirmation number - { - let public_key = RistrettoPublic::try_from(&output.public_key).unwrap(); - assert!(confirmation.validate(&public_key, &recipient.view_private_key())); - } + // The output should have the correct value and confirmation number + { + let public_key = RistrettoPublic::try_from(&output.public_key).unwrap(); + assert!(confirmation.validate(&public_key, &recipient.view_private_key())); + } - // The output's fog hint should contain the correct public key. - { - let mut output_fog_hint = FogHint::new(RistrettoPublic::from_random(&mut rng)); - assert!(bool::from(FogHint::ct_decrypt( - &ingest_private_key, - &output.e_fog_hint, - &mut output_fog_hint - ))); - assert_eq!( - output_fog_hint.get_view_pubkey(), - &CompressedRistrettoPublic::from(recipient.default_subaddress().view_public_key()) - ); + // The transaction should have a valid signature. + assert!(validate_signature(&tx, &mut rng).is_ok()); } - - // The transaction should have a valid signature. - assert!(validate_signature(&tx, &mut rng).is_ok()); } #[test] - // Use a custom PublicAddress to create the fog hint. - fn test_custom_fog_hint_address() { + // Spend a single input and send its full value to a single fog recipient. + fn test_simple_fog_transaction() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); - let fog_hint_address = AccountKey::random_with_fog(&mut rng).default_subaddress(); - let ingest_private_key = RistrettoPrivate::from_random(&mut rng); - let value = 1475 * MILLIMOB_TO_PICOMOB; - - let fog_resolver = MockFogResolver(btreemap! { - fog_hint_address - .fog_report_url() - .unwrap() - .to_string() - => - FullyValidatedFogPubkey { - pubkey: RistrettoPublic::from(&ingest_private_key), - pubkey_expiry: 1000, - }, - }); - - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), EmptyMemoBuilder::default()); - - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); - - let (_txout, _confirmation) = transaction_builder - .add_output_with_fog_hint_address( - value - Mob::MINIMUM_FEE, - &recipient.default_subaddress(), - &fog_hint_address, - |_| Ok(Default::default()), - &mut rng, - ) - .unwrap(); - - let tx = transaction_builder.build(&mut rng).unwrap(); - // The transaction should have one output. - assert_eq!(tx.prefix.outputs.len(), 1); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random_with_fog(&mut rng); + let ingest_private_key = RistrettoPrivate::from_random(&mut rng); - let output: &TxOut = tx.prefix.outputs.get(0).unwrap(); + let fog_resolver = MockFogResolver(btreemap! { + recipient + .default_subaddress() + .fog_report_url() + .unwrap() + .to_string() + => + FullyValidatedFogPubkey { + pubkey: RistrettoPublic::from(&ingest_private_key), + pubkey_expiry: 1000, + }, + }); - // The output should belong to the correct recipient. - assert!(subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).unwrap()); + let value = 1475 * MILLIMOB_TO_PICOMOB; - // The output's fog hint should contain the correct public key. - { - let mut output_fog_hint = FogHint::new(RistrettoPublic::from_random(&mut rng)); - assert!(bool::from(FogHint::ct_decrypt( - &ingest_private_key, - &output.e_fog_hint, - &mut output_fog_hint - ))); - assert_eq!( - output_fog_hint.get_view_pubkey(), - &CompressedRistrettoPublic::from(fog_hint_address.view_public_key()) - ); - } - } + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); - #[test] - // Test that fog pubkey expiry limit is enforced on the tombstone block - fn test_fog_pubkey_expiry_limit_enforced() { - let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random_with_fog(&mut rng); - let recipient_address = recipient.default_subaddress(); - let ingest_private_key = RistrettoPrivate::from_random(&mut rng); - let value = 1475 * MILLIMOB_TO_PICOMOB; - - let fog_resolver = MockFogResolver(btreemap! { - recipient_address - .fog_report_url() - .unwrap() - .to_string() - => - FullyValidatedFogPubkey { - pubkey: RistrettoPublic::from(&ingest_private_key), - pubkey_expiry: 1000, - }, - }); + let membership_proofs = input_credentials.membership_proofs.clone(); + let key_image = KeyImage::from(&input_credentials.onetime_private_key); - { let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), EmptyMemoBuilder::default()); - - transaction_builder.set_tombstone_block(2000); + TransactionBuilder::new(block_version, fog_resolver, EmptyMemoBuilder::default()); - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); transaction_builder.add_input(input_credentials); - - let (_txout, _confirmation) = transaction_builder - .add_output(value - Mob::MINIMUM_FEE, &recipient_address, &mut rng) + let (_txout, confirmation) = transaction_builder + .add_output( + value - Mob::MINIMUM_FEE, + &recipient.default_subaddress(), + &mut rng, + ) .unwrap(); let tx = transaction_builder.build(&mut rng).unwrap(); - // The transaction should have one output. - assert_eq!(tx.prefix.outputs.len(), 1); + // The transaction should have a single input. + assert_eq!(tx.prefix.inputs.len(), 1); - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 1000); - } + assert_eq!(tx.prefix.inputs[0].proofs.len(), membership_proofs.len()); - { - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), EmptyMemoBuilder::default()); + let expected_key_images = vec![key_image]; + assert_eq!(tx.key_images(), expected_key_images); - transaction_builder.set_tombstone_block(500); + // The transaction should have one output. + assert_eq!(tx.prefix.outputs.len(), 1); - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); + let output: &TxOut = tx.prefix.outputs.get(0).unwrap(); - let (_txout, _confirmation) = transaction_builder - .add_output(value - Mob::MINIMUM_FEE, &recipient_address, &mut rng) - .unwrap(); + // The output should belong to the correct recipient. + assert!( + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).unwrap() + ); - let tx = transaction_builder.build(&mut rng).unwrap(); + // The output should have the correct value and confirmation number + { + let public_key = RistrettoPublic::try_from(&output.public_key).unwrap(); + assert!(confirmation.validate(&public_key, &recipient.view_private_key())); + } - // The transaction should have one output. - assert_eq!(tx.prefix.outputs.len(), 1); + // The output's fog hint should contain the correct public key. + { + let mut output_fog_hint = FogHint::new(RistrettoPublic::from_random(&mut rng)); + assert!(bool::from(FogHint::ct_decrypt( + &ingest_private_key, + &output.e_fog_hint, + &mut output_fog_hint + ))); + assert_eq!( + output_fog_hint.get_view_pubkey(), + &CompressedRistrettoPublic::from( + recipient.default_subaddress().view_public_key() + ) + ); + } - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 500); + // The transaction should have a valid signature. + assert!(validate_signature(&tx, &mut rng).is_ok()); } } #[test] - // Test that sending a fog transaction with change, and recoverable transaction - // history, produces appropriate memos - fn test_fog_transaction_with_change() { + // Use a custom PublicAddress to create the fog hint. + fn test_custom_fog_hint_address() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender = AccountKey::random_with_fog(&mut rng); - let sender_change_dest = ChangeDestination::from(&sender); - let recipient = AccountKey::random_with_fog(&mut rng); - let recipient_address = recipient.default_subaddress(); - let ingest_private_key = RistrettoPrivate::from_random(&mut rng); - let value = 1475 * MILLIMOB_TO_PICOMOB; - let change_value = 128 * MILLIMOB_TO_PICOMOB; - - let fog_resolver = MockFogResolver(btreemap! { - recipient_address - .fog_report_url() - .unwrap() - .to_string() - => - FullyValidatedFogPubkey { - pubkey: RistrettoPublic::from(&ingest_private_key), - pubkey_expiry: 1000, - }, - }); - { - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), EmptyMemoBuilder::default()); - - transaction_builder.set_tombstone_block(2000); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); + let fog_hint_address = AccountKey::random_with_fog(&mut rng).default_subaddress(); + let ingest_private_key = RistrettoPrivate::from_random(&mut rng); + let value = 1475 * MILLIMOB_TO_PICOMOB; + + let fog_resolver = MockFogResolver(btreemap! { + fog_hint_address + .fog_report_url() + .unwrap() + .to_string() + => + FullyValidatedFogPubkey { + pubkey: RistrettoPublic::from(&ingest_private_key), + pubkey_expiry: 1000, + }, + }); + + let mut transaction_builder = TransactionBuilder::new( + block_version, + fog_resolver.clone(), + EmptyMemoBuilder::default(), + ); - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder - .add_output( - value - change_value - Mob::MINIMUM_FEE, - &recipient_address, + .add_output_with_fog_hint_address( + value - Mob::MINIMUM_FEE, + &recipient.default_subaddress(), + &fog_hint_address, + |_| Ok(Default::default()), &mut rng, ) .unwrap(); - transaction_builder - .add_change_output(change_value, &sender_change_dest, &mut rng) - .unwrap(); - let tx = transaction_builder.build(&mut rng).unwrap(); - // The transaction should have two output. - assert_eq!(tx.prefix.outputs.len(), 2); - - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 1000); + // The transaction should have one output. + assert_eq!(tx.prefix.outputs.len(), 1); - let output = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find recipient's output"); - let change = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find sender's output"); + let output: &TxOut = tx.prefix.outputs.get(0).unwrap(); + // The output should belong to the correct recipient. assert!( - !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).unwrap() ); - assert!( - !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!(!subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap()); - assert!( - !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output).unwrap() - ); - - // The 1st output should belong to the correct recipient and have correct amount - // and have an empty memo - { - let ss = get_tx_out_shared_secret( - recipient.view_private_key(), - &RistrettoPublic::try_from(&output.public_key).unwrap(), - ); - let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); - - let memo = output.e_memo.clone().unwrap().decrypt(&ss); - assert_eq!(memo, MemoPayload::default()); - } - // The 1st output's fog hint should contain the correct public key. + // The output's fog hint should contain the correct public key. { let mut output_fog_hint = FogHint::new(RistrettoPublic::from_random(&mut rng)); assert!(bool::from(FogHint::ct_decrypt( @@ -952,684 +864,960 @@ pub mod transaction_builder_tests { ))); assert_eq!( output_fog_hint.get_view_pubkey(), - &CompressedRistrettoPublic::from(recipient_address.view_public_key()) - ); - } - - // The 2nd output should belong to the correct recipient and have correct amount - // and have empty memo - { - let ss = get_tx_out_shared_secret( - sender.view_private_key(), - &RistrettoPublic::try_from(&change.public_key).unwrap(), - ); - let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, change_value); - - let memo = change.e_memo.clone().unwrap().decrypt(&ss); - assert_eq!(memo, MemoPayload::default()); - } - - // The 2nd output's fog hint should contain the correct public key. - { - let mut output_fog_hint = FogHint::new(RistrettoPublic::from_random(&mut rng)); - assert!(bool::from(FogHint::ct_decrypt( - &ingest_private_key, - &change.e_fog_hint, - &mut output_fog_hint - ))); - assert_eq!( - output_fog_hint.get_view_pubkey(), - &CompressedRistrettoPublic::from(sender.default_subaddress().view_public_key()) + &CompressedRistrettoPublic::from(fog_hint_address.view_public_key()) ); } } } #[test] - // Test that sending a fog transaction with change, using add_change_output - // produces change owned by the sender as expected, with appropriate memos - fn test_fog_transaction_with_change_and_rth_memos() { + // Test that fog pubkey expiry limit is enforced on the tombstone block + fn test_fog_pubkey_expiry_limit_enforced() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender = AccountKey::random_with_fog(&mut rng); - let sender_addr = sender.default_subaddress(); - let sender_change_dest = ChangeDestination::from(&sender); - let recipient = AccountKey::random_with_fog(&mut rng); - let recipient_address = recipient.default_subaddress(); - let ingest_private_key = RistrettoPrivate::from_random(&mut rng); - let value = 1475 * MILLIMOB_TO_PICOMOB; - let change_value = 128 * MILLIMOB_TO_PICOMOB; - - let fog_resolver = MockFogResolver(btreemap! { - recipient_address - .fog_report_url() - .unwrap() - .to_string() - => - FullyValidatedFogPubkey { - pubkey: RistrettoPublic::from(&ingest_private_key), - pubkey_expiry: 1000, - }, - }); - - // Enable both sender and destination memos - { - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); - memo_builder.enable_destination_memo(); - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), memo_builder); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random_with_fog(&mut rng); + let recipient_address = recipient.default_subaddress(); + let ingest_private_key = RistrettoPrivate::from_random(&mut rng); + let value = 1475 * MILLIMOB_TO_PICOMOB; + + let fog_resolver = MockFogResolver(btreemap! { + recipient_address + .fog_report_url() + .unwrap() + .to_string() + => + FullyValidatedFogPubkey { + pubkey: RistrettoPublic::from(&ingest_private_key), + pubkey_expiry: 1000, + }, + }); - transaction_builder.set_tombstone_block(2000); - - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); + { + let mut transaction_builder = TransactionBuilder::new( + block_version, + fog_resolver.clone(), + EmptyMemoBuilder::default(), + ); - let (_txout, _confirmation) = transaction_builder - .add_output( - value - change_value - Mob::MINIMUM_FEE, - &recipient_address, - &mut rng, - ) - .unwrap(); + transaction_builder.set_tombstone_block(2000); - transaction_builder - .add_change_output(change_value, &sender_change_dest, &mut rng) - .unwrap(); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - let tx = transaction_builder.build(&mut rng).unwrap(); + let (_txout, _confirmation) = transaction_builder + .add_output(value - Mob::MINIMUM_FEE, &recipient_address, &mut rng) + .unwrap(); - // The transaction should have two output. - assert_eq!(tx.prefix.outputs.len(), 2); + let tx = transaction_builder.build(&mut rng).unwrap(); - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 1000); + // The transaction should have one output. + assert_eq!(tx.prefix.outputs.len(), 1); - let output = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find recipient's output"); - let change = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find sender's output"); - - assert!( - !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!( - !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!(!subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap()); - assert!( - !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output).unwrap() - ); - - // The 1st output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - recipient.view_private_key(), - &RistrettoPublic::try_from(&output.public_key).unwrap(), - ); - let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); - - let memo = output.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::AuthenticatedSender(memo) => { - assert_eq!( - memo.sender_address_hash(), - ShortAddressHash::from(&sender_addr), - "lookup based on address hash failed" - ); - assert!( - bool::from(memo.validate( - &sender_addr, - &recipient.subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), - &output.public_key - )), - "hmac validation failed" - ); - } - _ => { - panic!("unexpected memo type") - } - } + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 1000); } - // The 2nd output should belong to the correct recipient and have correct amount - // and have correct memo { - let ss = get_tx_out_shared_secret( - sender.view_private_key(), - &RistrettoPublic::try_from(&change.public_key).unwrap(), + let mut transaction_builder = TransactionBuilder::new( + block_version, + fog_resolver.clone(), + EmptyMemoBuilder::default(), ); - let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, change_value); - - let memo = change.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::Destination(memo) => { - assert_eq!( - memo.get_address_hash(), - &ShortAddressHash::from(&recipient_address), - "lookup based on address hash failed" - ); - assert_eq!(memo.get_num_recipients(), 1); - assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE); - assert_eq!( - memo.get_total_outlay(), - value - change_value, - "outlay should be amount sent to recipient + fee" - ); - } - _ => { - panic!("unexpected memo type") - } - } - } - } - // Enable both sender and destination memos, and try increasing the fee - { - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); - memo_builder.enable_destination_memo(); + transaction_builder.set_tombstone_block(500); - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), memo_builder); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - transaction_builder.set_tombstone_block(2000); - transaction_builder.set_fee(Mob::MINIMUM_FEE * 4).unwrap(); + let (_txout, _confirmation) = transaction_builder + .add_output(value - Mob::MINIMUM_FEE, &recipient_address, &mut rng) + .unwrap(); - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); + let tx = transaction_builder.build(&mut rng).unwrap(); - let (_txout, _confirmation) = transaction_builder - .add_output( - value - change_value - Mob::MINIMUM_FEE * 4, - &recipient_address, - &mut rng, - ) - .unwrap(); + // The transaction should have one output. + assert_eq!(tx.prefix.outputs.len(), 1); - transaction_builder - .add_change_output(change_value, &sender_change_dest, &mut rng) - .unwrap(); + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 500); + } + } + } - let tx = transaction_builder.build(&mut rng).unwrap(); + #[test] + // Test that sending a fog transaction with change, and recoverable transaction + // history, produces appropriate memos + fn test_fog_transaction_with_change() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - // The transaction should have two output. - assert_eq!(tx.prefix.outputs.len(), 2); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let sender = AccountKey::random_with_fog(&mut rng); + let sender_change_dest = ChangeDestination::from(&sender); + let recipient = AccountKey::random_with_fog(&mut rng); + let recipient_address = recipient.default_subaddress(); + let ingest_private_key = RistrettoPrivate::from_random(&mut rng); + let value = 1475 * MILLIMOB_TO_PICOMOB; + let change_value = 128 * MILLIMOB_TO_PICOMOB; + + let fog_resolver = MockFogResolver(btreemap! { + recipient_address + .fog_report_url() + .unwrap() + .to_string() + => + FullyValidatedFogPubkey { + pubkey: RistrettoPublic::from(&ingest_private_key), + pubkey_expiry: 1000, + }, + }); - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 1000); + { + let mut transaction_builder = TransactionBuilder::new( + block_version, + fog_resolver.clone(), + EmptyMemoBuilder::default(), + ); - let output = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find recipient's output"); - let change = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find sender's output"); + transaction_builder.set_tombstone_block(2000); - assert!( - !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!( - !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!(!subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap()); - assert!( - !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output).unwrap() - ); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - // The 1st output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - recipient.view_private_key(), - &RistrettoPublic::try_from(&output.public_key).unwrap(), + let (_txout, _confirmation) = transaction_builder + .add_output( + value - change_value - Mob::MINIMUM_FEE, + &recipient_address, + &mut rng, + ) + .unwrap(); + + transaction_builder + .add_change_output(change_value, &sender_change_dest, &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + + // The transaction should have two output. + assert_eq!(tx.prefix.outputs.len(), 2); + + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 1000); + + let output = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out) + .unwrap() + }) + .expect("Didn't find recipient's output"); + let change = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() + }) + .expect("Didn't find sender's output"); + + assert!( + !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change) + .unwrap() ); - let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE * 4); - - let memo = output.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::AuthenticatedSender(memo) => { - assert_eq!( - memo.sender_address_hash(), - ShortAddressHash::from(&sender_addr), - "lookup based on address hash failed" - ); - assert!( - bool::from(memo.validate( - &sender_addr, - &recipient.subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), - &output.public_key - )), - "hmac validation failed" - ); - } - _ => { - panic!("unexpected memo type") + assert!( + !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output) + .unwrap() + ); + + // The 1st output should belong to the correct recipient and have correct amount + // and have an empty memo + { + let ss = get_tx_out_shared_secret( + recipient.view_private_key(), + &RistrettoPublic::try_from(&output.public_key).unwrap(), + ); + let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); + + if block_version.e_memo_feature_is_supported() { + let memo = output.e_memo.clone().unwrap().decrypt(&ss); + assert_eq!(memo, MemoPayload::default()); } } - } - // The 2nd output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - sender.view_private_key(), - &RistrettoPublic::try_from(&change.public_key).unwrap(), - ); - let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, change_value); - - let memo = change.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::Destination(memo) => { - assert_eq!( - memo.get_address_hash(), - &ShortAddressHash::from(&recipient_address), - "lookup based on address hash failed" - ); - assert_eq!(memo.get_num_recipients(), 1); - assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE * 4); - assert_eq!( - memo.get_total_outlay(), - value - change_value, - "outlay should be amount sent to recipient + fee" - ); - } - _ => { - panic!("unexpected memo type") + // The 1st output's fog hint should contain the correct public key. + { + let mut output_fog_hint = FogHint::new(RistrettoPublic::from_random(&mut rng)); + assert!(bool::from(FogHint::ct_decrypt( + &ingest_private_key, + &output.e_fog_hint, + &mut output_fog_hint + ))); + assert_eq!( + output_fog_hint.get_view_pubkey(), + &CompressedRistrettoPublic::from(recipient_address.view_public_key()) + ); + } + + // The 2nd output should belong to the correct recipient and have correct amount + // and have empty memo + { + let ss = get_tx_out_shared_secret( + sender.view_private_key(), + &RistrettoPublic::try_from(&change.public_key).unwrap(), + ); + let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, change_value); + + if block_version.e_memo_feature_is_supported() { + let memo = change.e_memo.clone().unwrap().decrypt(&ss); + assert_eq!(memo, MemoPayload::default()); } } + + // The 2nd output's fog hint should contain the correct public key. + { + let mut output_fog_hint = FogHint::new(RistrettoPublic::from_random(&mut rng)); + assert!(bool::from(FogHint::ct_decrypt( + &ingest_private_key, + &change.e_fog_hint, + &mut output_fog_hint + ))); + assert_eq!( + output_fog_hint.get_view_pubkey(), + &CompressedRistrettoPublic::from( + sender.default_subaddress().view_public_key() + ) + ); + } } } + } - // Enable both sender and destination memos, and set a payment request id - { - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); - memo_builder.enable_destination_memo(); - memo_builder.set_payment_request_id(42); - - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), memo_builder); - - transaction_builder.set_tombstone_block(2000); - - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); - - let (_txout, _confirmation) = transaction_builder - .add_output( - value - change_value - Mob::MINIMUM_FEE, - &recipient_address, - &mut rng, - ) - .unwrap(); - - transaction_builder - .add_change_output(change_value, &sender_change_dest, &mut rng) - .unwrap(); + #[test] + // Test that sending a fog transaction with change, using add_change_output + // produces change owned by the sender as expected, with appropriate memos + fn test_fog_transaction_with_change_and_rth_memos() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let tx = transaction_builder.build(&mut rng).unwrap(); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let sender = AccountKey::random_with_fog(&mut rng); + let sender_addr = sender.default_subaddress(); + let sender_change_dest = ChangeDestination::from(&sender); + let recipient = AccountKey::random_with_fog(&mut rng); + let recipient_address = recipient.default_subaddress(); + let ingest_private_key = RistrettoPrivate::from_random(&mut rng); + let value = 1475 * MILLIMOB_TO_PICOMOB; + let change_value = 128 * MILLIMOB_TO_PICOMOB; + + let fog_resolver = MockFogResolver(btreemap! { + recipient_address + .fog_report_url() + .unwrap() + .to_string() + => + FullyValidatedFogPubkey { + pubkey: RistrettoPublic::from(&ingest_private_key), + pubkey_expiry: 1000, + }, + }); + + // Enable both sender and destination memos + { + let mut memo_builder = RTHMemoBuilder::default(); + memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); + memo_builder.enable_destination_memo(); - // The transaction should have two output. - assert_eq!(tx.prefix.outputs.len(), 2); + let mut transaction_builder = + TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 1000); + transaction_builder.set_tombstone_block(2000); - let output = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find recipient's output"); - let change = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find sender's output"); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - assert!( - !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!( - !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!(!subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap()); - assert!( - !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output).unwrap() - ); + let (_txout, _confirmation) = transaction_builder + .add_output( + value - change_value - Mob::MINIMUM_FEE, + &recipient_address, + &mut rng, + ) + .unwrap(); - // The 1st output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - recipient.view_private_key(), - &RistrettoPublic::try_from(&output.public_key).unwrap(), + transaction_builder + .add_change_output(change_value, &sender_change_dest, &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + + // The transaction should have two output. + assert_eq!(tx.prefix.outputs.len(), 2); + + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 1000); + + let output = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out) + .unwrap() + }) + .expect("Didn't find recipient's output"); + let change = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() + }) + .expect("Didn't find sender's output"); + + assert!( + !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change) + .unwrap() ); - let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); - - let memo = output.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::AuthenticatedSenderWithPaymentRequestId(memo) => { - assert_eq!( - memo.sender_address_hash(), - ShortAddressHash::from(&sender_addr), - "lookup based on address hash failed" - ); - assert!( - bool::from(memo.validate( - &sender_addr, - &recipient.subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), - &output.public_key - )), - "hmac validation failed" - ); - assert_eq!(memo.payment_request_id(), 42); - } - _ => { - panic!("unexpected memo type") + assert!( + !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output) + .unwrap() + ); + + // The 1st output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + recipient.view_private_key(), + &RistrettoPublic::try_from(&output.public_key).unwrap(), + ); + let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); + + if block_version.e_memo_feature_is_supported() { + let memo = output.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::AuthenticatedSender(memo) => { + assert_eq!( + memo.sender_address_hash(), + ShortAddressHash::from(&sender_addr), + "lookup based on address hash failed" + ); + assert!( + bool::from( + memo.validate( + &sender_addr, + &recipient + .subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), + &output.public_key + ) + ), + "hmac validation failed" + ); + } + _ => { + panic!("unexpected memo type") + } + } } } - } - // The 2nd output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - sender.view_private_key(), - &RistrettoPublic::try_from(&change.public_key).unwrap(), - ); - let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, change_value); - - let memo = change.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::Destination(memo) => { - assert_eq!( - memo.get_address_hash(), - &ShortAddressHash::from(&recipient_address), - "lookup based on address hash failed" - ); - assert_eq!(memo.get_num_recipients(), 1); - assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE); - assert_eq!( - memo.get_total_outlay(), - value - change_value, - "outlay should be amount sent to recipient + fee" - ); - } - _ => { - panic!("unexpected memo type") + // The 2nd output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + sender.view_private_key(), + &RistrettoPublic::try_from(&change.public_key).unwrap(), + ); + let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, change_value); + + if block_version.e_memo_feature_is_supported() { + let memo = change.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::Destination(memo) => { + assert_eq!( + memo.get_address_hash(), + &ShortAddressHash::from(&recipient_address), + "lookup based on address hash failed" + ); + assert_eq!(memo.get_num_recipients(), 1); + assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE); + assert_eq!( + memo.get_total_outlay(), + value - change_value, + "outlay should be amount sent to recipient + fee" + ); + } + _ => { + panic!("unexpected memo type") + } + } } } } - } - // Enable sender memos, and set a payment request id, no destination_memo - { - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); - memo_builder.set_payment_request_id(47); + // Enable both sender and destination memos, and try increasing the fee + { + let mut memo_builder = RTHMemoBuilder::default(); + memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); + memo_builder.enable_destination_memo(); - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), memo_builder); + let mut transaction_builder = + TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); - transaction_builder.set_tombstone_block(2000); + transaction_builder.set_tombstone_block(2000); + transaction_builder.set_fee(Mob::MINIMUM_FEE * 4).unwrap(); - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - let (_txout, _confirmation) = transaction_builder - .add_output( - value - change_value - Mob::MINIMUM_FEE, - &recipient_address, - &mut rng, - ) - .unwrap(); + let (_txout, _confirmation) = transaction_builder + .add_output( + value - change_value - Mob::MINIMUM_FEE * 4, + &recipient_address, + &mut rng, + ) + .unwrap(); - transaction_builder - .add_change_output(change_value, &sender_change_dest, &mut rng) - .unwrap(); + transaction_builder + .add_change_output(change_value, &sender_change_dest, &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + + // The transaction should have two output. + assert_eq!(tx.prefix.outputs.len(), 2); + + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 1000); + + let output = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out) + .unwrap() + }) + .expect("Didn't find recipient's output"); + let change = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() + }) + .expect("Didn't find sender's output"); + + assert!( + !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change) + .unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output) + .unwrap() + ); - let tx = transaction_builder.build(&mut rng).unwrap(); + // The 1st output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + recipient.view_private_key(), + &RistrettoPublic::try_from(&output.public_key).unwrap(), + ); + let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE * 4); + + if block_version.e_memo_feature_is_supported() { + let memo = output.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::AuthenticatedSender(memo) => { + assert_eq!( + memo.sender_address_hash(), + ShortAddressHash::from(&sender_addr), + "lookup based on address hash failed" + ); + assert!( + bool::from( + memo.validate( + &sender_addr, + &recipient + .subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), + &output.public_key + ) + ), + "hmac validation failed" + ); + } + _ => { + panic!("unexpected memo type") + } + } + } + } - // The transaction should have two output. - assert_eq!(tx.prefix.outputs.len(), 2); + // The 2nd output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + sender.view_private_key(), + &RistrettoPublic::try_from(&change.public_key).unwrap(), + ); + let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, change_value); + + if block_version.e_memo_feature_is_supported() { + let memo = change.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::Destination(memo) => { + assert_eq!( + memo.get_address_hash(), + &ShortAddressHash::from(&recipient_address), + "lookup based on address hash failed" + ); + assert_eq!(memo.get_num_recipients(), 1); + assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE * 4); + assert_eq!( + memo.get_total_outlay(), + value - change_value, + "outlay should be amount sent to recipient + fee" + ); + } + _ => { + panic!("unexpected memo type") + } + } + } + } + } - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 1000); + // Enable both sender and destination memos, and set a payment request id + { + let mut memo_builder = RTHMemoBuilder::default(); + memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); + memo_builder.enable_destination_memo(); + memo_builder.set_payment_request_id(42); - let output = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find recipient's output"); - let change = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find sender's output"); + let mut transaction_builder = + TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); - assert!( - !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!( - !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!(!subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap()); - assert!( - !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output).unwrap() - ); + transaction_builder.set_tombstone_block(2000); - // The 1st output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - recipient.view_private_key(), - &RistrettoPublic::try_from(&output.public_key).unwrap(), + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); + + let (_txout, _confirmation) = transaction_builder + .add_output( + value - change_value - Mob::MINIMUM_FEE, + &recipient_address, + &mut rng, + ) + .unwrap(); + + transaction_builder + .add_change_output(change_value, &sender_change_dest, &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + + // The transaction should have two output. + assert_eq!(tx.prefix.outputs.len(), 2); + + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 1000); + + let output = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out) + .unwrap() + }) + .expect("Didn't find recipient's output"); + let change = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() + }) + .expect("Didn't find sender's output"); + + assert!( + !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change) + .unwrap() ); - let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); - - let memo = output.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::AuthenticatedSenderWithPaymentRequestId(memo) => { - assert_eq!( - memo.sender_address_hash(), - ShortAddressHash::from(&sender_addr), - "lookup based on address hash failed" - ); - assert!( - bool::from(memo.validate( - &sender_addr, - &recipient.subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), - &output.public_key - )), - "hmac validation failed" - ); - assert_eq!(memo.payment_request_id(), 47); - } - _ => { - panic!("unexpected memo type") + assert!( + !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output) + .unwrap() + ); + + // The 1st output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + recipient.view_private_key(), + &RistrettoPublic::try_from(&output.public_key).unwrap(), + ); + let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); + + if block_version.e_memo_feature_is_supported() { + let memo = output.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::AuthenticatedSenderWithPaymentRequestId(memo) => { + assert_eq!( + memo.sender_address_hash(), + ShortAddressHash::from(&sender_addr), + "lookup based on address hash failed" + ); + assert!( + bool::from( + memo.validate( + &sender_addr, + &recipient + .subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), + &output.public_key + ) + ), + "hmac validation failed" + ); + assert_eq!(memo.payment_request_id(), 42); + } + _ => { + panic!("unexpected memo type") + } + } } } - } - // The 2nd output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - sender.view_private_key(), - &RistrettoPublic::try_from(&change.public_key).unwrap(), - ); - let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, change_value); - - let memo = change.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::Unused(_) => {} - _ => { - panic!("unexpected memo type") + // The 2nd output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + sender.view_private_key(), + &RistrettoPublic::try_from(&change.public_key).unwrap(), + ); + let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, change_value); + + if block_version.e_memo_feature_is_supported() { + let memo = change.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::Destination(memo) => { + assert_eq!( + memo.get_address_hash(), + &ShortAddressHash::from(&recipient_address), + "lookup based on address hash failed" + ); + assert_eq!(memo.get_num_recipients(), 1); + assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE); + assert_eq!( + memo.get_total_outlay(), + value - change_value, + "outlay should be amount sent to recipient + fee" + ); + } + _ => { + panic!("unexpected memo type") + } + } } } } - } - // Enable destination memo, and set a payment request id, but no sender - // credential - { - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.enable_destination_memo(); - memo_builder.set_payment_request_id(47); + // Enable sender memos, and set a payment request id, no destination_memo + { + let mut memo_builder = RTHMemoBuilder::default(); + memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); + memo_builder.set_payment_request_id(47); - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), memo_builder); + let mut transaction_builder = + TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); - transaction_builder.set_tombstone_block(2000); + transaction_builder.set_tombstone_block(2000); - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - let (_txout, _confirmation) = transaction_builder - .add_output( - value - change_value - Mob::MINIMUM_FEE, - &recipient_address, - &mut rng, - ) - .unwrap(); + let (_txout, _confirmation) = transaction_builder + .add_output( + value - change_value - Mob::MINIMUM_FEE, + &recipient_address, + &mut rng, + ) + .unwrap(); - transaction_builder - .add_change_output(change_value, &sender_change_dest, &mut rng) - .unwrap(); + transaction_builder + .add_change_output(change_value, &sender_change_dest, &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + + // The transaction should have two output. + assert_eq!(tx.prefix.outputs.len(), 2); + + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 1000); + + let output = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out) + .unwrap() + }) + .expect("Didn't find recipient's output"); + let change = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() + }) + .expect("Didn't find sender's output"); + + assert!( + !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change) + .unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output) + .unwrap() + ); - let tx = transaction_builder.build(&mut rng).unwrap(); + // The 1st output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + recipient.view_private_key(), + &RistrettoPublic::try_from(&output.public_key).unwrap(), + ); + let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); + + if block_version.e_memo_feature_is_supported() { + let memo = output.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::AuthenticatedSenderWithPaymentRequestId(memo) => { + assert_eq!( + memo.sender_address_hash(), + ShortAddressHash::from(&sender_addr), + "lookup based on address hash failed" + ); + assert!( + bool::from( + memo.validate( + &sender_addr, + &recipient + .subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), + &output.public_key + ) + ), + "hmac validation failed" + ); + assert_eq!(memo.payment_request_id(), 47); + } + _ => { + panic!("unexpected memo type") + } + } + } + } - // The transaction should have two output. - assert_eq!(tx.prefix.outputs.len(), 2); + // The 2nd output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + sender.view_private_key(), + &RistrettoPublic::try_from(&change.public_key).unwrap(), + ); + let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, change_value); + + if block_version.e_memo_feature_is_supported() { + let memo = change.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::Unused(_) => {} + _ => { + panic!("unexpected memo type") + } + } + } + } + } - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 1000); + // Enable destination memo, and set a payment request id, but no sender + // credential + { + let mut memo_builder = RTHMemoBuilder::default(); + memo_builder.enable_destination_memo(); + memo_builder.set_payment_request_id(47); - let output = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find recipient's output"); - let change = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find sender's output"); + let mut transaction_builder = + TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); - assert!( - !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!( - !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!(!subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap()); - assert!( - !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output).unwrap() - ); + transaction_builder.set_tombstone_block(2000); - // The 1st output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - recipient.view_private_key(), - &RistrettoPublic::try_from(&output.public_key).unwrap(), + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); + + let (_txout, _confirmation) = transaction_builder + .add_output( + value - change_value - Mob::MINIMUM_FEE, + &recipient_address, + &mut rng, + ) + .unwrap(); + + transaction_builder + .add_change_output(change_value, &sender_change_dest, &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + + // The transaction should have two output. + assert_eq!(tx.prefix.outputs.len(), 2); + + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 1000); + + let output = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, tx_out) + .unwrap() + }) + .expect("Didn't find recipient's output"); + let change = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() + }) + .expect("Didn't find sender's output"); + + assert!( + !subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &change) + .unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&sender, CHANGE_SUBADDRESS_INDEX, &output).unwrap() ); - let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); - - let memo = output.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::Unused(_) => {} - _ => { - panic!("unexpected memo type") + assert!( + !subaddress_matches_tx_out(&recipient, CHANGE_SUBADDRESS_INDEX, &output) + .unwrap() + ); + + // The 1st output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + recipient.view_private_key(), + &RistrettoPublic::try_from(&output.public_key).unwrap(), + ); + let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); + + if block_version.e_memo_feature_is_supported() { + let memo = output.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::Unused(_) => {} + _ => { + panic!("unexpected memo type") + } + } } } - } - // The 2nd output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - sender.view_private_key(), - &RistrettoPublic::try_from(&change.public_key).unwrap(), - ); - let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, change_value); - - let memo = change.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::Destination(memo) => { - assert_eq!( - memo.get_address_hash(), - &ShortAddressHash::from(&recipient_address), - "lookup based on address hash failed" - ); - assert_eq!(memo.get_num_recipients(), 1); - assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE); - assert_eq!( - memo.get_total_outlay(), - value - change_value, - "outlay should be amount sent to recipient + fee" - ); - } - _ => { - panic!("unexpected memo type") + // The 2nd output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + sender.view_private_key(), + &RistrettoPublic::try_from(&change.public_key).unwrap(), + ); + let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, change_value); + + if block_version.e_memo_feature_is_supported() { + let memo = change.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::Destination(memo) => { + assert_eq!( + memo.get_address_hash(), + &ShortAddressHash::from(&recipient_address), + "lookup based on address hash failed" + ); + assert_eq!(memo.get_num_recipients(), 1); + assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE); + assert_eq!( + memo.get_total_outlay(), + value - change_value, + "outlay should be amount sent to recipient + fee" + ); + } + _ => { + panic!("unexpected memo type") + } + } } } } @@ -1640,153 +1828,171 @@ pub mod transaction_builder_tests { // Transaction builder with RTH memo builder and custom sender credential fn test_transaction_builder_memo_custom_sender() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let alice = AccountKey::random_with_fog(&mut rng); - let alice_change_dest = ChangeDestination::from(&alice); - let bob = AccountKey::random_with_fog(&mut rng); - let bob_address = bob.default_subaddress(); - let charlie = AccountKey::random_with_fog(&mut rng); - let charlie_addr = charlie.default_subaddress(); - let ingest_private_key = RistrettoPrivate::from_random(&mut rng); - let value = 1475 * MILLIMOB_TO_PICOMOB; - let change_value = 128 * MILLIMOB_TO_PICOMOB; - - let fog_resolver = MockFogResolver(btreemap! { - bob_address - .fog_report_url() - .unwrap() - .to_string() - => - FullyValidatedFogPubkey { - pubkey: RistrettoPublic::from(&ingest_private_key), - pubkey_expiry: 1000, - }, - }); - - // Enable both sender and destination memos, but use a sender credential from - // Charlie's identity - { - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.set_sender_credential(SenderMemoCredential::from(&charlie)); - memo_builder.enable_destination_memo(); - - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), memo_builder); - - transaction_builder.set_tombstone_block(2000); - - let input_credentials = get_input_credentials(&alice, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); - - let (_txout, _confirmation) = transaction_builder - .add_output( - value - change_value - Mob::MINIMUM_FEE, - &bob_address, - &mut rng, - ) - .unwrap(); - - transaction_builder - .add_change_output(change_value, &alice_change_dest, &mut rng) - .unwrap(); - let tx = transaction_builder.build(&mut rng).unwrap(); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let alice = AccountKey::random_with_fog(&mut rng); + let alice_change_dest = ChangeDestination::from(&alice); + let bob = AccountKey::random_with_fog(&mut rng); + let bob_address = bob.default_subaddress(); + let charlie = AccountKey::random_with_fog(&mut rng); + let charlie_addr = charlie.default_subaddress(); + let ingest_private_key = RistrettoPrivate::from_random(&mut rng); + let value = 1475 * MILLIMOB_TO_PICOMOB; + let change_value = 128 * MILLIMOB_TO_PICOMOB; + + let fog_resolver = MockFogResolver(btreemap! { + bob_address + .fog_report_url() + .unwrap() + .to_string() + => + FullyValidatedFogPubkey { + pubkey: RistrettoPublic::from(&ingest_private_key), + pubkey_expiry: 1000, + }, + }); + + // Enable both sender and destination memos, but use a sender credential from + // Charlie's identity + { + let mut memo_builder = RTHMemoBuilder::default(); + memo_builder.set_sender_credential(SenderMemoCredential::from(&charlie)); + memo_builder.enable_destination_memo(); - // The transaction should have two output. - assert_eq!(tx.prefix.outputs.len(), 2); + let mut transaction_builder = + TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); - // The tombstone block should be the min of what the user requested, and what - // fog limits it to - assert_eq!(tx.prefix.tombstone_block, 1000); + transaction_builder.set_tombstone_block(2000); - let output = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&bob, DEFAULT_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find recipient's output"); - let change = tx - .prefix - .outputs - .iter() - .find(|tx_out| { - subaddress_matches_tx_out(&alice, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() - }) - .expect("Didn't find sender's output"); + let input_credentials = + get_input_credentials(block_version, &alice, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - assert!(!subaddress_matches_tx_out(&bob, DEFAULT_SUBADDRESS_INDEX, &change).unwrap()); - assert!(!subaddress_matches_tx_out(&alice, DEFAULT_SUBADDRESS_INDEX, &change).unwrap()); - assert!(!subaddress_matches_tx_out(&alice, CHANGE_SUBADDRESS_INDEX, &output).unwrap()); - assert!(!subaddress_matches_tx_out(&bob, CHANGE_SUBADDRESS_INDEX, &output).unwrap()); - assert!( - !subaddress_matches_tx_out(&charlie, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() - ); - assert!( - !subaddress_matches_tx_out(&charlie, DEFAULT_SUBADDRESS_INDEX, &output).unwrap() - ); + let (_txout, _confirmation) = transaction_builder + .add_output( + value - change_value - Mob::MINIMUM_FEE, + &bob_address, + &mut rng, + ) + .unwrap(); - // The 1st output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - bob.view_private_key(), - &RistrettoPublic::try_from(&output.public_key).unwrap(), + transaction_builder + .add_change_output(change_value, &alice_change_dest, &mut rng) + .unwrap(); + + let tx = transaction_builder.build(&mut rng).unwrap(); + + // The transaction should have two output. + assert_eq!(tx.prefix.outputs.len(), 2); + + // The tombstone block should be the min of what the user requested, and what + // fog limits it to + assert_eq!(tx.prefix.tombstone_block, 1000); + + let output = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&bob, DEFAULT_SUBADDRESS_INDEX, tx_out).unwrap() + }) + .expect("Didn't find recipient's output"); + let change = tx + .prefix + .outputs + .iter() + .find(|tx_out| { + subaddress_matches_tx_out(&alice, CHANGE_SUBADDRESS_INDEX, tx_out).unwrap() + }) + .expect("Didn't find sender's output"); + + assert!( + !subaddress_matches_tx_out(&bob, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() ); - let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); - - let memo = output.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::AuthenticatedSender(memo) => { - assert_eq!( - memo.sender_address_hash(), - ShortAddressHash::from(&charlie_addr), - "lookup based on address hash failed" - ); - assert!( - bool::from(memo.validate( - &charlie_addr, - &bob.subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), - &output.public_key - )), - "hmac validation failed" - ); - } - _ => { - panic!("unexpected memo type") + assert!( + !subaddress_matches_tx_out(&alice, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&alice, CHANGE_SUBADDRESS_INDEX, &output).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&bob, CHANGE_SUBADDRESS_INDEX, &output).unwrap() + ); + assert!( + !subaddress_matches_tx_out(&charlie, DEFAULT_SUBADDRESS_INDEX, &change) + .unwrap() + ); + assert!( + !subaddress_matches_tx_out(&charlie, DEFAULT_SUBADDRESS_INDEX, &output) + .unwrap() + ); + + // The 1st output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + bob.view_private_key(), + &RistrettoPublic::try_from(&output.public_key).unwrap(), + ); + let (tx_out_value, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, value - change_value - Mob::MINIMUM_FEE); + + if block_version.e_memo_feature_is_supported() { + let memo = output.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::AuthenticatedSender(memo) => { + assert_eq!( + memo.sender_address_hash(), + ShortAddressHash::from(&charlie_addr), + "lookup based on address hash failed" + ); + assert!( + bool::from(memo.validate( + &charlie_addr, + &bob.subaddress_view_private(DEFAULT_SUBADDRESS_INDEX), + &output.public_key + )), + "hmac validation failed" + ); + } + _ => { + panic!("unexpected memo type") + } + } } } - } - // The 2nd output should belong to the correct recipient and have correct amount - // and have correct memo - { - let ss = get_tx_out_shared_secret( - alice.view_private_key(), - &RistrettoPublic::try_from(&change.public_key).unwrap(), - ); - let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); - assert_eq!(tx_out_value, change_value); - - let memo = change.e_memo.clone().unwrap().decrypt(&ss); - match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { - MemoType::Destination(memo) => { - assert_eq!( - memo.get_address_hash(), - &ShortAddressHash::from(&bob_address), - "lookup based on address hash failed" - ); - assert_eq!(memo.get_num_recipients(), 1); - assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE); - assert_eq!( - memo.get_total_outlay(), - value - change_value, - "outlay should be amount sent to recipient + fee" - ); - } - _ => { - panic!("unexpected memo type") + // The 2nd output should belong to the correct recipient and have correct amount + // and have correct memo + { + let ss = get_tx_out_shared_secret( + alice.view_private_key(), + &RistrettoPublic::try_from(&change.public_key).unwrap(), + ); + let (tx_out_value, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(tx_out_value, change_value); + + if block_version.e_memo_feature_is_supported() { + let memo = change.e_memo.clone().unwrap().decrypt(&ss); + match MemoType::try_from(&memo).expect("Couldn't decrypt memo") { + MemoType::Destination(memo) => { + assert_eq!( + memo.get_address_hash(), + &ShortAddressHash::from(&bob_address), + "lookup based on address hash failed" + ); + assert_eq!(memo.get_num_recipients(), 1); + assert_eq!(memo.get_fee(), Mob::MINIMUM_FEE); + assert_eq!( + memo.get_total_outlay(), + value - change_value, + "outlay should be amount sent to recipient + fee" + ); + } + _ => { + panic!("unexpected memo type") + } + } } } } @@ -1798,72 +2004,80 @@ pub mod transaction_builder_tests { // after change output fn transaction_builder_rth_memo_expected_failures() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let sender = AccountKey::random_with_fog(&mut rng); - let sender_change_dest = ChangeDestination::from(&sender); - let recipient = AccountKey::random_with_fog(&mut rng); - let recipient_address = recipient.default_subaddress(); - let ingest_private_key = RistrettoPrivate::from_random(&mut rng); - let value = 1475 * MILLIMOB_TO_PICOMOB; - let change_value = 128 * MILLIMOB_TO_PICOMOB; - - let fog_resolver = MockFogResolver(btreemap! { - recipient_address - .fog_report_url() - .unwrap() - .to_string() - => - FullyValidatedFogPubkey { - pubkey: RistrettoPublic::from(&ingest_private_key), - pubkey_expiry: 1000, - }, - }); - - // Test that changing things after the change output causes an error as expected - { - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); - memo_builder.enable_destination_memo(); - let mut transaction_builder = - TransactionBuilder::new(fog_resolver.clone(), memo_builder); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + if !block_version.e_memo_feature_is_supported() { + continue; + } - transaction_builder.set_tombstone_block(2000); + let sender = AccountKey::random_with_fog(&mut rng); + let sender_change_dest = ChangeDestination::from(&sender); + let recipient = AccountKey::random_with_fog(&mut rng); + let recipient_address = recipient.default_subaddress(); + let ingest_private_key = RistrettoPrivate::from_random(&mut rng); + let value = 1475 * MILLIMOB_TO_PICOMOB; + let change_value = 128 * MILLIMOB_TO_PICOMOB; + + let fog_resolver = MockFogResolver(btreemap! { + recipient_address + .fog_report_url() + .unwrap() + .to_string() + => + FullyValidatedFogPubkey { + pubkey: RistrettoPublic::from(&ingest_private_key), + pubkey_expiry: 1000, + }, + }); + + // Test that changing things after the change output causes an error as expected + { + let mut memo_builder = RTHMemoBuilder::default(); + memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); + memo_builder.enable_destination_memo(); - let input_credentials = get_input_credentials(&sender, value, &fog_resolver, &mut rng); - transaction_builder.add_input(input_credentials); + let mut transaction_builder = + TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); - let (_txout, _confirmation) = transaction_builder - .add_output( - value - change_value - Mob::MINIMUM_FEE, - &recipient_address, - &mut rng, - ) - .unwrap(); + transaction_builder.set_tombstone_block(2000); - transaction_builder - .add_change_output(change_value, &sender_change_dest, &mut rng) - .unwrap(); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - assert!( - transaction_builder.set_fee(Mob::MINIMUM_FEE * 4).is_err(), - "setting fee after change output should be rejected" - ); + let (_txout, _confirmation) = transaction_builder + .add_output( + value - change_value - Mob::MINIMUM_FEE, + &recipient_address, + &mut rng, + ) + .unwrap(); - assert!( - transaction_builder - .add_output(Mob::MINIMUM_FEE, &recipient_address, &mut rng,) - .is_err(), - "Adding another output after chnage output should be rejected" - ); - - assert!( transaction_builder .add_change_output(change_value, &sender_change_dest, &mut rng) - .is_err(), - "Adding a second change output should be rejected" - ); + .unwrap(); + + assert!( + transaction_builder.set_fee(Mob::MINIMUM_FEE * 4).is_err(), + "setting fee after change output should be rejected" + ); - transaction_builder.build(&mut rng).unwrap(); + assert!( + transaction_builder + .add_output(Mob::MINIMUM_FEE, &recipient_address, &mut rng,) + .is_err(), + "Adding another output after chnage output should be rejected" + ); + + assert!( + transaction_builder + .add_change_output(change_value, &sender_change_dest, &mut rng) + .is_err(), + "Adding a second change output should be rejected" + ); + + transaction_builder.build(&mut rng).unwrap(); + } } } @@ -1880,52 +2094,56 @@ pub mod transaction_builder_tests { // outputs and the fee. fn test_inputs_do_not_equal_outputs() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let fpr = MockFogResolver::default(); - let alice = AccountKey::random(&mut rng); - let bob = AccountKey::random(&mut rng); - let value = 1475; - // Mint an initial collection of outputs, including one belonging to Alice. - let (ring, real_index) = get_ring(3, &alice, value, &fpr, &mut rng); - let real_output = ring[real_index].clone(); + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let fpr = MockFogResolver::default(); + let alice = AccountKey::random(&mut rng); + let bob = AccountKey::random(&mut rng); + let value = 1475; - let onetime_private_key = recover_onetime_private_key( - &RistrettoPublic::try_from(&real_output.public_key).unwrap(), - &alice.view_private_key(), - &alice.subaddress_spend_private(DEFAULT_SUBADDRESS_INDEX), - ); + // Mint an initial collection of outputs, including one belonging to Alice. + let (ring, real_index) = get_ring(block_version, 3, &alice, value, &fpr, &mut rng); + let real_output = ring[real_index].clone(); - let membership_proofs: Vec = ring - .iter() - .map(|_tx_out| { - // TransactionBuilder does not validate membership proofs, but does require one - // for each ring member. - TxOutMembershipProof::default() - }) - .collect(); - - let input_credentials = InputCredentials::new( - ring, - membership_proofs, - real_index, - onetime_private_key, - *alice.view_private_key(), - ) - .unwrap(); + let onetime_private_key = recover_onetime_private_key( + &RistrettoPublic::try_from(&real_output.public_key).unwrap(), + &alice.view_private_key(), + &alice.subaddress_spend_private(DEFAULT_SUBADDRESS_INDEX), + ); - let mut transaction_builder = TransactionBuilder::new(fpr, EmptyMemoBuilder::default()); - transaction_builder.add_input(input_credentials); + let membership_proofs: Vec = ring + .iter() + .map(|_tx_out| { + // TransactionBuilder does not validate membership proofs, but does require one + // for each ring member. + TxOutMembershipProof::default() + }) + .collect(); - let wrong_value = 999; - transaction_builder - .add_output(wrong_value, &bob.default_subaddress(), &mut rng) + let input_credentials = InputCredentials::new( + ring, + membership_proofs, + real_index, + onetime_private_key, + *alice.view_private_key(), + ) .unwrap(); - let result = transaction_builder.build(&mut rng); - // Signing should fail if value is not conserved. - match result { - Err(TxBuilderError::RingSignatureFailed) => {} // Expected. - _ => panic!("Unexpected result {:?}", result), + let mut transaction_builder = + TransactionBuilder::new(block_version, fpr, EmptyMemoBuilder::default()); + transaction_builder.add_input(input_credentials); + + let wrong_value = 999; + transaction_builder + .add_output(wrong_value, &bob.default_subaddress(), &mut rng) + .unwrap(); + + let result = transaction_builder.build(&mut rng); + // Signing should fail if value is not conserved. + match result { + Err(TxBuilderError::RingSignatureFailed) => {} // Expected. + _ => panic!("Unexpected result {:?}", result), + } } } @@ -1933,39 +2151,54 @@ pub mod transaction_builder_tests { // `build` should succeed with MAX_INPUTS and MAX_OUTPUTS. fn test_max_transaction_size() { let mut rng: StdRng = SeedableRng::from_seed([18u8; 32]); - let fpr = MockFogResolver::default(); - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); - let tx = get_transaction( - MAX_INPUTS as usize, - MAX_OUTPUTS as usize, - &sender, - &recipient, - fpr, - &mut rng, - ) - .unwrap(); - assert_eq!(tx.prefix.inputs.len(), MAX_INPUTS as usize); - assert_eq!(tx.prefix.outputs.len(), MAX_OUTPUTS as usize); + + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let fpr = MockFogResolver::default(); + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); + let tx = get_transaction( + block_version, + MAX_INPUTS as usize, + MAX_OUTPUTS as usize, + &sender, + &recipient, + fpr, + &mut rng, + ) + .unwrap(); + assert_eq!(tx.prefix.inputs.len(), MAX_INPUTS as usize); + assert_eq!(tx.prefix.outputs.len(), MAX_OUTPUTS as usize); + } } #[test] // Ring elements should be sorted by tx_out.public_key fn test_ring_elements_are_sorted() { let mut rng: StdRng = SeedableRng::from_seed([97u8; 32]); - let fpr = MockFogResolver::default(); - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); - let num_inputs = 3; - let num_outputs = 11; - let tx = - get_transaction(num_inputs, num_outputs, &sender, &recipient, fpr, &mut rng).unwrap(); - - for tx_in in &tx.prefix.inputs { - assert!(tx_in - .ring - .windows(2) - .all(|w| w[0].public_key < w[1].public_key)); + + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let fpr = MockFogResolver::default(); + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); + let num_inputs = 3; + let num_outputs = 11; + let tx = get_transaction( + block_version, + num_inputs, + num_outputs, + &sender, + &recipient, + fpr, + &mut rng, + ) + .unwrap(); + + for tx_in in &tx.prefix.inputs { + assert!(tx_in + .ring + .windows(2) + .all(|w| w[0].public_key < w[1].public_key)); + } } } @@ -1973,18 +2206,29 @@ pub mod transaction_builder_tests { // Transaction outputs should be sorted by public key. fn test_outputs_are_sorted() { let mut rng: StdRng = SeedableRng::from_seed([92u8; 32]); - let fpr = MockFogResolver::default(); - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); - let num_inputs = 3; - let num_outputs = 11; - let tx = - get_transaction(num_inputs, num_outputs, &sender, &recipient, fpr, &mut rng).unwrap(); - - let outputs = tx.prefix.outputs; - let mut expected_outputs = outputs.clone(); - expected_outputs.sort_by(|a, b| a.public_key.cmp(&b.public_key)); - assert_eq!(outputs, expected_outputs); + + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let fpr = MockFogResolver::default(); + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); + let num_inputs = 3; + let num_outputs = 11; + let tx = get_transaction( + block_version, + num_inputs, + num_outputs, + &sender, + &recipient, + fpr, + &mut rng, + ) + .unwrap(); + + let outputs = tx.prefix.outputs; + let mut expected_outputs = outputs.clone(); + expected_outputs.sort_by(|a, b| a.public_key.cmp(&b.public_key)); + assert_eq!(outputs, expected_outputs); + } } #[test] @@ -1992,17 +2236,28 @@ pub mod transaction_builder_tests { // element. fn test_inputs_are_sorted() { let mut rng: StdRng = SeedableRng::from_seed([92u8; 32]); - let fpr = MockFogResolver::default(); - let sender = AccountKey::random(&mut rng); - let recipient = AccountKey::random(&mut rng); - let num_inputs = 3; - let num_outputs = 11; - let tx = - get_transaction(num_inputs, num_outputs, &sender, &recipient, fpr, &mut rng).unwrap(); - - let inputs = tx.prefix.inputs; - let mut expected_inputs = inputs.clone(); - expected_inputs.sort_by(|a, b| a.ring[0].public_key.cmp(&b.ring[0].public_key)); - assert_eq!(inputs, expected_inputs); + + for (block_version, _token_id) in get_block_version_token_id_pairs() { + let fpr = MockFogResolver::default(); + let sender = AccountKey::random(&mut rng); + let recipient = AccountKey::random(&mut rng); + let num_inputs = 3; + let num_outputs = 11; + let tx = get_transaction( + block_version, + num_inputs, + num_outputs, + &sender, + &recipient, + fpr, + &mut rng, + ) + .unwrap(); + + let inputs = tx.prefix.inputs; + let mut expected_inputs = inputs.clone(); + expected_inputs.sort_by(|a, b| a.ring[0].public_key.cmp(&b.ring[0].public_key)); + assert_eq!(inputs, expected_inputs); + } } } diff --git a/util/generate-sample-ledger/src/lib.rs b/util/generate-sample-ledger/src/lib.rs index 9c19c02b3b..c1043eea80 100644 --- a/util/generate-sample-ledger/src/lib.rs +++ b/util/generate-sample-ledger/src/lib.rs @@ -9,13 +9,16 @@ use mc_transaction_core::{ encrypted_fog_hint::{EncryptedFogHint, ENCRYPTED_FOG_HINT_LEN}, ring_signature::KeyImage, tx::TxOut, - Block, BlockContents, BLOCK_VERSION, + Block, BlockContents, BlockVersion, }; use mc_util_from_random::FromRandom; use rand::{RngCore, SeedableRng}; use rand_hc::Hc128Rng as FixedRng; use std::{path::Path, vec::Vec}; +// This is historically the version created by bootstrap +const BLOCK_VERSION: BlockVersion = BlockVersion::ONE; + /// Deterministically populates a testnet ledger. /// /// Distributes the full value of the ledger equally to each recipient. diff --git a/watcher/src/watcher_db.rs b/watcher/src/watcher_db.rs index 325771e4c0..4d8ab6ee7c 100644 --- a/watcher/src/watcher_db.rs +++ b/watcher/src/watcher_db.rs @@ -1107,7 +1107,7 @@ pub mod tests { use mc_attest_core::VerificationSignature; use mc_common::logger::{test_with_logger, Logger}; use mc_crypto_keys::Ed25519Pair; - use mc_transaction_core::{Block, BlockContents}; + use mc_transaction_core::{Block, BlockContents, BlockVersion}; use mc_transaction_core_test_utils::get_blocks; use mc_util_from_random::FromRandom; use mc_util_test_helper::run_with_one_seed; @@ -1131,7 +1131,15 @@ pub mod tests { .iter() .map(|account| account.default_subaddress()) .collect::>(); - get_blocks(&recipient_pub_keys, 10, 1, 10, &origin, &mut rng) + get_blocks( + BlockVersion::ONE, + &recipient_pub_keys, + 10, + 1, + 10, + &origin, + &mut rng, + ) } // SignatureStore should insert and get multiple signatures.