From 849bd25d3de093361c496760fba9c6fce780046d Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Wed, 9 Feb 2022 10:54:50 -0700 Subject: [PATCH 01/17] proof of concept for block-version-based protocol evolution This makes transaction builder and validation aware of BlockVersion. It also plumbs this config into consensus. Still TODO: make it part of the responder id, review exactly how consensus is being changed. This commit adds support for the change in fog sample paykit, but I didn't do mobilecoind properly yet, want to talk to Eran about that. add code comment about latest_block_version max Update transaction/core/src/blockchain/block_version.rs Co-authored-by: Eran Rundstein Use eran suggestions about responder id config, also implement Digestible for BTreeMap This makes `(&A, &B)` and `BTreeMap` implement `Digestible` which simplifies the fee map cleanups and adding tests add some tests of Btreemap hashing split mc-tests into two jobs, `mc-consensus-*` and other fix ledger db test --- .circleci/config.yml | 36 +- android-bindings/src/bindings.rs | 19 +- api/src/convert/archive_block.rs | 4 +- api/src/convert/tx.rs | 168 +- consensus/enclave/api/src/config.rs | 146 + consensus/enclave/api/src/fee_map.rs | 79 +- consensus/enclave/api/src/lib.rs | 20 +- consensus/enclave/api/src/messages.rs | 4 +- consensus/enclave/impl/src/lib.rs | 1256 ++++---- consensus/enclave/mock/src/lib.rs | 28 +- .../mock/src/mock_consensus_enclave.rs | 7 +- consensus/enclave/src/lib.rs | 13 +- consensus/enclave/trusted/src/lib.rs | 11 +- .../service/src/api/blockchain_api_service.rs | 34 +- consensus/service/src/bin/main.rs | 13 +- consensus/service/src/byzantine_ledger/mod.rs | 13 +- consensus/service/src/config/mod.rs | 7 + consensus/service/src/tx_manager/mod.rs | 16 +- consensus/service/src/validators.rs | 761 ++--- crypto/digestible/src/lib.rs | 47 +- crypto/digestible/tests/basic.rs | 40 + fog/distribution/src/main.rs | 15 +- fog/ingest/server/tests/three_node_cluster.rs | 9 +- fog/ledger/server/src/key_image_service.rs | 2 +- fog/ledger/server/src/merkle_proof_service.rs | 2 +- fog/ledger/server/tests/connection.rs | 653 +++-- fog/load_testing/src/bin/ingest.rs | 6 +- fog/overseer/server/tests/utils/mod.rs | 9 +- fog/sample-paykit/src/cached_tx_data/mod.rs | 23 + fog/sample-paykit/src/client.rs | 229 +- fog/sample-paykit/src/client_builder.rs | 12 - fog/sample-paykit/src/error.rs | 11 +- fog/test-client/src/error.rs | 9 + fog/test-client/src/test_client.rs | 149 +- fog/test_infra/src/bin/add_test_block.rs | 7 +- fog/test_infra/src/db_tests.rs | 4 +- fog/test_infra/src/lib.rs | 4 +- fog/view/server/tests/smoke_tests.rs | 14 +- ledger/db/src/lib.rs | 61 +- ledger/db/src/test_utils/mock_ledger.rs | 4 +- ledger/sync/src/test_app/main.rs | 3 +- libmobilecoin/src/transaction.rs | 21 +- mobilecoind-json/src/data_types.rs | 5 +- mobilecoind/src/conversions.rs | 5 +- mobilecoind/src/database.rs | 3 +- mobilecoind/src/monitor_store.rs | 4 +- mobilecoind/src/payments.rs | 13 +- mobilecoind/src/processed_block_store.rs | 3 +- mobilecoind/src/service.rs | 143 +- mobilecoind/src/sync.rs | 25 +- mobilecoind/src/test_utils.rs | 16 +- mobilecoind/src/utxo_store.rs | 4 +- slam/src/main.rs | 20 +- transaction/core/src/blockchain/block.rs | 19 +- .../core/src/blockchain/block_version.rs | 147 + transaction/core/src/blockchain/mod.rs | 2 + transaction/core/src/token.rs | 4 +- transaction/core/src/validation/validate.rs | 711 ++--- transaction/core/test-utils/src/lib.rs | 32 +- transaction/core/tests/blockchain.rs | 50 +- transaction/core/tests/digest-test-vectors.rs | 17 +- transaction/std/src/error.rs | 3 + transaction/std/src/lib.rs | 2 +- transaction/std/src/memo_builder/mod.rs | 50 +- .../std/src/memo_builder/rth_memo_builder.rs | 10 +- transaction/std/src/transaction_builder.rs | 2612 +++++++++-------- util/generate-sample-ledger/src/lib.rs | 5 +- watcher/src/watcher_db.rs | 12 +- 68 files changed, 4555 insertions(+), 3331 deletions(-) create mode 100644 consensus/enclave/api/src/config.rs create mode 100644 transaction/core/src/blockchain/block_version.rs 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/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/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/src/lib.rs b/consensus/enclave/impl/src/lib.rs index 85bf494ace..6da4fd90d2 100644 --- a/consensus/enclave/impl/src/lib.rs +++ b/consensus/enclave/impl/src/lib.rs @@ -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,7 +50,7 @@ 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, }; use prost::Message; use rand_core::{CryptoRng, RngCore}; @@ -108,8 +109,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: Mutex>, } impl SgxConsensusEnclave { @@ -121,7 +126,7 @@ impl SgxConsensusEnclave { &mut McRng::default(), )), logger, - fee_map: Mutex::new(FeeMap::default()), + blockchain_config: Mutex::new(None), } } @@ -172,10 +177,13 @@ 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.lock().unwrap() = Some(blockchain_config); // Init AKE. self.ake.init(peer_self_id, client_self_id.clone())?; @@ -197,8 +205,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 +215,14 @@ 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 + .lock()? + .as_ref() + .expect("enclave was not initialized") + .get_config() + .fee_map + .get_fee_for_token(token_id)) } fn get_identity(&self) -> Result { @@ -247,8 +260,13 @@ 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 + .lock()? + .as_ref() + .expect("enclave was not initialized") + .responder_id(peer_id); Ok(self.ake.peer_init(&peer_id)?) } @@ -262,8 +280,13 @@ 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 passing off to the AKE + let peer_id = self + .blockchain_config + .lock()? + .as_ref() + .expect("enclave was not initialized") + .responder_id(peer_id); Ok(self.ake.peer_connect(&peer_id, msg)?) } @@ -341,6 +364,12 @@ impl ConsensusEnclave for SgxConsensusEnclave { block_index: u64, proofs: Vec, ) -> Result<(WellFormedEncryptedTx, WellFormedTxContext)> { + let blockchain_config = self.blockchain_config.lock()?; + let config = blockchain_config + .as_ref() + .expect("enclave was not initialized") + .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 +392,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 +401,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { mc_transaction_core::validation::validate( &tx, block_index, + config.block_version, &proofs, minimum_fee, &mut csprng, @@ -424,6 +453,12 @@ impl ConsensusEnclave for SgxConsensusEnclave { encrypted_txs_with_proofs: &[(WellFormedEncryptedTx, Vec)], root_element: &TxOutMembershipElement, ) -> Result<(Block, BlockContents, BlockSignature)> { + let blockchain_config = self.blockchain_config.lock()?; + let config = blockchain_config + .as_ref() + .expect("enclave was not initialized") + .get_config(); + // This implicitly converts Vec),_>> into // Result)>, _>, and terminates the // iteration when the first Error is encountered. @@ -441,9 +476,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 +487,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 +599,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 +664,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 +682,767 @@ 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(); - // 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]; + let tx = create_transaction( + block_version, + &mut ledger, + &tx_out, + &sender, + &recipient.default_subaddress(), + n_blocks + 1, + &mut rng, + ); - assert_eq!( - enclave.tx_is_well_formed(corrputed_locally_encrypted_tx, block_index, proofs), - Err(Error::CacheCipher( - mc_crypto_message_cipher::CipherError::MacFailure - )) - ); + // 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` 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(); + .unwrap(); + + // Verify signature. + { + assert_eq!( + signature.signer(), + &enclave + .ake + .get_identity() + .signing_keypair + .lock() + .unwrap() + .public_key() + ); + + assert!(signature.verify(&block).is_ok()); + } - // 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(); + // `block_contents` should include the aggregate fee. - // 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 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 (block, block_contents, signature) = enclave - .form_block( - &parent_block, - &well_formed_encrypted_txs_with_proofs, - &root_element, - ) - .unwrap(); + let fee_output_public_key = RistrettoPublic::try_from(&fee_output.public_key).unwrap(); - // Verify signature. - { - assert_eq!( - signature.signer(), - &enclave - .ake - .get_identity() - .signing_keypair - .lock() - .unwrap() - .public_key() - ); - - 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(); + + 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]; - // Check - let expected = Err(Error::FormBlock(format!( - "Duplicate key image: {:?}", - expected_duplicate_key_image - ))); + // Check + let expected = Err(Error::FormBlock(format!( + "Duplicate key image: {:?}", + expected_duplicate_key_image + ))); - assert_eq!(form_block_result, expected); + 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, + ); + } + + // 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 root_element = ledger.get_root_tx_out_membership_element().unwrap(); + // 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]; + 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(); - // Check - let expected = Err(Error::MalformedTx( - TransactionValidationError::InvalidTxOutMembershipProof, - )); - assert_eq!(form_block_result, expected); + 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); + } } #[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, - ); - 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<_> = 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 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(); + // 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]; + // 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, - ); + let form_block_result = enclave.form_block( + &parent_block, + &well_formed_encrypted_txs_with_proofs, + &root_element, + ); - // Check - let expected = Err(Error::InvalidLocalMembershipRootElement); - assert_eq!(form_block_result, expected); + // Check + let expected = Err(Error::InvalidLocalMembershipRootElement); + assert_eq!(form_block_result, expected); + } } } 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/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..3047931c0d 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..6afe8b2701 100644 --- a/crypto/digestible/src/lib.rs +++ b/crypto/digestible/src/lib.rs @@ -446,6 +446,25 @@ 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-dreive handles tuple structs and enums in +// tuples. + +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 +548,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 +627,32 @@ cfg_if! { } } } + + // Treat a BTreeMap as a (sorted) sequence + // This implementation should match that for &[(T, U)] + // + // Note: We don't currently implement digestible for tuples, but we should. + // Digestible derive works on tuple structs and that's what we are mirroring + 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..86bc366d26 100644 --- a/crypto/digestible/tests/basic.rs +++ b/crypto/digestible/tests/basic.rs @@ -466,6 +466,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 diff --git a/fog/distribution/src/main.rs b/fog/distribution/src/main.rs index 693f6e29dd..9f576c1640 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,12 @@ fn build_tx( // Sanity assert_eq!(utxos_with_proofs.len(), rings.len()); + 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..3c9c821c91 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: 1u32, 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. @@ -662,6 +676,15 @@ impl CachedTxData { Ok(response) => { self.latest_global_txo_count = core::cmp::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 = + core::cmp::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..1998e428f3 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::*; @@ -763,8 +765,13 @@ 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..783a957829 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,15 @@ fn build_tx( // Sanity assert_eq!(utxos_with_proofs.len(), rings.len()); + 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..75ac6897ca --- /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) + } +} + +#[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..26c3b69888 100644 --- a/transaction/core/src/validation/validate.rs +++ b/transaction/core/src/validation/validate.rs @@ -11,7 +11,7 @@ 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,6 +30,7 @@ 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, @@ -61,12 +62,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 +218,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 +430,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 +450,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 +481,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 +521,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 +556,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"), - ); - - // 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) - ); - } + for block_version in BlockVersion::iterator() { + let (tx, ledger) = create_test_tx(block_version); - // 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 +593,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); + + // 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); + 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 +952,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 +974,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); - match validate_signature(&tx, &mut rng) { - Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. - Err(e) => { - panic!("Unexpected error {}", e); + tx.prefix.fee = tx.prefix.fee + 1; + + 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..a364993936 100644 --- a/transaction/std/src/error.rs +++ b/transaction/std/src/error.rs @@ -44,6 +44,9 @@ pub enum TxBuilderError { /// Memo: {0} Memo(NewMemoError), + + /// Block version ({0} < {1}) is too old to be supported + BlockVersionTooOld(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..6222e559ef 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,10 @@ impl TransactionBuilder { /// Consume the builder and return the transaction. pub fn build(mut self, rng: &mut RNG) -> Result { + if self.block_version < BlockVersion::ONE { + return Err(TxBuilderError::BlockVersionTooOld(*self.block_version, 1)); + } + if self.input_credentials.is_empty() { return Err(TxBuilderError::NoInputs); } @@ -434,6 +458,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 +476,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 +487,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 +508,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 +520,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 +550,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 +586,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 +594,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 +623,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); - - let mut transaction_builder = TransactionBuilder::new(fpr, 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(); - - 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); + 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 output: &TxOut = tx.prefix.outputs.get(0).unwrap(); + // Mint an initial collection of outputs, including one belonging to Alice. + let input_credentials = + get_input_credentials(block_version, &sender, value, &fpr, &mut rng); - // The output should belong to the correct recipient. - assert!(subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).unwrap()); + let membership_proofs = input_credentials.membership_proofs.clone(); + let key_image = KeyImage::from(&input_credentials.onetime_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 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(); + 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); - // The transaction should have one output. - assert_eq!(tx.prefix.outputs.len(), 1); + 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 output: &TxOut = tx.prefix.outputs.get(0).unwrap(); + let value = 1475 * MILLIMOB_TO_PICOMOB; - // The output should belong to the correct recipient. - assert!(subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).unwrap()); - - // 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() - ); - assert!( - !subaddress_matches_tx_out(&sender, DEFAULT_SUBADDRESS_INDEX, &change).unwrap() + subaddress_matches_tx_out(&recipient, DEFAULT_SUBADDRESS_INDEX, &output).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 +849,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); - 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_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 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(); + { + let mut transaction_builder = TransactionBuilder::new( + block_version, + fog_resolver.clone(), + EmptyMemoBuilder::default(), + ); - transaction_builder - .add_change_output(change_value, &sender_change_dest, &mut rng) - .unwrap(); + transaction_builder.set_tombstone_block(2000); - let tx = transaction_builder.build(&mut rng).unwrap(); + let input_credentials = + get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + transaction_builder.add_input(input_credentials); - // The transaction should have two output. - assert_eq!(tx.prefix.outputs.len(), 2); + let (_txout, _confirmation) = transaction_builder + .add_output(value - Mob::MINIMUM_FEE, &recipient_address, &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); + let tx = transaction_builder.build(&mut rng).unwrap(); - 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"); + // The transaction should have one output. + assert_eq!(tx.prefix.outputs.len(), 1); - 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 +1813,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 +1989,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 +2079,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 +2136,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 +2191,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 +2221,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. From 8649a724f7067a08de20114f4a661d127bde10d4 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 17 Feb 2022 18:51:55 -0700 Subject: [PATCH 02/17] fix lint --- consensus/service/src/config/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/service/src/config/mod.rs b/consensus/service/src/config/mod.rs index 3047931c0d..d1bc89c9c4 100644 --- a/consensus/service/src/config/mod.rs +++ b/consensus/service/src/config/mod.rs @@ -9,7 +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_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}; From 0b0ff3d24bb9cd9e388bf8bda05744664f8f4584 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 17 Feb 2022 19:05:26 -0700 Subject: [PATCH 03/17] add more run-time checks against block version in validation and building block version is "guaranteed" by the class invariant not to exceed the max, but strange things can happen if there is unsafe code or a bug in serde or something. these checks will not be optimized out in my understanding, because, the compiler has to assume that class invariants can be violated by transmute -- transmute may be unsafe but it does not make your program ill-formed. reviewers seemed to agree that this is fine, and it can't hurt anything, the worst case is it gets stripped out by optimizer. --- transaction/core/src/validation/validate.rs | 9 ++++++++- transaction/std/src/error.rs | 3 +++ transaction/std/src/transaction_builder.rs | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/transaction/core/src/validation/validate.rs b/transaction/core/src/validation/validate.rs index 26c3b69888..1075e03ccb 100644 --- a/transaction/core/src/validation/validate.rs +++ b/transaction/core/src/validation/validate.rs @@ -4,7 +4,7 @@ extern crate alloc; -use alloc::vec::Vec; +use alloc::{format, vec::Vec}; use super::error::{TransactionValidationError, TransactionValidationResult}; use crate::{ @@ -35,6 +35,13 @@ pub fn validate( 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)?; diff --git a/transaction/std/src/error.rs b/transaction/std/src/error.rs index a364993936..879e47d857 100644 --- a/transaction/std/src/error.rs +++ b/transaction/std/src/error.rs @@ -47,6 +47,9 @@ pub enum TxBuilderError { /// 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/transaction_builder.rs b/transaction/std/src/transaction_builder.rs index 6222e559ef..2418ced72b 100644 --- a/transaction/std/src/transaction_builder.rs +++ b/transaction/std/src/transaction_builder.rs @@ -294,6 +294,13 @@ impl TransactionBuilder { return Err(TxBuilderError::BlockVersionTooOld(*self.block_version, 1)); } + 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); } From 585708a4ff51fc41a32d5901d44017c7ee0833e1 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 17 Feb 2022 20:22:09 -0700 Subject: [PATCH 04/17] fixup slam and fog distro --- fog/distribution/src/main.rs | 4 +++- slam/src/main.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/fog/distribution/src/main.rs b/fog/distribution/src/main.rs index 9f576c1640..c6f3d9d01c 100755 --- a/fog/distribution/src/main.rs +++ b/fog/distribution/src/main.rs @@ -568,7 +568,9 @@ fn build_tx( // Sanity assert_eq!(utxos_with_proofs.len(), rings.len()); - let block_version = BlockVersion::try_from(BLOCK_VERSION.load(Ordering::SeqCst)) + // 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(max(BLOCK_VERSION.load(Ordering::SeqCst), 1)) .expect("Unsupported block version"); // Create tx_builder. diff --git a/slam/src/main.rs b/slam/src/main.rs index 783a957829..a12c5f2505 100755 --- a/slam/src/main.rs +++ b/slam/src/main.rs @@ -496,7 +496,9 @@ fn build_tx( // Sanity assert_eq!(utxos_with_proofs.len(), rings.len()); - let block_version = BlockVersion::try_from(BLOCK_VERSION.load(Ordering::SeqCst)) + // 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(max(BLOCK_VERSION.load(Ordering::SeqCst), 1)) .expect("Unsupported block version"); // Create tx_builder. No fog reports. From 84e23dbac5dc19d5dc5a6b621cff57199cace199 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 17 Feb 2022 20:52:05 -0700 Subject: [PATCH 05/17] fix previous --- fog/distribution/src/main.rs | 2 +- slam/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fog/distribution/src/main.rs b/fog/distribution/src/main.rs index c6f3d9d01c..dc7afdc1c8 100755 --- a/fog/distribution/src/main.rs +++ b/fog/distribution/src/main.rs @@ -1,6 +1,6 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation -use core::{cell::RefCell, convert::TryFrom}; +use core::{cell::RefCell, cmp::max, convert::TryFrom}; use lazy_static::lazy_static; use mc_account_keys::AccountKey; use mc_attest_verifier::{Verifier, DEBUG_ENCLAVE}; diff --git a/slam/src/main.rs b/slam/src/main.rs index a12c5f2505..87104b8b84 100755 --- a/slam/src/main.rs +++ b/slam/src/main.rs @@ -1,6 +1,6 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation -use core::{cell::RefCell, convert::TryFrom}; +use core::{cell::RefCell, cmp::max, convert::TryFrom}; use lazy_static::lazy_static; use mc_account_keys::{AccountKey, PublicAddress}; use mc_attest_verifier::{MrSignerVerifier, Verifier, DEBUG_ENCLAVE}; From a23c23c13e31e3a1457167a6a4f8e7f3c17649bd Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 17 Feb 2022 21:14:08 -0700 Subject: [PATCH 06/17] make mobilecoind get the block version from the ledger --- mobilecoind/src/payments.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/mobilecoind/src/payments.rs b/mobilecoind/src/payments.rs index 1998e428f3..cf004abd79 100644 --- a/mobilecoind/src/payments.rs +++ b/mobilecoind/src/payments.rs @@ -31,7 +31,7 @@ use mc_util_uri::FogUri; use rand::Rng; use rayon::prelude::*; use std::{ - cmp::Reverse, + cmp::{max, Reverse}, convert::TryFrom, iter::empty, str::FromStr, @@ -261,11 +261,17 @@ impl>, + block_version: BlockVersion, fee: u64, from_account_key: &AccountKey, change_subaddress: u64, @@ -765,9 +784,7 @@ impl Date: Thu, 17 Feb 2022 21:17:57 -0700 Subject: [PATCH 07/17] fix remoun comment --- fog/sample-paykit/src/cached_tx_data/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fog/sample-paykit/src/cached_tx_data/mod.rs b/fog/sample-paykit/src/cached_tx_data/mod.rs index 3c9c821c91..999f639b2b 100644 --- a/fog/sample-paykit/src/cached_tx_data/mod.rs +++ b/fog/sample-paykit/src/cached_tx_data/mod.rs @@ -133,7 +133,7 @@ impl CachedTxData { owned_tx_outs: Default::default(), key_image_data_completeness: BlockCount::MAX, latest_global_txo_count: 0, - latest_block_version: 1u32, + latest_block_version: 1, memo_handler: MemoHandler::new(address_book, logger.clone()), spsk_to_index, missed_block_ranges: Vec::new(), From 78be1ba496ac2e709a0928dc53c03db4d5d69ca6 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 17 Feb 2022 21:40:03 -0700 Subject: [PATCH 08/17] add tests for btree_set and btree_map comparison to vec of items, pairs --- crypto/digestible/tests/basic.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crypto/digestible/tests/basic.rs b/crypto/digestible/tests/basic.rs index 86bc366d26..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 // @@ -520,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"), + ) +} From d053f790a2d728dac15cb449a41334f3247605ff Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Feb 2022 09:31:11 -0700 Subject: [PATCH 09/17] Update consensus/enclave/impl/src/lib.rs Co-authored-by: Eran Rundstein --- consensus/enclave/impl/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/enclave/impl/src/lib.rs b/consensus/enclave/impl/src/lib.rs index 6da4fd90d2..5f918f5273 100644 --- a/consensus/enclave/impl/src/lib.rs +++ b/consensus/enclave/impl/src/lib.rs @@ -280,7 +280,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { peer_id: &ResponderId, msg: PeerAuthResponse, ) -> Result<(PeerSession, VerificationReport)> { - // Inject the blockchain config hash passing off to the AKE + // Inject the blockchain config hash before passing off to the AKE let peer_id = self .blockchain_config .lock()? From 5c212cc73046c36cdec7abe08fa89f96fe5f9514 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Feb 2022 11:45:09 -0700 Subject: [PATCH 10/17] make transaction builder accept block version zero, remove "max(, 1)" everywhere --- fog/distribution/src/main.rs | 4 ++-- mobilecoind/src/payments.rs | 17 +++++++---------- slam/src/main.rs | 4 ++-- transaction/std/src/transaction_builder.rs | 12 ++++++++++-- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/fog/distribution/src/main.rs b/fog/distribution/src/main.rs index dc7afdc1c8..2197be2817 100755 --- a/fog/distribution/src/main.rs +++ b/fog/distribution/src/main.rs @@ -1,6 +1,6 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation -use core::{cell::RefCell, cmp::max, convert::TryFrom}; +use core::{cell::RefCell, convert::TryFrom}; use lazy_static::lazy_static; use mc_account_keys::AccountKey; use mc_attest_verifier::{Verifier, DEBUG_ENCLAVE}; @@ -570,7 +570,7 @@ fn build_tx( // 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(max(BLOCK_VERSION.load(Ordering::SeqCst), 1)) + let block_version = BlockVersion::try_from(BLOCK_VERSION.load(Ordering::SeqCst)) .expect("Unsupported block version"); // Create tx_builder. diff --git a/mobilecoind/src/payments.rs b/mobilecoind/src/payments.rs index cf004abd79..b812051814 100644 --- a/mobilecoind/src/payments.rs +++ b/mobilecoind/src/payments.rs @@ -31,7 +31,7 @@ use mc_util_uri::FogUri; use rand::Rng; use rayon::prelude::*; use std::{ - cmp::{max, Reverse}, + cmp::Reverse, convert::TryFrom, iter::empty, str::FromStr, @@ -262,9 +262,8 @@ impl= 1 - let block_version = BlockVersion::try_from(max(BLOCK_VERSION.load(Ordering::SeqCst), 1)) + let block_version = BlockVersion::try_from(BLOCK_VERSION.load(Ordering::SeqCst)) .expect("Unsupported block version"); // Create tx_builder. No fog reports. diff --git a/transaction/std/src/transaction_builder.rs b/transaction/std/src/transaction_builder.rs index 2418ced72b..6b18195221 100644 --- a/transaction/std/src/transaction_builder.rs +++ b/transaction/std/src/transaction_builder.rs @@ -290,8 +290,16 @@ impl TransactionBuilder { /// Consume the builder and return the transaction. pub fn build(mut self, rng: &mut RNG) -> Result { - if self.block_version < BlockVersion::ONE { - return Err(TxBuilderError::BlockVersionTooOld(*self.block_version, 1)); + // 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 { From f55fc2ddd5db83d176f92f3c53c6db595530e1c8 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Feb 2022 12:48:33 -0700 Subject: [PATCH 11/17] make form block reject decreasing block version, fix bug in block version formatting --- consensus/enclave/impl/src/lib.rs | 96 +++++++++++++++++++ .../core/src/blockchain/block_version.rs | 2 +- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/consensus/enclave/impl/src/lib.rs b/consensus/enclave/impl/src/lib.rs index 5f918f5273..02e7d545a9 100644 --- a/consensus/enclave/impl/src/lib.rs +++ b/consensus/enclave/impl/src/lib.rs @@ -459,6 +459,10 @@ impl ConsensusEnclave for SgxConsensusEnclave { .expect("enclave was not initialized") .get_config(); + if parent_block.version > *config.block_version { + return Err(Error::FormBlock(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. @@ -1445,4 +1449,96 @@ mod tests { assert_eq!(form_block_result, expected); } } + + #[test_with_logger] + fn form_block_refuses_decreasing_block_version(logger: Logger) { + let mut rng = Hc128Rng::from_seed([77u8; 32]); + + 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(); + + // 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); + + // The first block contains a single transaction with 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 { + 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::FormBlock(_)) => {} + _ => panic!( + "Expected a FormBlock error due to config.block_version being less than parent" + ), + } + } + } } diff --git a/transaction/core/src/blockchain/block_version.rs b/transaction/core/src/blockchain/block_version.rs index 75ac6897ca..cf6d4b1ee5 100644 --- a/transaction/core/src/blockchain/block_version.rs +++ b/transaction/core/src/blockchain/block_version.rs @@ -89,7 +89,7 @@ impl Deref for BlockVersion { impl fmt::Display for BlockVersion { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self) + write!(f, "{}", self.0) } } From 35e423773d25c6a1fa185ce182c384860f85834c Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Feb 2022 13:57:40 -0700 Subject: [PATCH 12/17] Update crypto/digestible/src/lib.rs Co-authored-by: Remoun Metyas --- crypto/digestible/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto/digestible/src/lib.rs b/crypto/digestible/src/lib.rs index 6afe8b2701..44ab8b5821 100644 --- a/crypto/digestible/src/lib.rs +++ b/crypto/digestible/src/lib.rs @@ -448,7 +448,7 @@ 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-dreive handles tuple structs and enums in +// because that is how digestible-derive handles tuple structs and enums in // tuples. impl Digestible for (&T, &U) { From 974ff80e8cc45a488533f4b90e66ec3082beb36b Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Feb 2022 14:10:03 -0700 Subject: [PATCH 13/17] fix comments in digestible crate --- crypto/digestible/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crypto/digestible/src/lib.rs b/crypto/digestible/src/lib.rs index 44ab8b5821..c99616e7b2 100644 --- a/crypto/digestible/src/lib.rs +++ b/crypto/digestible/src/lib.rs @@ -450,7 +450,10 @@ impl> DigestibleAsBytes for GenericArray {} // 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( @@ -630,9 +633,6 @@ cfg_if! { // Treat a BTreeMap as a (sorted) sequence // This implementation should match that for &[(T, U)] - // - // Note: We don't currently implement digestible for tuples, but we should. - // Digestible derive works on tuple structs and that's what we are mirroring impl Digestible for BTreeMap { #[inline] fn append_to_transcript(&self, context: &'static [u8], transcript: &mut DT) { From d2a58f622d442be6856bcf7414eb3d2d05979ba3 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Feb 2022 14:19:07 -0700 Subject: [PATCH 14/17] fix nits --- fog/sample-paykit/src/cached_tx_data/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fog/sample-paykit/src/cached_tx_data/mod.rs b/fog/sample-paykit/src/cached_tx_data/mod.rs index 999f639b2b..267064b055 100644 --- a/fog/sample-paykit/src/cached_tx_data/mod.rs +++ b/fog/sample-paykit/src/cached_tx_data/mod.rs @@ -675,7 +675,7 @@ 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 @@ -684,7 +684,7 @@ impl CachedTxData { // 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 = - core::cmp::max(self.latest_block_version, response.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) { From aed2e61f53e0c37420e2082327263b78b302c4ab Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Feb 2022 14:26:46 -0700 Subject: [PATCH 15/17] add an error type for enclave not initialized --- consensus/enclave/api/src/error.rs | 3 +++ consensus/enclave/impl/src/lib.rs | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/consensus/enclave/api/src/error.rs b/consensus/enclave/api/src/error.rs index 441e6e1ebd..f123ca0d55 100644 --- a/consensus/enclave/api/src/error.rs +++ b/consensus/enclave/api/src/error.rs @@ -55,6 +55,9 @@ pub enum Error { /// Invalid fee configuration: {0} FeeMap(FeeMapError), + + /// Enclave not initialized + NotInited, } impl From for Error { diff --git a/consensus/enclave/impl/src/lib.rs b/consensus/enclave/impl/src/lib.rs index 02e7d545a9..a8c1538e2f 100644 --- a/consensus/enclave/impl/src/lib.rs +++ b/consensus/enclave/impl/src/lib.rs @@ -219,7 +219,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { .blockchain_config .lock()? .as_ref() - .expect("enclave was not initialized") + .ok_or(Error::NotInited)? .get_config() .fee_map .get_fee_for_token(token_id)) @@ -265,7 +265,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { .blockchain_config .lock()? .as_ref() - .expect("enclave was not initialized") + .ok_or(Error::NotInited)? .responder_id(peer_id); Ok(self.ake.peer_init(&peer_id)?) @@ -285,7 +285,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { .blockchain_config .lock()? .as_ref() - .expect("enclave was not initialized") + .ok_or(Error::NotInited)? .responder_id(peer_id); Ok(self.ake.peer_connect(&peer_id, msg)?) @@ -367,7 +367,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { let blockchain_config = self.blockchain_config.lock()?; let config = blockchain_config .as_ref() - .expect("enclave was not initialized") + .ok_or(Error::NotInited)? .get_config(); // Enforce that all membership proofs provided by the untrusted system for From 00c8aab5c6c398a33195f719a49131e238fa5716 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 18 Feb 2022 17:13:48 -0700 Subject: [PATCH 16/17] add once-cell for config in block-version-aware PR --- Cargo.lock | 1 + consensus/enclave/impl/Cargo.toml | 1 + consensus/enclave/impl/src/lib.rs | 36 +++++++++++++++------------- consensus/enclave/trusted/Cargo.lock | 7 ++++++ 4 files changed, 28 insertions(+), 17 deletions(-) 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/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 a8c1538e2f..0be077e799 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; @@ -52,6 +52,9 @@ use mc_transaction_core::{ validation::TransactionValidationError, 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}; @@ -114,7 +117,7 @@ pub struct SgxConsensusEnclave { /// 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: Mutex>, + blockchain_config: OnceBox, } impl SgxConsensusEnclave { @@ -126,7 +129,7 @@ impl SgxConsensusEnclave { &mut McRng::default(), )), logger, - blockchain_config: Mutex::new(None), + blockchain_config: Default::default(), } } @@ -183,7 +186,9 @@ impl ConsensusEnclave for SgxConsensusEnclave { // 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.lock().unwrap() = Some(blockchain_config); + self.blockchain_config + .set(Box::new(blockchain_config)) + .expect("enclave already initialized"); // Init AKE. self.ake.init(peer_self_id, client_self_id.clone())?; @@ -217,8 +222,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { fn get_minimum_fee(&self, token_id: &TokenId) -> Result> { Ok(self .blockchain_config - .lock()? - .as_ref() + .get() .ok_or(Error::NotInited)? .get_config() .fee_map @@ -263,8 +267,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { // Inject the blockchain config hash, passing off to the AKE let peer_id = self .blockchain_config - .lock()? - .as_ref() + .get() .ok_or(Error::NotInited)? .responder_id(peer_id); @@ -283,8 +286,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { // Inject the blockchain config hash before passing off to the AKE let peer_id = self .blockchain_config - .lock()? - .as_ref() + .get() .ok_or(Error::NotInited)? .responder_id(peer_id); @@ -364,9 +366,9 @@ impl ConsensusEnclave for SgxConsensusEnclave { block_index: u64, proofs: Vec, ) -> Result<(WellFormedEncryptedTx, WellFormedTxContext)> { - let blockchain_config = self.blockchain_config.lock()?; - let config = blockchain_config - .as_ref() + let config = self + .blockchain_config + .get() .ok_or(Error::NotInited)? .get_config(); @@ -453,10 +455,10 @@ impl ConsensusEnclave for SgxConsensusEnclave { encrypted_txs_with_proofs: &[(WellFormedEncryptedTx, Vec)], root_element: &TxOutMembershipElement, ) -> Result<(Block, BlockContents, BlockSignature)> { - let blockchain_config = self.blockchain_config.lock()?; - let config = blockchain_config - .as_ref() - .expect("enclave was not initialized") + let config = self + .blockchain_config + .get() + .ok_or(Error::NotInited)? .get_config(); if parent_block.version > *config.block_version { 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" From 0af895f887486df2399adf0ca1c913c595e0d3a2 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Sat, 19 Feb 2022 11:23:50 -0700 Subject: [PATCH 17/17] resolve jcape comments --- consensus/enclave/api/src/error.rs | 5 ++++- consensus/enclave/impl/src/lib.rs | 16 ++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/consensus/enclave/api/src/error.rs b/consensus/enclave/api/src/error.rs index f123ca0d55..55aff5005d 100644 --- a/consensus/enclave/api/src/error.rs +++ b/consensus/enclave/api/src/error.rs @@ -57,7 +57,10 @@ pub enum Error { FeeMap(FeeMapError), /// Enclave not initialized - NotInited, + NotInitialized, + + /// Block Version Error: {0} + BlockVersion(String), } impl From for Error { diff --git a/consensus/enclave/impl/src/lib.rs b/consensus/enclave/impl/src/lib.rs index 0be077e799..5c5a307285 100644 --- a/consensus/enclave/impl/src/lib.rs +++ b/consensus/enclave/impl/src/lib.rs @@ -223,7 +223,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { Ok(self .blockchain_config .get() - .ok_or(Error::NotInited)? + .ok_or(Error::NotInitialized)? .get_config() .fee_map .get_fee_for_token(token_id)) @@ -268,7 +268,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { let peer_id = self .blockchain_config .get() - .ok_or(Error::NotInited)? + .ok_or(Error::NotInitialized)? .responder_id(peer_id); Ok(self.ake.peer_init(&peer_id)?) @@ -287,7 +287,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { let peer_id = self .blockchain_config .get() - .ok_or(Error::NotInited)? + .ok_or(Error::NotInitialized)? .responder_id(peer_id); Ok(self.ake.peer_connect(&peer_id, msg)?) @@ -369,7 +369,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { let config = self .blockchain_config .get() - .ok_or(Error::NotInited)? + .ok_or(Error::NotInitialized)? .get_config(); // Enforce that all membership proofs provided by the untrusted system for @@ -458,11 +458,11 @@ impl ConsensusEnclave for SgxConsensusEnclave { let config = self .blockchain_config .get() - .ok_or(Error::NotInited)? + .ok_or(Error::NotInitialized)? .get_config(); if parent_block.version > *config.block_version { - return Err(Error::FormBlock(format!("Block version cannot decrease: parent_block.version = {}, config.block_version = {}", 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 @@ -1536,9 +1536,9 @@ mod tests { // Check if we get a form block error as expected match form_block_result { - Err(Error::FormBlock(_)) => {} + Err(Error::BlockVersion(_)) => {} _ => panic!( - "Expected a FormBlock error due to config.block_version being less than parent" + "Expected a BlockVersion error due to config.block_version being less than parent" ), } }