From a4939aa780f0892860a1919ef7fd1a33f35b47bb Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 27 Jan 2022 23:08:31 -0700 Subject: [PATCH 1/4] WIP confidential asset types --- .gitignore | 3 + Cargo.lock | 4 +- android-bindings/src/bindings.rs | 23 +- api/proto/external.proto | 14 + api/src/convert/amount.rs | 3 + api/src/convert/archive_block.rs | 10 +- api/src/convert/tx.rs | 4 +- api/src/convert/tx_out.rs | 16 +- api/src/convert/tx_prefix.rs | 3 + consensus/api/proto/consensus_common.proto | 3 + consensus/api/src/conversions.rs | 6 + consensus/enclave/impl/src/lib.rs | 83 ++-- consensus/enclave/trusted/Cargo.lock | 16 + consensus/service/src/validators.rs | 18 +- fog/api/proto/view.proto | 2 + fog/api/tests/fog_types.rs | 8 +- fog/distribution/Cargo.toml | 3 +- fog/distribution/src/config.rs | 55 +- fog/distribution/src/lib.rs | 5 + fog/distribution/src/main.rs | 396 ++++++++++----- fog/distribution/tests/bootstrap.rs | 62 +++ .../enclave/impl/tests/tx_processing.rs | 13 +- fog/ingest/enclave/trusted/Cargo.lock | 1 + fog/ledger/enclave/trusted/Cargo.lock | 1 + fog/ledger/server/src/merkle_proof_service.rs | 9 +- fog/ledger/server/tests/connection.rs | 4 +- .../src/bin/sample_paykit_remote_wallet.rs | 10 +- fog/sample-paykit/src/cached_tx_data/mod.rs | 59 +-- fog/sample-paykit/src/client.rs | 54 +- fog/sample-paykit/src/lib.rs | 2 +- fog/test-client/src/bin/main.rs | 1 + fog/test-client/src/config.rs | 6 +- fog/test-client/src/test_client.rs | 61 ++- fog/test_infra/src/bin/add_test_block.rs | 7 +- fog/test_infra/src/bin/init_test_ledger.rs | 1 + fog/test_infra/src/mock_users.rs | 8 +- fog/types/src/view.rs | 29 +- fog/view/enclave/trusted/Cargo.lock | 1 + fog/view/protocol/src/user_private.rs | 4 +- ledger/db/src/lib.rs | 16 +- ledger/db/src/test_utils/mock_ledger.rs | 5 +- ledger/db/src/tx_out_store.rs | 7 +- libmobilecoin/libmobilecoin_cbindgen.h | 2 +- libmobilecoin/src/transaction.rs | 40 +- mobilecoind-json/src/data_types.rs | 19 +- mobilecoind/api/proto/mobilecoind_api.proto | 3 + mobilecoind/src/bin/main.rs | 2 + mobilecoind/src/config.rs | 5 + mobilecoind/src/conversions.rs | 21 +- mobilecoind/src/payments.rs | 150 ++++-- mobilecoind/src/processed_block_store.rs | 5 +- mobilecoind/src/service.rs | 45 +- mobilecoind/src/sync.rs | 5 +- mobilecoind/src/test_utils.rs | 8 +- mobilecoind/src/utxo_store.rs | 6 + slam/src/main.rs | 46 +- transaction/core/Cargo.toml | 1 + transaction/core/src/amount/commitment.rs | 25 +- .../core/src/amount/compressed_commitment.rs | 19 +- transaction/core/src/amount/error.rs | 9 +- transaction/core/src/amount/mod.rs | 192 ++++++- transaction/core/src/blockchain/block.rs | 10 +- .../core/src/blockchain/block_contents.rs | 1 + transaction/core/src/blockchain/block_data.rs | 9 + .../core/src/blockchain/block_version.rs | 14 +- transaction/core/src/constants.rs | 2 +- transaction/core/src/domain_separators.rs | 6 + transaction/core/src/encrypted_fog_hint.rs | 13 +- transaction/core/src/fog_hint.rs | 6 + transaction/core/src/lib.rs | 7 +- transaction/core/src/membership_proofs/mod.rs | 3 + .../core/src/membership_proofs/range.rs | 4 + transaction/core/src/memo.rs | 1 + transaction/core/src/proptest_fixtures.rs | 9 +- transaction/core/src/range_proofs/README.md | 4 +- transaction/core/src/range_proofs/error.rs | 3 + transaction/core/src/range_proofs/mod.rs | 31 +- .../core/src/ring_signature/curve_scalar.rs | 2 + transaction/core/src/ring_signature/error.rs | 6 + .../core/src/ring_signature/key_image.rs | 1 + transaction/core/src/ring_signature/mlsag.rs | 322 +++++------- transaction/core/src/ring_signature/mod.rs | 80 ++- .../src/ring_signature/rct_bulletproofs.rs | 470 ++++++++++++------ transaction/core/src/token.rs | 26 +- transaction/core/src/tx.rs | 56 ++- transaction/core/src/validation/error.rs | 9 + transaction/core/src/validation/mod.rs | 2 + transaction/core/src/validation/validate.rs | 123 ++++- transaction/core/test-utils/src/lib.rs | 10 +- transaction/core/tests/digest-test-vectors.rs | 6 +- transaction/std/src/error.rs | 8 +- transaction/std/src/transaction_builder.rs | 419 ++++++++++++---- util/generate-sample-ledger/Cargo.toml | 2 +- .../src/bin/generate_sample_ledger.rs | 17 +- util/generate-sample-ledger/src/lib.rs | 81 ++- .../generate-sample-ledger/tests/bootstrap.rs | 41 ++ 96 files changed, 2450 insertions(+), 993 deletions(-) create mode 100644 fog/distribution/tests/bootstrap.rs create mode 100644 util/generate-sample-ledger/tests/bootstrap.rs diff --git a/.gitignore b/.gitignore index fcbec5a5e2..07b3520cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ logs *.so *.a +# Proptest regression files for when proptest's fail +**/proptest-regressions + # fog-test directory from fog-local-network instructions /fog-test/ diff --git a/Cargo.lock b/Cargo.lock index aa15b66dd4..562160dba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3291,6 +3291,7 @@ dependencies = [ "mc-mobilecoind", "mc-transaction-core", "mc-transaction-std", + "mc-util-generate-sample-ledger", "mc-util-keyfile", "mc-util-uri", "rand 0.8.5", @@ -4890,6 +4891,7 @@ version = "1.3.0-pre0" dependencies = [ "aes", "bulletproofs-og", + "crc", "curve25519-dalek", "displaydoc", "generic-array 0.14.5", @@ -5066,7 +5068,7 @@ dependencies = [ "rand 0.8.5", "rand_hc 0.3.1", "structopt", - "tempdir", + "tempfile", ] [[package]] diff --git a/android-bindings/src/bindings.rs b/android-bindings/src/bindings.rs index 245c7c206e..3cb21ec6cb 100644 --- a/android-bindings/src/bindings.rs +++ b/android-bindings/src/bindings.rs @@ -44,8 +44,9 @@ use mc_transaction_core::{ create_shared_secret, recover_onetime_private_key, recover_public_subaddress_spend_key, }, ring_signature::KeyImage, + tokens::Mob, tx::{Tx, TxOut, TxOutConfirmationNumber, TxOutMembershipProof}, - Amount, BlockVersion, CompressedCommitment, + Amount, BlockVersion, CompressedCommitment, Token, }; use mc_transaction_std::{InputCredentials, RTHMemoBuilder, TransactionBuilder}; use mc_util_from_random::FromRandom; @@ -310,9 +311,12 @@ pub unsafe extern "C" fn Java_com_mobilecoin_lib_Amount_init_1jni( jni_ffi_call(&env, |env| { let commitment_bytes = env.convert_byte_array(commitment)?; + // FIXME #1595: We should get a masked token id also, here we default to + // 0 bytes, which is backwards compatible let amount = Amount { commitment: CompressedCommitment::try_from(&commitment_bytes[..])?, masked_value: masked_value as u64, + masked_token_id: Default::default(), }; Ok(env.set_rust_field(obj, RUST_OBJ_FIELD, amount)?) }) @@ -328,9 +332,9 @@ pub unsafe extern "C" fn Java_com_mobilecoin_lib_Amount_init_1jni_1with_1secret( jni_ffi_call(&env, |env| { let tx_out_shared_secret: MutexGuard = env.get_rust_field(tx_out_shared_secret, RUST_OBJ_FIELD)?; - let value = - (masked_value as u64) ^ mc_transaction_core::get_value_mask(&tx_out_shared_secret); - let amount: Amount = Amount::new(value, &tx_out_shared_secret)?; + // FIXME #1595: the masked token id should be 0 or 4 bytes. + // To avoid breaking changes, it is hard coded to 0 bytes here + let amount = Amount::reconstruct(masked_value as u64, &[], &tx_out_shared_secret)?; Ok(env.set_rust_field(obj, RUST_OBJ_FIELD, amount)?) }) @@ -377,14 +381,15 @@ pub unsafe extern "C" fn Java_com_mobilecoin_lib_Amount_unmask_1value( let tx_pub_key: MutexGuard = env.get_rust_field(tx_pub_key, RUST_OBJ_FIELD)?; let shared_secret = create_shared_secret(&tx_pub_key, &view_key); - let value = amount.get_value(&shared_secret)?.0; + let (amount_data, _) = amount.get_value(&shared_secret)?; Ok(env .new_object( "java/math/BigInteger", "(I[B)V", // public BigInteger(int signum, byte[] magnitude) &[ 1.into(), - env.byte_array_from_slice(&value.to_be_bytes())?.into(), + env.byte_array_from_slice(&amount_data.value.to_be_bytes())? + .into(), ], )? .into_inner()) @@ -1177,7 +1182,11 @@ pub unsafe extern "C" fn Java_com_mobilecoin_lib_TransactionBuilder_init_1jni( // 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); + // FIXME #1595: The token id should be a parameter and not hard coded to Mob + // here + let token_id = Mob::ID; + let tx_builder = + TransactionBuilder::new(block_version, token_id, fog_resolver.clone(), memo_builder); Ok(env.set_rust_field(obj, RUST_OBJ_FIELD, tx_builder)?) }) } diff --git a/api/proto/external.proto b/api/proto/external.proto index 8d5c4c760a..b11678741c 100644 --- a/api/proto/external.proto +++ b/api/proto/external.proto @@ -127,6 +127,14 @@ message ViewKey { // `trasaction/core` crate /////////////////////////////////////////////////////////////////////////////// +/// A list of "known" token id's and their names. +/// +/// Note that this is not an exhaustive list and clients should gracefully handle +/// the scenario that they receive a tx out with a token id they don't know about yet. +enum KnownTokenId { + MOB = 0; +} + /// A 32-byte scalar associated to the ristretto group. /// This is the same as RistrettoPrivate, but they are used in different places. /// TODO: MC-1605 Consider to factor out this type, or just this proto message. @@ -175,6 +183,9 @@ message Amount { // `masked_value = value XOR_8 Blake2B("value_mask" || shared_secret)` fixed64 masked_value = 2; + + // `masked_token_id = token_id XOR_8 Blake2B("token_id_mask" || shared_secret)` + bytes masked_token_id = 3; } // The bytes of encrypted fog hint @@ -228,6 +239,9 @@ message TxPrefix { // The block index at which this transaction is no longer valid. uint64 tombstone_block = 4; + + // Token id for this transaction + fixed32 token_id = 5; } message RingMLSAG { diff --git a/api/src/convert/amount.rs b/api/src/convert/amount.rs index 434b867573..4ea99aca1e 100644 --- a/api/src/convert/amount.rs +++ b/api/src/convert/amount.rs @@ -11,6 +11,7 @@ impl From<&Amount> for external::Amount { let mut amount = external::Amount::new(); amount.mut_commitment().set_data(commitment_bytes); amount.set_masked_value(source.masked_value); + amount.set_masked_token_id(source.masked_token_id.clone()); amount } } @@ -21,9 +22,11 @@ impl TryFrom<&external::Amount> for Amount { fn try_from(source: &external::Amount) -> Result { let commitment = CompressedCommitment::try_from(source.get_commitment())?; let masked_value = source.get_masked_value(); + let masked_token_id = source.get_masked_token_id(); let amount = Amount { commitment, masked_value, + masked_token_id: masked_token_id.to_vec(), }; Ok(amount) } diff --git a/api/src/convert/archive_block.rs b/api/src/convert/archive_block.rs index 9ea5035598..c5e63a2bd6 100644 --- a/api/src/convert/archive_block.rs +++ b/api/src/convert/archive_block.rs @@ -121,8 +121,10 @@ mod tests { encrypted_fog_hint::ENCRYPTED_FOG_HINT_LEN, membership_proofs::Range, ring_signature::KeyImage, + tokens::Mob, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Amount, Block, BlockContents, BlockData, BlockID, BlockSignature, BlockVersion, + Amount, AmountData, Block, BlockContents, BlockData, BlockID, BlockSignature, BlockVersion, + Token, }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -133,8 +135,12 @@ mod tests { let mut last_block: Option = None; for block_idx in 0..num_blocks { + let amount_data = AmountData { + value: 1u64 << 13, + token_id: Mob::ID, + }; let tx_out = TxOut { - amount: Amount::new(1u64 << 13, &RistrettoPublic::from_random(&mut rng)).unwrap(), + amount: Amount::new(amount_data, &RistrettoPublic::from_random(&mut rng)).unwrap(), target_key: RistrettoPublic::from_random(&mut rng).into(), public_key: RistrettoPublic::from_random(&mut rng).into(), e_fog_hint: (&[0u8; ENCRYPTED_FOG_HINT_LEN]).into(), diff --git a/api/src/convert/tx.rs b/api/src/convert/tx.rs index 6f79b310cb..fef2fd9b00 100644 --- a/api/src/convert/tx.rs +++ b/api/src/convert/tx.rs @@ -32,8 +32,9 @@ mod tests { use mc_crypto_keys::RistrettoPublic; use mc_transaction_core::{ onetime_keys::recover_onetime_private_key, + tokens::Mob, tx::{Tx, TxOut, TxOutMembershipProof}, - BlockVersion, + BlockVersion, Token, }; use mc_transaction_core_test_utils::MockFogResolver; use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; @@ -70,6 +71,7 @@ mod tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); diff --git a/api/src/convert/tx_out.rs b/api/src/convert/tx_out.rs index d5d5e2f2a2..2f69e7b6b6 100644 --- a/api/src/convert/tx_out.rs +++ b/api/src/convert/tx_out.rs @@ -78,7 +78,9 @@ mod tests { use super::*; use generic_array::GenericArray; use mc_crypto_keys::RistrettoPublic; - use mc_transaction_core::{encrypted_fog_hint::ENCRYPTED_FOG_HINT_LEN, Amount}; + use mc_transaction_core::{ + encrypted_fog_hint::ENCRYPTED_FOG_HINT_LEN, tokens::Mob, Amount, AmountData, Token, + }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -87,8 +89,12 @@ mod tests { fn test_tx_out_from_tx_out_stored() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); + let amount_data = AmountData { + value: 1u64 << 13, + token_id: Mob::ID, + }; let source = tx::TxOut { - amount: Amount::new(1u64 << 13, &RistrettoPublic::from_random(&mut rng)).unwrap(), + amount: Amount::new(amount_data, &RistrettoPublic::from_random(&mut rng)).unwrap(), target_key: RistrettoPublic::from_random(&mut rng).into(), public_key: RistrettoPublic::from_random(&mut rng).into(), e_fog_hint: (&[0u8; ENCRYPTED_FOG_HINT_LEN]).into(), @@ -106,8 +112,12 @@ mod tests { fn test_tx_out_from_tx_out_stored_with_memo() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); + let amount_data = AmountData { + value: 1u64 << 13, + token_id: Mob::ID, + }; let source = tx::TxOut { - amount: Amount::new(1u64 << 13, &RistrettoPublic::from_random(&mut rng)).unwrap(), + amount: Amount::new(amount_data, &RistrettoPublic::from_random(&mut rng)).unwrap(), target_key: RistrettoPublic::from_random(&mut rng).into(), public_key: RistrettoPublic::from_random(&mut rng).into(), e_fog_hint: (&[0u8; ENCRYPTED_FOG_HINT_LEN]).into(), diff --git a/api/src/convert/tx_prefix.rs b/api/src/convert/tx_prefix.rs index 4f46c26392..2d6a4e0f44 100644 --- a/api/src/convert/tx_prefix.rs +++ b/api/src/convert/tx_prefix.rs @@ -18,6 +18,8 @@ impl From<&tx::TxPrefix> for external::TxPrefix { tx_prefix.set_fee(source.fee); + tx_prefix.set_token_id(source.token_id); + tx_prefix.set_tombstone_block(source.tombstone_block); tx_prefix @@ -45,6 +47,7 @@ impl TryFrom<&external::TxPrefix> for tx::TxPrefix { inputs, outputs, fee: source.get_fee(), + token_id: source.get_token_id(), tombstone_block: source.get_tombstone_block(), }; Ok(tx_prefix) diff --git a/consensus/api/proto/consensus_common.proto b/consensus/api/proto/consensus_common.proto index 75e6e0330f..8f5c13bb93 100644 --- a/consensus/api/proto/consensus_common.proto +++ b/consensus/api/proto/consensus_common.proto @@ -77,6 +77,9 @@ enum ProposeTxResult { UnsortedInputs = 39; MissingMemo = 40; MemosNotAllowed = 41; + TokenNotYetConfigured = 42; + MissingMaskedTokenId = 43; + MaskedTokenIdNotAllowed = 44; } /// Response from TxPropose RPC call. diff --git a/consensus/api/src/conversions.rs b/consensus/api/src/conversions.rs index a38ca5e9b5..e8f4e614d4 100644 --- a/consensus/api/src/conversions.rs +++ b/consensus/api/src/conversions.rs @@ -48,6 +48,9 @@ impl From for ProposeTxResult { Error::UnsortedInputs => Self::UnsortedInputs, Error::MissingMemo => Self::MissingMemo, Error::MemosNotAllowed => Self::MemosNotAllowed, + Error::TokenNotYetConfigured => Self::TokenNotYetConfigured, + Error::MissingMaskedTokenId => Self::MissingMaskedTokenId, + Error::MaskedTokenIdNotAllowed => Self::MaskedTokenIdNotAllowed, } } } @@ -93,6 +96,9 @@ impl TryInto for ProposeTxResult { Self::UnsortedInputs => Ok(Error::UnsortedInputs), Self::MissingMemo => Ok(Error::MissingMemo), Self::MemosNotAllowed => Ok(Error::MemosNotAllowed), + Self::TokenNotYetConfigured => Ok(Error::TokenNotYetConfigured), + Self::MissingMaskedTokenId => Ok(Error::MissingMaskedTokenId), + Self::MaskedTokenIdNotAllowed => Ok(Error::MaskedTokenIdNotAllowed), } } } diff --git a/consensus/enclave/impl/src/lib.rs b/consensus/enclave/impl/src/lib.rs index 91bd9b97d8..cd95f76eb4 100644 --- a/consensus/enclave/impl/src/lib.rs +++ b/consensus/enclave/impl/src/lib.rs @@ -17,7 +17,13 @@ mod identity; // Include autogenerated constants.rs include!(concat!(env!("OUT_DIR"), "/constants.rs")); -use alloc::{boxed::Box, collections::BTreeSet, format, string::String, vec::Vec}; +use alloc::{ + boxed::Box, + collections::{BTreeMap, BTreeSet}, + format, + string::String, + vec::Vec, +}; use core::convert::TryFrom; use identity::Ed25519Identity; use mc_account_keys::PublicAddress; @@ -34,9 +40,9 @@ use mc_common::{ ResponderId, }; use mc_consensus_enclave_api::{ - BlockchainConfig, BlockchainConfigWithDigest, ConsensusEnclave, Error, FeeMapError, - FeePublicKey, LocallyEncryptedTx, Result, SealedBlockSigningKey, TxContext, - WellFormedEncryptedTx, WellFormedTxContext, + BlockchainConfig, BlockchainConfigWithDigest, ConsensusEnclave, Error, FeePublicKey, + LocallyEncryptedTx, Result, SealedBlockSigningKey, TxContext, WellFormedEncryptedTx, + WellFormedTxContext, }; use mc_crypto_ake_enclave::AkeEnclaveState; use mc_crypto_digestible::{DigestTranscript, Digestible, MerlinTranscript}; @@ -392,14 +398,14 @@ impl ConsensusEnclave for SgxConsensusEnclave { .decrypt_bytes(locally_encrypted_tx.0)?; let tx: Tx = mc_util_serial::decode(&decrypted_bytes)?; + let token_id = TokenId::from(tx.prefix.token_id); + // Validate. let mut csprng = McRng::default(); let minimum_fee = config .fee_map - .get_fee_for_token(&TokenId::MOB) - // This should actually never happen since the map enforces the existence of - // MOB. - .ok_or(Error::FeeMap(FeeMapError::MissingFee(TokenId::MOB)))?; + .get_fee_for_token(&token_id) + .ok_or(TransactionValidationError::TokenNotYetConfigured)?; mc_transaction_core::validation::validate( &tx, block_index, @@ -482,14 +488,13 @@ 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 = config - .fee_map - .get_fee_for_token(&TokenId::MOB) - // This should actually never happen since the map enforces the existence of - // MOB. - .ok_or(Error::FeeMap(FeeMapError::MissingFee(TokenId::MOB)))?; for (tx, proofs) in transactions_with_proofs.iter() { + let minimum_fee = config + .fee_map + .get_fee_for_token(&TokenId::from(tx.prefix.token_id)) + .ok_or(TransactionValidationError::TokenNotYetConfigured)?; + mc_transaction_core::validation::validate( tx, parent_block.index + 1, @@ -570,7 +575,14 @@ impl ConsensusEnclave for SgxConsensusEnclave { } // Create an aggregate fee output. - let total_fee: u64 = transactions.iter().map(|tx| tx.prefix.fee).sum(); + // TODO #1597: This should be constant time with respect to token id + let mut total_fees_by_token_id = BTreeMap::::default(); + for tx in transactions.iter() { + *total_fees_by_token_id + .entry(tx.prefix.token_id) + .or_default() += tx.prefix.fee; + } + let fee_public_key = self.get_fee_recipient().map_err(|e| { Error::FeePublicAddress(format!("Could not get fee public address: {:?}", e)) })?; @@ -579,13 +591,19 @@ impl ConsensusEnclave for SgxConsensusEnclave { &fee_public_key.view_public_key, ); - let fee_output = mint_output( - &fee_recipient, - FEES_OUTPUT_PRIVATE_KEY_DOMAIN_TAG.as_bytes(), - parent_block, - &transactions, - total_fee, - )?; + let fee_outputs = total_fees_by_token_id + .iter() + .map(|(token_id, total_fee)| { + mint_output( + &fee_recipient, + FEES_OUTPUT_PRIVATE_KEY_DOMAIN_TAG.as_bytes(), + parent_block, + &transactions, + *total_fee, + TokenId::from(*token_id), + ) + }) + .collect::>>()?; // Collect outputs and key images. let mut outputs: Vec = Vec::new(); @@ -594,7 +612,7 @@ impl ConsensusEnclave for SgxConsensusEnclave { outputs.extend(tx.prefix.outputs.iter().cloned()); key_images.extend(tx.key_images().iter().cloned()); } - outputs.push(fee_output); + outputs.extend(fee_outputs); // Sort outputs and key images. This removes ordering information which could be // used to infer the per-transaction relationships among outputs and/or @@ -635,6 +653,7 @@ fn mint_output( parent_block: &Block, transactions: &[T], amount: u64, + token_id: TokenId, ) -> Result { // Create a determinstic private key based on the block contents. let tx_private_key = { @@ -655,8 +674,14 @@ fn mint_output( }; // Create a single TxOut - let output = TxOut::new(amount, recipient, &tx_private_key, Default::default()) - .map_err(|e| Error::FormBlock(format!("AmountError: {:?}", e)))?; + let output = TxOut::new( + amount, + token_id, + recipient, + &tx_private_key, + Default::default(), + ) + .map_err(|e| Error::FormBlock(format!("AmountError: {:?}", e)))?; Ok(output) } @@ -668,9 +693,10 @@ mod tests { use mc_ledger_db::Ledger; use mc_transaction_core::{ onetime_keys::{create_shared_secret, view_key_matches_output}, + tokens::Mob, tx::TxOutMembershipHash, validation::TransactionValidationError, - BlockVersion, + BlockVersion, Token, }; use mc_transaction_core_test_utils::{ create_ledger, create_transaction, initialize_ledger, AccountKey, ViewKey, @@ -1036,8 +1062,9 @@ mod tests { // 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); + let (amount, _blinding) = fee_output.amount.get_value(&shared_secret).unwrap(); + assert_eq!(amount.value, total_fee); + assert_eq!(amount.token_id, Mob::ID); } } diff --git a/consensus/enclave/trusted/Cargo.lock b/consensus/enclave/trusted/Cargo.lock index dc6cae530e..c135316ce8 100644 --- a/consensus/enclave/trusted/Cargo.lock +++ b/consensus/enclave/trusted/Cargo.lock @@ -292,6 +292,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" + [[package]] name = "crypto-common" version = "0.1.3" @@ -1142,6 +1157,7 @@ version = "1.3.0-pre0" dependencies = [ "aes", "bulletproofs-og", + "crc", "curve25519-dalek", "displaydoc", "generic-array", diff --git a/consensus/service/src/validators.rs b/consensus/service/src/validators.rs index f9519ffe29..f6b5b06177 100644 --- a/consensus/service/src/validators.rs +++ b/consensus/service/src/validators.rs @@ -499,8 +499,9 @@ mod combine_tests { use mc_ledger_db::test_utils::get_mock_ledger; use mc_transaction_core::{ onetime_keys::recover_onetime_private_key, + tokens::Mob, tx::{TxOut, TxOutMembershipProof}, - BlockVersion, + BlockVersion, Token, }; use mc_transaction_core_test_utils::{AccountKey, MockFogResolver}; use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; @@ -542,6 +543,7 @@ mod combine_tests { let tx_out = TxOut::new( 123, + Mob::ID, &alice.default_subaddress(), &tx_secret_key_for_txo, Default::default(), @@ -580,6 +582,7 @@ mod combine_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -620,6 +623,7 @@ mod combine_tests { let tx_out = TxOut::new( 88, + Mob::ID, &alice.default_subaddress(), &tx_secret_key_for_txo, Default::default(), @@ -634,6 +638,7 @@ mod combine_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -696,6 +701,7 @@ mod combine_tests { // Create a TxOut that was sent to Alice. let tx_out = TxOut::new( 123, + Mob::ID, &alice.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -731,6 +737,7 @@ mod combine_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -767,6 +774,7 @@ mod combine_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -789,6 +797,7 @@ mod combine_tests { let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); let tx_out = TxOut::new( 123, + Mob::ID, &alice.default_subaddress(), &tx_secret_key_for_txo, Default::default(), @@ -826,6 +835,7 @@ mod combine_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -863,6 +873,7 @@ mod combine_tests { // Create two TxOuts that were sent to Alice. let tx_out1 = TxOut::new( 123, + Mob::ID, &alice.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -871,6 +882,7 @@ mod combine_tests { let tx_out2 = TxOut::new( 123, + Mob::ID, &alice.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -912,6 +924,7 @@ mod combine_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -949,6 +962,7 @@ mod combine_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -972,6 +986,7 @@ mod combine_tests { let tx_secret_key_for_txo = RistrettoPrivate::from_random(&mut rng); let tx_out = TxOut::new( 123, + Mob::ID, &alice.default_subaddress(), &tx_secret_key_for_txo, Default::default(), @@ -1009,6 +1024,7 @@ mod combine_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); diff --git a/fog/api/proto/view.proto b/fog/api/proto/view.proto index 5244ed8793..4f5c37d803 100644 --- a/fog/api/proto/view.proto +++ b/fog/api/proto/view.proto @@ -266,4 +266,6 @@ message TxOutRecord { /// This exactly 66 bytes when present. /// This is omitted for TxOut's from before the upgrade that introduced memos. bytes tx_out_e_memo_data = 9; + /// The masked token id associated to the amount field in the TxOut that was recovered + bytes tx_out_amount_masked_token_id = 10; } diff --git a/fog/api/tests/fog_types.rs b/fog/api/tests/fog_types.rs index 40572dccff..1d9de6e7fc 100644 --- a/fog/api/tests/fog_types.rs +++ b/fog/api/tests/fog_types.rs @@ -12,7 +12,7 @@ use mc_transaction_core::{ encrypted_fog_hint::EncryptedFogHint, membership_proofs::Range, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash, TxOutMembershipProof}, - Amount, EncryptedMemo, + Amount, AmountData, EncryptedMemo, }; use mc_util_from_random::FromRandom; use mc_util_test_helper::{run_with_several_seeds, CryptoRng, RngCore}; @@ -439,7 +439,11 @@ impl Sample for mc_fog_types::view::TxOutSearchResult { impl Sample for Amount { fn sample(rng: &mut T) -> Self { - Amount::new(rng.next_u32() as u64, &RistrettoPublic::from_random(rng)).unwrap() + let amount_data = AmountData { + value: rng.next_u32() as u64, + token_id: rng.next_u32().into(), + }; + Amount::new(amount_data, &RistrettoPublic::from_random(rng)).unwrap() } } diff --git a/fog/distribution/Cargo.toml b/fog/distribution/Cargo.toml index a9bd81ddfd..df30c5492b 100644 --- a/fog/distribution/Cargo.toml +++ b/fog/distribution/Cargo.toml @@ -21,7 +21,7 @@ mc-fog-ingest-enclave-measurement = { path = "../ingest/enclave/measurement" } mc-fog-report-connection = { path = "../../fog/report/connection" } mc-fog-report-validation = { path = "../../fog/report/validation" } mc-ledger-db = { path = "../../ledger/db" } -mc-mobilecoind = { path = "../../mobilecoind" } +mc-mobilecoind = { path = "../../mobilecoind" } # This is needed for PeersConfig mc-transaction-core = { path = "../../transaction/core" } mc-transaction-std = { path = "../../transaction/std" } mc-util-keyfile = { path = "../../util/keyfile" } @@ -44,3 +44,4 @@ curve25519-dalek = { version = "4.0.0-pre.2", default-features = false, features [dev-dependencies] mc-common = { path = "../../common", features = ["loggers"] } +mc-util-generate-sample-ledger = { path = "../../util/generate-sample-ledger" } diff --git a/fog/distribution/src/config.rs b/fog/distribution/src/config.rs index 8d9ce87c82..be4cb61de6 100644 --- a/fog/distribution/src/config.rs +++ b/fog/distribution/src/config.rs @@ -10,11 +10,15 @@ use mc_connection::{ }; use mc_mobilecoind::config::PeersConfig; use mc_util_uri::ConnectionUri; -use std::{fs, path::PathBuf, str::FromStr, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; use structopt::StructOpt; +/// Config options for the fog-distribution tool #[derive(Clone, Debug, StructOpt)] -#[structopt(name = "fog-distribution", about = "Generate valid fog txs.")] +#[structopt( + name = "fog-distribution", + about = "Transfer funds from source accounts (bootstrapped) to destination accounts (which may have fog). This slams the network with many Txs in parallel as a stress test." +)] pub struct Config { /// Path to sample data for keys/ and ledger/ #[structopt(long, parse(from_os_str))] @@ -32,24 +36,25 @@ pub struct Config { #[structopt(long, default_value = "50")] pub tombstone_block: u64, + /// Number of inputs to use per transaction #[structopt(long, default_value = "1")] pub num_inputs: usize, - /// Ask consensus for the current block to set tombstone appropriately - #[structopt(long)] - pub query_consensus_for_cur_block: bool, - /// Offset into transactions to start #[structopt(long, default_value = "0")] pub start_offset: usize, - /// Num transactions per account - must set this if using start_offset + /// Num transactions per source account in the bootstrapped ledger - must + /// set this if using start_offset #[structopt(long, default_value = "0")] - pub num_transactions_per_account: usize, + pub num_transactions_per_source_account: usize, - /// Offset into accounts - #[structopt(long, default_value = "0")] - pub account_offset: usize, + /// Num seed transactions per destination account. Each destination is + /// guaranteed to receive at least this many TxOuts. If the ledger is + /// bootstrapped with multiple token ids, this can be set to guarantee no + /// destination has a zero balance for any token. + #[structopt(long, default_value = "1")] + pub num_seed_transactions_per_destination_account: usize, /// Number of threads with which to submit transactions (threadpool uses min /// with cpu) @@ -60,17 +65,26 @@ pub struct Config { #[structopt(long, default_value = "0")] pub add_tx_delay_ms: u64, - /// URLs to use for transaction data. - /// - /// For example: https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.master.mobilecoin.com/ + /// Block version to use (otherwise fall back to ledger) + #[structopt(long, env)] + pub block_version: Option, + + /// Destination keys subdirectory. Defaults to `fog_keys` #[structopt(long, default_value = "fog_keys")] pub fog_keys_subdir: String, + /// Peers configuration #[structopt(flatten)] pub peers_config: PeersConfig, + + /// Dry run (don't actually submit transactions, just load from bootstrapped + /// ledger) + #[structopt(long)] + pub dry_run: bool, } impl Config { + /// Get thick client connections to all configured consensus nodes pub fn get_connections( &self, logger: &Logger, @@ -107,16 +121,3 @@ impl Config { .collect() } } - -#[derive(Clone, Debug)] -pub struct FileData(pub Vec); - -impl FromStr for FileData { - type Err = String; - - fn from_str(s: &str) -> Result { - Ok(Self(fs::read(s).map_err(|e| { - format!("Failed reading \"{}\": {:?}", s, e) - })?)) - } -} diff --git a/fog/distribution/src/lib.rs b/fog/distribution/src/lib.rs index 5400b5c31a..e2aec8c5f7 100644 --- a/fog/distribution/src/lib.rs +++ b/fog/distribution/src/lib.rs @@ -1,5 +1,10 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! Fog-distribution is a tool which serves two purposes: +//! * Transfer funds from bootstrapped ledger to fog accounts (which can't get +//! bootstrapped directly) +//! * Slam the network with transactions as a load test + pub mod config; pub use crate::config::Config; diff --git a/fog/distribution/src/main.rs b/fog/distribution/src/main.rs index c58747d507..c1742fc73a 100755 --- a/fog/distribution/src/main.rs +++ b/fog/distribution/src/main.rs @@ -1,5 +1,22 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! Fog distribution is a transfer script which moves funds from a set of +//! accounts funded by a ledger bootstrap, to another set of accounts (which may +//! have fog). +//! +//! This is necessary because it is generally impossible to fund fog accounts +//! with bootstrap, because fog would have to exist and a public key be +//! available before the network has been stood up, or we cannot encrypt fog +//! hints. +//! +//! Fog distribution also has a secondary purpose of "slamming" the network with +//! as high of a volume of Tx's as possible. Fog distro fires and forgets its +//! Tx's rather than checking to see if they land, once it is in the slam step. +//! +//! Fog distro guarantees to pay each destination account at least once. + +#![deny(missing_docs)] + use core::{cell::RefCell, convert::TryFrom}; use lazy_static::lazy_static; use mc_account_keys::AccountKey; @@ -24,7 +41,7 @@ use mc_transaction_core::{ tokens::Mob, tx::{Tx, TxOut, TxOutMembershipProof}, validation::TransactionValidationError, - BlockVersion, Token, + AmountData, BlockVersion, Token, }; use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; use mc_util_uri::FogUri; @@ -32,6 +49,7 @@ use rand::{seq::SliceRandom, thread_rng, Rng}; use rayon::prelude::*; use retry::{delay, retry, OperationResult}; use std::{ + collections::BTreeMap, convert::TryInto, iter::empty, path::Path, @@ -47,6 +65,7 @@ use structopt::StructOpt; use tempfile::tempdir; thread_local! { + /// global variable storing connections to the consensus network pub static CONNS: RefCell>>>> = RefCell::new(None); } @@ -70,22 +89,29 @@ fn get_conns( } lazy_static! { + /// Keeps track of current block height of the block chain pub static ref BLOCK_HEIGHT: AtomicU64 = AtomicU64::default(); + + /// Keeps track of block version we are targetting pub static ref BLOCK_VERSION: AtomicU32 = AtomicU32::new(1); + /// Keeps track of the current fee value pub static ref FEE: AtomicU64 = AtomicU64::default(); - // A map of tx pub keys to account index. This is used in conjunction with ledger syncing to - // identify which new txs belong to which accounts without having to do any slow crypto. + /// A map of tx pub keys to account index. This is used in conjunction with ledger syncing to + /// identify which new txs belong to which accounts without having to do any slow crypto. pub static ref TX_PUB_KEY_TO_ACCOUNT_KEY: Mutex> = Mutex::new(HashMap::default()); - } +/// A TxOut found from the bootstrapped ledger that we can spend #[derive(Clone, Debug, Eq, PartialEq)] pub struct SpendableTxOut { + /// The tx out that is spendable pub tx_out: TxOut, - pub amount: u64, - from_account_key: AccountKey, + /// The amount of the tx out + pub amount: AmountData, + /// The account that owns this tx out + pub from_account_key: AccountKey, } fn main() { @@ -95,7 +121,7 @@ fn main() { let config = Config::from_args(); // Read account root_entropies from disk - let accounts: Vec = mc_util_keyfile::keygen::read_default_root_entropies( + let src_accounts: Vec = mc_util_keyfile::keygen::read_default_root_entropies( config.sample_data_dir.join(Path::new("keys")), ) .expect("Could not read default root entropies from keys") @@ -103,7 +129,7 @@ fn main() { .map(AccountKey::from) .collect(); - let fog_accounts: Vec = mc_util_keyfile::keygen::read_default_root_entropies( + let dest_accounts: Vec = mc_util_keyfile::keygen::read_default_root_entropies( config .sample_data_dir .join(Path::new(&config.fog_keys_subdir)), @@ -125,11 +151,13 @@ fn main() { let ledger_db = LedgerDB::open(ledger_dir.path()).expect("Could not open ledger_db"); + let block_version = config + .block_version + .clone() + .unwrap_or_else(|| ledger_db.get_latest_block().unwrap().version); + BLOCK_HEIGHT.store(ledger_db.num_blocks().unwrap(), Ordering::SeqCst); - BLOCK_VERSION.store( - ledger_db.get_latest_block().unwrap().version, - Ordering::SeqCst, - ); + BLOCK_VERSION.store(block_version, Ordering::SeqCst); // Use the maximum fee of all configured consensus nodes FEE.store( @@ -142,88 +170,36 @@ fn main() { Ordering::SeqCst, ); - // The number of blocks we've processed so far. - let mut block_count = 0; - // Load the bootstrapped transactions. - log::info!(logger, "Processing transactions"); - let mut num_transactions_per_account = config.num_transactions_per_account; - - let (spendable_txouts_sender, spendable_txouts_receiver) = - crossbeam_channel::unbounded::(); - - let mut spendable_txouts: Vec = Vec::new(); + let spendable_tx_outs = select_spendable_tx_outs(&ledger_db, &config, src_accounts, &logger); - while let Ok(block_contents) = ledger_db.get_block_contents(block_count) { - let transactions = block_contents.outputs; - // Only get num_transactions per account for the first block, then assume - // future blocks that were bootstrapped are similar - if num_transactions_per_account == 0 { - num_transactions_per_account = - get_num_transactions_per_account(&accounts[0], &transactions, &logger); + // Count how many of each token type + { + let mut token_count: BTreeMap = Default::default(); + for tx_out in &spendable_tx_outs { + *token_count.entry(*tx_out.amount.token_id).or_default() += 1; } + log::info!( logger, - "Loaded {:?} transactions from block {:?}", - transactions.len(), - block_count + "Loaded {} spendable tx outs", + spendable_tx_outs.len() ); - - // NOTE: This will start at the same offset per block - we may want just the - // first offset - let mut account_index = config.account_offset; - let mut account = &accounts[account_index]; - let mut num_per_account_processed = 0; - for (index, tx_out) in transactions.iter().enumerate().skip(config.start_offset) { - // Makes strong assumption about bootstrapped ledger layout - if num_per_account_processed >= num_transactions_per_account { - log::trace!( - logger, - "Moving on to next account {:?} at tx index {:?}", - account_index + 1, - index - ); - account_index += 1; - if account_index >= accounts.len() { - log::info!(logger, "Finished processing accounts. If no transactions sent, you may need to re-bootstrap."); - break; - } - account = &accounts[account_index]; - num_per_account_processed = 0; - } - - if config.num_tx_to_send == -1 - || num_per_account_processed < config.num_tx_to_send.try_into().unwrap() - { - let public_key = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); - let shared_secret = - get_tx_out_shared_secret(account.view_private_key(), &public_key); - - let (input_amount, _blinding_factor) = tx_out - .amount - .get_value(&shared_secret) - .expect("Malformed amount"); - - log::trace!( - logger, - "(account = {:?}) and (tx_index {:?}) = {}", - account_index, - index, - input_amount, - ); - - // Push to queue - spendable_txouts.push(SpendableTxOut { - tx_out: tx_out.clone(), - amount: input_amount, - from_account_key: account.clone(), - }); - } - num_per_account_processed += 1; + for (token_id, count) in token_count { + log::info!(logger, "TokenId({}): {} tx outs", token_id, count); } - block_count += 1; } + // If we got this far and it's a dry-run, end successfully + if config.dry_run { + return; + } + + // A channel to load with spendable tx outs, where worker threads will grab them + // from. + let (spendable_txouts_sender, spendable_txouts_receiver) = + crossbeam_channel::unbounded::(); + let env = Arc::new( grpcio::EnvBuilder::new() .name_prefix("FogPubkeyResolver-RPC".to_string()) @@ -231,13 +207,14 @@ fn main() { ); let fog_uri = FogUri::from_str( - fog_accounts[0] + dest_accounts[0] .default_subaddress() .fog_report_url() .expect("No fog report url"), ) .expect("Could not parse fog url"); + // A channel for worker threads to communicate when they have finished let (running_threads_sender, running_threads_receiver) = crossbeam_channel::unbounded::(); @@ -249,38 +226,59 @@ fn main() { let mut seed_fog_resolver = build_fog_resolver(&fog_uri, &env, &logger); let conns = get_conns(&config, &logger); - log::info!(logger, "Seeding Fog Accounts with initial TxOuts."); - for (i, fog_account) in fog_accounts.iter().enumerate() { - seed_fog_resolver = build_and_submit_transaction( + // Split tx outs into a group for the seed step and a group for the slam step + let (seed_tx_outs, slam_tx_outs) = spendable_tx_outs + .split_at(dest_accounts.len() * config.num_seed_transactions_per_destination_account); + + log::info!( + logger, + "Seeding Fog Accounts with {} initial TxOuts.", + seed_tx_outs.len() + ); + for (i, fog_account) in dest_accounts.iter().enumerate() { + // We now send this account the next j seed tx outs, looping infinitely + // until success + for j in 0..config.num_seed_transactions_per_destination_account { + let idx = i * config.num_seed_transactions_per_destination_account + j; + seed_fog_resolver = build_and_submit_transaction( + idx, + // For this seed phase, only use one TxOut for each transaction. + vec![seed_tx_outs[idx].clone()], + fog_account, + &config, + &ledger_db, + seed_fog_resolver, + &logger, + &conns, + &env, + &fog_uri, + ); + } + log::info!( + logger, + "Seeded {} / {} accounts successfully", i, - // For this seed phase, only use one TxOut for each transaction. - vec![spendable_txouts[i].clone()], - fog_account, - &config, - &ledger_db, - seed_fog_resolver, - &logger, - &conns, - &env, - &fog_uri, + dest_accounts.len() ); } - // Don't use spendable_txouts that were used in the seed step. - spendable_txouts = spendable_txouts[fog_accounts.len()..].to_vec(); - for spendable_txout in spendable_txouts { + // Submit remaining tx outs to the crossbeam queue where the worker threads will + // find them. Don't use spendable_txouts that were used in the seed step. + for spendable_txout in slam_tx_outs { spendable_txouts_sender - .send(spendable_txout) + .send(spendable_txout.clone()) .expect("failed sending to spendable_txouts_sender"); } + log::info!(logger, "Spawning workers for slam step"); + // Spawn worker threads for i in 0..config.max_threads { let spendable_txouts_receiver2 = spendable_txouts_receiver.clone(); let running_threads_sender2 = running_threads_sender.clone(); let config2 = config.clone(); let ledger_db2 = ledger_db.clone(); - let fog_accounts2 = fog_accounts.clone(); + let dest_accounts2 = dest_accounts.clone(); let logger2 = logger.new(o!("num" => i)); let env2 = env.clone(); let fog_resolver = build_fog_resolver(&fog_uri, &env2, &logger); @@ -293,7 +291,7 @@ fn main() { running_threads_sender2, config2, ledger_db2, - fog_accounts2, + dest_accounts2, fog_resolver, logger2, env2, @@ -309,7 +307,7 @@ fn main() { let _thread_died = running_threads_receiver.recv().unwrap(); running_threads -= 1; - log::info!(logger, "A thread died {} remaining", running_threads); + log::info!(logger, "A thread finished, {} remaining", running_threads); } log::info!(logger, "Done!"); @@ -318,6 +316,128 @@ fn main() { thread::sleep(Duration::from_secs(1)); } +/// Reads TxOut's from the ledger, adding them to a queue of spendable TxOuts: +/// * Confirms that they are owned by the expected owner +/// * Notes their amount and token id +/// +/// This function assumes that: +/// * Bootstrap assigned a fixed number of outputs to consecutive accounts +/// (src_accounts) +/// * We either discover that number from the ledger, or take it as config +/// +/// Returns all the tx outs we matched, their amounts, and the account that owns +/// them. +/// +/// Arguments: +/// * ledger_db: The ledger db to read tx outs from +/// * config: configuration options: +/// * config.num_transactions_per_source_account: Override the automatic +/// detection of num_transactions_per_account in ledger +/// * config.num_tx_to_send: Caps the number of tx outs per account which +/// this slam will actually spend +/// * config.start_offset: Instructs to skip the first N transactions in +/// each block +/// * src_accounts: The source accounts which currently own the TxOut's in the +/// ledger db +fn select_spendable_tx_outs( + ledger_db: &LedgerDB, + config: &Config, + src_accounts: Vec, + logger: &Logger, +) -> Vec { + log::info!(logger, "Processing transactions"); + let mut num_transactions_per_account = config.num_transactions_per_source_account; + + let mut spendable_tx_outs: Vec = Vec::new(); + + let mut block_count = 0; + while let Ok(block_contents) = ledger_db.get_block_contents(block_count) { + let transactions = block_contents.outputs; + // If num_transactions_per_source_account is zero, then automatically detect + // the number of transactions. + // Only get num_transactions per account for the first block, then assume + // future blocks that were bootstrapped are similar. + if num_transactions_per_account == 0 { + num_transactions_per_account = + get_num_transactions_per_account(&src_accounts[0], &transactions, logger); + } + log::info!( + logger, + "Loaded {:?} transactions from block {:?}", + transactions.len(), + block_count + ); + + // NOTE: This will start at the same offset per block - we may want just the + // first offset + + // The index of the account we expect to own the next Tx + let mut account_index = 0; + let mut account = &src_accounts[account_index]; + // The number of tx's we have processed for this account + let mut num_processed_this_account = 0; + for (index, tx_out) in transactions.iter().enumerate().skip(config.start_offset) { + // If we have already seen num_transactions_per_account on this account, then we + // have to increment account index + if num_processed_this_account >= num_transactions_per_account { + log::trace!( + logger, + "Moving on to next account {:?} at tx index {:?}", + account_index + 1, + index + ); + account_index += 1; + if account_index >= src_accounts.len() { + log::info!(logger, "Finished processing accounts. If no transactions sent, you may need to re-bootstrap."); + break; + } + account = &src_accounts[account_index]; + num_processed_this_account = 0; + } + + // Num tx_to_send is a cap on how many Tx's of this accounts that we actually + // spend If it is -1 then there is no cap. + if config.num_tx_to_send == -1 + || num_processed_this_account < config.num_tx_to_send.try_into().unwrap() + { + let public_key = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); + let shared_secret = + get_tx_out_shared_secret(account.view_private_key(), &public_key); + + let (amount, _blinding_factor) = tx_out + .amount + .get_value(&shared_secret) + .unwrap_or_else(|err| { + panic!( + "TX ownership is not as expected: tx #{} not owned by account_index {}: {}", + index, account_index, err + ) + }); + + log::trace!( + logger, + "(account = {:?}) and (tx_index {:?}) = {:?}", + account_index, + index, + amount, + ); + + // Push to queue + spendable_tx_outs.push(SpendableTxOut { + tx_out: tx_out.clone(), + amount, + from_account_key: account.clone(), + }); + } + num_processed_this_account += 1; + } + block_count += 1; + } + + spendable_tx_outs +} + +/// Make a request to fog report server, return fog resolver object fn build_fog_resolver( fog_uri: &FogUri, env: &Arc, @@ -360,12 +480,14 @@ fn build_fog_resolver( FogResolver::new(responses, &report_verifier).expect("Could not get FogResolver") } +/// Entry point for a worker thread which tries to pull spendable tx outs rom +/// queue and then build and submit transactions from them. fn worker_thread_entry( spendable_txouts_receiver: crossbeam_channel::Receiver, running_threads_sender: crossbeam_channel::Sender, config: Config, ledger_db: LedgerDB, - fog_accounts: Vec, + dest_accounts: Vec, mut fog_resolver: FogResolver, logger: Logger, env: Arc, @@ -397,9 +519,9 @@ fn worker_thread_entry( } // Send to the next fog account - let to_account = &fog_accounts[txs_created % fog_accounts.len()]; + let to_account = &dest_accounts[txs_created % dest_accounts.len()]; let fog_uri = FogUri::from_str( - fog_accounts[0] + dest_accounts[0] .default_subaddress() .fog_report_url() .expect("No fog report url"), @@ -423,6 +545,7 @@ fn worker_thread_entry( } /// Builds and submits a transaction to a given FogAccount. +/// This retries infinitely until the tx succeeds, possibly rebuilding it. /// /// If a transaction submit errors, then we get and use a new FogResolver /// to build and submit transactions. In this case, we return this new @@ -470,6 +593,8 @@ fn build_and_submit_transaction( } } +/// Submit a built tx to any of the possible connections, with retries. +/// Returns true on success and false on failure fn submit_tx( counter: usize, conns: &[SyncConnection>], @@ -484,7 +609,7 @@ fn submit_tx( // Submit to a node in round robin fashion, starting with a random node let node_index = (i + counter) % conns.len(); let conn = &conns[node_index]; - log::debug!( + log::info!( logger, "Submitting transaction {} to node {} (attempt {} / {})", counter, @@ -516,7 +641,7 @@ fn submit_tx( TransactionValidationError::TombstoneBlockExceeded, ) = error { - log::warn!( + log::debug!( logger, "Transaction {:?} could not be submitted before tombstone block passed, giving up", counter); return false; @@ -546,17 +671,9 @@ fn submit_tx( ); thread::sleep(retry_sleep_duration); } - Err(RetryError::Internal(s)) => { - log::warn!( - logger, - "Internal retry error while submitting transaction {:?} to node {} (attempt {} / {}): {}", - counter, - conn, - i, - max_retries, - s - ); - return false; + Err(RetryError::Internal(_s)) => { + // Retry crate never actually returns Internal on any code path + unreachable!() } } } @@ -569,6 +686,7 @@ fn submit_tx( false } +/// Build a tx using one or more spendable tx outs, to a particualr account. fn build_tx( spendable_txouts: &[SpendableTxOut], to_account: &AccountKey, @@ -590,9 +708,16 @@ fn build_tx( let block_version = BlockVersion::try_from(BLOCK_VERSION.load(Ordering::SeqCst)) .expect("Unsupported block version"); + // Use token id for first spendable tx out + let token_id = spendable_txouts.first().unwrap().amount.token_id; + // Create tx_builder. - let mut tx_builder = - TransactionBuilder::new(block_version, fog_resolver, EmptyMemoBuilder::default()); + let mut tx_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver, + EmptyMemoBuilder::default(), + ); tx_builder.set_fee(FEE.load(Ordering::SeqCst)).unwrap(); @@ -673,17 +798,19 @@ fn build_tx( // Add ouputs for (i, (utxo, _proof)) in utxos_with_proofs.iter().enumerate() { - let mut amount = utxo.amount; - // Use the first input to pay for the fee. - if i == 0 { - amount -= FEE.load(Ordering::SeqCst); - } + if utxo.amount.token_id == token_id { + let mut value = utxo.amount.value; + // Use the first input to pay for the fee. + if i == 0 { + value -= FEE.load(Ordering::SeqCst); + } - let target_address = to_account.default_subaddress(); + let target_address = to_account.default_subaddress(); - tx_builder - .add_output(amount, &target_address, &mut rng) - .expect("failed to add output"); + tx_builder + .add_output(value, &target_address, &mut rng) + .expect("failed to add output"); + } } // Set tombstone block. @@ -694,6 +821,7 @@ fn build_tx( tx_builder.build(&mut rng).expect("failed building tx") } +/// Get merkle proofs of membership from the ledger for several utxos fn get_membership_proofs( ledger_db: &LedgerDB, utxos: &[SpendableTxOut], @@ -711,6 +839,7 @@ fn get_membership_proofs( utxos.iter().cloned().zip(proofs.into_iter()).collect() } +/// Get ring mixins for a transaction from the ledger fn get_rings( ledger_db: &LedgerDB, ring_size: usize, @@ -754,6 +883,7 @@ fn get_rings( rings_with_proofs } +/// Count how many consecutive TxOut's in a range are owned by a given account fn get_num_transactions_per_account( account: &AccountKey, transactions: &[TxOut], diff --git a/fog/distribution/tests/bootstrap.rs b/fog/distribution/tests/bootstrap.rs new file mode 100644 index 0000000000..eb0cf74e2d --- /dev/null +++ b/fog/distribution/tests/bootstrap.rs @@ -0,0 +1,62 @@ +use std::{ + env::{args, set_current_dir}, + path::PathBuf, + process::Command, +}; +use tempfile::TempDir; + +// Test that fog distro can find the spendable tx outs from the bootstrap +// +// This is meant to be kept in sync with CD and help catch bootstrap / fog +// distro problems earlier and with faster iteration times. +#[test] +fn test_find_spendable_tx_outs() { + let me = PathBuf::from(args().next().unwrap()); + let bin = me.parent().unwrap().parent().unwrap(); + println!("bin = {:?}", bin); + + let dir = TempDir::new().unwrap(); + set_current_dir(dir.path()).unwrap(); + println!("dir = {:?}", dir); + + assert!(Command::new(bin.join("sample-keys")) + .args([ + "--num", + "10", + "--seed", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ]) + .status() + .unwrap() + .success()); + + assert!(Command::new(bin.join("generate-sample-ledger")) + .args(["--txs", "10", "--max-token-id", "1"]) + .status() + .unwrap() + .success()); + + assert!(Command::new(bin.join("sample-keys")) + .args(["--num", "5", "--output-dir", "./fog_keys"]) + .status() + .unwrap() + .success()); + + assert!(Command::new(bin.join("fog-distribution")) + .args([ + "--sample-data-dir", + "./", + "--peer", + "mc://test.com", + "--num-tx-to-send", + "5", + "--block-version", + "3", + "--num-seed-transactions-per-destination-account", + "2", + "--dry-run" + ]) + .status() + .unwrap() + .success()); +} diff --git a/fog/ingest/enclave/impl/tests/tx_processing.rs b/fog/ingest/enclave/impl/tests/tx_processing.rs index 907a706661..c7bede828a 100644 --- a/fog/ingest/enclave/impl/tests/tx_processing.rs +++ b/fog/ingest/enclave/impl/tests/tx_processing.rs @@ -16,7 +16,9 @@ use mc_oblivious_traits::HeapORAMStorageCreator; use mc_transaction_core::{ encrypted_fog_hint::EncryptedFogHint, fog_hint::{FogHint, PlaintextArray}, + tokens::Mob, tx::TxOut, + Token, }; use mc_util_from_random::FromRandom; use mc_util_logger_macros::test_with_logger; @@ -27,6 +29,7 @@ use std::collections::HashMap; #[test_with_logger] fn test_ingest_enclave(logger: Logger) { mc_util_test_helper::run_with_several_seeds(|mut rng| { + let token_id = Mob::ID; // make alice and bob let alice_account = AccountKey::random_with_fog(&mut rng); let bob_account = AccountKey::random_with_fog(&mut rng); @@ -57,6 +60,7 @@ fn test_ingest_enclave(logger: Logger) { let e_fog_hint = FogHint::from(&bob_public_address).encrypt(&fog_pubkey, &mut rng); TxOut::new( 10, + token_id, &bob_account.default_subaddress(), &tx_private_key, e_fog_hint, @@ -82,7 +86,7 @@ fn test_ingest_enclave(logger: Logger) { assert_eq!(tx_rows.len(), 10); // Check that the tx row ciphertexts have the right size - const EXPECTED_PAYLOAD_SIZE: usize = 227; // The observed tx_row.payload size + const EXPECTED_PAYLOAD_SIZE: usize = 233; // The observed tx_row.payload size for tx_row in tx_rows.iter() { assert_eq!( tx_row.payload.len(), EXPECTED_PAYLOAD_SIZE, @@ -206,6 +210,8 @@ fn test_ingest_enclave_malformed_txos(logger: Logger) { let bob_public_address = bob_account.default_subaddress(); + let token_id = Mob::ID; + // make some tx outs let tx_outs: Vec<_> = (0..40usize) .map(|idx| { @@ -224,6 +230,7 @@ fn test_ingest_enclave_malformed_txos(logger: Logger) { }; TxOut::new( 10, + token_id, &bob_account.default_subaddress(), &tx_private_key, e_fog_hint, @@ -317,6 +324,8 @@ fn test_ingest_enclave_overflow(logger: Logger) { let alice_public_address = alice_account.default_subaddress(); let bob_public_address = bob_account.default_subaddress(); + let token_id = Mob::ID; + // Repeat the test 5 times to try to smoke out failures let repetitions = 5; for iteration in 0..repetitions { @@ -363,7 +372,7 @@ fn test_ingest_enclave_overflow(logger: Logger) { }; let tx_private_key = RistrettoPrivate::from_random(&mut rng); let e_fog_hint = FogHint::from(pub_addr).encrypt(&fog_pubkey, &mut rng); - TxOut::new(10, pub_addr, &tx_private_key, e_fog_hint).unwrap() + TxOut::new(10, token_id, pub_addr, &tx_private_key, e_fog_hint).unwrap() }) .collect(); diff --git a/fog/ingest/enclave/trusted/Cargo.lock b/fog/ingest/enclave/trusted/Cargo.lock index 9c623591ac..f6d9a69161 100644 --- a/fog/ingest/enclave/trusted/Cargo.lock +++ b/fog/ingest/enclave/trusted/Cargo.lock @@ -1271,6 +1271,7 @@ version = "1.3.0-pre0" dependencies = [ "aes", "bulletproofs-og", + "crc", "curve25519-dalek", "displaydoc", "generic-array", diff --git a/fog/ledger/enclave/trusted/Cargo.lock b/fog/ledger/enclave/trusted/Cargo.lock index f9e93a50d9..9ddf7292e3 100644 --- a/fog/ledger/enclave/trusted/Cargo.lock +++ b/fog/ledger/enclave/trusted/Cargo.lock @@ -1256,6 +1256,7 @@ version = "1.3.0-pre0" dependencies = [ "aes", "bulletproofs-og", + "crc", "curve25519-dalek", "displaydoc", "generic-array", diff --git a/fog/ledger/server/src/merkle_proof_service.rs b/fog/ledger/server/src/merkle_proof_service.rs index da0dad69f9..d34fc0b7e9 100644 --- a/fog/ledger/server/src/merkle_proof_service.rs +++ b/fog/ledger/server/src/merkle_proof_service.rs @@ -226,8 +226,9 @@ mod test { encrypted_fog_hint::{EncryptedFogHint, ENCRYPTED_FOG_HINT_LEN}, membership_proofs::Range, onetime_keys::{create_shared_secret, create_tx_out_public_key, create_tx_out_target_key}, + tokens::Mob, tx::{TxOut, TxOutMembershipElement, TxOutMembershipProof}, - Amount, + Amount, AmountData, Token, }; use mc_util_from_random::FromRandom; use mc_util_grpc::AnonymousAuthenticator; @@ -255,7 +256,11 @@ mod test { let shared_secret: RistrettoPublic = create_shared_secret(&target_key, &tx_secret_key); // FIXME: Without a different value, the txouts are identical - that // may be fine, or we may want a more robust mock ledger populator. - let amount = Amount::new(value + output_index as u64, &shared_secret).unwrap(); + let amount_data = AmountData { + value: value + output_index as u64, + token_id: Mob::ID, + }; + let amount = Amount::new(amount_data, &shared_secret).unwrap(); let tx_out = TxOut { amount, target_key: target_key.into(), diff --git a/fog/ledger/server/tests/connection.rs b/fog/ledger/server/tests/connection.rs index 326f32df38..70f2f659b0 100644 --- a/fog/ledger/server/tests/connection.rs +++ b/fog/ledger/server/tests/connection.rs @@ -24,7 +24,8 @@ 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, BlockVersion, + ring_signature::KeyImage, tokens::Mob, tx::TxOut, Block, BlockContents, BlockSignature, + BlockVersion, Token, }; use mc_util_from_random::FromRandom; use mc_util_grpc::GrpcRetryConfig; @@ -788,6 +789,7 @@ fn add_block_to_ledger_db( TxOut::new( // TODO: allow for subaddress index! value, + Mob::ID, recipient, &RistrettoPrivate::from_random(rng), Default::default(), diff --git a/fog/sample-paykit/src/bin/sample_paykit_remote_wallet.rs b/fog/sample-paykit/src/bin/sample_paykit_remote_wallet.rs index 882f0860d8..5e9a4a95cc 100644 --- a/fog/sample-paykit/src/bin/sample_paykit_remote_wallet.rs +++ b/fog/sample-paykit/src/bin/sample_paykit_remote_wallet.rs @@ -17,6 +17,7 @@ use mc_fog_sample_paykit::{ Client, ClientBuilder, }; use mc_fog_uri::{FogLedgerUri, FogViewUri}; +use mc_transaction_core::{tokens::Mob, Token}; use mc_util_grpc::{ rpc_internal_error, rpc_invalid_arg_error, send_result, ConnectionUriGrpcioServer, }; @@ -125,10 +126,13 @@ impl RemoteWalletService { ) .build(); - let (balance, block_count) = client + let (balances, block_count) = client .check_balance() .map_err(|err| rpc_internal_error("check_balance", err, &self.logger))?; + // conformance tests only does MOB right now + let balance = balances.get(&Mob::ID).cloned().unwrap_or_default(); + let mut state = self.state.lock().expect("mutex poisoned"); let client_id = state.clients.len(); state.clients.push(Some(client)); @@ -150,10 +154,12 @@ impl RemoteWalletService { let mut state = self.state.lock().expect("mutex poisoned"); match state.clients.get_mut(request.client_id as usize) { Some(Some(client)) => { - let (balance, block_count) = client + let (balances, block_count) = client .check_balance() .map_err(|err| rpc_internal_error("check_balance", err, &self.logger))?; + let balance = balances.get(&Mob::ID).cloned().unwrap_or_default(); + let response = BalanceCheckResponse { client_id: request.client_id, balance, diff --git a/fog/sample-paykit/src/cached_tx_data/mod.rs b/fog/sample-paykit/src/cached_tx_data/mod.rs index 267064b055..88a158e9e4 100644 --- a/fog/sample-paykit/src/cached_tx_data/mod.rs +++ b/fog/sample-paykit/src/cached_tx_data/mod.rs @@ -8,10 +8,7 @@ use core::{ }; use displaydoc::Display; use mc_account_keys::{AccountKey, PublicAddress, CHANGE_SUBADDRESS_INDEX}; -use mc_common::{ - logger::{log, Logger}, - HashMap, -}; +use mc_common::logger::{log, Logger}; use mc_crypto_keys::RistrettoPublic; use mc_fog_api::{fog_common, ledger}; use mc_fog_ledger_connection::{ @@ -30,11 +27,11 @@ use mc_transaction_core::{ onetime_keys::{recover_onetime_private_key, recover_public_subaddress_spend_key}, ring_signature::KeyImage, tx::TxOut, - BlockIndex, + AmountData, BlockIndex, TokenId, }; use mc_transaction_std::MemoType; use mc_util_telemetry::{telemetry_static_key, tracer, Key, TraceContextExt, Tracer}; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; mod memo_handler; pub use memo_handler::{MemoHandler, MemoHandlerError}; @@ -220,11 +217,11 @@ impl CachedTxData { /// Compute our current balance /// - /// Returns (balance, block_count) - /// where balance is the correct balance when the ledger has exactly - /// block_count blocks + /// Returns (map, block_count) + /// where the map indicates our balances of all tokens, in our account + /// when the ledger has exactly block_count blocks #[allow(clippy::nonminimal_bool)] - pub fn get_balance(&self) -> (u64, BlockCount) { + pub fn get_balance(&self) -> (HashMap, BlockCount) { let num_blocks = self.get_num_blocks(); assert!( self.key_image_data_completeness >= num_blocks, @@ -278,15 +275,16 @@ impl CachedTxData { true } }; - log::trace!(self.logger, "{}: global_index {} block_index {} value {} status {}", result, our_txo.global_index, our_txo.block_index, our_txo.value, our_txo.status); + log::trace!(self.logger, "{}: global_index {} block_index {} amount {:?} status {}", result, our_txo.global_index, our_txo.block_index, our_txo.amount, our_txo.status); result }) - .fold(0u64, |running_balance, our_txo| { - running_balance + our_txo.value + .fold(HashMap::default(), |mut running_balance, our_txo| { + *running_balance.entry(our_txo.amount.token_id).or_default() += our_txo.amount.value; + running_balance }); log::trace!( self.logger, - "Computed balance: {}, num_blocks {}", + "Computed balance: {:?}, num_blocks {}", balance, num_blocks ); @@ -319,19 +317,24 @@ impl CachedTxData { /// lower. pub fn get_transaction_inputs( &self, - amount: u64, + amount: AmountData, max_inputs: usize, ) -> Result> { // All transactions that we could choose to use as an input - let available = self.get_unspent_txos(); + let available = self + .get_unspent_txos() + .iter() + .cloned() + .filter(|txo| txo.amount.token_id == amount.token_id) + .collect::>(); - let tx_values: Vec<_> = available.iter().map(|txo| txo.value).collect(); + let tx_values: Vec = available.iter().map(|txo| txo.amount.value).collect(); - let selected = input_selection_heuristic(&tx_values, amount, max_inputs)?; + let selected = input_selection_heuristic(&tx_values, amount.value, max_inputs)?; Ok(selected .into_iter() - .map(|idx| available[idx].clone()) + .map(|idx| (*available[idx]).clone()) .collect()) } @@ -353,10 +356,10 @@ impl CachedTxData { // Insert into owned_tx_outs log::trace!( self.logger, - "Found new txo: global_index {}, block_index {}, value {}", + "Found new txo: global_index {}, block_index {}, amount {:?}", otxo.global_index, otxo.block_index, - otxo.value + otxo.amount ); let maybe_prev = self.owned_tx_outs.insert(otxo.global_index, otxo.clone()); if let Some(prev) = maybe_prev { @@ -365,8 +368,8 @@ impl CachedTxData { "Saw {}'th tx out a second time", otxo.global_index ); - if prev.value != otxo.value { - log::warn!(self.logger, "Got two different values after view key scanning new and old versions of {}'th tx_out", otxo.global_index); + if prev.amount != otxo.amount { + log::warn!(self.logger, "Got two different amounts after view key scanning new and old versions of {}'th tx_out", otxo.global_index); } } // Keep the invariant of key_image_data_completeness working @@ -853,10 +856,10 @@ pub struct OwnedTxOut { /// The tx_out that we recovered from the view server, or from view-key /// scanning a missed block. pub tx_out: TxOut, - /// The value of the TxOut, computed when we matched this tx_out + /// The value of the TxOut, computed when we matched this tx out /// successfully against our account key. - pub value: u64, - // The subaddress index this tx_out was sent to. + pub amount: AmountData, + /// The subaddress index this tx_out was sent to. pub subaddress_index: u64, /// The key image that we computed when matching this tx_out against our /// account key. @@ -886,7 +889,7 @@ impl OwnedTxOut { let decompressed_tx_pub = RistrettoPublic::try_from(&tx_out.public_key)?; let shared_secret = get_tx_out_shared_secret(account_key.view_private_key(), &decompressed_tx_pub); - let (value, _blinding) = tx_out.amount.get_value(&shared_secret)?; + let (amount_data, _blinding) = tx_out.amount.get_value(&shared_secret)?; // Calculate the subaddress spend public key for tx_out. let tx_out_target_key = RistrettoPublic::try_from(&tx_out.target_key)?; @@ -918,7 +921,7 @@ impl OwnedTxOut { block_index: rec.block_index, tx_out, key_image, - value, + amount: amount_data, subaddress_index: *subaddress_index, status, }) diff --git a/fog/sample-paykit/src/client.rs b/fog/sample-paykit/src/client.rs index cc6135192d..4af7434a27 100644 --- a/fog/sample-paykit/src/client.rs +++ b/fog/sample-paykit/src/client.rs @@ -10,10 +10,7 @@ use crate::{ use core::{convert::TryFrom, result::Result as StdResult, str::FromStr}; use mc_account_keys::{AccountKey, PublicAddress}; use mc_attest_verifier::Verifier; -use mc_common::{ - logger::{log, Logger}, - HashSet, -}; +use mc_common::logger::{log, Logger}; use mc_connection::{ BlockchainConnection, Connection, HardcodedCredentialsProvider, ThickClient, UserTxConnection, }; @@ -33,7 +30,7 @@ use mc_transaction_core::{ ring_signature::KeyImage, tokens::Mob, tx::{Tx, TxOut, TxOutMembershipProof}, - BlockIndex, BlockVersion, Token, + AmountData, BlockIndex, BlockVersion, Token, TokenId, }; use mc_transaction_std::{ ChangeDestination, InputCredentials, MemoType, RTHMemoBuilder, SenderMemoCredential, @@ -42,6 +39,7 @@ use mc_transaction_std::{ use mc_util_telemetry::{block_span_builder, telemetry_static_key, tracer, Key, Span}; use mc_util_uri::{ConnectionUri, FogUri}; use rand::Rng; +use std::collections::{HashMap, HashSet}; /// Default number of blocks used for calculating transaction tombstone block /// number. See `new_tx_block_attempts` below. @@ -134,10 +132,10 @@ impl Client { /// Check this user's current available balance. /// /// Returns: - /// * Balance (in picomob) + /// * Balances (for all token types) (in picomob) /// * Number of blocks in the chain at the time that this was the correct /// balance - pub fn check_balance(&mut self) -> Result<(u64, BlockCount)> { + pub fn check_balance(&mut self) -> Result<(HashMap, BlockCount)> { mc_common::trace_time!(self.logger, "MobileCoinClient.get_balance"); self.tx_data.poll_fog( &mut self.fog_view, @@ -151,10 +149,11 @@ impl Client { /// Does NOT make any new network calls. /// /// Returns: - /// * Balance (in picomob) + /// * HashMap Balance (in picomob or equivalent for each + /// token) /// * Number of blocks in the chain at the time that this was the correct /// balance - pub fn compute_balance(&self) -> (u64, BlockCount) { + pub fn compute_balance(&self) -> (HashMap, BlockCount) { self.tx_data.get_balance() } @@ -302,7 +301,7 @@ impl Client { /// * `fee` - The transaction fee to use pub fn build_transaction( &mut self, - amount: u64, + amount: AmountData, target_address: &PublicAddress, rng: &mut T, fee: u64, @@ -317,12 +316,18 @@ impl Client { target_address ); + let required_input_amount = { + let mut amount = amount.clone(); + amount.value += fee; + amount + }; + // Arbitrarily choose 3 as the maximum number of inputs // TODO: Should be based on fee scaling and fee choice const TARGET_NUM_INPUTS: usize = 3; let inputs = self .tx_data - .get_transaction_inputs(amount + Mob::MINIMUM_FEE, TARGET_NUM_INPUTS)?; + .get_transaction_inputs(required_input_amount, TARGET_NUM_INPUTS)?; let inputs: Vec<(OwnedTxOut, TxOutMembershipProof)> = self.get_proofs(&inputs)?; let rings: Vec> = self.get_rings(&inputs, rng)?; @@ -568,7 +573,7 @@ fn build_transaction_helper( block_version: BlockVersion, inputs: Vec<(OwnedTxOut, TxOutMembershipProof)>, rings: Vec>, - amount: u64, + amount: AmountData, source_account_key: &AccountKey, target_address: &PublicAddress, tombstone_block: BlockIndex, @@ -594,16 +599,18 @@ fn build_transaction_helper( memo_builder.set_sender_credential(SenderMemoCredential::from(source_account_key)); memo_builder.enable_destination_memo(); - TransactionBuilder::new(block_version, fog_resolver, memo_builder) + TransactionBuilder::new(block_version, amount.token_id, fog_resolver, memo_builder) }; tx_builder.set_fee(fee)?; - let input_amount = inputs.iter().fold(0, |acc, (txo, _)| acc + txo.value); + let input_amount = inputs + .iter() + .fold(0, |acc, (txo, _)| acc + txo.amount.value); let fee = tx_builder.get_fee(); - if (amount + fee) > input_amount { + if (amount.value + fee) > input_amount { return Err(Error::InsufficientFunds); } - let change = input_amount - (amount + fee); + let change = input_amount - (amount.value + fee); // Unzip each vec of tuples into a tuple of vecs. let mut rings_and_proofs: Vec<(Vec, Vec)> = rings @@ -686,7 +693,7 @@ fn build_transaction_helper( // Resolve account server key if the receiver specifies an account service in // their public address tx_builder - .add_output(amount, target_address, rng) + .add_output(amount.value, target_address, rng) .map_err(Error::AddOutput)?; let change_destination = ChangeDestination::from(source_account_key); @@ -708,19 +715,17 @@ mod test_build_transaction_helper { use super::*; use core::result::Result as StdResult; use mc_account_keys::{AccountKey, PublicAddress, DEFAULT_SUBADDRESS_INDEX}; - use mc_common::{ - logger::{test_with_logger, Logger}, - HashMap, - }; + use mc_common::logger::{test_with_logger, Logger}; use mc_fog_report_validation::{FogPubkeyError, FullyValidatedFogPubkey}; use mc_fog_types::view::{FogTxOut, FogTxOutMetadata, TxOutRecord}; use mc_transaction_core::{ constants::MILLIMOB_TO_PICOMOB, tx::{TxOut, TxOutMembershipProof}, + AmountData, }; use mc_transaction_core_test_utils::get_outputs; use rand::{rngs::StdRng, SeedableRng}; - use std::iter::FromIterator; + use std::{collections::HashMap, iter::FromIterator}; // Mock of FogPubkeyResolver struct FakeAcctResolver {} @@ -745,7 +750,10 @@ mod test_build_transaction_helper { // Amount per input. let initial_amount = 300 * MILLIMOB_TO_PICOMOB; - let amount_to_send = 457 * MILLIMOB_TO_PICOMOB; + let amount_to_send = AmountData { + value: 457 * MILLIMOB_TO_PICOMOB, + token_id: Mob::ID, + }; let num_inputs = 3; let ring_size = 1; diff --git a/fog/sample-paykit/src/lib.rs b/fog/sample-paykit/src/lib.rs index 39aa21bd85..11282ae1eb 100644 --- a/fog/sample-paykit/src/lib.rs +++ b/fog/sample-paykit/src/lib.rs @@ -35,7 +35,7 @@ pub use mc_transaction_core::{ onetime_keys::recover_onetime_private_key, ring_signature::KeyImage, tx::{Tx, TxOutMembershipProof}, - BlockIndex, + BlockIndex, TokenId, }; /// A status that a submitted transaction can have diff --git a/fog/test-client/src/bin/main.rs b/fog/test-client/src/bin/main.rs index 586cc6cde8..65de42ba2d 100644 --- a/fog/test-client/src/bin/main.rs +++ b/fog/test-client/src/bin/main.rs @@ -40,6 +40,7 @@ fn main() { tx_receive_deadline: config.consensus_wait, double_spend_wait: config.ledger_sync_wait, transfer_amount: config.transfer_amount, + token_id: config.token_id, ..Default::default() }; diff --git a/fog/test-client/src/config.rs b/fog/test-client/src/config.rs index c37f545985..f64ce57221 100644 --- a/fog/test-client/src/config.rs +++ b/fog/test-client/src/config.rs @@ -3,7 +3,7 @@ //! Configuration parameters for the test client binary use mc_common::logger::{log, Logger}; -use mc_fog_sample_paykit::AccountKey; +use mc_fog_sample_paykit::{AccountKey, TokenId}; use mc_fog_uri::{FogLedgerUri, FogViewUri}; use mc_util_grpc::GrpcRetryConfig; use mc_util_parse::parse_duration_in_seconds; @@ -120,6 +120,10 @@ pub struct TestClientConfig { /// Grpc retry config #[structopt(flatten)] pub grpc_retry_config: GrpcRetryConfig, + + /// What token id to use for the test + #[structopt(long, env = "MC_TOKEN_ID", default_value = "0")] + pub token_id: TokenId, } impl TestClientConfig { diff --git a/fog/test-client/src/test_client.rs b/fog/test-client/src/test_client.rs index e23ff77300..c42f6c5511 100644 --- a/fog/test-client/src/test_client.rs +++ b/fog/test-client/src/test_client.rs @@ -11,10 +11,12 @@ use hex_fmt::HexList; use mc_account_keys::ShortAddressHash; use mc_common::logger::{log, Logger}; use mc_crypto_rand::McRng; -use mc_fog_sample_paykit::{AccountKey, Client, ClientBuilder, TransactionStatus, Tx}; +use mc_fog_sample_paykit::{AccountKey, Client, ClientBuilder, TokenId, TransactionStatus, Tx}; use mc_fog_uri::{FogLedgerUri, FogViewUri}; use mc_sgx_css::Signature; -use mc_transaction_core::{constants::RING_SIZE, tokens::Mob, BlockIndex, BlockVersion, Token}; +use mc_transaction_core::{ + constants::RING_SIZE, tokens::Mob, AmountData, BlockIndex, BlockVersion, Token, +}; use mc_transaction_std::MemoType; use mc_util_grpc::GrpcRetryConfig; use mc_util_telemetry::{ @@ -61,6 +63,8 @@ pub struct TestClientPolicy { pub polling_wait: Duration, /// A transaction amount to send pub transfer_amount: u64, + /// A token id to use + pub token_id: TokenId, /// Whether to test RTH memos pub test_rth_memos: bool, } @@ -84,11 +88,22 @@ impl Default for TestClientPolicy { double_spend_wait: Duration::from_secs(10), polling_wait: Duration::from_millis(50), transfer_amount: Mob::MINIMUM_FEE, + token_id: Mob::ID, test_rth_memos: false, } } } +impl TestClientPolicy { + /// Get the amount that should be transfered according to the policy + pub fn amount(&self) -> AmountData { + AmountData { + value: self.transfer_amount, + token_id: self.token_id, + } + } +} + /// An object which can run test transfers pub struct TestClient { policy: TestClientPolicy, @@ -264,7 +279,7 @@ impl TestClient { let transaction = { let start = Instant::now(); let transaction = source_client - .build_transaction(self.policy.transfer_amount, &target_address, &mut rng, fee) + .build_transaction(self.policy.amount(), &target_address, &mut rng, fee) .map_err(TestClientError::BuildTx)?; counters::TX_BUILD_TIME.observe(start.elapsed().as_secs_f64()); transaction @@ -363,15 +378,17 @@ impl TestClient { &self, client: &mut Client, block_index: BlockIndex, + token_id: TokenId, expected_balance: u64, ) -> Result<(), TestClientError> { let start = Instant::now(); let mut deadline = Some(start + self.policy.tx_receive_deadline); loop { - let (new_balance, new_block_count) = client + let (new_balances, new_block_count) = client .check_balance() .map_err(TestClientError::CheckBalance)?; + let new_balance = new_balances.get(&token_id).cloned().unwrap_or_default(); // Wait for client cursor to include the index where the transaction landed. if u64::from(new_block_count) > block_index { @@ -458,6 +475,7 @@ impl TestClient { /// Conduct a test transfer from source client to target client /// /// Arguments: + /// * token_id: The token id to use for the test transfer /// * source_client: The client to send from /// * source_client_index: The index of this client in the list of clients /// (for debugging info) @@ -466,6 +484,7 @@ impl TestClient { /// (for debugging info) fn test_transfer( &self, + token_id: TokenId, source_client: Arc>, source_client_index: usize, target_client: Arc>, @@ -484,23 +503,29 @@ impl TestClient { let (src_balance, tgt_balance) = tracer.in_span( "test_transfer_pre_checks", |_cx| -> Result<(u64, u64), TestClientError> { - let (src_balance, src_cursor) = source_client_lk + let (src_balances, src_cursor) = source_client_lk .check_balance() .map_err(TestClientError::CheckBalance)?; + let src_balance = src_balances.get(&token_id).cloned().unwrap_or_default(); + log::info!( self.logger, - "client {} has a balance of {} after {} blocks", + "client {} has a TokenId({}) balance of {} after {} blocks", source_client_index, + token_id, src_balance, src_cursor ); - let (tgt_balance, tgt_cursor) = target_client_lk + let (tgt_balances, tgt_cursor) = target_client_lk .check_balance() .map_err(TestClientError::CheckBalance)?; + let tgt_balance = tgt_balances.get(&token_id).cloned().unwrap_or_default(); + log::info!( self.logger, - "client {} has a balance of {} after {} blocks", + "client {} has a TokenId({}) balance of {} after {} blocks", target_client_index, + token_id, tgt_balance, tgt_cursor ); @@ -526,6 +551,7 @@ impl TestClient { drop(target_client_lk); let mut receive_tx_worker = ReceiveTxWorker::new( target_client, + token_id, tgt_balance, tgt_balance + self.policy.transfer_amount, self.policy.clone(), @@ -552,6 +578,7 @@ impl TestClient { self.ensure_expected_balance_after_block( &mut source_client_lk, transaction_appeared, + token_id, src_balance - self.policy.transfer_amount - transfer_data.fee, ) })?; @@ -628,6 +655,7 @@ impl TestClient { /// Run a test that lasts a fixed duration and fails fast on an error /// /// Arguments: + /// * token_id: The token id to use /// * num_transactions: The number of transactions to run pub fn run_test(&self, num_transactions: usize) -> Result<(), TestClientError> { let client_count = self.account_keys.len() as usize; @@ -647,6 +675,7 @@ impl TestClient { let target_client = clients[target_index].clone(); let transaction = self.test_transfer( + self.policy.token_id, source_client.clone(), source_index, target_client, @@ -704,7 +733,13 @@ impl TestClient { let target_client = clients[target_index].clone(); let transfer_start = Instant::now(); - match self.test_transfer(source_client, source_index, target_client, target_index) { + match self.test_transfer( + self.policy.token_id, + source_client, + source_index, + target_client, + target_index, + ) { Ok(_) => { log::info!(self.logger, "Transfer succeeded"); counters::TX_SUCCESS_COUNT.inc(); @@ -797,7 +832,8 @@ impl ReceiveTxWorker { /// /// Arguments: /// * client: The receiving client to check - /// * current balance: The current balance of that client + /// * token_id: The token id we are transferring + /// * current balance: The current balance of that client (in this token id) /// * expected balance: The expected balance after the Tx is received /// * policy: The test client policy object /// * expected_memo_contents: Optional short address hash matching the @@ -805,6 +841,7 @@ impl ReceiveTxWorker { /// * logger pub fn new( client: Arc>, + token_id: TokenId, current_balance: u64, expected_balance: u64, policy: TestClientPolicy, @@ -838,10 +875,12 @@ impl ReceiveTxWorker { return Ok(()); } - let (new_balance, new_block_count) = client + let (new_balances, new_block_count) = client .check_balance() .map_err(TestClientError::CheckBalance)?; + let new_balance = new_balances.get(&token_id).cloned().unwrap_or_default(); + if new_balance == expected_balance { counters::TX_RECEIVED_TIME.observe(start.elapsed().as_secs_f64()); diff --git a/fog/test_infra/src/bin/add_test_block.rs b/fog/test_infra/src/bin/add_test_block.rs index a210ef516f..fadffa6c1b 100644 --- a/fog/test_infra/src/bin/add_test_block.rs +++ b/fog/test_infra/src/bin/add_test_block.rs @@ -35,8 +35,9 @@ use mc_transaction_core::{ membership_proofs::Range, onetime_keys::recover_onetime_private_key, ring_signature::KeyImage, + tokens::Mob, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Block, BlockContents, BlockData, BlockSignature, BlockVersion, + Block, BlockContents, BlockData, BlockSignature, BlockVersion, Token, }; use mc_util_from_random::FromRandom; use rand_core::SeedableRng; @@ -162,9 +163,13 @@ fn main() { ); let e_fog_hint = fog_hint.encrypt(&fog_pubkey, &mut rng); + // Assume MOB token for these tests + let token_id = Mob::ID; + let tx_private_key = RistrettoPrivate::from_random(&mut rng); let tx_out = TxOut::new( credit.amount, + token_id, &account_keys[credit.account].default_subaddress(), &tx_private_key, e_fog_hint, diff --git a/fog/test_infra/src/bin/init_test_ledger.rs b/fog/test_infra/src/bin/init_test_ledger.rs index 4a8b901c8d..503312eebf 100644 --- a/fog/test_infra/src/bin/init_test_ledger.rs +++ b/fog/test_infra/src/bin/init_test_ledger.rs @@ -55,6 +55,7 @@ fn main() { 0, seed, None, + 0, logger.clone(), ); diff --git a/fog/test_infra/src/mock_users.rs b/fog/test_infra/src/mock_users.rs index 27ec741414..7c9ab06508 100644 --- a/fog/test_infra/src/mock_users.rs +++ b/fog/test_infra/src/mock_users.rs @@ -11,7 +11,7 @@ use mc_fog_types::{ BlockCount, }; use mc_fog_view_protocol::{FogViewConnection, UserPrivate, UserRngSet}; -use mc_transaction_core::{fog_hint::FogHint, tx::TxOut, Amount}; +use mc_transaction_core::{fog_hint::FogHint, tokens::Mob, tx::TxOut, Amount, AmountData, Token}; use mc_util_from_random::FromRandom; use rand_core::{CryptoRng, RngCore}; use std::collections::{HashMap, HashSet}; @@ -110,8 +110,12 @@ pub fn make_random_tx( ) -> TxOut { let target_key = RistrettoPublic::from_random(rng); let public_key = RistrettoPublic::from_random(rng); + let amount_data = AmountData { + value: 1, + token_id: Mob::ID, + }; TxOut { - amount: Amount::new(1, &public_key).expect("amount failed unexpectedly"), + amount: Amount::new(amount_data, &public_key).expect("amount failed unexpectedly"), target_key: target_key.into(), public_key: public_key.into(), e_fog_hint: recipient.encrypt(acct_server_pubkey, rng), diff --git a/fog/types/src/view.rs b/fog/types/src/view.rs index c596879f1c..b4079f7085 100644 --- a/fog/types/src/view.rs +++ b/fog/types/src/view.rs @@ -234,6 +234,11 @@ pub struct TxOutRecord { /// The encrypted memo bytes of the TxOut #[prost(bytes, tag = "9")] pub tx_out_e_memo_data: Vec, + + /// The masked token id associated to the amount field in the TxOut that + /// was recovered + #[prost(bytes, tag = "10")] + pub tx_out_amount_masked_token_id: Vec, } impl TxOutRecord { @@ -249,6 +254,7 @@ impl TxOutRecord { tx_out_amount_commitment_data: Default::default(), tx_out_amount_commitment_data_crc32: fog_tx_out.amount_commitment_data_crc32, tx_out_amount_masked_value: fog_tx_out.amount_masked_value, + tx_out_amount_masked_token_id: fog_tx_out.amount_masked_token_id, tx_out_target_key_data: fog_tx_out.target_key.as_bytes().to_vec(), tx_out_public_key_data: fog_tx_out.public_key.as_bytes().to_vec(), tx_out_e_memo_data: fog_tx_out @@ -269,6 +275,7 @@ impl TxOutRecord { target_key: CompressedRistrettoPublic::try_from(&self.tx_out_target_key_data[..])?, public_key: CompressedRistrettoPublic::try_from(&self.tx_out_public_key_data[..])?, amount_masked_value: self.tx_out_amount_masked_value, + amount_masked_token_id: self.tx_out_amount_masked_token_id.clone(), amount_commitment_data_crc32: self.get_amount_data_crc32()?, e_memo: self.get_e_memo()?, }) @@ -324,6 +331,9 @@ pub struct FogTxOut { /// The tx out masked amount pub amount_masked_value: u64, + /// The tx out masked token id + pub amount_masked_token_id: Vec, + /// The crc32 of the tx out amount commitment bytes pub amount_commitment_data_crc32: u32, @@ -340,6 +350,7 @@ impl core::convert::From<&TxOut> for FogTxOut { target_key: src.target_key, public_key: src.public_key, amount_masked_value: src.amount.masked_value, + amount_masked_token_id: src.amount.masked_token_id.clone(), amount_commitment_data_crc32: Crc::::new(&crc::CRC_32_ISO_HDLC) .checksum(src.amount.commitment.point.as_bytes()), e_memo: src.e_memo, @@ -366,20 +377,18 @@ impl FogTxOut { // Reconstruct compressed commitment based on our view key. // The first step is reconstructing the TxOut shared secret let public_key = RistrettoPublic::try_from(&self.public_key)?; + let tx_out_shared_secret = mc_transaction_core::get_tx_out_shared_secret(view_key, &public_key); - // The next step is unblinding the amount value - let value = - self.amount_masked_value ^ mc_transaction_core::get_value_mask(&tx_out_shared_secret); - - // Now we can rebuild the Amount object from the value and shared secret - let amount = Amount::new(value, &tx_out_shared_secret)?; + let (amount, _) = Amount::reconstruct( + self.amount_masked_value, + &self.amount_masked_token_id, + &tx_out_shared_secret, + ) + .map_err(|_| FogTxOutError::ChecksumMismatch)?; - // Check that the crc32 of amount compressed commitment matches - if self.amount_commitment_data_crc32 - != Crc::::new(&crc::CRC_32_ISO_HDLC).checksum(amount.commitment.point.as_bytes()) - { + if amount.commitment_crc32() != self.amount_commitment_data_crc32 { return Err(FogTxOutError::ChecksumMismatch); } diff --git a/fog/view/enclave/trusted/Cargo.lock b/fog/view/enclave/trusted/Cargo.lock index 0a7a61acdf..6294ec1d97 100644 --- a/fog/view/enclave/trusted/Cargo.lock +++ b/fog/view/enclave/trusted/Cargo.lock @@ -1290,6 +1290,7 @@ version = "1.3.0-pre0" dependencies = [ "aes", "bulletproofs-og", + "crc", "curve25519-dalek", "displaydoc", "generic-array", diff --git a/fog/view/protocol/src/user_private.rs b/fog/view/protocol/src/user_private.rs index 159e580586..95dd4b5743 100644 --- a/fog/view/protocol/src/user_private.rs +++ b/fog/view/protocol/src/user_private.rs @@ -111,7 +111,7 @@ mod testing { use mc_crypto_box::{CryptoBox, VersionedCryptoBox}; use mc_crypto_keys::CompressedRistrettoPublic; use mc_fog_types::view::{FogTxOut, FogTxOutMetadata}; - use mc_transaction_core::{fog_hint::FogHint, tx::TxOut}; + use mc_transaction_core::{fog_hint::FogHint, tokens::Mob, tx::TxOut, Token}; pub use rand_core::{CryptoRng, RngCore, SeedableRng}; use rand_hc::Hc128Rng; @@ -145,8 +145,10 @@ mod testing { // Arbitrary TxOut let tx_private_key = RistrettoPrivate::from_random(&mut rng); + let token_id = Mob::ID; let txo = TxOut::new( 10, + token_id, &recipient.default_subaddress(), &tx_private_key, hint.encrypt(&ingest_public, &mut rng), diff --git a/ledger/db/src/lib.rs b/ledger/db/src/lib.rs index 8472c23fb9..a3299cb2b8 100644 --- a/ledger/db/src/lib.rs +++ b/ledger/db/src/lib.rs @@ -745,7 +745,8 @@ mod ledger_db_test { use mc_account_keys::AccountKey; use mc_crypto_keys::RistrettoPrivate; use mc_transaction_core::{ - compute_block_id, membership_proofs::compute_implied_merkle_root, BlockVersion, + compute_block_id, membership_proofs::compute_implied_merkle_root, tokens::Mob, + BlockVersion, Token, }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -791,6 +792,7 @@ mod ledger_db_test { .map(|_i| { let mut result = TxOut::new( initial_amount, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -849,6 +851,7 @@ mod ledger_db_test { let mut output = TxOut::new( 1000, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -907,6 +910,7 @@ mod ledger_db_test { .map(|_i| { TxOut::new( 1000, + Mob::ID, &recipient_account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -988,6 +992,7 @@ mod ledger_db_test { .map(|_i| { TxOut::new( 1000, + Mob::ID, &recipient_account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1150,6 +1155,7 @@ mod ledger_db_test { let tx_out = TxOut::new( 10, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1195,6 +1201,7 @@ mod ledger_db_test { let tx_out = TxOut::new( 10, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1312,6 +1319,7 @@ mod ledger_db_test { .map(|_i| { TxOut::new( 1000, + Mob::ID, &recipient_account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1356,6 +1364,7 @@ mod ledger_db_test { .map(|_i| { TxOut::new( 1000, + Mob::ID, &recipient_account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1413,6 +1422,7 @@ mod ledger_db_test { let tx_out = TxOut::new( 100, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1470,6 +1480,7 @@ mod ledger_db_test { let block_one_contents = { let tx_out = TxOut::new( 10, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1494,6 +1505,7 @@ mod ledger_db_test { let block_two_contents = { let tx_out = TxOut::new( 33, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1538,6 +1550,7 @@ mod ledger_db_test { let block_one_contents = { let mut tx_out = TxOut::new( 33, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -1599,6 +1612,7 @@ mod ledger_db_test { { let tx_out = TxOut::new( 100, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), diff --git a/ledger/db/src/test_utils/mock_ledger.rs b/ledger/db/src/test_utils/mock_ledger.rs index 98fb954ded..d37f07be87 100644 --- a/ledger/db/src/test_utils/mock_ledger.rs +++ b/ledger/db/src/test_utils/mock_ledger.rs @@ -6,8 +6,9 @@ use mc_common::{HashMap, HashSet}; use mc_crypto_keys::{CompressedRistrettoPublic, RistrettoPrivate}; use mc_transaction_core::{ ring_signature::KeyImage, + tokens::Mob, tx::{TxOut, TxOutMembershipElement, TxOutMembershipProof}, - Block, BlockContents, BlockData, BlockID, BlockSignature, BlockVersion, + Block, BlockContents, BlockData, BlockID, BlockSignature, BlockVersion, Token, }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -221,6 +222,7 @@ pub fn get_test_ledger_blocks(n_blocks: usize) -> Vec<(Block, BlockContents)> { // Create the origin block. let mut tx_out = TxOut::new( value, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), @@ -238,6 +240,7 @@ pub fn get_test_ledger_blocks(n_blocks: usize) -> Vec<(Block, BlockContents)> { // Create a normal block. let tx_out = TxOut::new( 16, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), Default::default(), diff --git a/ledger/db/src/tx_out_store.rs b/ledger/db/src/tx_out_store.rs index 634e45cb06..9531298f8d 100644 --- a/ledger/db/src/tx_out_store.rs +++ b/ledger/db/src/tx_out_store.rs @@ -788,8 +788,9 @@ pub mod tx_out_store_tests { encrypted_fog_hint::{EncryptedFogHint, ENCRYPTED_FOG_HINT_LEN}, membership_proofs::{hash_leaf, hash_nodes, Range, NIL_HASH}, onetime_keys::*, + tokens::Mob, tx::TxOut, - Amount, MemoPayload, + Amount, AmountData, MemoPayload, Token, }; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -822,6 +823,7 @@ pub mod tx_out_store_tests { let mut tx_outs: Vec = Vec::new(); let recipient_account = AccountKey::random(&mut rng); let value: u64 = 100; + let token_id = Mob::ID; for _i in 0..num_tx_outs { let tx_private_key = RistrettoPrivate::from_random(&mut rng); @@ -832,7 +834,8 @@ pub mod tx_out_store_tests { recipient_account.default_subaddress().spend_public_key(), ); let shared_secret: RistrettoPublic = create_shared_secret(&target_key, &tx_private_key); - let amount = Amount::new(value, &shared_secret).unwrap(); + let amount_data = AmountData { value, token_id }; + let amount = Amount::new(amount_data, &shared_secret).unwrap(); let tx_out = TxOut { amount, target_key: target_key.into(), diff --git a/libmobilecoin/libmobilecoin_cbindgen.h b/libmobilecoin/libmobilecoin_cbindgen.h index a3988ab9f6..0caa104e09 100644 --- a/libmobilecoin/libmobilecoin_cbindgen.h +++ b/libmobilecoin/libmobilecoin_cbindgen.h @@ -625,7 +625,7 @@ bool mc_tx_out_commitment_crc32(FfiRefPtr tx_out_commitment, * * * `view_private_key` - must be a valid 32-byte Ristretto-format scalar. */ -bool mc_tx_out_matches_any_subaddress(FfiRefPtr tx_out_amount, +bool mc_tx_out_matches_any_subaddress(FfiRefPtr _tx_out_amount, FfiRefPtr tx_out_public_key, FfiRefPtr view_private_key, FfiMutPtr out_matches); diff --git a/libmobilecoin/src/transaction.rs b/libmobilecoin/src/transaction.rs index 1bbbf99d6a..71d4c35d39 100644 --- a/libmobilecoin/src/transaction.rs +++ b/libmobilecoin/src/transaction.rs @@ -7,11 +7,12 @@ use mc_account_keys::PublicAddress; use mc_crypto_keys::{ReprBytes, RistrettoPrivate, RistrettoPublic}; use mc_fog_report_validation::FogResolver; use mc_transaction_core::{ - get_tx_out_shared_secret, get_value_mask, + get_tx_out_shared_secret, onetime_keys::{recover_onetime_private_key, recover_public_subaddress_spend_key}, ring_signature::KeyImage, + tokens::Mob, tx::{TxOut, TxOutConfirmationNumber, TxOutMembershipProof}, - Amount, BlockVersion, CompressedCommitment, + Amount, BlockVersion, CompressedCommitment, Token, }; use mc_transaction_std::{InputCredentials, RTHMemoBuilder, TransactionBuilder}; use mc_util_ffi::*; @@ -41,9 +42,10 @@ pub extern "C" fn mc_tx_out_reconstruct_commitment( let tx_out_public_key = RistrettoPublic::try_from_ffi(&tx_out_public_key)?; let shared_secret = get_tx_out_shared_secret(&view_private_key, &tx_out_public_key); - let value = (tx_out_amount.masked_value as u64) ^ get_value_mask(&shared_secret); - let amount: Amount = Amount::new(value, &shared_secret)?; + // FIXME #1596: McTxOutAmount should include the masked_token_id bytes, which + // are 0 or 4 bytes For now zero to avoid breaking changes to FFI + let (amount, _) = Amount::reconstruct(tx_out_amount.masked_value, &[], &shared_secret)?; let out_tx_out_commitment = out_tx_out_commitment .into_mut() @@ -81,22 +83,22 @@ pub extern "C" fn mc_tx_out_commitment_crc32( /// * `view_private_key` - must be a valid 32-byte Ristretto-format scalar. #[no_mangle] pub extern "C" fn mc_tx_out_matches_any_subaddress( - tx_out_amount: FfiRefPtr, + _tx_out_amount: FfiRefPtr, tx_out_public_key: FfiRefPtr, view_private_key: FfiRefPtr, out_matches: FfiMutPtr, ) -> bool { ffi_boundary(|| { - let view_private_key = RistrettoPrivate::try_from_ffi(&view_private_key) + let _view_private_key = RistrettoPrivate::try_from_ffi(&view_private_key) .expect("view_private_key is not a valid RistrettoPrivate"); let mut matches = false; - if let Ok(public_key) = RistrettoPublic::try_from_ffi(&tx_out_public_key) { - let shared_secret = get_tx_out_shared_secret(&view_private_key, &public_key); - let value = (tx_out_amount.masked_value as u64) ^ get_value_mask(&shared_secret); - let amount: Amount = - Amount::new(value, &shared_secret).expect("could not create amount object"); - matches = amount.get_value(&shared_secret).is_ok() + if let Ok(_public_key) = RistrettoPublic::try_from_ffi(&tx_out_public_key) { + // FIXME #1596: This function doesn't make sense unless we have access to the + // amount.commitment from the TxOut, or the commitment_crc32 from the fog tx + // out, so that we have some way to check if we recovered the + // correct commitment. + matches = true; } *out_matches.into_mut() = matches; }) @@ -192,11 +194,11 @@ pub extern "C" fn mc_tx_out_get_value( let view_private_key = RistrettoPrivate::try_from_ffi(&view_private_key)?; let shared_secret = get_tx_out_shared_secret(&view_private_key, &tx_out_public_key); - let value = (tx_out_amount.masked_value as u64) ^ get_value_mask(&shared_secret); - let amount: Amount = Amount::new(value, &shared_secret)?; - let (val, _blinding) = amount.get_value(&shared_secret)?; + let (_amount, amount_data) = + Amount::reconstruct(tx_out_amount.masked_value, &[], &shared_secret)?; - *out_value.into_mut() = val; + // FIXME #1596: This should also return the amount_data.token_id + *out_value.into_mut() = amount_data.value; Ok(()) }) } @@ -345,6 +347,10 @@ pub extern "C" fn mc_transaction_builder_create( // 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; + + // TODO #1596: Support token id other than Mob + let token_id = Mob::ID; + // 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. @@ -354,7 +360,7 @@ pub extern "C" fn mc_transaction_builder_create( // from(source_account_key)); memo_builder.enable_destination_memo(); let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver, memo_builder); + TransactionBuilder::new(block_version, token_id, 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 67aa6376f9..8703cf50df 100644 --- a/mobilecoind-json/src/data_types.rs +++ b/mobilecoind-json/src/data_types.rs @@ -497,6 +497,7 @@ impl TryFrom<&JsonOutlay> for mc_mobilecoind_api::Outlay { pub struct JsonAmount { pub commitment: String, pub masked_value: String, + pub masked_token_id: String, } impl From<&Amount> for JsonAmount { @@ -504,6 +505,7 @@ impl From<&Amount> for JsonAmount { Self { commitment: hex::encode(src.get_commitment().get_data()), masked_value: src.get_masked_value().to_string(), + masked_token_id: hex::encode(src.get_masked_token_id()), } } } @@ -547,6 +549,10 @@ impl TryFrom<&JsonTxOut> for mc_api::external::TxOut { .parse::() .map_err(|err| format!("Failed to parse u64 from value: {}", err))?, ); + amount.set_masked_token_id( + hex::decode(&src.amount.masked_token_id) + .map_err(|err| format!("Failed to decode masked token id hex: {}", err))?, + ); let mut target_key = CompressedRistretto::new(); target_key.set_data( hex::decode(&src.target_key) @@ -835,7 +841,7 @@ impl From<&RingMLSAG> for JsonRingMLSAG { pub struct JsonSignatureRctBulletproofs { pub ring_signatures: Vec, pub pseudo_output_commitments: Vec, - range_proofs: String, + pub range_proofs: String, } impl From<&SignatureRctBulletproofs> for JsonSignatureRctBulletproofs { @@ -1300,7 +1306,10 @@ 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, BlockVersion}; + use mc_transaction_core::{ + encrypted_fog_hint::ENCRYPTED_FOG_HINT_LEN, tokens::Mob, Amount, AmountData, BlockVersion, + Token, + }; use mc_transaction_core_test_utils::{ create_ledger, create_transaction, initialize_ledger, AccountKey, }; @@ -1335,8 +1344,12 @@ mod test { }; let utxo = { + let amount_data = AmountData { + value: 1u64 << 13, + token_id: Mob::ID, + }; let tx_out = mc_transaction_core::tx::TxOut { - amount: Amount::new(1u64 << 13, &RistrettoPublic::from_random(&mut rng)).unwrap(), + amount: Amount::new(amount_data, &RistrettoPublic::from_random(&mut rng)).unwrap(), target_key: RistrettoPublic::from_random(&mut rng).into(), public_key: RistrettoPublic::from_random(&mut rng).into(), e_fog_hint: (&[0u8; ENCRYPTED_FOG_HINT_LEN]).into(), diff --git a/mobilecoind/api/proto/mobilecoind_api.proto b/mobilecoind/api/proto/mobilecoind_api.proto index 4a98335d68..de2dda56d7 100644 --- a/mobilecoind/api/proto/mobilecoind_api.proto +++ b/mobilecoind/api/proto/mobilecoind_api.proto @@ -126,6 +126,9 @@ message UnspentTxOut { // The tombstone block used when we attempted to spend the UTXO. uint64 attempted_spend_tombstone = 6; + // The token id of the TxOut + uint32 token_id = 7; + // The monitor id this UnspentTxOut belongs to. // Note that this field is not included in the Rust `utxo_store::UnspentTxOut` struct. bytes monitor_id = 10; diff --git a/mobilecoind/src/bin/main.rs b/mobilecoind/src/bin/main.rs index 0c585c29af..a554abdc5d 100644 --- a/mobilecoind/src/bin/main.rs +++ b/mobilecoind/src/bin/main.rs @@ -119,6 +119,7 @@ fn main() { ledger_db.clone(), mobilecoind_db.clone(), peer_manager, + config.token_id, config.get_fog_resolver_factory(logger.clone()), logger.clone(), ); @@ -131,6 +132,7 @@ fn main() { network_state, listen_uri, config.num_workers, + config.token_id, logger, ); diff --git a/mobilecoind/src/config.rs b/mobilecoind/src/config.rs index aa6d51cd53..28f032af54 100644 --- a/mobilecoind/src/config.rs +++ b/mobilecoind/src/config.rs @@ -11,6 +11,7 @@ use mc_fog_report_connection::GrpcFogReportConnection; use mc_fog_report_validation::FogResolver; use mc_mobilecoind_api::MobilecoindUri; use mc_sgx_css::Signature; +use mc_transaction_core::TokenId; use mc_util_parse::{load_css_file, parse_duration_in_seconds}; use mc_util_uri::{ConnectionUri, ConsensusClientUri, FogUri}; #[cfg(feature = "ip-check")] @@ -85,6 +86,10 @@ pub struct Config { /// Automatically migrate the ledger db into the most recent version. #[structopt(long)] pub ledger_db_migrate: bool, + + /// Token id + #[structopt(long, env = "MC_TOKEN_ID", default_value = "0")] + pub token_id: TokenId, } fn parse_quorum_set_from_json(src: &str) -> Result, String> { diff --git a/mobilecoind/src/conversions.rs b/mobilecoind/src/conversions.rs index b91206941d..e3571a3d1b 100644 --- a/mobilecoind/src/conversions.rs +++ b/mobilecoind/src/conversions.rs @@ -28,6 +28,7 @@ impl From<&UnspentTxOut> for mc_mobilecoind_api::UnspentTxOut { dst.set_value(src.value); dst.set_attempted_spend_height(src.attempted_spend_height); dst.set_attempted_spend_tombstone(src.attempted_spend_tombstone); + dst.set_token_id(src.token_id); dst } @@ -43,6 +44,7 @@ impl TryFrom<&mc_mobilecoind_api::UnspentTxOut> for UnspentTxOut { let value = src.value; let attempted_spend_height = src.attempted_spend_height; let attempted_spend_tombstone = src.attempted_spend_tombstone; + let token_id = src.token_id; Ok(Self { tx_out, @@ -51,6 +53,7 @@ impl TryFrom<&mc_mobilecoind_api::UnspentTxOut> for UnspentTxOut { value, attempted_spend_height, attempted_spend_tombstone, + token_id, }) } } @@ -173,7 +176,9 @@ 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, tokens::Mob, Amount, AmountData, Token, + }; use mc_transaction_core_test_utils::{ create_ledger, create_transaction, initialize_ledger, AccountKey, BlockVersion, }; @@ -186,8 +191,12 @@ mod test { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); // Rust -> Proto + let amount_data = AmountData { + value: 1u64 << 13, + token_id: Mob::ID, + }; let tx_out = TxOut { - amount: Amount::new(1u64 << 13, &RistrettoPublic::from_random(&mut rng)).unwrap(), + amount: Amount::new(amount_data, &RistrettoPublic::from_random(&mut rng)).unwrap(), target_key: RistrettoPublic::from_random(&mut rng).into(), public_key: RistrettoPublic::from_random(&mut rng).into(), e_fog_hint: (&[0u8; ENCRYPTED_FOG_HINT_LEN]).into(), @@ -207,6 +216,7 @@ mod test { value, attempted_spend_height, attempted_spend_tombstone, + token_id: *Mob::ID, }; let proto = mc_mobilecoind_api::UnspentTxOut::from(&rust); @@ -272,8 +282,12 @@ mod test { }; let utxo = { + let amount_data = AmountData { + value: 1u64 << 13, + token_id: Mob::ID, + }; let tx_out = TxOut { - amount: Amount::new(1u64 << 13, &RistrettoPublic::from_random(&mut rng)).unwrap(), + amount: Amount::new(amount_data, &RistrettoPublic::from_random(&mut rng)).unwrap(), target_key: RistrettoPublic::from_random(&mut rng).into(), public_key: RistrettoPublic::from_random(&mut rng).into(), e_fog_hint: (&[0u8; ENCRYPTED_FOG_HINT_LEN]).into(), @@ -293,6 +307,7 @@ mod test { value, attempted_spend_height, attempted_spend_tombstone, + token_id: *Mob::ID, } }; diff --git a/mobilecoind/src/payments.rs b/mobilecoind/src/payments.rs index b812051814..37f41c67f7 100644 --- a/mobilecoind/src/payments.rs +++ b/mobilecoind/src/payments.rs @@ -20,9 +20,8 @@ use mc_transaction_core::{ constants::{MAX_INPUTS, MILLIMOB_TO_PICOMOB, RING_SIZE}, onetime_keys::recover_onetime_private_key, ring_signature::KeyImage, - tokens::Mob, tx::{Tx, TxOut, TxOutConfirmationNumber, TxOutMembershipProof}, - BlockIndex, BlockVersion, Token, + BlockIndex, BlockVersion, TokenId, }; use mc_transaction_std::{ ChangeDestination, EmptyMemoBuilder, InputCredentials, TransactionBuilder, @@ -103,17 +102,20 @@ pub struct TransactionsManager< /// Peer manager, for communicating with validator nodes. peer_manager: ConnectionManager, - /// Logger. - logger: Logger, - /// Monotonically increasing counter. This is used for node round-robin /// selection. submit_node_offset: Arc, + /// Token id which we will transact in + token_id: TokenId, + /// Fog resolver maker, used when constructing outputs to fog recipients. /// This is abstracted because in tests, we don't want to form grpc /// connections to fog fog_resolver_factory: Arc Result + Send + Sync>, + + /// Logger. + logger: Logger, } impl Clone @@ -124,15 +126,17 @@ impl( peer_manager: &ConnectionManager, + token_id: TokenId, opt_fee: u64, ) -> u64 { if opt_fee > 0 { @@ -147,7 +151,7 @@ fn get_fee( .conns() .par_iter() .filter_map(|conn| conn.fetch_block_info(empty()).ok()) - .filter_map(|block_info| block_info.minimum_fee_or_none(&Mob::ID)) + .filter_map(|block_info| block_info.minimum_fee_or_none(&token_id)) .max() .unwrap_or(FALLBACK_FEE) } @@ -160,6 +164,7 @@ impl, + token_id: TokenId, fog_resolver_factory: Arc Result + Send + Sync>, logger: Logger, ) -> Self { @@ -168,16 +173,18 @@ impl receiver.to_string())); log::trace!(logger, "Generating txo list transaction..."); - let fee = get_fee(&self.peer_manager, fee); + let fee = get_fee(&self.peer_manager, self.token_id, fee); - // All inputs are to be spent - let total_value: u64 = inputs.iter().map(|utxo| utxo.value).sum(); + // All inputs are to be spent, except those with wrong token id + let total_value: u64 = inputs + .iter() + .filter(|utxo| utxo.token_id == self.token_id) + .map(|utxo| utxo.value) + .sum(); if total_value < fee { return Err(Error::InsufficientFunds); @@ -438,7 +456,11 @@ impl = { - let tx_outs: Vec = inputs.iter().map(|utxo| utxo.tx_out.clone()).collect(); + let tx_outs: Vec = inputs + .iter() + .filter(|utxo| utxo.token_id == self.token_id) + .map(|utxo| utxo.tx_out.clone()) + .collect(); let proofs = self.get_membership_proofs(&tx_outs)?; inputs.iter().cloned().zip(proofs.into_iter()).collect() }; @@ -450,7 +472,7 @@ impl Result, Error> { // Sort the utxos in descending order by value. - let mut sorted_utxos = utxos.to_vec(); + let mut sorted_utxos: Vec = utxos + .iter() + .filter(|utxo| utxo.token_id == token_id) + .cloned() + .collect(); sorted_utxos.sort_by_key(|utxo| Reverse(utxo.value)); // The maximum spendable is limited by the maximal number of inputs we can use. @@ -592,6 +620,7 @@ impl Result, Error> { if max_inputs < 2 { @@ -604,6 +633,7 @@ impl = inputs .iter() .filter(|utxo| num_blocks_in_ledger >= utxo.attempted_spend_tombstone) + .filter(|utxo| token_id == utxo.token_id) .collect(); // No point in merging if we are able to spend all inputs at once. @@ -731,6 +761,8 @@ impl>, block_version: BlockVersion, + token_id: TokenId, fee: u64, from_account_key: &AccountKey, change_subaddress: u64, @@ -784,8 +817,12 @@ impl, MockFogPubkeyResolver, - >::select_utxos_for_value(&utxos, 300, utxos.len()) + >::select_utxos_for_value(Mob::ID, &utxos, 300, utxos.len()) .unwrap(); assert_eq!(selected_utxos, vec![utxos[0].clone(), utxos[1].clone()]); @@ -1024,7 +1065,7 @@ mod test { let selected_utxos = TransactionsManager::< ThickClient, MockFogPubkeyResolver, - >::select_utxos_for_value(&utxos, 301, utxos.len()) + >::select_utxos_for_value(Mob::ID, &utxos, 301, utxos.len()) .unwrap(); assert_eq!( @@ -1036,7 +1077,7 @@ mod test { let selected_utxos = TransactionsManager::< ThickClient, MockFogPubkeyResolver, - >::select_utxos_for_value(&utxos, 301, 2) + >::select_utxos_for_value(Mob::ID, &utxos, 301, 2) .unwrap(); assert_eq!(selected_utxos, vec![utxos[1].clone(), utxos[2].clone()]); @@ -1048,7 +1089,7 @@ mod test { // While we have enough utxos to sum to 5, if the input limit is 4 we should // fail. match TransactionsManager::, MockFogPubkeyResolver>::select_utxos_for_value( - &utxos, 5, 4, + Mob::ID, &utxos, 5, 4, ) { Err(Error::InsufficientFundsFragmentedUtxos) => { // Expected. @@ -1063,7 +1104,7 @@ mod test { // While we have enough utxos to sum to 5, if the input limit is 4 we should // fail. match TransactionsManager::, MockFogPubkeyResolver>::select_utxos_for_value( - &utxos, 50, 100, + Mob::ID, &utxos, 50, 100, ) { Err(Error::InsufficientFunds) => { // Expected. @@ -1085,12 +1126,13 @@ mod test { utxos[4].value = 2000 * MILLIMOB_TO_PICOMOB; utxos[5].value = 1000 * MILLIMOB_TO_PICOMOB; - let selected_utxos = - TransactionsManager::< - ThickClient, - MockFogPubkeyResolver, - >::select_utxos_for_optimization(1000, &utxos, 2, Mob::MINIMUM_FEE) - .unwrap(); + let selected_utxos = TransactionsManager::< + ThickClient, + MockFogPubkeyResolver, + >::select_utxos_for_optimization( + 1000, &utxos, 2, Mob::ID, Mob::MINIMUM_FEE + ) + .unwrap(); assert_eq!(selected_utxos, vec![utxos[0].clone(), utxos[4].clone()]); } @@ -1106,12 +1148,13 @@ mod test { utxos[4].value = 2000 * MILLIMOB_TO_PICOMOB; utxos[5].value = 1000 * MILLIMOB_TO_PICOMOB; - let selected_utxos = - TransactionsManager::< - ThickClient, - MockFogPubkeyResolver, - >::select_utxos_for_optimization(1000, &utxos, 3, Mob::MINIMUM_FEE) - .unwrap(); + let selected_utxos = TransactionsManager::< + ThickClient, + MockFogPubkeyResolver, + >::select_utxos_for_optimization( + 1000, &utxos, 3, Mob::ID, Mob::MINIMUM_FEE + ) + .unwrap(); assert_eq!( selected_utxos, @@ -1144,7 +1187,7 @@ mod test { ThickClient, MockFogPubkeyResolver, >::select_utxos_for_optimization( - 1000, &utxos, 100, Mob::MINIMUM_FEE + 1000, &utxos, 100, Mob::ID, Mob::MINIMUM_FEE ); assert!(result.is_err()); } @@ -1161,7 +1204,7 @@ mod test { ThickClient, MockFogPubkeyResolver, >::select_utxos_for_optimization( - 1000, &utxos, 100, Mob::MINIMUM_FEE + 1000, &utxos, 100, Mob::ID, Mob::MINIMUM_FEE ); assert!(result.is_err()); } @@ -1175,12 +1218,13 @@ mod test { utxos[2].value = Mob::MINIMUM_FEE / 10; utxos[3].value = Mob::MINIMUM_FEE / 5; - let selected_utxos = - TransactionsManager::< - ThickClient, - MockFogPubkeyResolver, - >::select_utxos_for_optimization(1000, &utxos, 3, Mob::MINIMUM_FEE) - .unwrap(); + let selected_utxos = TransactionsManager::< + ThickClient, + MockFogPubkeyResolver, + >::select_utxos_for_optimization( + 1000, &utxos, 3, Mob::ID, Mob::MINIMUM_FEE + ) + .unwrap(); // Since we're limited to 3 inputs, the lowest input (of value 1) is going to // get excluded. assert_eq!( @@ -1201,14 +1245,16 @@ mod test { let result = TransactionsManager::< ThickClient, MockFogPubkeyResolver, - >::select_utxos_for_optimization(1000, &[], 100, Mob::MINIMUM_FEE); + >::select_utxos_for_optimization( + 1000, &[], 100, Mob::ID, Mob::MINIMUM_FEE + ); assert!(result.is_err()); let result = TransactionsManager::< ThickClient, MockFogPubkeyResolver, >::select_utxos_for_optimization( - 1000, &utxos[0..1], 100, Mob::MINIMUM_FEE + 1000, &utxos[0..1], 100, Mob::ID, Mob::MINIMUM_FEE ); assert!(result.is_err()); @@ -1218,7 +1264,7 @@ mod test { ThickClient, MockFogPubkeyResolver, >::select_utxos_for_optimization( - 1000, &utxos[0..2], 2, Mob::MINIMUM_FEE + 1000, &utxos[0..2], 2, Mob::ID, Mob::MINIMUM_FEE ); assert!(result.is_ok()); @@ -1226,7 +1272,7 @@ mod test { ThickClient, MockFogPubkeyResolver, >::select_utxos_for_optimization( - 1000, &utxos[0..2], 3, Mob::MINIMUM_FEE + 1000, &utxos[0..2], 3, Mob::ID, Mob::MINIMUM_FEE ); assert!(result.is_err()); } diff --git a/mobilecoind/src/processed_block_store.rs b/mobilecoind/src/processed_block_store.rs index 6cad964f4f..dc26b5c5d5 100644 --- a/mobilecoind/src/processed_block_store.rs +++ b/mobilecoind/src/processed_block_store.rs @@ -255,7 +255,9 @@ mod test { use mc_crypto_keys::RistrettoPublic; use mc_crypto_rand::{CryptoRng, RngCore}; use mc_ledger_db::{Ledger, LedgerDB}; - use mc_transaction_core::{onetime_keys::recover_onetime_private_key, tx::TxOut}; + use mc_transaction_core::{ + onetime_keys::recover_onetime_private_key, tokens::Mob, tx::TxOut, Token, + }; use rand::{rngs::StdRng, SeedableRng}; use std::iter::FromIterator; use tempdir::TempDir; @@ -311,6 +313,7 @@ mod test { value: DEFAULT_PER_RECIPIENT_AMOUNT, attempted_spend_height: 0, attempted_spend_tombstone: 0, + token_id: *Mob::ID, } }) .collect(); diff --git a/mobilecoind/src/service.rs b/mobilecoind/src/service.rs index cd21a2b992..f3a03853f1 100644 --- a/mobilecoind/src/service.rs +++ b/mobilecoind/src/service.rs @@ -36,6 +36,7 @@ use mc_transaction_core::{ onetime_keys::recover_onetime_private_key, ring_signature::KeyImage, tx::{TxOut, TxOutConfirmationNumber, TxOutMembershipProof}, + TokenId, }; use mc_util_from_random::FromRandom; use mc_util_grpc::{ @@ -69,6 +70,7 @@ impl Service { network_state: Arc>>, listen_uri: &MobilecoindUri, num_workers: Option, + token_id: TokenId, logger: Logger, ) -> Self { let sync_thread = if mobilecoind_db.is_db_encrypted() { @@ -109,6 +111,7 @@ impl Service { watcher_db, network_state, start_sync_thread, + token_id, logger.clone(), ); @@ -166,6 +169,7 @@ pub struct ServiceApi< watcher_db: Option, network_state: Arc>>, start_sync_thread: Arc, + token_id: TokenId, logger: Logger, } @@ -180,6 +184,7 @@ impl, network_state: Arc>>, start_sync_thread: Arc, + token_id: TokenId, logger: Logger, ) -> Self { Self { @@ -204,6 +210,7 @@ impl = utxos + .into_iter() + .filter(|utxo| utxo.token_id == self.token_id) + .collect(); + // Convert to protos. let proto_utxos: Vec = utxos.iter().map(|utxo| utxo.into()).collect(); @@ -559,7 +574,7 @@ impl continue, @@ -4949,6 +4972,7 @@ mod test { let mut transaction_builder = TransactionBuilder::new( BLOCK_VERSION, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -5065,6 +5089,7 @@ mod test { let mut transaction_builder = TransactionBuilder::new( BLOCK_VERSION, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); diff --git a/mobilecoind/src/sync.rs b/mobilecoind/src/sync.rs index e488094e33..8d1aa52126 100644 --- a/mobilecoind/src/sync.rs +++ b/mobilecoind/src/sync.rs @@ -384,7 +384,7 @@ fn match_tx_outs_into_utxos( let shared_secret = get_tx_out_shared_secret(account_key.view_private_key(), &tx_public_key); - let (value, _blinding) = tx_out + let (amount, _blinding) = tx_out .amount .get_value(&shared_secret) .expect("Malformed amount"); // TODO @@ -401,9 +401,10 @@ fn match_tx_outs_into_utxos( tx_out: tx_out.clone(), subaddress_index: subaddress_id.index, key_image, - value, + value: amount.value, attempted_spend_height: 0, attempted_spend_tombstone: 0, + token_id: *amount.token_id, }); } diff --git a/mobilecoind/src/test_utils.rs b/mobilecoind/src/test_utils.rs index 63fe5c2ccd..89e468e733 100644 --- a/mobilecoind/src/test_utils.rs +++ b/mobilecoind/src/test_utils.rs @@ -23,7 +23,9 @@ use mc_fog_report_validation_test_utils::{FogPubkeyResolver, MockFogResolver}; use mc_ledger_db::{Ledger, LedgerDB}; use mc_ledger_sync::PollingNetworkState; use mc_mobilecoind_api::{mobilecoind_api_grpc::MobilecoindApiClient, MobilecoindUri}; -use mc_transaction_core::{ring_signature::KeyImage, tx::TxOut, Block, BlockContents}; +use mc_transaction_core::{ + ring_signature::KeyImage, tokens::Mob, tx::TxOut, Block, BlockContents, Token, +}; use mc_util_from_random::FromRandom; use mc_util_grpc::ConnectionUriGrpcioChannel; use mc_util_uri::{ConnectionUri, FogUri}; @@ -167,6 +169,8 @@ pub fn add_block_to_ledger_db( let mut result = TxOut::new( // TODO: allow for subaddress index! output_value, + // TODO: allow for other token id + Mob::ID, recipient, &RistrettoPrivate::from_random(rng), Default::default(), @@ -277,6 +281,7 @@ pub fn setup_server( ledger_db.clone(), mobilecoind_db.clone(), conn_manager.clone(), + Mob::ID, fog_resolver_factory.unwrap_or(Arc::new(|_| Ok(FPR::default()))), logger.clone(), ); @@ -289,6 +294,7 @@ pub fn setup_server( network_state, uri, None, + Mob::ID, logger, ); diff --git a/mobilecoind/src/utxo_store.rs b/mobilecoind/src/utxo_store.rs index 7b5e396a88..87881cb745 100644 --- a/mobilecoind/src/utxo_store.rs +++ b/mobilecoind/src/utxo_store.rs @@ -56,6 +56,10 @@ pub struct UnspentTxOut { /// The tombstone block used when we attempted to spend the UTXO. #[prost(uint64, tag = "6")] pub attempted_spend_tombstone: u64, + + /// The token id of this TxOut + #[prost(uint32, tag = "7")] + pub token_id: u32, } /// Type used as the key in the utxo_id_to_utxo database. @@ -426,6 +430,7 @@ mod test { }; use mc_crypto_rand::{CryptoRng, RngCore}; use mc_ledger_db::{Ledger, LedgerDB}; + use mc_transaction_core::{tokens::Mob, Token}; use rand::{rngs::StdRng, SeedableRng}; use std::iter::FromIterator; use tempdir::TempDir; @@ -449,6 +454,7 @@ mod test { value: idx, attempted_spend_height: 0, attempted_spend_tombstone: 0, + token_id: *Mob::ID, } }) .collect(); diff --git a/slam/src/main.rs b/slam/src/main.rs index 7df72170e6..a49f1513b4 100755 --- a/slam/src/main.rs +++ b/slam/src/main.rs @@ -25,7 +25,7 @@ use mc_transaction_core::{ ring_signature::KeyImage, tokens::Mob, tx::{Tx, TxOut, TxOutMembershipProof}, - BlockVersion, Token, + AmountData, BlockVersion, Token, }; use mc_transaction_std::{EmptyMemoBuilder, InputCredentials, TransactionBuilder}; use mc_util_uri::ConnectionUri; @@ -84,7 +84,7 @@ lazy_static! { #[derive(Clone, Debug, Eq, PartialEq)] pub struct SpendableTxOut { pub tx_out: TxOut, - pub amount: u64, + pub amount: AmountData, from_account_key: AccountKey, } @@ -195,24 +195,24 @@ fn main() { let public_key = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); let shared_secret = get_tx_out_shared_secret(account.view_private_key(), &public_key); - let (input_amount, _blinding_factor) = tx_out + let (amount, _blinding_factor) = tx_out .amount .get_value(&shared_secret) .expect("Malformed amount"); log::trace!( logger, - "(account = {:?}) and (tx_index {:?}) = {}", + "(account = {:?}) and (tx_index {:?}) = {:?}", account_index, index, - input_amount, + amount, ); // Push to queue spendable_txouts_sender .send(SpendableTxOut { tx_out: tx_out.clone(), - amount: input_amount, + amount, from_account_key: account.clone(), }) .expect("failed sending to spendable_txouts_sender"); @@ -327,22 +327,18 @@ fn main() { let public_key = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); let shared_secret = get_tx_out_shared_secret(account.view_private_key(), &public_key); - let (input_amount, _blinding_factor) = tx_out + let (amount, _blinding_factor) = tx_out .amount .get_value(&shared_secret) .expect("Malformed amount"); - log::trace!( - logger, - "amount of {} is {}", - tx_out.public_key, - input_amount - ); + + log::trace!(logger, "amount of {} is {:?}", tx_out.public_key, amount); // Push to queue spendable_txouts_sender .send(SpendableTxOut { tx_out: tx_out.clone(), - amount: input_amount, + amount, from_account_key: account.clone(), }) .expect("failed sending to spendable_txouts_sender"); @@ -501,9 +497,13 @@ fn build_tx( let block_version = BlockVersion::try_from(BLOCK_VERSION.load(Ordering::SeqCst)) .expect("Unsupported block version"); + // Use token id for first spendable tx out + let token_id = spendable_txouts.first().unwrap().amount.token_id; + // Create tx_builder. No fog reports. let mut tx_builder = TransactionBuilder::new( block_version, + token_id, FogResolver::default(), EmptyMemoBuilder::default(), ); @@ -589,15 +589,17 @@ fn build_tx( // Add ouputs for (i, (utxo, _proof)) in utxos_with_proofs.iter().enumerate() { - let mut amount = utxo.amount; - // Use the first input to pay for the fee. - if i == 0 { - amount -= FEE.load(Ordering::SeqCst); - } + if utxo.amount.token_id == token_id { + let mut value = utxo.amount.value; + // Use the first input to pay for the fee. + if i == 0 { + value -= FEE.load(Ordering::SeqCst); + } - tx_builder - .add_output(amount, &to_account.default_subaddress(), &mut rng) - .expect("failed to add output"); + tx_builder + .add_output(value, &to_account.default_subaddress(), &mut rng) + .expect("failed to add output"); + } } // Set tombstone block. diff --git a/transaction/core/Cargo.toml b/transaction/core/Cargo.toml index 0974e78ed5..f7479d864c 100644 --- a/transaction/core/Cargo.toml +++ b/transaction/core/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" [dependencies] # External dependencies aes = { version = "0.7.5", default-features = false, features = ["ctr"] } +crc = { version = "2.1.0", default-features = false } displaydoc = { version = "0.2", default-features = false } generic-array = { version = "0.14", features = ["serde", "more_lengths"] } hex_fmt = "0.3" diff --git a/transaction/core/src/amount/commitment.rs b/transaction/core/src/amount/commitment.rs index b06b161451..2c5ae594b8 100644 --- a/transaction/core/src/amount/commitment.rs +++ b/transaction/core/src/amount/commitment.rs @@ -1,5 +1,5 @@ use crate::{ - ring_signature::{Error, Scalar, GENERATORS}, + ring_signature::{Error, PedersenGens, Scalar}, CompressedCommitment, }; use core::{convert::TryFrom, fmt}; @@ -19,9 +19,19 @@ pub struct Commitment { } impl Commitment { - pub fn new(value: u64, blinding: Scalar) -> Self { + /// Create a new commitment, given a value, blinding factor, and pedersen + /// gens to use + /// + /// Note that the choice of generator implies what the token id is for this + /// value. The Pedersen generators should be `generators(token_id)`. + /// + /// Arguments: + /// * value: The (u64) value that we are committing to + /// * blinding: The blinding factor for the Pedersen commitment + /// * generators: The generators used to make the commitment + pub fn new(value: u64, blinding: Scalar, generators: &PedersenGens) -> Self { Self { - point: GENERATORS.commit(Scalar::from(value), blinding), + point: generators.commit(Scalar::from(value), blinding), } } } @@ -66,7 +76,7 @@ derive_try_from_slice_from_repr_bytes!(Commitment); #[allow(non_snake_case)] mod commitment_tests { use crate::{ - ring_signature::{Scalar, GENERATORS}, + ring_signature::{generators, Scalar, B_BLINDING}, Commitment, }; use curve25519_dalek::ristretto::RistrettoPoint; @@ -78,12 +88,13 @@ mod commitment_tests { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); let value = rng.next_u64(); let blinding = Scalar::random(&mut rng); + let gens = generators(rng.next_u32()); - let commitment = Commitment::new(value, blinding); + let commitment = Commitment::new(value, blinding, &gens); let expected_point: RistrettoPoint = { - let H = GENERATORS.B; - let G = GENERATORS.B_blinding; + let H = gens.B; + let G = B_BLINDING; Scalar::from(value) * H + blinding * G }; diff --git a/transaction/core/src/amount/compressed_commitment.rs b/transaction/core/src/amount/compressed_commitment.rs index 8726331ddc..21caa14967 100644 --- a/transaction/core/src/amount/compressed_commitment.rs +++ b/transaction/core/src/amount/compressed_commitment.rs @@ -1,5 +1,5 @@ use crate::{ - ring_signature::{Error, Scalar, GENERATORS}, + ring_signature::{Error, PedersenGens, Scalar}, Commitment, }; use core::fmt; @@ -20,10 +20,10 @@ pub struct CompressedCommitment { } impl CompressedCommitment { - pub fn new(value: u64, blinding: Scalar) -> Self { - Self { - point: GENERATORS.commit(Scalar::from(value), blinding).compress(), - } + /// Create a new compressed commitment from value, blinding factor, and + /// pedersen generators + pub fn new(value: u64, blinding: Scalar, generator: &PedersenGens) -> Self { + Self::from(&Commitment::new(value, blinding, generator)) } } @@ -87,7 +87,7 @@ derive_try_from_slice_from_repr_bytes!(CompressedCommitment); #[allow(non_snake_case)] mod compressed_commitment_tests { use crate::{ - ring_signature::{Scalar, GENERATORS}, + ring_signature::{generators, Scalar}, CompressedCommitment, }; use curve25519_dalek::ristretto::CompressedRistretto; @@ -99,12 +99,13 @@ mod compressed_commitment_tests { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); let value = rng.next_u64(); let blinding = Scalar::random(&mut rng); + let generator = generators(0); - let commitment = CompressedCommitment::new(value, blinding); + let commitment = CompressedCommitment::new(value, blinding, &generator); let expected_point: CompressedRistretto = { - let H = GENERATORS.B; - let G = GENERATORS.B_blinding; + let H = generator.B; + let G = generator.B_blinding; let point = Scalar::from(value) * H + blinding * G; point.compress() }; diff --git a/transaction/core/src/amount/error.rs b/transaction/core/src/amount/error.rs index d4e281e1ea..fb9f9929e1 100644 --- a/transaction/core/src/amount/error.rs +++ b/transaction/core/src/amount/error.rs @@ -1,12 +1,17 @@ -//! Errors that can occur when constructing an amount. +//! Errors that can occur when handling an amount commitment. use displaydoc::Display; +/// An error which can occur when handling an amount commitment. #[derive(Debug, Display, Eq, PartialEq)] pub enum AmountError { /** - * The masked value, blinding, or shared secret are not consistent with + * The masked value, token id, or shared secret are not consistent with * the commitment. */ InconsistentCommitment, + /** + * The masked token id has an invalid number of bytes + */ + InvalidMaskedTokenId, } diff --git a/transaction/core/src/amount/mod.rs b/transaction/core/src/amount/mod.rs index 2fe7655adf..747383bf10 100644 --- a/transaction/core/src/amount/mod.rs +++ b/transaction/core/src/amount/mod.rs @@ -7,7 +7,16 @@ #![cfg_attr(test, allow(clippy::unnecessary_operation))] -use crate::domain_separators::{AMOUNT_BLINDING_DOMAIN_TAG, AMOUNT_VALUE_DOMAIN_TAG}; +use crate::{ + domain_separators::{ + AMOUNT_BLINDING_DOMAIN_TAG, AMOUNT_TOKEN_ID_DOMAIN_TAG, AMOUNT_VALUE_DOMAIN_TAG, + }, + ring_signature::generators, + token::TokenId, +}; +use alloc::vec::Vec; +use core::convert::TryInto; +use crc::Crc; use curve25519_dalek::scalar::Scalar; use mc_crypto_digestible::Digestible; use mc_crypto_hashes::{Blake2b512, Digest}; @@ -23,6 +32,15 @@ pub use commitment::Commitment; pub use compressed_commitment::CompressedCommitment; pub use error::AmountError; +/// The data blinded by a MobileCoin amount commitment +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct AmountData { + /// The "raw" value of this amount as a u64 + pub value: u64, + /// The token-id which is the denomination of this amount + pub token_id: TokenId, +} + /// A commitment to an amount of MobileCoin, denominated in picoMOB. #[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Message, Digestible)] pub struct Amount { @@ -34,6 +52,12 @@ pub struct Amount { /// `masked_value = value XOR_8 Blake2B(value_mask | shared_secret)` #[prost(fixed64, required, tag = "2")] pub masked_value: u64, + + /// `masked_token_id = token_id XOR_8 Blake2B(token_id_mask | + /// shared_secret)` 4 bytes long when used, 0 bytes for older amounts + /// that don't have this. + #[prost(bytes, tag = "3")] + pub masked_token_id: Vec, } impl Amount { @@ -41,24 +65,33 @@ impl Amount { /// secrets so that they can be recovered by the recipient. /// /// # Arguments - /// * `value` - The committed value `v`, in picoMOB. + /// * `data` - The committed value `v` and token id `i`, in picoMOB. /// * `shared_secret` - The shared secret, e.g. `rB` for transaction private /// key `r` and recipient public key `B`. #[inline] - pub fn new(value: u64, shared_secret: &RistrettoPublic) -> Result { + pub fn new(data: AmountData, shared_secret: &RistrettoPublic) -> Result { // The blinding is `Blake2B("blinding" | shared_secret)` let blinding: Scalar = get_blinding(shared_secret); - // Pedersen commitment `v*H + b*G`. - let commitment = CompressedCommitment::new(value, blinding); + // Pedersen generators + let generator = generators(*data.token_id); + + // Pedersen commitment `v*H_i + b*G`. + let commitment = CompressedCommitment::new(data.value, blinding, &generator); // The value is XORed with the first 8 bytes of the mask. - // `v XOR_8 Blake2B(value_mask | shared_secret)` - let masked_value: u64 = value ^ get_value_mask(shared_secret); + // `v XOR_8 Scalar::from_hash(Blake2B(value_mask | shared_secret))` + let masked_value: u64 = data.value ^ get_value_mask(shared_secret); + + // The token_id is XORed with the first 4 bytes of the mask. + // `v XOR_4 Blake2B(token_id_mask | shared_secret)` + let masked_token_id_val: u32 = *data.token_id ^ get_token_id_mask(shared_secret); + let masked_token_id = masked_token_id_val.to_le_bytes().to_vec(); Ok(Amount { commitment, masked_value, + masked_token_id, }) } @@ -68,11 +101,12 @@ impl Amount { /// /// # Arguments /// * `shared_secret` - The shared secret, e.g. `rB`. - pub fn get_value(&self, shared_secret: &RistrettoPublic) -> Result<(u64, Scalar), AmountError> { - let value: u64 = self.unmask_value(shared_secret); - let blinding = get_blinding(shared_secret); - - let expected_commitment = CompressedCommitment::new(value, blinding); + pub fn get_value( + &self, + shared_secret: &RistrettoPublic, + ) -> Result<(AmountData, Scalar), AmountError> { + let (expected_commitment, amount_data, blinding) = + Self::compute_commitment(self.masked_value, &self.masked_token_id, shared_secret)?; if self.commitment != expected_commitment { // The commitment does not agree with the provided value and blinding. // This either means that the commitment does not correspond to the shared @@ -81,12 +115,92 @@ impl Amount { return Err(AmountError::InconsistentCommitment); } - Ok((value, blinding)) + Ok((amount_data, blinding)) + } + + /// Compute the crc32 of the compressed commitment + pub fn commitment_crc32(&self) -> u32 { + Self::compute_commitment_crc32(&self.commitment) + } + + /// Recovers an Amount from only the masked value and masked_token_id, and + /// shared secret. + /// + /// Note: This fails and produces gibberish if the shared secret is wrong. + /// + /// * You should confirm by checking against the real commitment, or the the + /// crc32 of commitment. + /// + /// Arguments: + /// * masked_value: u64 + /// * masked_token_id: &[u8], either 0 or 4 bytes + /// * shared_secret: The shared secret curve point + /// + /// Returns: + /// * Amount + /// * AmountData (token id and value) + /// or + /// * An amount error + pub fn reconstruct( + masked_value: u64, + masked_token_id: &[u8], + shared_secret: &RistrettoPublic, + ) -> Result<(Self, AmountData), AmountError> { + let (expected_commitment, amount_data, _) = + Self::compute_commitment(masked_value, masked_token_id, shared_secret)?; + + let result = Self { + commitment: expected_commitment, + masked_value, + masked_token_id: masked_token_id.to_vec(), + }; + + Ok((result, amount_data)) + } + + fn compute_commitment( + masked_value: u64, + masked_token_id: &[u8], + shared_secret: &RistrettoPublic, + ) -> Result<(CompressedCommitment, AmountData, Scalar), AmountError> { + let token_id = TokenId::from(Self::unmask_token_id(masked_token_id, shared_secret)?); + let value: u64 = Self::unmask_value(masked_value, shared_secret); + let blinding = get_blinding(shared_secret); + + // Pedersen generators + let generator = generators(*token_id); + + let expected_commitment = CompressedCommitment::new(value, blinding, &generator); + + Ok(( + expected_commitment, + AmountData { value, token_id }, + blinding, + )) + } + + fn compute_commitment_crc32(commitment: &CompressedCommitment) -> u32 { + Crc::::new(&crc::CRC_32_ISO_HDLC).checksum(commitment.point.as_bytes()) } /// Reveals `masked_value`. - fn unmask_value(&self, shared_secret: &RistrettoPublic) -> u64 { - self.masked_value ^ get_value_mask(shared_secret) + fn unmask_value(masked_value: u64, shared_secret: &RistrettoPublic) -> u64 { + masked_value ^ get_value_mask(shared_secret) + } + + /// Reveals `masked_token_id`, with backwards compat + fn unmask_token_id( + masked_token_id: &[u8], + shared_secret: &RistrettoPublic, + ) -> Result { + match masked_token_id.len() { + 0 => Ok(0), + 4 => { + let masked_token_id_val = u32::from_le_bytes(masked_token_id.try_into().unwrap()); + Ok(masked_token_id_val ^ get_token_id_mask(shared_secret)) + } + _ => Err(AmountError::InvalidMaskedTokenId), + } } } @@ -96,7 +210,7 @@ impl Amount { /// /// # Arguments /// * `shared_secret` - The shared secret, e.g. `rB`. -pub fn get_value_mask(shared_secret: &RistrettoPublic) -> u64 { +fn get_value_mask(shared_secret: &RistrettoPublic) -> u64 { let mut hasher = Blake2b512::new(); hasher.update(&AMOUNT_VALUE_DOMAIN_TAG); hasher.update(&shared_secret.to_bytes()); @@ -106,6 +220,19 @@ pub fn get_value_mask(shared_secret: &RistrettoPublic) -> u64 { u64::from_le_bytes(temp) } +/// Computes `Blake2B(token_id_mask | shared_secret)`, +/// then interprets the first 4 canonical bytes as a u32 number in +/// little-endian representation. +/// +/// # Arguments +/// * `shared_secret` - The shared secret, e.g. `rB`. +fn get_token_id_mask(shared_secret: &RistrettoPublic) -> u32 { + let mut hasher = Blake2b512::new(); + hasher.update(&AMOUNT_TOKEN_ID_DOMAIN_TAG); + hasher.update(&shared_secret.to_bytes()); + u32::from_le_bytes(hasher.finalize()[0..4].try_into().unwrap()) +} + /// Computes `Blake2B("blinding" | shared_secret)`. /// /// # Arguments @@ -120,8 +247,9 @@ fn get_blinding(shared_secret: &RistrettoPublic) -> Scalar { #[cfg(test)] mod amount_tests { use crate::{ - amount::{get_blinding, Amount, AmountError}, + amount::{get_blinding, Amount, AmountData, AmountError}, proptest_fixtures::*, + ring_signature::generators, CompressedCommitment, }; use proptest::prelude::*; @@ -132,8 +260,10 @@ mod amount_tests { /// Amount::new() should return Ok for valid values and blindings. fn test_new_ok( value in any::(), + token_id in any::(), shared_secret in arbitrary_ristretto_public()) { - assert!(Amount::new(value, &shared_secret).is_ok()); + let amount_data = AmountData { value, token_id: token_id.into() }; + assert!(Amount::new(amount_data, &shared_secret).is_ok()); } #[test] @@ -141,10 +271,12 @@ mod amount_tests { /// amount.commitment should agree with the value and blinding. fn test_commitment( value in any::(), + token_id in any::(), shared_secret in arbitrary_ristretto_public()) { - let amount = Amount::new(value, &shared_secret).unwrap(); + let amount_data = AmountData { value, token_id: token_id.into() }; + let amount = Amount::new(amount_data, &shared_secret).unwrap(); let blinding = get_blinding(&shared_secret); - let expected_commitment = CompressedCommitment::new(value, blinding.into()); + let expected_commitment = CompressedCommitment::new(value, blinding.into(), &generators(token_id)); assert_eq!(amount.commitment, expected_commitment); } @@ -152,12 +284,14 @@ mod amount_tests { /// amount.unmask_value should return the value used to construct the amount. fn test_unmask_value( value in any::(), + token_id in any::(), shared_secret in arbitrary_ristretto_public()) { - let amount = Amount::new(value, &shared_secret).unwrap(); + let amount_data = AmountData { value, token_id: token_id.into() }; + let amount = Amount::new(amount_data, &shared_secret).unwrap(); assert_eq!( value, - amount.unmask_value(&shared_secret) + Amount::unmask_value(amount.masked_value, &shared_secret) ); } @@ -165,11 +299,13 @@ mod amount_tests { /// get_value should return the correct value and blinding. fn test_get_value_ok( value in any::(), + token_id in any::(), shared_secret in arbitrary_ristretto_public()) { - let amount = Amount::new(value, &shared_secret).unwrap(); + let amount_data = AmountData { value, token_id: token_id.into() }; + let amount = Amount::new(amount_data.clone(), &shared_secret).unwrap(); let result = amount.get_value(&shared_secret); let blinding = get_blinding(&shared_secret); - let expected = Ok((value, blinding)); + let expected = Ok((amount_data, blinding)); assert_eq!(result, expected); } @@ -178,12 +314,14 @@ mod amount_tests { /// get_value should return InconsistentCommitment if the masked value is incorrect. fn test_get_value_incorrect_masked_value( value in any::(), + token_id in any::(), other_masked_value in any::(), shared_secret in arbitrary_ristretto_public()) { // Mutate amount to use a different masked value. // With high probability, amount.masked_value won't equal other_masked_value. - let mut amount = Amount::new(value, &shared_secret).unwrap(); + let amount_data = AmountData { value, token_id: token_id.into() }; + let mut amount = Amount::new(amount_data, &shared_secret).unwrap(); amount.masked_value = other_masked_value; let result = amount.get_value(&shared_secret); let expected = Err(AmountError::InconsistentCommitment); @@ -194,10 +332,12 @@ mod amount_tests { /// get_value should return an Error if shared_secret is incorrect. fn test_get_value_invalid_shared_secret( value in any::(), + token_id in any::(), shared_secret in arbitrary_ristretto_public(), other_shared_secret in arbitrary_ristretto_public(), ) { - let amount = Amount::new(value, &shared_secret).unwrap(); + let amount_data = AmountData { value, token_id: token_id.into() }; + let amount = Amount::new(amount_data, &shared_secret).unwrap(); let result = amount.get_value(&other_shared_secret); let expected = Err(AmountError::InconsistentCommitment); assert_eq!(result, expected); diff --git a/transaction/core/src/blockchain/block.rs b/transaction/core/src/blockchain/block.rs index 24f160f725..b729f6abf7 100644 --- a/transaction/core/src/blockchain/block.rs +++ b/transaction/core/src/blockchain/block.rs @@ -203,8 +203,9 @@ mod block_tests { encrypted_fog_hint::EncryptedFogHint, membership_proofs::Range, ring_signature::KeyImage, + tokens::Mob, tx::{TxOut, TxOutMembershipElement, TxOutMembershipHash}, - Block, BlockContents, BlockContentsHash, BlockID, BlockVersion, + Block, BlockContents, BlockContentsHash, BlockID, BlockVersion, Token, }; use alloc::vec::Vec; use core::convert::TryFrom; @@ -227,13 +228,16 @@ mod block_tests { let outputs: Vec = (0..8) .map(|_i| { - TxOut::new( + let mut result = TxOut::new( rng.next_u64(), + Mob::ID, &recipient.default_subaddress(), &RistrettoPrivate::from_random(rng), EncryptedFogHint::fake_onetime_hint(rng), ) - .unwrap() + .unwrap(); + result.amount.masked_token_id = Default::default(); + result }) .collect(); diff --git a/transaction/core/src/blockchain/block_contents.rs b/transaction/core/src/blockchain/block_contents.rs index 91485f9050..29143782b1 100644 --- a/transaction/core/src/blockchain/block_contents.rs +++ b/transaction/core/src/blockchain/block_contents.rs @@ -24,6 +24,7 @@ pub struct BlockContents { } impl BlockContents { + /// Create new BlockContents from a set of KeyImages and Outputs pub fn new(key_images: Vec, outputs: Vec) -> Self { Self { key_images, diff --git a/transaction/core/src/blockchain/block_data.rs b/transaction/core/src/blockchain/block_data.rs index 9534e578db..7a8d6ec297 100644 --- a/transaction/core/src/blockchain/block_data.rs +++ b/transaction/core/src/blockchain/block_data.rs @@ -18,6 +18,12 @@ pub struct BlockData { } impl BlockData { + /// Create new block data: + /// + /// Arguments: + /// `block`: The block header + /// `contents`: The block contents + /// `signature`: A signature over the block pub fn new(block: Block, contents: BlockContents, signature: Option) -> Self { Self { block, @@ -26,14 +32,17 @@ impl BlockData { } } + /// Get the block pub fn block(&self) -> &Block { &self.block } + /// Get the contents pub fn contents(&self) -> &BlockContents { &self.contents } + /// Get the signature pub fn signature(&self) -> &Option { &self.signature } diff --git a/transaction/core/src/blockchain/block_version.rs b/transaction/core/src/blockchain/block_version.rs index cf6d4b1ee5..e71a86ec30 100644 --- a/transaction/core/src/blockchain/block_version.rs +++ b/transaction/core/src/blockchain/block_version.rs @@ -57,7 +57,7 @@ impl FromStr for BlockVersion { impl BlockVersion { /// The maximum value of block_version that this build of /// mc-transaction-core has support for - pub const MAX: Self = Self(2); + pub const MAX: Self = Self(3); /// Refers to the block version number at network launch. /// Note: The origin blocks use block version zero. @@ -66,6 +66,9 @@ impl BlockVersion { /// Constant for block version two pub const TWO: Self = Self(2); + /// Constant for block version three + pub const THREE: Self = Self(3); + /// Iterator over block versions from one up to max, inclusive. For use in /// tests. pub fn iterator() -> BlockVersionIterator { @@ -77,6 +80,12 @@ impl BlockVersion { pub fn e_memo_feature_is_supported(&self) -> bool { self.0 >= 2 } + + /// The confidential token ids [MCIP #25](https://github.com/mobilecoinfoundation/mcips/pull/3) + /// feature is introduced in block version 3. + pub fn masked_token_id_feature_is_supported(&self) -> bool { + self.0 >= 3 + } } impl Deref for BlockVersion { @@ -93,6 +102,7 @@ impl fmt::Display for BlockVersion { } } +/// An iterator over block versions from 1 up to Max, for use in test code #[derive(Debug, Clone)] pub struct BlockVersionIterator(u32); @@ -109,6 +119,8 @@ impl Iterator for BlockVersionIterator { } } +/// An error that can occur when parsing a block version or interpreting u32 as +/// a block version #[derive(Clone, Display, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] pub enum BlockVersionError { /// Unsupported block version: {0} > {1}. Try upgrading your software diff --git a/transaction/core/src/constants.rs b/transaction/core/src/constants.rs index 554d645d3c..7cf0adc50d 100644 --- a/transaction/core/src/constants.rs +++ b/transaction/core/src/constants.rs @@ -31,6 +31,6 @@ pub const MICROMOB_TO_PICOMOB: u64 = 1_000_000; pub const MILLIMOB_TO_PICOMOB: u64 = 1_000_000_000; lazy_static! { - // Blinding for the implicit fee outputs. + /// Blinding for the implicit fee outputs. pub static ref FEE_BLINDING: Scalar = Scalar::zero(); } diff --git a/transaction/core/src/domain_separators.rs b/transaction/core/src/domain_separators.rs index 32a2df61ae..b1cf3c3a8d 100644 --- a/transaction/core/src/domain_separators.rs +++ b/transaction/core/src/domain_separators.rs @@ -17,6 +17,9 @@ /// Domain separator for Amount's value mask hash function. pub const AMOUNT_VALUE_DOMAIN_TAG: &str = "mc_amount_value"; +/// Domain separator for Amount's token_id mask hash function. +pub const AMOUNT_TOKEN_ID_DOMAIN_TAG: &str = "mc_amount_token_id"; + /// Domain separator for Amount's blinding mask hash function. pub const AMOUNT_BLINDING_DOMAIN_TAG: &str = "mc_amount_blinding"; @@ -43,3 +46,6 @@ pub const RING_MLSAG_CHALLENGE_DOMAIN_TAG: &str = "mc_ring_mlsag_challenge"; /// Domain separator for hashing the confirmation number pub const TXOUT_CONFIRMATION_NUMBER_DOMAIN_TAG: &str = "mc_tx_out_confirmation_number"; + +/// Domain separator for computing the extended message digest +pub const EXTENDED_MESSAGE_DOMAIN_TAG: &str = "mc_extended_message"; diff --git a/transaction/core/src/encrypted_fog_hint.rs b/transaction/core/src/encrypted_fog_hint.rs index 03cd2f1c3d..54613d9e19 100644 --- a/transaction/core/src/encrypted_fog_hint.rs +++ b/transaction/core/src/encrypted_fog_hint.rs @@ -25,15 +25,17 @@ use prost::{ use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; -// The length of the encrypted fog hint field in the ledger. -// Must be at least as large as mc_crypto_box::VersionedCryptoBox::FooterSize. -// Footersize = 50, + 32 for one curve point, + 2 bytes of magic / padding space -// for future needs +/// The length of the encrypted fog hint field in the ledger. +/// Must be at least as large as mc_crypto_box::VersionedCryptoBox::FooterSize. +/// Footersize = 50, + 32 for one curve point, + 2 bytes of magic / padding +/// space for future needs pub type EncryptedFogHintSize = U84; +/// Length of encrypted fog hint as a usize pub const ENCRYPTED_FOG_HINT_LEN: usize = EncryptedFogHintSize::USIZE; type Bytes = GenericArray; +/// An encrypted fog hint payload in the ledger #[derive( Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize, Default, Digestible, )] @@ -85,12 +87,15 @@ impl<'bytes> TryFrom<&'bytes [u8]> for EncryptedFogHint { } impl EncryptedFogHint { + /// Create a new encrypted fog hint from byte array #[inline] pub fn new(a: &[u8; ENCRYPTED_FOG_HINT_LEN]) -> Self { Self { bytes: GenericArray::clone_from_slice(&a[..]), } } + + /// Convert to byte array #[inline] pub fn to_bytes(&self) -> [u8; ENCRYPTED_FOG_HINT_LEN] { let mut result = [0u8; ENCRYPTED_FOG_HINT_LEN]; diff --git a/transaction/core/src/fog_hint.rs b/transaction/core/src/fog_hint.rs index 0a4545cad6..5c30bb0ba8 100644 --- a/transaction/core/src/fog_hint.rs +++ b/transaction/core/src/fog_hint.rs @@ -28,6 +28,7 @@ pub type PlaintextArray = GenericArray< Diff>::FooterSize>, >; +/// A (decrypted) Fog hint #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct FogHint { view_pubkey: CompressedRistrettoPublic, // aka A from cryptonote public address @@ -50,16 +51,21 @@ impl From<&PublicAddress> for FogHint { } impl FogHint { + /// Create a new fog hint payload pub fn new(view_pubkey: RistrettoPublic) -> Self { Self { view_pubkey: CompressedRistrettoPublic::from(view_pubkey), } } + + /// Create from a byte slice pub fn from_slice(bytes: &[u8]) -> Result { Ok(Self { view_pubkey: CompressedRistrettoPublic::try_from(bytes).map_err(CryptoBoxError::Key)?, }) } + + /// Convert to a byte array pub fn to_bytes(&self) -> [u8; RISTRETTO_PUBLIC_LEN] { *self.view_pubkey.as_bytes() } diff --git a/transaction/core/src/lib.rs b/transaction/core/src/lib.rs index 3be49b5163..9ff8ec6bb3 100644 --- a/transaction/core/src/lib.rs +++ b/transaction/core/src/lib.rs @@ -1,7 +1,10 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! MobileCoin transaction data types, transaction construction and validation +//! routines + #![no_std] -// #![deny(missing_docs)] +#![deny(missing_docs)] extern crate alloc; @@ -35,7 +38,7 @@ pub mod validation; #[cfg(test)] pub mod proptest_fixtures; -pub use amount::{get_value_mask, Amount, AmountError, Commitment, CompressedCommitment}; +pub use amount::{Amount, AmountData, AmountError, Commitment, CompressedCommitment}; pub use blockchain::*; pub use memo::{EncryptedMemo, MemoError, MemoPayload}; pub use token::{tokens, Token, TokenId}; diff --git a/transaction/core/src/membership_proofs/mod.rs b/transaction/core/src/membership_proofs/mod.rs index aab57cce48..de312495cc 100644 --- a/transaction/core/src/membership_proofs/mod.rs +++ b/transaction/core/src/membership_proofs/mod.rs @@ -1,5 +1,7 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! Validation for merkle proofs of membership + #![allow(clippy::if_same_then_else)] use crate::{ @@ -23,6 +25,7 @@ pub use self::{ }; lazy_static! { + /// A value hashed in connection to a nil node in the tree pub static ref NIL_HASH: [u8; 32] = hash_nil(); } diff --git a/transaction/core/src/membership_proofs/range.rs b/transaction/core/src/membership_proofs/range.rs index 751e26fe47..d2ae9c5e7d 100644 --- a/transaction/core/src/membership_proofs/range.rs +++ b/transaction/core/src/membership_proofs/range.rs @@ -6,6 +6,7 @@ use prost::Message; // These require the serde "derive" feature to be enabled. use serde::{Deserialize, Serialize}; +/// An error which occurs in connection to a membership proof range #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub struct RangeError {} @@ -18,13 +19,16 @@ impl core::fmt::Display for RangeError { /// A range [from,to] of indices. #[derive(Clone, Copy, Deserialize, Eq, Hash, PartialEq, Serialize, Message, Digestible)] pub struct Range { + /// The left endpoint of the range #[prost(uint64, tag = "1")] pub from: u64, + /// The right endpoint of the range #[prost(uint64, tag = "2")] pub to: u64, } #[allow(clippy::len_without_is_empty)] impl Range { + /// Create a new range pub fn new(from: u64, to: u64) -> Result { if from <= to { Ok(Self { from, to }) diff --git a/transaction/core/src/memo.rs b/transaction/core/src/memo.rs index 6e9cafeff2..7744ef8c4d 100644 --- a/transaction/core/src/memo.rs +++ b/transaction/core/src/memo.rs @@ -216,6 +216,7 @@ derive_into_vec_from_repr_bytes!(MemoPayload); derive_serde_from_repr_bytes!(MemoPayload); derive_prost_message_from_repr_bytes!(MemoPayload); +/// An error which can occur when handling memos #[derive(Display, Debug)] pub enum MemoError { /// Wrong length for memo payload: {0} diff --git a/transaction/core/src/proptest_fixtures.rs b/transaction/core/src/proptest_fixtures.rs index f3713645eb..b35e25b67f 100644 --- a/transaction/core/src/proptest_fixtures.rs +++ b/transaction/core/src/proptest_fixtures.rs @@ -1,6 +1,6 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation -use crate::{ring_signature::CurveScalar, Amount}; +use crate::{ring_signature::CurveScalar, tokens::Mob, Amount, AmountData, Token}; use curve25519_dalek::scalar::Scalar; use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; use proptest::prelude::*; @@ -27,8 +27,13 @@ pub fn arbitrary_ristretto_public() -> impl Strategy { prop_compose! { /// Generates an arbitrary amount with value in [0,max_value]. + /// Of token_id = 0 pub fn arbitrary_amount(max_value: u64, shared_secret: RistrettoPublic) (value in 0..=max_value) -> Amount { - Amount::new(value, &shared_secret).unwrap() + let amount_data = AmountData { + value, + token_id: Mob::ID, + }; + Amount::new(amount_data, &shared_secret).unwrap() } } diff --git a/transaction/core/src/range_proofs/README.md b/transaction/core/src/range_proofs/README.md index 4e026a0bee..c6c227b602 100644 --- a/transaction/core/src/range_proofs/README.md +++ b/transaction/core/src/range_proofs/README.md @@ -1,6 +1,6 @@ -# bulletproofs_utils +# Range Proofs -Helpers for creating and verifying [Bulletproofs](https://doc-internal.dalek.rs/bulletproofs/index.html). +Helpers for creating and verifying range proofs using [Bulletproofs](https://doc-internal.dalek.rs/bulletproofs/index.html). "Bulletproofs are short non-interactive zero-knowledge proofs that require no trusted setup. A bulletproof can be used to convince a verifier that an encrypted plaintext is well formed. For example, prove that an encrypted number is in a given range, without revealing anything else about the number." diff --git a/transaction/core/src/range_proofs/error.rs b/transaction/core/src/range_proofs/error.rs index 1d56d994b7..9c79f2d929 100644 --- a/transaction/core/src/range_proofs/error.rs +++ b/transaction/core/src/range_proofs/error.rs @@ -1,8 +1,11 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! Error types for range proofs + use bulletproofs_og::ProofError; use displaydoc::Display; +/// An error which can occur in connection to a range proof #[derive(Debug, Display, PartialEq)] pub enum Error { /// ProofError: `{0:?}` diff --git a/transaction/core/src/range_proofs/mod.rs b/transaction/core/src/range_proofs/mod.rs index 4c7686fe8e..140e12796d 100644 --- a/transaction/core/src/range_proofs/mod.rs +++ b/transaction/core/src/range_proofs/mod.rs @@ -1,5 +1,12 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! Range proofs are used to prove that a set of committed values are all +//! in a well-defined range, without revealing the values. +//! +//! A range proof is relative to a Pedersen Generator. If a prover can construct +//! a range proof relative to one generator, they cannot construct a range proof +//! relative to another generator, if those generators are orthogonal. + extern crate alloc; use alloc::vec::Vec; use bulletproofs_og::RangeProof; @@ -10,7 +17,7 @@ use rand_core::{CryptoRng, RngCore}; pub mod error; use crate::{ domain_separators::BULLETPROOF_DOMAIN_TAG, - ring_signature::{BP_GENERATORS, GENERATORS}, + ring_signature::{PedersenGens, BP_GENERATORS}, }; use error::Error; @@ -21,6 +28,8 @@ use error::Error; /// # Arguments /// `values` - Secret values that we want to prove are in [0,2^64). /// `blindings` - Pedersen commitment blinding for each value. +/// `pedersen_generators` - Generators on which the commitments are based +/// `rng` - randomness /// /// # Returns /// The proof and the Pedersen commitments from `values` and `blindings` (padded @@ -28,6 +37,7 @@ use error::Error; pub fn generate_range_proofs( values: &[u64], blindings: &[Scalar], + pedersen_generators: &PedersenGens, rng: &mut T, ) -> Result<(RangeProof, Vec), Error> { // Most of this comes directly from the example at @@ -41,7 +51,7 @@ pub fn generate_range_proofs( // Create a 64-bit RangeProof and corresponding commitments. RangeProof::prove_multiple_with_rng( &BP_GENERATORS, - &GENERATORS, + pedersen_generators, &mut Transcript::new(BULLETPROOF_DOMAIN_TAG.as_ref()), &values_padded, &blindings_padded, @@ -58,10 +68,12 @@ pub fn generate_range_proofs( /// # Arguments /// `range_proof` - A RangeProof. /// `commitments` - Commitments to secret values that lie in the range [0,2^64). -/// `rng` - Randomness. +/// `pedersen_generators` - Pedersen generators on which the commitments are +/// based `rng` - Randomness. pub fn check_range_proofs( range_proof: &RangeProof, commitments: &[CompressedRistretto], + pedersen_generators: &PedersenGens, rng: &mut T, ) -> Result<(), Error> { // The length of `commitments` must be a power of 2. If not, resize it. @@ -69,7 +81,7 @@ pub fn check_range_proofs( range_proof .verify_multiple_with_rng( &BP_GENERATORS, - &GENERATORS, + pedersen_generators, &mut Transcript::new(BULLETPROOF_DOMAIN_TAG.as_ref()), &resized_commitments, 64, @@ -103,15 +115,17 @@ fn resize_slice_to_pow2(slice: &[T]) -> Result, Error> { #[cfg(test)] pub mod tests { use super::*; + use crate::ring_signature::generators; use curve25519_dalek::ristretto::RistrettoPoint; use rand::{rngs::StdRng, SeedableRng}; use rand_core::RngCore; fn generate_and_check(values: Vec, blindings: Vec) { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - let (proof, commitments) = generate_range_proofs(&values, &blindings, &mut rng).unwrap(); + let (proof, commitments) = + generate_range_proofs(&values, &blindings, &generators(0), &mut rng).unwrap(); - match check_range_proofs(&proof, &commitments, &mut rng) { + match check_range_proofs(&proof, &commitments, &generators(0), &mut rng) { Ok(_) => {} // This is expected. Err(e) => panic!("{:?}", e), } @@ -142,13 +156,14 @@ pub mod tests { let num_values: usize = 4; let values: Vec = (0..num_values).map(|_| rng.next_u64()).collect(); let blindings: Vec = (0..num_values).map(|_| Scalar::random(&mut rng)).collect(); - let (proof, commitments) = generate_range_proofs(&values, &blindings, &mut rng).unwrap(); + let (proof, commitments) = + generate_range_proofs(&values, &blindings, &generators(0), &mut rng).unwrap(); // Modify a commitment. let mut wrong_commitments = commitments.clone(); wrong_commitments[0] = RistrettoPoint::random(&mut rng).compress(); - match check_range_proofs(&proof, &wrong_commitments, &mut rng) { + match check_range_proofs(&proof, &wrong_commitments, &generators(0), &mut rng) { Ok(_) => panic!(), Err(_e) => {} // This is expected. } diff --git a/transaction/core/src/ring_signature/curve_scalar.rs b/transaction/core/src/ring_signature/curve_scalar.rs index 60edd6e37c..ed276577bb 100644 --- a/transaction/core/src/ring_signature/curve_scalar.rs +++ b/transaction/core/src/ring_signature/curve_scalar.rs @@ -18,9 +18,11 @@ use rand_core::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use zeroize::Zeroize; +/// A curve scalar #[derive(Copy, Clone, Default, Eq, Serialize, Deserialize, Digestible, Zeroize)] #[digestible(transparent)] pub struct CurveScalar { + /// The scalar value pub scalar: Scalar, } diff --git a/transaction/core/src/ring_signature/error.rs b/transaction/core/src/ring_signature/error.rs index ef987c84e6..2bde0f750f 100644 --- a/transaction/core/src/ring_signature/error.rs +++ b/transaction/core/src/ring_signature/error.rs @@ -1,8 +1,11 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! Errors which can occur in connection to ring signatures + use displaydoc::Display; use serde::{Deserialize, Serialize}; +/// An error which can occur in connection to a ring signature #[derive( Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize, )] @@ -48,6 +51,9 @@ pub enum Error { /// Invalid RangeProof RangeProofError, + + /// TokenId is not allowed at this block version + TokenIdNotAllowed, } impl From for Error { diff --git a/transaction/core/src/ring_signature/key_image.rs b/transaction/core/src/ring_signature/key_image.rs index 675b78fd62..6eaf2a6cb9 100644 --- a/transaction/core/src/ring_signature/key_image.rs +++ b/transaction/core/src/ring_signature/key_image.rs @@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize}; #[digestible(transparent)] /// The "image" of a private key `x`: I = x * Hp(x * G) = x * Hp(P). pub struct KeyImage { + /// The curve point corresponding to the key image pub point: CompressedRistretto, } diff --git a/transaction/core/src/ring_signature/mlsag.rs b/transaction/core/src/ring_signature/mlsag.rs index 65a07cedb9..30ad70faa2 100644 --- a/transaction/core/src/ring_signature/mlsag.rs +++ b/transaction/core/src/ring_signature/mlsag.rs @@ -16,7 +16,9 @@ use zeroize::Zeroizing; use crate::{ domain_separators::RING_MLSAG_CHALLENGE_DOMAIN_TAG, - ring_signature::{hash_to_point, CurveScalar, Error, KeyImage, Scalar, GENERATORS}, + ring_signature::{ + hash_to_point, CurveScalar, Error, KeyImage, PedersenGens, Scalar, B_BLINDING, + }, Commitment, CompressedCommitment, }; @@ -40,21 +42,23 @@ pub struct RingMLSAG { } impl RingMLSAG { - // Sign a ring of input addresses and amount commitments. - // - // Sign a ring of input addresses and amount commitments using a modified MLSAG - // that omits the "key image" term for the amount commitments (which do not need - // to be linkable). - // - // # Arguments - // * `message` - Message to be signed. - // * `ring` - A ring of input onetime addresses and amount commitments. - // * `real_index` - The index in the ring of the real input. - // * `onetime_private_key` - The real input's private key. - // * `value` - Value of the real input. - // * `blinding` - Blinding of the real input. - // * `output_blinding` - The output amount's blinding factor. - // * `rng` - Randomness. + /// Sign a ring of input addresses and amount commitments. + /// + /// Sign a ring of input addresses and amount commitments using a modified + /// MLSAG that omits the "key image" term for the amount commitments + /// (which do not need to be linkable). + /// + /// # Arguments + /// * `message` - Message to be signed. + /// * `ring` - A ring of input onetime addresses and amount commitments. + /// * `real_index` - The index in the ring of the real input. + /// * `onetime_private_key` - The real input's private key. + /// * `value` - Value of the real input. + /// * `blinding` - Blinding of the real input. + /// * `output_blinding` - The output amount's blinding factor. + /// * `generator` - The pedersen generator to use for this commitment and + /// signature + /// * `rng` - Randomness. pub fn sign( message: &[u8], ring: &[(CompressedRistrettoPublic, CompressedCommitment)], @@ -63,6 +67,7 @@ impl RingMLSAG { value: u64, blinding: &Scalar, output_blinding: &Scalar, + generator: &PedersenGens, rng: &mut CSPRNG, ) -> Result { RingMLSAG::sign_with_balance_check( @@ -73,6 +78,7 @@ impl RingMLSAG { value, blinding, output_blinding, + generator, true, rng, ) @@ -92,6 +98,8 @@ impl RingMLSAG { // * `value` - Value of the real input. // * `blinding` - Blinding of the real input. // * `output_blinding` - The output amount's blinding factor. + // * `generator` - The pedersen generator to use for this commitment and + // signature // * `check_value_is_preserved` - If true, check that the value of inputs equals // value of outputs. // * `rng` - Randomness. @@ -103,6 +111,7 @@ impl RingMLSAG { value: u64, blinding: &Scalar, output_blinding: &Scalar, + generator: &PedersenGens, check_value_is_preserved: bool, rng: &mut CSPRNG, ) -> Result { @@ -112,7 +121,11 @@ impl RingMLSAG { return Err(Error::IndexOutOfBounds); } - let G = GENERATORS.B_blinding; + let G = B_BLINDING; + debug_assert!( + generator.B_blinding == G, + "basepoint for blindings mismatch" + ); let key_image = KeyImage::from(onetime_private_key); @@ -122,7 +135,7 @@ impl RingMLSAG { // Uncompressed output commitment. // This ensures that each address and commitment encodes a valid Ristretto // point. - let output_commitment = Commitment::new(value, *output_blinding); + let output_commitment = Commitment::new(value, *output_blinding, generator); // Ring must decompress. let decompressed_ring = decompress_ring(ring)?; @@ -228,7 +241,7 @@ impl RingMLSAG { return Err(Error::LengthMismatch(2 * ring_size, self.responses.len())); } - let G = GENERATORS.B_blinding; + let G = B_BLINDING; // The key image must decompress. // This ensures that the key image encodes a valid Ristretto point. @@ -335,7 +348,9 @@ fn decompress_ring( #[cfg(test)] mod mlsag_tests { use crate::{ - ring_signature::{mlsag::RingMLSAG, CurveScalar, Error, KeyImage, Scalar}, + ring_signature::{ + generators, mlsag::RingMLSAG, CurveScalar, Error, KeyImage, PedersenGens, Scalar, + }, CompressedCommitment, }; use alloc::vec::Vec; @@ -348,7 +363,7 @@ mod mlsag_tests { extern crate std; - #[derive(Debug)] + #[derive(Clone)] struct RingMLSAGParameters { message: [u8; 32], ring: Vec<(CompressedRistrettoPublic, CompressedCommitment)>, @@ -357,6 +372,7 @@ mod mlsag_tests { value: u64, blinding: Scalar, pseudo_output_blinding: Scalar, + generator: PedersenGens, } impl RingMLSAGParameters { @@ -368,13 +384,15 @@ mod mlsag_tests { let mut message = [0u8; 32]; rng.fill_bytes(&mut message); + let generator = generators(rng.next_u32()); + let mut ring: Vec<(CompressedRistrettoPublic, CompressedCommitment)> = Vec::new(); for _i in 0..num_mixins { let address = CompressedRistrettoPublic::from(RistrettoPublic::from_random(rng)); let commitment = { let value = rng.next_u64(); let blinding = Scalar::random(rng); - CompressedCommitment::new(value, blinding) + CompressedCommitment::new(value, blinding, &generator) }; ring.push((address, commitment)); } @@ -386,7 +404,7 @@ mod mlsag_tests { let value = rng.next_u64(); let blinding = Scalar::random(rng); - let commitment = CompressedCommitment::new(value, blinding); + let commitment = CompressedCommitment::new(value, blinding, &generator); let real_index = rng.next_u64() as usize % (num_mixins + 1); ring.insert(real_index, (onetime_public_key, commitment)); @@ -400,12 +418,45 @@ mod mlsag_tests { value, blinding, pseudo_output_blinding, + generator, } } + + fn sign(&self, rng: &mut RNG) -> Result { + RingMLSAG::sign( + &self.message, + &self.ring, + self.real_index, + &self.onetime_private_key, + self.value, + &self.blinding, + &self.pseudo_output_blinding, + &self.generator, + rng, + ) + } + + fn sign_without_balance_check( + &self, + rng: &mut RNG, + ) -> Result { + RingMLSAG::sign_with_balance_check( + &self.message, + &self.ring, + self.real_index, + &self.onetime_private_key, + self.value, + &self.blinding, + &self.pseudo_output_blinding, + &self.generator, + false, + rng, + ) + } } proptest! { - #![proptest_config(ProptestConfig::with_cases(3))] + #![proptest_config(ProptestConfig::with_cases(6))] #[test] // `sign` should return a signature with 2*ring_size responses. @@ -416,20 +467,10 @@ mod mlsag_tests { let mut rng: StdRng = SeedableRng::from_seed(seed); let pseudo_output_blinding = Scalar::random(&mut rng); - let ring_mlsag_parameters = + let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let signature = RingMLSAG::sign( - &ring_mlsag_parameters.message, - &ring_mlsag_parameters.ring, - ring_mlsag_parameters.real_index, - &ring_mlsag_parameters.onetime_private_key, - ring_mlsag_parameters.value, - &ring_mlsag_parameters.blinding, - &ring_mlsag_parameters.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); let ring_size = num_mixins + 1; assert_eq!(signature.responses.len(), 2 * ring_size); @@ -450,17 +491,7 @@ mod mlsag_tests { let pseudo_output_blinding = Scalar::random(&mut rng); let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); let expected_key_image = KeyImage::from(¶ms.onetime_private_key); assert_eq!(signature.key_image, expected_key_image); @@ -474,19 +505,11 @@ mod mlsag_tests { ) { let mut rng: StdRng = SeedableRng::from_seed(seed); let pseudo_output_blinding = Scalar::random(&mut rng); - let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); + let mut params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); let wrong_value = rng.next_u64(); + params.value = wrong_value; - let result = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - wrong_value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ); + let result = params.sign(&mut rng); match result { Err(Error::ValueNotConserved) => {} // Expected @@ -501,21 +524,14 @@ mod mlsag_tests { ) { let mut rng: StdRng = SeedableRng::from_seed(seed); let pseudo_output_blinding = Scalar::random(&mut rng); - let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); + let mut params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); // The ring contains num_mixins + 1 elements, with indices 0..num_mixins. // This is the smallest out of bounds index. let wrong_real_index = num_mixins + 1; - let result = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - wrong_real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ); + params.real_index = wrong_real_index; + + let result = params.sign(&mut rng); match result { Err(Error::IndexOutOfBounds) => {} // Expected @@ -533,19 +549,9 @@ mod mlsag_tests { let pseudo_output_blinding = Scalar::random(&mut rng); let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); - let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding, ¶ms.generator); assert!(signature .verify(¶ms.message, ¶ms.ring, &output_commitment) @@ -560,22 +566,13 @@ mod mlsag_tests { ) { let mut rng: StdRng = SeedableRng::from_seed(seed); let pseudo_output_blinding = Scalar::random(&mut rng); - let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); + let mut params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); let wrong_onetime_private_key = RistrettoPrivate::from_random(&mut rng); + params.onetime_private_key = wrong_onetime_private_key; - let signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - &wrong_onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); - let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding, ¶ms.generator); match signature.verify(¶ms.message, ¶ms.ring, &output_commitment) { Err(Error::InvalidSignature) => {} // This is expected. @@ -595,22 +592,13 @@ mod mlsag_tests { // Sign with an input value that differs from the real input's amount commitment. { + let mut params = params.clone(); let wrong_value = rng.next_u64(); + params.value = wrong_value; // Disable value checking in order to create an invalid signature. - let invalid_signature = RingMLSAG::sign_with_balance_check( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - wrong_value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - false, - &mut rng, - ) - .unwrap(); - - let output_commitment = CompressedCommitment::new(wrong_value, params.pseudo_output_blinding); + let invalid_signature = params.sign_without_balance_check(&mut rng).unwrap(); + + let output_commitment = CompressedCommitment::new(wrong_value, params.pseudo_output_blinding, ¶ms.generator); let result = invalid_signature.verify(¶ms.message, ¶ms.ring, &output_commitment); @@ -623,23 +611,15 @@ mod mlsag_tests { // Sign with an input blinding that differs from the real input's amount commitment. { + let mut params = params.clone(); let wrong_blinding = Scalar::random(&mut rng); + params.blinding = wrong_blinding; + // Disable value checking in order to create an invalid signature. - let invalid_signature = RingMLSAG::sign_with_balance_check( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - &wrong_blinding, - ¶ms.pseudo_output_blinding, - false, - &mut rng, - ) - .unwrap(); - - let output_commitment = CompressedCommitment::new(params.value, wrong_blinding); + let invalid_signature = params.sign_without_balance_check(&mut rng).unwrap(); + + let output_commitment = CompressedCommitment::new(params.value, wrong_blinding, ¶ms.generator); let result = invalid_signature.verify(¶ms.message, ¶ms.ring, &output_commitment); @@ -661,17 +641,7 @@ mod mlsag_tests { let pseudo_output_blinding = Scalar::random(&mut rng); let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let mut signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let mut signature = params.sign(&mut rng).unwrap(); // Replace the key image with a non-canonical compressed Ristretto point. // This is constants::EDWARDS_D.to_bytes(), which is a negative point, so decompression should fail. @@ -682,7 +652,7 @@ mod mlsag_tests { signature.key_image = KeyImage{point: bad_compressed}; - let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding, ¶ms.generator); match signature.verify(¶ms.message, ¶ms.ring, &output_commitment) { Err(Error::InvalidKeyImage) => {} // This is expected. @@ -701,23 +671,13 @@ mod mlsag_tests { let pseudo_output_blinding = Scalar::random(&mut rng); let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let mut signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let mut signature = params.sign(&mut rng).unwrap(); // Modify the key image. let wrong_key_image = KeyImage::from(rng.next_u64()); signature.key_image = wrong_key_image; - let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding, ¶ms.generator); match signature.verify(¶ms.message, ¶ms.ring, &output_commitment) { Err(Error::InvalidSignature) => {} // This is expected. @@ -735,23 +695,13 @@ mod mlsag_tests { let pseudo_output_blinding = Scalar::random(&mut rng); let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); // Modify the message. let mut wrong_message = [0u8; 32]; rng.fill_bytes(&mut wrong_message); - let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding, ¶ms.generator); let result = signature.verify(&wrong_message, ¶ms.ring, &output_commitment); @@ -771,19 +721,9 @@ mod mlsag_tests { let pseudo_output_blinding = Scalar::random(&mut rng); let mut params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); - let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding, ¶ms.generator); // Modify a ring element's public key. { @@ -803,7 +743,7 @@ mod mlsag_tests { let index = (rng.next_u64() as usize) % num_mixins; let value = rng.next_u64(); let blinding = Scalar::random(&mut rng); - params.ring[index].1 = CompressedCommitment::new(value, blinding); + params.ring[index].1 = CompressedCommitment::new(value, blinding, ¶ms.generator); let result = signature.verify(¶ms.message, ¶ms.ring, &output_commitment); @@ -824,21 +764,11 @@ mod mlsag_tests { let pseudo_output_blinding = Scalar::random(&mut rng); let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); // The output_commitment should match the value and pseudo_output_blinding used by the signature. // Here, the output_commitment uses a different value. - let wrong_output_commitment = CompressedCommitment::new(rng.next_u64(), params.pseudo_output_blinding); + let wrong_output_commitment = CompressedCommitment::new(rng.next_u64(), params.pseudo_output_blinding, ¶ms.generator); let result = signature.verify(¶ms.message, ¶ms.ring, &wrong_output_commitment); @@ -858,19 +788,9 @@ mod mlsag_tests { let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); - let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding); + let output_commitment = CompressedCommitment::new(params.value, params.pseudo_output_blinding, ¶ms.generator); // Modify the signature to have too few responses. { @@ -911,17 +831,7 @@ mod mlsag_tests { let pseudo_output_blinding = Scalar::random(&mut rng); let params = RingMLSAGParameters::random(num_mixins, pseudo_output_blinding, &mut rng); - let signature = RingMLSAG::sign( - ¶ms.message, - ¶ms.ring, - params.real_index, - ¶ms.onetime_private_key, - params.value, - ¶ms.blinding, - ¶ms.pseudo_output_blinding, - &mut rng, - ) - .unwrap(); + let signature = params.sign(&mut rng).unwrap(); use mc_util_serial::prost::Message; diff --git a/transaction/core/src/ring_signature/mod.rs b/transaction/core/src/ring_signature/mod.rs index 9037ef3c78..c06a8c61a1 100644 --- a/transaction/core/src/ring_signature/mod.rs +++ b/transaction/core/src/ring_signature/mod.rs @@ -1,20 +1,16 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! MobileCoin ring signatures + #![allow(non_snake_case)] -#![macro_use] -extern crate alloc; + +pub use bulletproofs_og::{BulletproofGens, PedersenGens}; +pub use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar}; use crate::domain_separators::HASH_TO_POINT_DOMAIN_TAG; -use bulletproofs_og::{BulletproofGens, PedersenGens}; -pub use curve25519_dalek::scalar::Scalar; -use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT, ristretto::RistrettoPoint}; -pub use curve_scalar::*; -pub use error::Error; -pub use key_image::*; +use curve25519_dalek::constants::{RISTRETTO_BASEPOINT_COMPRESSED, RISTRETTO_BASEPOINT_POINT}; use mc_crypto_hashes::{Blake2b512, Digest}; use mc_crypto_keys::RistrettoPublic; -pub use mlsag::*; -pub use rct_bulletproofs::*; mod curve_scalar; mod error; @@ -22,15 +18,16 @@ mod key_image; mod mlsag; mod rct_bulletproofs; -lazy_static! { - /// Generators (base points) for Pedersen commitments. - /// For commitment to amount 'v' with blinding 'b', we want 'C = v*H + b*G' so commitments to zero are signed on G. - /// Note: our H is not the same point as the dalek library's default version - pub static ref GENERATORS: PedersenGens = PedersenGens { - B: hash_to_point(&RistrettoPublic::from(RISTRETTO_BASEPOINT_POINT)), - B_blinding: RISTRETTO_BASEPOINT_POINT - }; +pub use curve_scalar::*; +pub use error::Error; +pub use key_image::*; +pub use mlsag::*; +pub use rct_bulletproofs::*; + +/// The base point for blinding factors used with all amount commitments +pub const B_BLINDING: RistrettoPoint = RISTRETTO_BASEPOINT_POINT; +lazy_static! { /// Generators (base points) for Bulletproofs. /// The `party_capacity` is the maximum number of values in one proof. It should /// be at least 2 * MAX_INPUTS + MAX_OUTPUTS, which allows for inputs, pseudo outputs, and outputs. @@ -38,6 +35,41 @@ lazy_static! { BulletproofGens::new(64, 64); } +/// Generators (base points) for Pedersen commitments to amounts. +/// +/// For commitment to amount 'v' with blinding 'b', we want 'C = v*H + b*G' +/// so commitments to zero are signed on G, where G is the ristretto basepoint. +/// +/// Note: our H is not the same point as the dalek library's default version +/// +/// For amounts, H varies based on the token id. +pub fn generators(token_id: u32) -> PedersenGens { + let mut hasher = Blake2b512::new(); + hasher.update(&HASH_TO_POINT_DOMAIN_TAG); + + // This step xors the token id bytes on top of the "base point" bytes + // used prior to the introduction of token ids. + // + // This ensures: + // * The function is constant-time with respect to token id + // * The behavior for id 0 is the same as before + // * For different id values, the set of B points are orthogonal. + { + let id_bytes = token_id.to_le_bytes(); + let mut buf: [u8; 32] = RISTRETTO_BASEPOINT_COMPRESSED.to_bytes(); + buf[0] ^= id_bytes[0]; + buf[1] ^= id_bytes[1]; + buf[2] ^= id_bytes[2]; + buf[3] ^= id_bytes[3]; + hasher.update(buf); + } + + PedersenGens { + B: RistrettoPoint::from_hash(hasher), + B_blinding: B_BLINDING, + } +} + /// Applies a hash function and returns a RistrettoPoint. pub fn hash_to_point(ristretto_public: &RistrettoPublic) -> RistrettoPoint { let mut hasher = Blake2b512::new(); @@ -45,3 +77,15 @@ pub fn hash_to_point(ristretto_public: &RistrettoPublic) -> RistrettoPoint { hasher.update(&ristretto_public.to_bytes()); RistrettoPoint::from_hash(hasher) } + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_generator0() { + assert_eq!( + generators(0).B, + hash_to_point(&RistrettoPublic::from(RISTRETTO_BASEPOINT_POINT)) + ) + } +} diff --git a/transaction/core/src/ring_signature/rct_bulletproofs.rs b/transaction/core/src/ring_signature/rct_bulletproofs.rs index 7badacc7e3..6c3fec472d 100644 --- a/transaction/core/src/ring_signature/rct_bulletproofs.rs +++ b/transaction/core/src/ring_signature/rct_bulletproofs.rs @@ -13,7 +13,7 @@ use bulletproofs_og::RangeProof; use core::convert::TryFrom; use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint}; use mc_common::HashSet; -use mc_crypto_digestible::Digestible; +use mc_crypto_digestible::{DigestTranscript, Digestible, MerlinTranscript}; use mc_crypto_keys::{CompressedRistrettoPublic, RistrettoPrivate}; use mc_util_serial::prost::Message; use rand_core::{CryptoRng, RngCore}; @@ -21,12 +21,13 @@ use serde::{Deserialize, Serialize}; use crate::{ constants::FEE_BLINDING, + domain_separators::EXTENDED_MESSAGE_DOMAIN_TAG, range_proofs::{check_range_proofs, generate_range_proofs}, - ring_signature::{mlsag::RingMLSAG, Error, KeyImage, Scalar, GENERATORS}, - Commitment, CompressedCommitment, + ring_signature::{generators, mlsag::RingMLSAG, Error, KeyImage, Scalar}, + BlockVersion, Commitment, CompressedCommitment, }; -/// An RCT_TYPE_BULLETPROOFS_2 signature. +/// An RCT_TYPE_BULLETPROOFS_2 signature #[derive(Clone, Digestible, Eq, PartialEq, Serialize, Deserialize, Message)] pub struct SignatureRctBulletproofs { /// Signature for each input ring. @@ -50,6 +51,7 @@ impl SignatureRctBulletproofs { /// Sign. /// /// # Arguments + /// * `block_version` - This may influence details of the signature /// * `message` - The messages to be signed, e.g. Hash(TxPrefix). /// * `rings` - One or more rings of one-time addresses and amount /// commitments. @@ -59,22 +61,27 @@ impl SignatureRctBulletproofs { /// * `output_values_and_blindings` - Value and blinding for each output /// amount commitment. /// * `fee` - Value of the implicit fee output. + /// * `token id` - This determines the pedersen generator for commitments pub fn sign( + block_version: BlockVersion, message: &[u8; 32], rings: &[Vec<(CompressedRistrettoPublic, CompressedCommitment)>], real_input_indices: &[usize], input_secrets: &[(RistrettoPrivate, u64, Scalar)], output_values_and_blindings: &[(u64, Scalar)], fee: u64, + token_id: u32, rng: &mut CSPRNG, ) -> Result { sign_with_balance_check( + block_version, message, rings, real_input_indices, input_secrets, output_values_and_blindings, fee, + token_id, true, rng, ) @@ -83,20 +90,28 @@ impl SignatureRctBulletproofs { /// Verify. /// /// # Arguments + /// * `block_version` - This may influence details of the signature /// * `message` - The signed message. /// * `rings` - One or more rings of one-time addresses and amount /// commitments. /// * `output_commitments` - Output amount commitments. /// * `fee` - Value of the implicit fee output. - /// * `rng` - + /// * `token id` - This determines the pedersen generator for commitments + /// * `rng` - randomness pub fn verify( &self, + block_version: BlockVersion, message: &[u8; 32], rings: &[Vec<(CompressedRistrettoPublic, CompressedCommitment)>], output_commitments: &[CompressedCommitment], fee: u64, + token_id: u32, rng: &mut CSPRNG, ) -> Result<(), Error> { + if !block_version.masked_token_id_feature_is_supported() && token_id != 0 { + return Err(Error::TokenIdNotAllowed); + } + // Signature must contain one ring signature for each ring. if rings.len() != self.ring_signatures.len() { return Err(Error::LengthMismatch( @@ -140,6 +155,9 @@ impl SignatureRctBulletproofs { decompressed_pseudo_output_commitments.push(commitment); } + // Compute the pedersen generators for this token_id + let generator = generators(token_id); + // pseudo_output_commitments and output commitments must be in [0, 2^64). { let commitments: Vec = self @@ -152,10 +170,17 @@ impl SignatureRctBulletproofs { let range_proof = RangeProof::from_bytes(&self.range_proof_bytes) .map_err(|_e| Error::RangeProofError)?; - check_range_proofs(&range_proof, &commitments, rng) + check_range_proofs(&range_proof, &commitments, &generator, rng) .map_err(|_e| Error::RangeProofError)?; } + // Compute sum of pseudo outputs + let sum_of_pseudo_output_commitments: RistrettoPoint = + decompressed_pseudo_output_commitments + .iter() + .map(|commitment| commitment.point) + .sum(); + // Output commitments - pseudo_outputs must be zero. { let sum_of_output_commitments: RistrettoPoint = decompressed_output_commitments @@ -163,23 +188,18 @@ impl SignatureRctBulletproofs { .map(|commitment| commitment.point) .sum(); - let sum_of_pseudo_output_commitments: RistrettoPoint = - decompressed_pseudo_output_commitments - .iter() - .map(|commitment| commitment.point) - .sum(); - // The implicit fee output. - let fee_commitment = GENERATORS.commit(Scalar::from(fee), *FEE_BLINDING); + let fee_commitment = generator.commit(Scalar::from(fee), *FEE_BLINDING); let difference = sum_of_output_commitments + fee_commitment - sum_of_pseudo_output_commitments; - if difference != GENERATORS.commit(Scalar::zero(), Scalar::zero()) { + if difference != generator.commit(Scalar::zero(), Scalar::zero()) { return Err(Error::ValueNotConserved); } } // Extend the message with the range proof and pseudo_output_commitments. - let extended_message = extend_message( + let extended_message_digest = compute_extended_message_either_version( + block_version, message, &self.pseudo_output_commitments, &self.range_proof_bytes, @@ -189,7 +209,7 @@ impl SignatureRctBulletproofs { for (i, ring) in rings.iter().enumerate() { let ring_signature = &self.ring_signatures[i]; let pseudo_output = self.pseudo_output_commitments[i]; - ring_signature.verify(&extended_message, ring, &pseudo_output)?; + ring_signature.verify(&extended_message_digest, ring, &pseudo_output)?; } // Signature is valid. @@ -208,6 +228,7 @@ impl SignatureRctBulletproofs { /// Sign, with optional check for inputs = outputs. /// /// # Arguments +/// * `block_version` - This may influence details of the signature /// * `message` - The messages to be signed, e.g. Hash(TxPrefix). /// * `rings` - One or more rings of one-time addresses and amount commitments. /// * `real_input_indices` - The index of the real input in each ring. @@ -217,17 +238,23 @@ impl SignatureRctBulletproofs { /// commitment. /// * `fee` - Value of the implicit fee output. /// * `check_value_is_preserved` - If true, check that the value of inputs -/// equals value of outputs. +/// * `rng` - randomness fn sign_with_balance_check( + block_version: BlockVersion, message: &[u8; 32], rings: &[Vec<(CompressedRistrettoPublic, CompressedCommitment)>], real_input_indices: &[usize], input_secrets: &[(RistrettoPrivate, u64, Scalar)], output_values_and_blindings: &[(u64, Scalar)], fee: u64, + token_id: u32, check_value_is_preserved: bool, rng: &mut CSPRNG, ) -> Result { + if !block_version.masked_token_id_feature_is_supported() && token_id != 0 { + return Err(Error::TokenIdNotAllowed); + } + if rings.is_empty() { return Err(Error::NoInputs); } @@ -281,6 +308,9 @@ fn sign_with_balance_check( .map(|((_, value, _), blinding)| (*value, *blinding)) .collect(); + // Compute the pedersen generators for this token_id + let generator = generators(token_id); + let (range_proof, commitments) = { // The implicit fee output is omitted from the range proof because it is known. @@ -289,26 +319,27 @@ fn sign_with_balance_check( .chain(output_values_and_blindings.iter()) .map(|(value, blinding)| (*value, *blinding)) .unzip(); - generate_range_proofs(&values, &blindings, rng).map_err(|_e| Error::RangeProofError)? + generate_range_proofs(&values, &blindings, &generator, rng) + .map_err(|_e| Error::RangeProofError)? }; if check_value_is_preserved { let sum_of_output_commitments: RistrettoPoint = output_values_and_blindings .iter() - .map(|(value, blinding)| GENERATORS.commit(Scalar::from(*value), *blinding)) + .map(|(value, blinding)| generator.commit(Scalar::from(*value), *blinding)) .sum(); let sum_of_pseudo_output_commitments: RistrettoPoint = pseudo_output_values_and_blindings .iter() - .map(|(value, blinding)| GENERATORS.commit(Scalar::from(*value), *blinding)) + .map(|(value, blinding)| generator.commit(Scalar::from(*value), *blinding)) .sum(); // The implicit fee output. - let fee_commitment = GENERATORS.commit(Scalar::from(fee), *FEE_BLINDING); + let fee_commitment = generator.commit(Scalar::from(fee), *FEE_BLINDING); let difference = sum_of_output_commitments + fee_commitment - sum_of_pseudo_output_commitments; - if difference != GENERATORS.commit(Scalar::zero(), Scalar::zero()) { + if difference != generator.commit(Scalar::zero(), Scalar::zero()) { return Err(Error::ValueNotConserved); } } @@ -322,7 +353,12 @@ fn sign_with_balance_check( // Extend the message with the range proof and pseudo_output_commitments. // This ensures that they are signed by each RingMLSAG. let range_proof_bytes = range_proof.to_bytes(); - let extended_message = extend_message(message, &pseudo_output_commitments, &range_proof_bytes); + let extended_message_digest = compute_extended_message_either_version( + block_version, + message, + &pseudo_output_commitments, + &range_proof_bytes, + ); // Prove that the signer is allowed to spend a public key in each ring, and that // the input's value equals the value of the pseudo_output. @@ -331,13 +367,14 @@ fn sign_with_balance_check( let real_index = real_input_indices[i]; let (onetime_private_key, value, blinding) = input_secrets[i]; let ring_signature = RingMLSAG::sign( - &extended_message, + &extended_message_digest, &rings[i], real_index, &onetime_private_key, value, &blinding, &pseudo_output_blindings[i], + &generator, rng, )?; ring_signatures.push(ring_signature); @@ -350,7 +387,41 @@ fn sign_with_balance_check( }) } +/// Toggles between old-style and new-style extended message +fn compute_extended_message_either_version( + block_version: BlockVersion, + message: &[u8], + pseudo_output_commitments: &[CompressedCommitment], + range_proof_bytes: &[u8], +) -> Vec { + if block_version >= BlockVersion::THREE { + // New-style extended message using merlin + digest_extended_message(message, pseudo_output_commitments, range_proof_bytes).to_vec() + } else { + // Old-style extended message + extend_message(message, pseudo_output_commitments, range_proof_bytes) + } +} + +/// Computes a merlin digest of message, pseudo_output_commitments, range proof, +/// proof_of_opening +fn digest_extended_message( + message: &[u8], + pseudo_output_commitments: &[CompressedCommitment], + range_proof_bytes: &[u8], +) -> [u8; 32] { + let mut transcript = MerlinTranscript::new(EXTENDED_MESSAGE_DOMAIN_TAG.as_bytes()); + message.append_to_transcript(b"message", &mut transcript); + pseudo_output_commitments.append_to_transcript(b"pseudo_output_commitments", &mut transcript); + range_proof_bytes.append_to_transcript(b"range_proof_bytes", &mut transcript); + + let mut output = [0u8; 32]; + transcript.extract_digest(&mut output); + output +} + /// Concatenates [message || pseudo_output_commitments || range_proof]. +/// (Used before block version three) fn extend_message( message: &[u8], pseudo_output_commitments: &[CompressedCommitment], @@ -369,13 +440,14 @@ fn extend_message( #[cfg(test)] mod rct_bulletproofs_tests { - use super::sign_with_balance_check; + use super::*; use crate::{ range_proofs::generate_range_proofs, - ring_signature::{Error, KeyImage, SignatureRctBulletproofs}, + ring_signature::{generators, Error, KeyImage, PedersenGens}, CompressedCommitment, }; use alloc::vec::Vec; + use core::convert::TryInto; use curve25519_dalek::scalar::Scalar; use mc_crypto_keys::{CompressedRistrettoPublic, RistrettoPrivate, RistrettoPublic}; use mc_util_from_random::FromRandom; @@ -401,10 +473,21 @@ mod rct_bulletproofs_tests { /// Value and blinding for each output amount commitment. output_values_and_blindings: Vec<(u64, Scalar)>, + + /// Block Version + block_version: BlockVersion, + + /// Token id + token_id: u32, } impl SignatureParams { + fn generator(&self) -> PedersenGens { + generators(self.token_id) + } + fn random( + block_version: BlockVersion, num_inputs: usize, num_mixins: usize, rng: &mut RNG, @@ -412,6 +495,14 @@ mod rct_bulletproofs_tests { let mut message = [0u8; 32]; rng.fill_bytes(&mut message); + let token_id = if block_version.masked_token_id_feature_is_supported() { + rng.next_u32() + } else { + 0 + }; + + let generator = generators(token_id); + let mut rings = Vec::new(); let mut real_input_indices = Vec::new(); let mut input_secrets = Vec::new(); @@ -425,7 +516,7 @@ mod rct_bulletproofs_tests { let commitment = { let value = rng.next_u64(); let blinding = Scalar::random(rng); - CompressedCommitment::new(value, blinding) + CompressedCommitment::new(value, blinding, &generator) }; ring.push((address, commitment)); } @@ -436,7 +527,7 @@ mod rct_bulletproofs_tests { let value = rng.next_u64(); let blinding = Scalar::random(rng); - let commitment = CompressedCommitment::new(value, blinding); + let commitment = CompressedCommitment::new(value, blinding, &generator); let real_index = rng.next_u64() as usize % (num_mixins + 1); ring.insert(real_index, (onetime_public_key, commitment)); @@ -461,19 +552,60 @@ mod rct_bulletproofs_tests { real_input_indices, input_secrets, output_values_and_blindings, + block_version, + token_id, } } fn get_output_commitments(&self) -> Vec { self.output_values_and_blindings .iter() - .map(|(value, blinding)| CompressedCommitment::new(*value, *blinding)) + .map(|(value, blinding)| { + CompressedCommitment::new(*value, *blinding, &self.generator()) + }) .collect() } + + fn sign( + &self, + fee: u64, + rng: &mut RNG, + ) -> Result { + SignatureRctBulletproofs::sign( + self.block_version, + &self.message, + &self.rings, + &self.real_input_indices, + &self.input_secrets, + &self.output_values_and_blindings, + fee, + self.token_id, + rng, + ) + } + + fn sign_without_balance_check( + &self, + fee: u64, + rng: &mut RNG, + ) -> Result { + sign_with_balance_check( + self.block_version, + &self.message, + &self.rings, + &self.real_input_indices, + &self.input_secrets, + &self.output_values_and_blindings, + fee, + self.token_id, + false, + rng, + ) + } } proptest! { - #![proptest_config(ProptestConfig::with_cases(3))] + #![proptest_config(ProptestConfig::with_cases(6))] #[test] // `sign`should return an error if `rings` is empty. @@ -481,20 +613,14 @@ mod rct_bulletproofs_tests { num_inputs in 1..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let mut params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let mut params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); params.rings = Vec::new(); - let result = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - 0, - &mut rng, - ); + let result = params.sign(0, &mut rng); match result { Err(Error::NoInputs) => {} // OK, @@ -509,20 +635,14 @@ mod rct_bulletproofs_tests { num_inputs in 1..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let mut params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let mut params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); params.rings[0] = Vec::new(); - let result = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - 0, - &mut rng, - ); + let result = params.sign(0, &mut rng); match result { Err(Error::InvalidRingSize(0)) => {} // OK, @@ -536,19 +656,12 @@ mod rct_bulletproofs_tests { num_inputs in 1..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let params = SignatureParams::random(num_inputs, num_mixins, &mut rng); - let signature = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - 0, - &mut rng, - ) - .unwrap(); + let params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); + let signature = params.sign(0, &mut rng).unwrap(); // The signature must contain one ring signature per input. assert_eq!(signature.ring_signatures.len(), num_inputs); @@ -568,29 +681,24 @@ mod rct_bulletproofs_tests { num_inputs in 1..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); let fee = 0; - let signature = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - fee, - &mut rng, - ) - .unwrap(); + let signature = params.sign(fee, &mut rng).unwrap(); let result = signature.verify( + block_version, ¶ms.message, ¶ms.rings, ¶ms.get_output_commitments(), fee, + params.token_id, &mut rng, ); - assert!(result.is_ok()); + result.unwrap(); } #[test] @@ -599,30 +707,26 @@ mod rct_bulletproofs_tests { num_inputs in 1..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); let fee = 0; - let mut signature = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - fee, - &mut rng, - ) - .unwrap(); + + let mut signature = params.sign(fee, &mut rng).unwrap(); // Modify an MLSAG ring signature let index = rng.next_u64() as usize % (num_inputs); signature.ring_signatures[index].key_image = KeyImage::from(rng.next_u64()); let result = signature.verify( + block_version, ¶ms.message, ¶ms.rings, ¶ms.get_output_commitments(), fee, + params.token_id, &mut rng, ); @@ -635,9 +739,11 @@ mod rct_bulletproofs_tests { num_inputs in 1..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let mut params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let mut params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); let fee = 0; // Modify an output value { @@ -647,23 +753,15 @@ mod rct_bulletproofs_tests { } // Sign, without checking that value is preserved. - let invalid_signature = sign_with_balance_check( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - fee, - false, - &mut rng, - ) - .unwrap(); + let invalid_signature = params.sign_without_balance_check(fee, &mut rng).unwrap(); let result = invalid_signature.verify( + block_version, ¶ms.message, ¶ms.rings, ¶ms.get_output_commitments(), fee, + params.token_id, &mut rng, ); @@ -676,20 +774,13 @@ mod rct_bulletproofs_tests { num_inputs in 1..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); let fee = 0; - let mut signature = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - fee, - &mut rng, - ) - .unwrap(); + let mut signature = params.sign(fee, &mut rng).unwrap(); // Modify the range proof let wrong_range_proof = { @@ -699,17 +790,19 @@ mod rct_bulletproofs_tests { .map(|_value| Scalar::random(&mut rng)) .collect(); let (range_proof, _commitments) = - generate_range_proofs(&values, &blindings, &mut rng).unwrap(); + generate_range_proofs(&values, &blindings, ¶ms.generator(), &mut rng).unwrap(); range_proof }; signature.range_proof_bytes = wrong_range_proof.to_bytes(); let result = signature.verify( + block_version, ¶ms.message, ¶ms.rings, ¶ms.get_output_commitments(), fee, + params.token_id, &mut rng, ); @@ -722,9 +815,11 @@ mod rct_bulletproofs_tests { num_inputs in 4..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let mut params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let mut params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); let fee = 0; // Duplicate one of the rings. @@ -733,22 +828,15 @@ mod rct_bulletproofs_tests { params.input_secrets[2] = params.input_secrets[3].clone(); params.real_input_indices[2] = params.real_input_indices[3]; - let signature = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - fee, - &mut rng, - ) - .unwrap(); + let signature = params.sign(fee, &mut rng).unwrap(); let result = signature.verify( + block_version, ¶ms.message, ¶ms.rings, ¶ms.get_output_commitments(), fee, + params.token_id, &mut rng, ); @@ -761,20 +849,13 @@ mod rct_bulletproofs_tests { num_inputs in 4..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); let fee = 0; - let signature = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - fee, - &mut rng, - ) - .unwrap(); + let signature = params.sign(fee, &mut rng).unwrap(); use mc_util_serial::prost::Message; @@ -787,45 +868,42 @@ mod rct_bulletproofs_tests { assert_eq!(signature, recovered_signature); } + #[test] // `verify` should accept valid signatures with correct fee. fn verify_with_fee( num_inputs in 2..8usize, num_mixins in 1..17usize, seed in any::<[u8; 32]>(), + block_version in 2..=3u32, ) { + let block_version: BlockVersion = block_version.try_into().unwrap(); let mut rng: StdRng = SeedableRng::from_seed(seed); - let mut params = SignatureParams::random(num_inputs, num_mixins, &mut rng); + let mut params = SignatureParams::random(block_version, num_inputs, num_mixins, &mut rng); // Remove one of the outputs, and use its value as the fee. This conserves value. let (fee, _) = params.output_values_and_blindings.pop().unwrap(); - let signature = SignatureRctBulletproofs::sign( - ¶ms.message, - ¶ms.rings, - ¶ms.real_input_indices, - ¶ms.input_secrets, - ¶ms.output_values_and_blindings, - fee, - &mut rng, - ) - .unwrap(); - + let signature = params.sign(fee, &mut rng).unwrap(); let result = signature.verify( + block_version, ¶ms.message, ¶ms.rings, ¶ms.get_output_commitments(), fee, + params.token_id, &mut rng, ); - assert!(result.is_ok()); + result.unwrap(); // Verify should fail if the signature disagrees with the fee. let wrong_fee = fee + 1; match signature.verify( + block_version, ¶ms.message, ¶ms.rings, ¶ms.get_output_commitments(), wrong_fee, + params.token_id, &mut rng, ) { Err(Error::ValueNotConserved) => {} // Expected @@ -837,5 +915,111 @@ mod rct_bulletproofs_tests { } + #[test] + // block version two signatures should not validate at block version three + fn validate_block_version_two_as_three_should_fail( + num_inputs in 2..8usize, + num_mixins in 1..17usize, + seed in any::<[u8; 32]>(), + ) { + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut params = SignatureParams::random(BlockVersion::TWO, num_inputs, num_mixins, &mut rng); + // Remove one of the outputs, and use its value as the fee. This conserves value. + let (fee, _) = params.output_values_and_blindings.pop().unwrap(); + + let signature = params.sign(fee, &mut rng).unwrap(); + + let result = signature.verify( + BlockVersion::THREE, + ¶ms.message, + ¶ms.rings, + ¶ms.get_output_commitments(), + fee, + params.token_id, + &mut rng, + ); + assert!(result.is_err()); + } + + #[test] + // block version three signatures should not validate at block version two + fn validate_block_version_three_as_two_should_fail( + num_inputs in 2..8usize, + num_mixins in 1..17usize, + seed in any::<[u8; 32]>(), + ) { + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut params = SignatureParams::random(BlockVersion::THREE, num_inputs, num_mixins, &mut rng); + // Remove one of the outputs, and use its value as the fee. This conserves value. + let (fee, _) = params.output_values_and_blindings.pop().unwrap(); + + let signature = params.sign(fee, &mut rng).unwrap(); + + let result = signature.verify( + BlockVersion::TWO, + ¶ms.message, + ¶ms.rings, + ¶ms.get_output_commitments(), + fee, + params.token_id, + &mut rng, + ); + assert!(result.is_err()); + } + + #[test] + // block version three signatures should not validate if we change the token id + fn validate_block_version_three_with_changed_token_id_should_fail( + num_inputs in 2..8usize, + num_mixins in 1..17usize, + seed in any::<[u8; 32]>(), + ) { + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut params = SignatureParams::random(BlockVersion::THREE, num_inputs, num_mixins, &mut rng); + // Remove one of the outputs, and use its value as the fee. This conserves value. + let (fee, _) = params.output_values_and_blindings.pop().unwrap(); + + let signature = params.sign(fee, &mut rng).unwrap(); + + signature.verify( + BlockVersion::THREE, + ¶ms.message, + ¶ms.rings, + ¶ms.get_output_commitments(), + fee, + params.token_id, + &mut rng, + ).unwrap(); + + + let result = signature.verify( + BlockVersion::THREE, + ¶ms.message, + ¶ms.rings, + ¶ms.get_output_commitments(), + fee, + params.token_id + 1, + &mut rng, + ); + assert_eq!(result, Err(Error::RangeProofError)); + } + + #[test] + // block version two signatures should not work if token id is not zero + fn validate_block_version_two_with_token_id_should_fail( + num_inputs in 2..8usize, + num_mixins in 1..17usize, + seed in any::<[u8; 32]>(), + ) { + let mut rng: StdRng = SeedableRng::from_seed(seed); + let mut params = SignatureParams::random(BlockVersion::TWO, num_inputs, num_mixins, &mut rng); + // Remove one of the outputs, and use its value as the fee. This conserves value. + let (fee, _) = params.output_values_and_blindings.pop().unwrap(); + + params.token_id = 1; + + assert_eq!(params.sign(fee, &mut rng), Err(Error::TokenIdNotAllowed)); + } + } // end proptest } diff --git a/transaction/core/src/token.rs b/transaction/core/src/token.rs index f8d70b622b..c50cd98659 100644 --- a/transaction/core/src/token.rs +++ b/transaction/core/src/token.rs @@ -1,6 +1,8 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation -use core::{fmt, hash::Hash, ops::Deref}; +//! A new-type wrapper for representing TokenIds + +use core::{fmt, hash::Hash, num::ParseIntError, ops::Deref, str::FromStr}; use mc_crypto_digestible::Digestible; use serde::{Deserialize, Serialize}; @@ -23,6 +25,7 @@ impl fmt::Display for TokenId { } impl TokenId { + /// Represents the MobileCoin token id for MOB token pub const MOB: Self = Self(0); } @@ -34,6 +37,26 @@ impl Deref for TokenId { } } +impl FromStr for TokenId { + type Err = ParseIntError; + fn from_str(src: &str) -> Result { + let src = u32::from_str(src)?; + Ok(TokenId(src)) + } +} + +impl PartialEq for TokenId { + fn eq(&self, other: &u32) -> bool { + self.0 == *other + } +} + +impl PartialEq for u32 { + fn eq(&self, other: &TokenId) -> bool { + *self == other.0 + } +} + /// A generic representation of a token. pub trait Token { /// Token Id. @@ -43,6 +66,7 @@ pub trait Token { const MINIMUM_FEE: u64; } +/// Exports structures which expose constants related to tokens. pub mod tokens { use super::*; use crate::constants::MICROMOB_TO_PICOMOB; diff --git a/transaction/core/src/tx.rs b/transaction/core/src/tx.rs index 6d294bde6b..ef7fd10753 100644 --- a/transaction/core/src/tx.rs +++ b/transaction/core/src/tx.rs @@ -1,5 +1,7 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! Definition of a MobileCoin transaction and a MobileCoin TxOut + use alloc::vec::Vec; use core::{convert::TryFrom, fmt}; use mc_account_keys::PublicAddress; @@ -14,7 +16,7 @@ use prost::Message; use serde::{Deserialize, Serialize}; use crate::{ - amount::{Amount, AmountError}, + amount::{Amount, AmountData, AmountError}, domain_separators::TXOUT_CONFIRMATION_NUMBER_DOMAIN_TAG, encrypted_fog_hint::EncryptedFogHint, get_tx_out_shared_secret, @@ -22,7 +24,7 @@ use crate::{ memo::{EncryptedMemo, MemoPayload}, onetime_keys::{create_shared_secret, create_tx_out_public_key, create_tx_out_target_key}, ring_signature::{KeyImage, SignatureRctBulletproofs}, - CompressedCommitment, NewMemoError, NewTxError, + CompressedCommitment, NewMemoError, NewTxError, TokenId, }; /// Transaction hash length, in bytes. @@ -161,6 +163,10 @@ pub struct TxPrefix { /// The block index at which this transaction is no longer valid. #[prost(uint64, tag = "4")] pub tombstone_block: u64, + + /// Token id for this transaction + #[prost(fixed32, tag = "5")] + pub token_id: u32, } impl TxPrefix { @@ -172,11 +178,18 @@ impl TxPrefix { /// * `fee` - Transaction fee. /// * `tombstone_block` - The block index at which this transaction is no /// longer valid. - pub fn new(inputs: Vec, outputs: Vec, fee: u64, tombstone_block: u64) -> TxPrefix { + pub fn new( + inputs: Vec, + outputs: Vec, + fee: u64, + token_id: u32, + tombstone_block: u64, + ) -> TxPrefix { TxPrefix { inputs, outputs, fee, + token_id, tombstone_block, } } @@ -283,11 +296,12 @@ impl TxOut { /// * `hint` - Encrypted Fog hint for this output. pub fn new( value: u64, + token_id: TokenId, recipient: &PublicAddress, tx_private_key: &RistrettoPrivate, hint: EncryptedFogHint, ) -> Result { - TxOut::new_with_memo(value, recipient, tx_private_key, hint, |_| { + TxOut::new_with_memo(value, token_id, recipient, tx_private_key, hint, |_| { Ok(Some(MemoPayload::default())) }) .map_err(|err| match err { @@ -310,6 +324,7 @@ impl TxOut { /// MemoPayload, or a NewMemo error pub fn new_with_memo( value: u64, + token_id: TokenId, recipient: &PublicAddress, tx_private_key: &RistrettoPrivate, hint: EncryptedFogHint, @@ -320,7 +335,8 @@ impl TxOut { let shared_secret = create_shared_secret(recipient.view_public_key(), tx_private_key); - let amount = Amount::new(value, &shared_secret)?; + let amount_data = AmountData { value, token_id }; + let amount = Amount::new(amount_data, &shared_secret)?; let memo_ctxt = MemoContext { tx_public_key: &public_key, @@ -424,6 +440,7 @@ pub struct TxOutMembershipElement { } impl TxOutMembershipElement { + /// Create a new membership element pub fn new(range: Range, hash: [u8; 32]) -> Self { Self { range, @@ -492,6 +509,7 @@ impl TxOutConfirmationNumber { self.0.to_vec() } + /// Validate a confirmation number against tx pubkey and view private key pub fn validate( &self, tx_pubkey: &RistrettoPublic, @@ -556,7 +574,7 @@ mod tests { subaddress_matches_tx_out, tokens::Mob, tx::{Tx, TxIn, TxOut, TxPrefix}, - Amount, Token, + Amount, AmountData, Token, }; use alloc::vec::Vec; use core::convert::TryFrom; @@ -574,7 +592,11 @@ mod tests { let shared_secret = RistrettoPublic::from_random(&mut rng); let target_key = RistrettoPublic::from_random(&mut rng).into(); let public_key = RistrettoPublic::from_random(&mut rng).into(); - let amount = Amount::new(23u64, &shared_secret).unwrap(); + let amount_data = AmountData { + value: 23u64, + token_id: Mob::ID, + }; + let amount = Amount::new(amount_data, &shared_secret).unwrap(); TxOut { amount, target_key, @@ -603,6 +625,7 @@ mod tests { inputs: vec![tx_in], outputs: vec![tx_out], fee: Mob::MINIMUM_FEE, + token_id: *Mob::ID, tombstone_block: 23, }; @@ -632,7 +655,11 @@ mod tests { let shared_secret = RistrettoPublic::from_random(&mut rng); let target_key = RistrettoPublic::from_random(&mut rng).into(); let public_key = RistrettoPublic::from_random(&mut rng).into(); - let amount = Amount::new(23u64, &shared_secret).unwrap(); + let amount_data = AmountData { + value: 23u64, + token_id: Mob::ID, + }; + let amount = Amount::new(amount_data, &shared_secret).unwrap(); TxOut { amount, target_key, @@ -661,6 +688,7 @@ mod tests { inputs: vec![tx_in], outputs: vec![tx_out], fee: Mob::MINIMUM_FEE, + token_id: *Mob::ID, tombstone_block: 23, }; @@ -697,8 +725,14 @@ mod tests { let tx_private_key = RistrettoPrivate::from_random(&mut rng); // A tx out with an empty memo - let mut tx_out = - TxOut::new(13u64, &bob_addr, &tx_private_key, Default::default()).unwrap(); + let mut tx_out = TxOut::new( + 13u64, + Mob::ID, + &bob_addr, + &tx_private_key, + Default::default(), + ) + .unwrap(); assert!( tx_out.e_memo.is_some(), "All TxOut (except preexisting) should have a memo" @@ -733,6 +767,7 @@ mod tests { // A tx out with a memo let tx_out = TxOut::new_with_memo( 13u64, + Mob::ID, &bob_addr, &tx_private_key, Default::default(), @@ -766,6 +801,7 @@ mod tests { // A tx out with a memo let tx_out = TxOut::new_with_memo( 13u64, + Mob::ID, &bob.change_subaddress(), &tx_private_key, Default::default(), diff --git a/transaction/core/src/validation/error.rs b/transaction/core/src/validation/error.rs index 89aa1f117a..11c85c92da 100644 --- a/transaction/core/src/validation/error.rs +++ b/transaction/core/src/validation/error.rs @@ -128,6 +128,15 @@ pub enum TransactionValidationError { /// A TxOut includes a memo, but this is not allowed yet MemosNotAllowed, + + /// Tx indicates a token id that is not yet configured + TokenNotYetConfigured, + + /// A TxOut is missing the required masked token id field + MissingMaskedTokenId, + + /// A TxOut includes a masked token id, but this is not allowed yet + MaskedTokenIdNotAllowed, } impl From for TransactionValidationError { diff --git a/transaction/core/src/validation/mod.rs b/transaction/core/src/validation/mod.rs index 825fb8d920..ae06704a24 100644 --- a/transaction/core/src/validation/mod.rs +++ b/transaction/core/src/validation/mod.rs @@ -1,5 +1,7 @@ // Copyright (c) 2018-2021 The MobileCoin Foundation +//! Validation routines for a MobileCoin transaction + mod error; mod validate; diff --git a/transaction/core/src/validation/validate.rs b/transaction/core/src/validation/validate.rs index 63431086c2..d518b8f855 100644 --- a/transaction/core/src/validation/validate.rs +++ b/transaction/core/src/validation/validate.rs @@ -24,8 +24,11 @@ use rand_core::{CryptoRng, RngCore}; /// * `tx` - A pending transaction. /// * `current_block_index` - The index of the current block that is being /// built. +/// * `block_version` - The version of the transaction rules we are testing /// * `root_proofs` - Membership proofs for each input ring element contained in /// `tx`. +/// * `minimum_fee` - The minimum fee for the token indicated by +/// tx.prefix.token_id /// * `csprng` - Cryptographically secure random number generator. pub fn validate( tx: &Tx, @@ -56,7 +59,7 @@ pub fn validate( validate_membership_proofs(&tx.prefix, root_proofs)?; - validate_signature(tx, csprng)?; + validate_signature(block_version, tx, csprng)?; validate_transaction_fee(tx, minimum_fee)?; @@ -81,6 +84,18 @@ pub fn validate( validate_no_memos_exist(tx)?; } + // If masked token id is supported, then all outputs must have masked_token_id + // If masked token id is not yet supported, then no outputs may have + // masked_token_id + // + // Note: This rct_bulletproofs code enforces that token_id = 0 if this feature + // is not enabled + if block_version.masked_token_id_feature_is_supported() { + validate_masked_token_ids_exist(tx)?; + } else { + validate_no_masked_token_ids_exist(tx)?; + } + Ok(()) } @@ -237,6 +252,34 @@ fn validate_memos_exist(tx: &Tx) -> TransactionValidationResult<()> { Ok(()) } +/// All outputs have no masked token id (new-style TxOuts (Post MCIP #25) are +/// rejected) +fn validate_no_masked_token_ids_exist(tx: &Tx) -> TransactionValidationResult<()> { + if tx + .prefix + .outputs + .iter() + .any(|output| !output.amount.masked_token_id.is_empty()) + { + return Err(TransactionValidationError::MaskedTokenIdNotAllowed); + } + Ok(()) +} + +/// All outputs have a masked token id (old-style TxOuts (Pre MCIP #25) are +/// rejected) +fn validate_masked_token_ids_exist(tx: &Tx) -> TransactionValidationResult<()> { + if tx + .prefix + .outputs + .iter() + .any(|output| output.amount.masked_token_id.len() != 4) + { + return Err(TransactionValidationError::MissingMaskedTokenId); + } + Ok(()) +} + /// Verifies the transaction signature. /// /// A valid RctBulletproofs signature implies that: @@ -245,7 +288,9 @@ fn validate_memos_exist(tx: &Tx) -> TransactionValidationResult<()> { /// * Each key image corresponds to the spent ring element, /// * The outputs have values in [0,2^64), /// * The transaction does not create or destroy mobilecoins. +/// * The signature is valid according to the rules of this block version pub fn validate_signature( + block_version: BlockVersion, tx: &Tx, rng: &mut R, ) -> TransactionValidationResult<()> { @@ -268,7 +313,15 @@ pub fn validate_signature( let message = tx_prefix_hash.as_bytes(); tx.signature - .verify(message, &rings, &output_commitments, tx.prefix.fee, rng) + .verify( + block_version, + message, + &rings, + &output_commitments, + tx.prefix.fee, + tx.prefix.token_id, + rng, + ) .map_err(TransactionValidationError::InvalidTransactionSignature) } @@ -928,7 +981,7 @@ mod tests { for block_version in BlockVersion::iterator() { let (tx, _ledger) = create_test_tx(block_version); - assert_eq!(validate_signature(&tx, &mut rng), Ok(())); + assert_eq!(validate_signature(block_version, &tx, &mut rng), Ok(())); } } @@ -943,7 +996,7 @@ mod tests { // Remove an input. tx.prefix.inputs[0].ring.pop(); - match validate_signature(&tx, &mut rng) { + match validate_signature(block_version, &tx, &mut rng) { Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. Err(e) => { panic!("Unexpected error {}", e); @@ -965,7 +1018,7 @@ mod tests { let output = tx.prefix.outputs.get(0).unwrap().clone(); tx.prefix.outputs.push(output); - match validate_signature(&tx, &mut rng) { + match validate_signature(block_version, &tx, &mut rng) { Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. Err(e) => { panic!("Unexpected error {}", e); @@ -985,7 +1038,65 @@ mod tests { tx.prefix.fee = tx.prefix.fee + 1; - match validate_signature(&tx, &mut rng) { + match validate_signature(block_version, &tx, &mut rng) { + Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. + Err(e) => { + panic!("Unexpected error {}", e); + } + Ok(()) => panic!("Unexpected success"), + } + } + } + + #[test] + // Should return InvalidTransactionSignature if the token_id is modified + fn test_transaction_signature_err_modified_token_id() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); + + for _ in 0..3 { + let (mut tx, _ledger) = create_test_tx(BlockVersion::THREE); + + tx.prefix.token_id = tx.prefix.token_id + 1; + + match validate_signature(BlockVersion::THREE, &tx, &mut rng) { + Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. + Err(e) => { + panic!("Unexpected error {}", e); + } + Ok(()) => panic!("Unexpected success"), + } + } + } + + #[test] + // Should return InvalidTransactionSignature if block version 2 is validated as + // 3 + fn test_transaction_signature_err_version_two_as_three() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); + + for _ in 0..3 { + let (tx, _ledger) = create_test_tx(BlockVersion::TWO); + + match validate_signature(BlockVersion::THREE, &tx, &mut rng) { + Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. + Err(e) => { + panic!("Unexpected error {}", e); + } + Ok(()) => panic!("Unexpected success"), + } + } + } + + #[test] + // Should return InvalidTransactionSignature if block version 3 is validated as + // 2 + fn test_transaction_signature_err_version_three_as_two() { + let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); + + for _ in 0..3 { + let (tx, _ledger) = create_test_tx(BlockVersion::THREE); + + match validate_signature(BlockVersion::TWO, &tx, &mut rng) { Err(TransactionValidationError::InvalidTransactionSignature(_e)) => {} // Expected. Err(e) => { panic!("Unexpected error {}", e); diff --git a/transaction/core/test-utils/src/lib.rs b/transaction/core/test-utils/src/lib.rs index d6066ebaab..a63a7c639f 100644 --- a/transaction/core/test-utils/src/lib.rs +++ b/transaction/core/test-utils/src/lib.rs @@ -53,16 +53,16 @@ pub fn create_transaction( // Get the output value. let tx_out_public_key = RistrettoPublic::try_from(&tx_out.public_key).unwrap(); let shared_secret = get_tx_out_shared_secret(sender.view_private_key(), &tx_out_public_key); - let (value, _blinding) = tx_out.amount.get_value(&shared_secret).unwrap(); + let (amount_data, _blinding) = tx_out.amount.get_value(&shared_secret).unwrap(); - assert!(value >= Mob::MINIMUM_FEE); + assert!(amount_data.value >= Mob::MINIMUM_FEE); create_transaction_with_amount( block_version, ledger, tx_out, sender, recipient, - value - Mob::MINIMUM_FEE, + amount_data.value - Mob::MINIMUM_FEE, Mob::MINIMUM_FEE, tombstone_block, rng, @@ -92,6 +92,7 @@ pub fn create_transaction_with_amount( ) -> Tx { let mut transaction_builder = TransactionBuilder::new( block_version, + Mob::ID, MockFogResolver::default(), EmptyMemoBuilder::default(), ); @@ -214,6 +215,7 @@ pub fn initialize_ledger( .map(|_i| { let mut tx_out = TxOut::new( value, + Mob::ID, &account_key.default_subaddress(), &RistrettoPrivate::from_random(rng), Default::default(), @@ -308,6 +310,7 @@ pub fn get_outputs( .map(|(recipient, value)| { let mut result = TxOut::new( *value, + Mob::ID, recipient, &RistrettoPrivate::from_random(rng), Default::default(), @@ -316,6 +319,7 @@ pub fn get_outputs( if !block_version.e_memo_feature_is_supported() { result.e_memo = None; } + result.amount.masked_token_id = Default::default(); result }) .collect() diff --git a/transaction/core/tests/digest-test-vectors.rs b/transaction/core/tests/digest-test-vectors.rs index 7edbd2640c..959672028a 100644 --- a/transaction/core/tests/digest-test-vectors.rs +++ b/transaction/core/tests/digest-test-vectors.rs @@ -2,7 +2,8 @@ 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, BlockVersion, + encrypted_fog_hint::EncryptedFogHint, tokens::Mob, tx::TxOut, Block, BlockContents, + BlockVersion, Token, }; use mc_util_from_random::FromRandom; use rand_core::{RngCore, SeedableRng}; @@ -23,6 +24,7 @@ fn test_origin_tx_outs() -> Vec { .map(|acct| { let mut tx_out = TxOut::new( rng.next_u32() as u64, + Mob::ID, &acct.default_subaddress(), &RistrettoPrivate::from_random(&mut rng), EncryptedFogHint::fake_onetime_hint(&mut rng), @@ -30,6 +32,8 @@ fn test_origin_tx_outs() -> Vec { .expect("Could not create TxOut"); // Origin TxOuts do not have encrypted memo fields. tx_out.e_memo = None; + // Origin TxOuts do not have masked token id + tx_out.amount.masked_token_id = Default::default(); tx_out }) .collect() diff --git a/transaction/std/src/error.rs b/transaction/std/src/error.rs index 879e47d857..5e827ef4dd 100644 --- a/transaction/std/src/error.rs +++ b/transaction/std/src/error.rs @@ -3,7 +3,7 @@ use displaydoc::Display; use mc_fog_report_validation::FogPubkeyError; use mc_transaction_core::{ - ring_signature, ring_signature::Error, AmountError, NewMemoError, NewTxError, + ring_signature, ring_signature::Error, AmountError, NewMemoError, NewTxError, TokenId, }; /// An error that can occur when using the TransactionBuilder @@ -24,6 +24,9 @@ pub enum TxBuilderError { /// Bad Amount: {0} BadAmount(AmountError), + /// Input had wrong token id: Expected {0}, Found {1} + WrongTokenType(TokenId, TokenId), + /// New Tx: {0} NewTx(NewTxError), @@ -50,6 +53,9 @@ pub enum TxBuilderError { /// Block version ({0} > {1}) is too new to be supported BlockVersionTooNew(u32, u32), + + /// Feature is not supported at this block version ({0}): {1} + FeatureNotSupportedAtBlockVersion(u32, &'static str), } impl From for TxBuilderError { diff --git a/transaction/std/src/transaction_builder.rs b/transaction/std/src/transaction_builder.rs index 6577b1abb5..c5407c06f7 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}, - BlockVersion, CompressedCommitment, MemoContext, MemoPayload, NewMemoError, Token, + BlockVersion, CompressedCommitment, MemoContext, MemoPayload, NewMemoError, Token, TokenId, }; use mc_util_from_random::FromRandom; use rand_core::{CryptoRng, RngCore}; @@ -43,6 +43,8 @@ pub struct TransactionBuilder { tombstone_block: u64, /// The fee paid in connection to this transaction fee: u64, + /// The token id for this transaction + token_id: TokenId, /// The source of validated fog pubkeys used for this transaction fog_resolver: FPR, /// The limit on the tombstone block value imposed pubkey_expiry values in @@ -68,10 +70,16 @@ impl TransactionBuilder { /// transaction pub fn new( block_version: BlockVersion, + token_id: TokenId, fog_resolver: FPR, memo_builder: MB, ) -> Self { - TransactionBuilder::new_with_box(block_version, fog_resolver, Box::new(memo_builder)) + TransactionBuilder::new_with_box( + block_version, + token_id, + fog_resolver, + Box::new(memo_builder), + ) } /// Initializes a new TransactionBuilder, using a Box @@ -84,6 +92,7 @@ impl TransactionBuilder { /// transaction pub fn new_with_box( block_version: BlockVersion, + token_id: TokenId, fog_resolver: FPR, memo_builder: Box, ) -> Self { @@ -93,6 +102,7 @@ impl TransactionBuilder { outputs_and_shared_secrets: Vec::new(), tombstone_block: u64::max_value(), fee: Mob::MINIMUM_FEE, + token_id, fog_resolver, fog_tombstone_block_limit: u64::max_value(), memo_builder: Some(memo_builder), @@ -237,8 +247,15 @@ impl TransactionBuilder { rng: &mut RNG, ) -> Result<(TxOut, TxOutConfirmationNumber), TxBuilderError> { let (hint, pubkey_expiry) = create_fog_hint(fog_hint_address, &self.fog_resolver, rng)?; - let (tx_out, shared_secret) = - create_output_with_fog_hint(value, recipient, hint, memo_fn, rng)?; + let (tx_out, shared_secret) = create_output_with_fog_hint( + self.block_version, + value, + self.token_id, + recipient, + hint, + memo_fn, + rng, + )?; self.impose_tombstone_block_limit(pubkey_expiry); @@ -309,6 +326,13 @@ impl TransactionBuilder { )); } + if !self.block_version.masked_token_id_feature_is_supported() && self.token_id != Mob::ID { + return Err(TxBuilderError::FeatureNotSupportedAtBlockVersion( + *self.block_version, + "nonzero token id", + )); + } + if self.input_credentials.is_empty() { return Err(TxBuilderError::NoInputs); } @@ -346,17 +370,23 @@ impl TransactionBuilder { .iter() .map(|(tx_out, shared_secret)| { let amount = &tx_out.amount; - let (value, blinding) = amount + let (amount_data, blinding) = amount .get_value(shared_secret) .expect("TransactionBuilder created an invalid Amount"); - (value, blinding) + (amount_data.value, blinding) }) .collect(); let (outputs, _shared_serets): (Vec, Vec<_>) = self.outputs_and_shared_secrets.into_iter().unzip(); - let tx_prefix = TxPrefix::new(inputs, outputs, self.fee, self.tombstone_block); + let tx_prefix = TxPrefix::new( + inputs, + outputs, + self.fee, + *self.token_id, + self.tombstone_block, + ); let mut rings: Vec> = Vec::new(); for input in &tx_prefix.inputs { @@ -383,18 +413,26 @@ impl TransactionBuilder { &input_credential.real_output_public_key, &input_credential.view_private_key, ); - let (value, blinding) = amount.get_value(&shared_secret)?; - input_secrets.push((onetime_private_key, value, blinding)); + let (amount_data, blinding) = amount.get_value(&shared_secret)?; + if amount_data.token_id != self.token_id { + return Err(TxBuilderError::WrongTokenType( + self.token_id, + amount_data.token_id, + )); + } + input_secrets.push((onetime_private_key, amount_data.value, blinding)); } let message = tx_prefix.hash().0; let signature = SignatureRctBulletproofs::sign( + self.block_version, &message, &rings, &real_input_indices, &input_secrets, &output_values_and_blindings, self.fee, + *self.token_id, rng, )?; @@ -409,20 +447,32 @@ impl TransactionBuilder { /// `fog_hint`. /// /// # Arguments +/// * `block_version` - Block version rules to conform to /// * `value` - Value of the output, in picoMOB. /// * `recipient` - Recipient's address. /// * `fog_hint` - The encrypted fog hint to use /// * `memo_fn` - The memo function to use -- see TxOut::new_with_memo docu /// * `rng` - fn create_output_with_fog_hint( + block_version: BlockVersion, value: u64, + token_id: TokenId, recipient: &PublicAddress, fog_hint: EncryptedFogHint, memo_fn: impl FnOnce(MemoContext) -> Result, NewMemoError>, rng: &mut RNG, ) -> Result<(TxOut, RistrettoPublic), TxBuilderError> { let private_key = RistrettoPrivate::from_random(rng); - let tx_out = TxOut::new_with_memo(value, recipient, &private_key, fog_hint, memo_fn)?; + let mut tx_out = + TxOut::new_with_memo(value, token_id, recipient, &private_key, fog_hint, memo_fn)?; + + if !block_version.e_memo_feature_is_supported() { + tx_out.e_memo = None; + } + if !block_version.masked_token_id_feature_is_supported() { + tx_out.amount.masked_token_id.clear(); + } + let shared_secret = create_shared_secret(recipient.view_public_key(), &private_key); Ok((tx_out, shared_secret)) } @@ -493,6 +543,7 @@ pub mod transaction_builder_tests { /// * A transaction output, and the shared secret for this TxOut. fn create_output( block_version: BlockVersion, + token_id: TokenId, value: u64, recipient: &PublicAddress, fog_resolver: &FPR, @@ -500,7 +551,9 @@ pub mod transaction_builder_tests { ) -> Result<(TxOut, RistrettoPublic), TxBuilderError> { let (hint, _pubkey_expiry) = create_fog_hint(recipient, fog_resolver, rng)?; create_output_with_fog_hint( + block_version, value, + token_id, recipient, hint, |_| { @@ -517,14 +570,18 @@ pub mod transaction_builder_tests { /// Creates a ring of of TxOuts. /// /// # Arguments + /// * `block_version` - The block version for the TxOut's + /// * `token_id` - The token id for the real element /// * `ring_size` - Number of elements in the ring. /// * `account` - Owner of one of the ring elements. /// * `value` - Value of the real element. + /// * `fog_resolver` - Fog public keys /// * `rng` - Randomness. /// /// Returns (ring, real_index) fn get_ring( block_version: BlockVersion, + token_id: TokenId, ring_size: usize, account: &AccountKey, value: u64, @@ -533,11 +590,16 @@ pub mod transaction_builder_tests { ) -> (Vec, usize) { let mut ring: Vec = Vec::new(); - // Create ring_size - 1 mixins. - for _i in 0..ring_size - 1 { + // Create ring_size - 1 mixins with assorted token ids + for idx in 0..ring_size - 1 { let address = AccountKey::random(rng).default_subaddress(); + let token_id = if block_version.masked_token_id_feature_is_supported() { + TokenId::from(idx as u32) + } else { + Mob::ID + }; let (tx_out, _) = - create_output(block_version, value, &address, fog_resolver, rng).unwrap(); + create_output(block_version, token_id, value, &address, fog_resolver, rng).unwrap(); ring.push(tx_out); } @@ -545,6 +607,7 @@ pub mod transaction_builder_tests { let real_index = (rng.next_u64() % ring_size as u64) as usize; let (tx_out, _) = create_output( block_version, + token_id, value, &account.default_subaddress(), fog_resolver, @@ -560,19 +623,31 @@ pub mod transaction_builder_tests { /// Creates an `InputCredentials` for an account. /// /// # Arguments + /// * `block_version` - Block version to use for the tx outs + /// * `token_id` - Token id for the real element /// * `account` - Owner of one of the ring elements. /// * `value` - Value of the real element. + /// * `fog_resolver` - Fog public keys /// * `rng` - Randomness. /// /// Returns (input_credentials) fn get_input_credentials( block_version: BlockVersion, + token_id: TokenId, account: &AccountKey, value: u64, fog_resolver: &FPR, rng: &mut RNG, ) -> InputCredentials { - let (ring, real_index) = get_ring(block_version, 3, account, value, fog_resolver, rng); + let (ring, real_index) = get_ring( + block_version, + token_id, + 3, + account, + value, + fog_resolver, + rng, + ); let real_output = ring[real_index].clone(); let onetime_private_key = recover_onetime_private_key( @@ -603,6 +678,7 @@ pub mod transaction_builder_tests { // Uses TransactionBuilder to build a transaction. fn get_transaction( block_version: BlockVersion, + token_id: TokenId, num_inputs: usize, num_outputs: usize, sender: &AccountKey, @@ -612,6 +688,7 @@ pub mod transaction_builder_tests { ) -> Result { let mut transaction_builder = TransactionBuilder::new( block_version, + token_id, fog_resolver.clone(), EmptyMemoBuilder::default(), ); @@ -620,8 +697,14 @@ pub mod transaction_builder_tests { // Inputs for _i in 0..num_inputs { - let input_credentials = - get_input_credentials(block_version, sender, input_value, &fog_resolver, rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + sender, + input_value, + &fog_resolver, + rng, + ); transaction_builder.add_input(input_credentials); } @@ -645,6 +728,9 @@ pub mod transaction_builder_tests { vec![ (BlockVersion::try_from(1).unwrap(), TokenId::from(0)), (BlockVersion::try_from(2).unwrap(), TokenId::from(0)), + (BlockVersion::try_from(3).unwrap(), TokenId::from(0)), + (BlockVersion::try_from(3).unwrap(), TokenId::from(1)), + (BlockVersion::try_from(3).unwrap(), TokenId::from(2)), ] } @@ -653,7 +739,7 @@ pub mod transaction_builder_tests { fn test_simple_transaction() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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); @@ -661,13 +747,13 @@ pub mod transaction_builder_tests { // Mint an initial collection of outputs, including one belonging to Alice. let input_credentials = - get_input_credentials(block_version, &sender, value, &fpr, &mut rng); + get_input_credentials(block_version, token_id, &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(block_version, fpr, EmptyMemoBuilder::default()); + TransactionBuilder::new(block_version, token_id, fpr, EmptyMemoBuilder::default()); transaction_builder.add_input(input_credentials); let (_txout, confirmation) = transaction_builder @@ -705,7 +791,7 @@ pub mod transaction_builder_tests { } // The transaction should have a valid signature. - assert!(validate_signature(&tx, &mut rng).is_ok()); + assert!(validate_signature(block_version, &tx, &mut rng).is_ok()); } } @@ -714,7 +800,7 @@ pub mod transaction_builder_tests { fn test_simple_fog_transaction() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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); @@ -734,14 +820,24 @@ pub mod transaction_builder_tests { let value = 1475 * MILLIMOB_TO_PICOMOB; - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &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(block_version, fog_resolver, EmptyMemoBuilder::default()); + let mut transaction_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver, + EmptyMemoBuilder::default(), + ); transaction_builder.add_input(input_credentials); let (_txout, confirmation) = transaction_builder @@ -795,7 +891,7 @@ pub mod transaction_builder_tests { } // The transaction should have a valid signature. - assert!(validate_signature(&tx, &mut rng).is_ok()); + assert!(validate_signature(block_version, &tx, &mut rng).is_ok()); } } @@ -804,7 +900,7 @@ pub mod transaction_builder_tests { fn test_custom_fog_hint_address() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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(); @@ -825,12 +921,19 @@ pub mod transaction_builder_tests { let mut transaction_builder = TransactionBuilder::new( block_version, + token_id, fog_resolver.clone(), EmptyMemoBuilder::default(), ); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -876,7 +979,7 @@ pub mod transaction_builder_tests { fn test_fog_pubkey_expiry_limit_enforced() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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(); @@ -898,14 +1001,21 @@ pub mod transaction_builder_tests { { let mut transaction_builder = TransactionBuilder::new( block_version, + token_id, fog_resolver.clone(), EmptyMemoBuilder::default(), ); transaction_builder.set_tombstone_block(2000); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -925,14 +1035,21 @@ pub mod transaction_builder_tests { { let mut transaction_builder = TransactionBuilder::new( block_version, + token_id, fog_resolver.clone(), EmptyMemoBuilder::default(), ); transaction_builder.set_tombstone_block(500); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -957,7 +1074,7 @@ pub mod transaction_builder_tests { fn test_fog_transaction_with_change() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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); @@ -981,14 +1098,21 @@ pub mod transaction_builder_tests { { let mut transaction_builder = TransactionBuilder::new( block_version, + token_id, fog_resolver.clone(), EmptyMemoBuilder::default(), ); transaction_builder.set_tombstone_block(2000); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -1052,8 +1176,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, value - change_value - Mob::MINIMUM_FEE); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = output.e_memo.clone().unwrap().decrypt(&ss); @@ -1082,8 +1207,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, change_value); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = change.e_memo.clone().unwrap().decrypt(&ss); @@ -1116,7 +1242,7 @@ pub mod transaction_builder_tests { fn test_fog_transaction_with_change_and_rth_memos() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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); @@ -1144,13 +1270,23 @@ pub mod transaction_builder_tests { memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); memo_builder.enable_destination_memo(); - let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); + let mut transaction_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver.clone(), + memo_builder, + ); transaction_builder.set_tombstone_block(2000); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -1214,8 +1350,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, value - change_value - Mob::MINIMUM_FEE); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = output.e_memo.clone().unwrap().decrypt(&ss); @@ -1252,8 +1389,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, change_value); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = change.e_memo.clone().unwrap().decrypt(&ss); @@ -1286,14 +1424,24 @@ pub mod transaction_builder_tests { memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); memo_builder.enable_destination_memo(); - let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); + let mut transaction_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver.clone(), + memo_builder, + ); transaction_builder.set_tombstone_block(2000); transaction_builder.set_fee(Mob::MINIMUM_FEE * 4).unwrap(); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -1357,8 +1505,9 @@ pub mod transaction_builder_tests { 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); + let (amount, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, value - change_value - Mob::MINIMUM_FEE * 4); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = output.e_memo.clone().unwrap().decrypt(&ss); @@ -1395,8 +1544,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, change_value); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = change.e_memo.clone().unwrap().decrypt(&ss); @@ -1430,13 +1580,23 @@ pub mod transaction_builder_tests { memo_builder.enable_destination_memo(); memo_builder.set_payment_request_id(42); - let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); + let mut transaction_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver.clone(), + memo_builder, + ); transaction_builder.set_tombstone_block(2000); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -1500,8 +1660,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, value - change_value - Mob::MINIMUM_FEE); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = output.e_memo.clone().unwrap().decrypt(&ss); @@ -1539,8 +1700,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, change_value); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = change.e_memo.clone().unwrap().decrypt(&ss); @@ -1573,13 +1735,23 @@ pub mod transaction_builder_tests { memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); memo_builder.set_payment_request_id(47); - let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); + let mut transaction_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver.clone(), + memo_builder, + ); transaction_builder.set_tombstone_block(2000); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -1643,8 +1815,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, value - change_value - Mob::MINIMUM_FEE); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = output.e_memo.clone().unwrap().decrypt(&ss); @@ -1682,8 +1855,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, change_value); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = change.e_memo.clone().unwrap().decrypt(&ss); @@ -1704,13 +1878,23 @@ pub mod transaction_builder_tests { memo_builder.enable_destination_memo(); memo_builder.set_payment_request_id(47); - let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); + let mut transaction_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver.clone(), + memo_builder, + ); transaction_builder.set_tombstone_block(2000); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -1774,8 +1958,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, value - change_value - Mob::MINIMUM_FEE); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = output.e_memo.clone().unwrap().decrypt(&ss); @@ -1795,8 +1980,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, change_value); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = change.e_memo.clone().unwrap().decrypt(&ss); @@ -1830,7 +2016,7 @@ pub mod transaction_builder_tests { fn test_transaction_builder_memo_custom_sender() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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); @@ -1860,13 +2046,23 @@ pub mod transaction_builder_tests { memo_builder.set_sender_credential(SenderMemoCredential::from(&charlie)); memo_builder.enable_destination_memo(); - let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); + let mut transaction_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver.clone(), + memo_builder, + ); transaction_builder.set_tombstone_block(2000); - let input_credentials = - get_input_credentials(block_version, &alice, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &alice, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -1935,8 +2131,9 @@ pub mod transaction_builder_tests { 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); + let (amount, _) = output.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, value - change_value - Mob::MINIMUM_FEE); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = output.e_memo.clone().unwrap().decrypt(&ss); @@ -1970,8 +2167,9 @@ pub mod transaction_builder_tests { 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 (amount, _) = change.amount.get_value(&ss).unwrap(); + assert_eq!(amount.value, change_value); + assert_eq!(amount.token_id, token_id); if block_version.e_memo_feature_is_supported() { let memo = change.e_memo.clone().unwrap().decrypt(&ss); @@ -2006,7 +2204,7 @@ pub mod transaction_builder_tests { fn transaction_builder_rth_memo_expected_failures() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + for (block_version, token_id) in get_block_version_token_id_pairs() { if !block_version.e_memo_feature_is_supported() { continue; } @@ -2037,13 +2235,23 @@ pub mod transaction_builder_tests { memo_builder.set_sender_credential(SenderMemoCredential::from(&sender)); memo_builder.enable_destination_memo(); - let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); + let mut transaction_builder = TransactionBuilder::new( + block_version, + token_id, + fog_resolver.clone(), + memo_builder, + ); transaction_builder.set_tombstone_block(2000); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + token_id, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (_txout, _confirmation) = transaction_builder @@ -2096,14 +2304,15 @@ pub mod transaction_builder_tests { fn test_inputs_do_not_equal_outputs() { let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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; // 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 (ring, real_index) = + get_ring(block_version, token_id, 3, &alice, value, &fpr, &mut rng); let real_output = ring[real_index].clone(); let onetime_private_key = recover_onetime_private_key( @@ -2131,7 +2340,7 @@ pub mod transaction_builder_tests { .unwrap(); let mut transaction_builder = - TransactionBuilder::new(block_version, fpr, EmptyMemoBuilder::default()); + TransactionBuilder::new(block_version, token_id, fpr, EmptyMemoBuilder::default()); transaction_builder.add_input(input_credentials); let wrong_value = 999; @@ -2153,12 +2362,13 @@ pub mod transaction_builder_tests { fn test_max_transaction_size() { let mut rng: StdRng = SeedableRng::from_seed([18u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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, + token_id, MAX_INPUTS as usize, MAX_OUTPUTS as usize, &sender, @@ -2177,7 +2387,7 @@ pub mod transaction_builder_tests { fn test_ring_elements_are_sorted() { let mut rng: StdRng = SeedableRng::from_seed([97u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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); @@ -2185,6 +2395,7 @@ pub mod transaction_builder_tests { let num_outputs = 11; let tx = get_transaction( block_version, + token_id, num_inputs, num_outputs, &sender, @@ -2208,7 +2419,7 @@ pub mod transaction_builder_tests { fn test_outputs_are_sorted() { let mut rng: StdRng = SeedableRng::from_seed([92u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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); @@ -2216,6 +2427,7 @@ pub mod transaction_builder_tests { let num_outputs = 11; let tx = get_transaction( block_version, + token_id, num_inputs, num_outputs, &sender, @@ -2238,7 +2450,7 @@ pub mod transaction_builder_tests { fn test_inputs_are_sorted() { let mut rng: StdRng = SeedableRng::from_seed([92u8; 32]); - for (block_version, _token_id) in get_block_version_token_id_pairs() { + 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); @@ -2246,6 +2458,7 @@ pub mod transaction_builder_tests { let num_outputs = 11; let tx = get_transaction( block_version, + token_id, num_inputs, num_outputs, &sender, diff --git a/util/generate-sample-ledger/Cargo.toml b/util/generate-sample-ledger/Cargo.toml index 237a917040..99658e7631 100644 --- a/util/generate-sample-ledger/Cargo.toml +++ b/util/generate-sample-ledger/Cargo.toml @@ -24,4 +24,4 @@ rand_hc = "0.3" structopt = "0.3" [dev-dependencies] -tempdir = "0.3" +tempfile = "3.2" diff --git a/util/generate-sample-ledger/src/bin/generate_sample_ledger.rs b/util/generate-sample-ledger/src/bin/generate_sample_ledger.rs index 94331a69fb..242257078f 100644 --- a/util/generate-sample-ledger/src/bin/generate_sample_ledger.rs +++ b/util/generate-sample-ledger/src/bin/generate_sample_ledger.rs @@ -14,17 +14,27 @@ struct Config { #[structopt(long = "blocks", short = "b", default_value = "1")] pub num_blocks: usize, - /// Key images per transaction - #[structopt(long = "key-images", short = "k", default_value = "0")] + /// Key images per block + #[structopt(long = "key-images", short = "k", default_value = "1")] pub num_key_images: usize, - // Seed to use when generating blocks (e.g. + /// Seed to use when generating blocks (e.g. // 1234567812345678123456781234567812345678123456781234567812345678). #[structopt(long = "seed", short = "s", parse(try_from_str=hex::FromHex::from_hex))] pub seed: Option<[u8; 32]>, + /// Text to embed in the fog hints of the bootstrapped block, as an easter + /// egg #[structopt(long = "hint-text")] pub hint_text: Option, + + /// Max token id. If set to 1, then this will double the number of tx's in + /// the bootstrap. First will come all token id 0, then all token id 1. + /// + /// Historically this was not present, and is only added to support testing + /// of confidential token ids. + #[structopt(long, default_value = "0")] + pub max_token_id: u32, } fn main() { @@ -47,6 +57,7 @@ fn main() { config.num_key_images, config.seed, config.hint_text.as_deref(), + config.max_token_id, logger, ); } diff --git a/util/generate-sample-ledger/src/lib.rs b/util/generate-sample-ledger/src/lib.rs index c1043eea80..212ad05610 100644 --- a/util/generate-sample-ledger/src/lib.rs +++ b/util/generate-sample-ledger/src/lib.rs @@ -1,4 +1,8 @@ -// Copyright (c) 2018-2021 The MobileCoin Foundation +// Copyright (c) 2018-2022 The MobileCoin Foundation + +//! Generates a bootstrapped ledger for testing purposes + +#![deny(missing_docs)] use mc_account_keys::PublicAddress; use mc_common::logger::{log, Logger}; @@ -9,7 +13,7 @@ use mc_transaction_core::{ encrypted_fog_hint::{EncryptedFogHint, ENCRYPTED_FOG_HINT_LEN}, ring_signature::KeyImage, tx::TxOut, - Block, BlockContents, BlockVersion, + AmountData, Block, BlockContents, BlockVersion, }; use mc_util_from_random::FromRandom; use rand::{RngCore, SeedableRng}; @@ -31,7 +35,10 @@ const BLOCK_VERSION: BlockVersion = BlockVersion::ONE; /// * `num_blocks` - Number of blocks that will be created. /// * `key_images_per_block` - Number of randomly generated key images per /// block. -/// * `hint_text` - A string to be hashed into the hints for the outputs +/// * `hint_text` - A string to be used as the hints for the outputs, as an +/// easter egg +/// * `max_token_id` - The maximum token id value to bootstrap a supply for. All +/// token ids will have the same bootstrapped supply. /// /// This will panic if it attempts to distribute the total value of mobilecoin /// into fewer than 16 outputs. @@ -43,6 +50,7 @@ pub fn bootstrap_ledger( key_images_per_block: usize, seed: Option<[u8; 32]>, hint_text: Option<&str>, + max_token_id: u32, logger: Logger, ) { // Create the DB @@ -50,7 +58,10 @@ pub fn bootstrap_ledger( LedgerDB::create(path).expect("Could not create ledger_db"); let mut db = LedgerDB::open(path).expect("Could not open ledger_db"); - let num_outputs: u64 = (recipients.len() * outputs_per_recipient_per_block * num_blocks) as u64; + let num_outputs: u64 = (recipients.len() + * outputs_per_recipient_per_block + * num_blocks + * (max_token_id as usize + 1)) as u64; assert!(num_outputs >= 16); let picomob_per_output: u64 = (TOTAL_MOB / num_outputs) * 1_000_000_000_000; @@ -68,31 +79,45 @@ pub fn bootstrap_ledger( let mut rng: FixedRng = SeedableRng::from_seed(seed.unwrap_or([33u8; 32])); + let block_version = if max_token_id > 0 { + BlockVersion::THREE + } else { + BLOCK_VERSION + }; + for block_index in 0..num_blocks as u64 { log::info!(logger, "Creating block {} of {}.", block_index, num_blocks); let mut outputs: Vec = Vec::new(); for recipient in recipients { for _i in 0..outputs_per_recipient_per_block { - outputs.push(create_output( - recipient, - picomob_per_output, - &mut rng, - hint_text, - &logger, - )); + // Create outputs of each token id in round-robin + for token_id in 0..=max_token_id { + let amount = AmountData { + value: picomob_per_output, + token_id: token_id.into(), + }; + outputs.push(create_output( + recipient, amount, &mut rng, hint_text, &logger, + )); + } } } - let key_images: Vec = (0..key_images_per_block) - .map(|_i| KeyImage::from(rng.next_u64())) - .collect(); + // The origin block doesn't have any key images + let key_images: Vec = if previous_block.is_some() { + (0..key_images_per_block) + .map(|_i| KeyImage::from(rng.next_u64())) + .collect() + } else { + Default::default() + }; let block_contents = BlockContents::new(key_images, outputs.clone()); let block = match previous_block { Some(parent) => { - Block::new_with_parent(BLOCK_VERSION, &parent, &Default::default(), &block_contents) + Block::new_with_parent(block_version, &parent, &Default::default(), &block_contents) } None => Block::new_origin_block(&outputs), }; @@ -119,7 +144,7 @@ pub fn bootstrap_ledger( fn create_output( recipient: &PublicAddress, - value: u64, + amount: AmountData, rng: &mut FixedRng, hint_slice: Option<&str>, logger: &Logger, @@ -139,10 +164,14 @@ fn create_output( EncryptedFogHint::fake_onetime_hint(rng) }; - let mut output = TxOut::new(value, recipient, &tx_private_key, hint).unwrap(); - // At this point, we clear the e_memo field, because, historically the genesis - // block did not have memo fields, even though they are expected now. - output.e_memo = None; + let output = TxOut::new( + amount.value, + amount.token_id, + recipient, + &tx_private_key, + hint, + ) + .unwrap(); log::debug!(logger, "Creating output: {:?}", output); output } @@ -152,6 +181,7 @@ mod tests { use super::*; use mc_account_keys::{AccountKey, RootIdentity}; use mc_common::logger::test_with_logger; + use mc_transaction_core::{tokens::Mob, Token}; use rand::{rngs::StdRng, SeedableRng}; #[test_with_logger] @@ -159,13 +189,18 @@ mod tests { let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); let mut fixed_rng: FixedRng = SeedableRng::from_seed([33u8; 32]); + let amount = AmountData { + value: 10, + token_id: Mob::ID, + }; + let account_key = AccountKey::from(&RootIdentity::from_random(&mut rng)); // Case with short hint text let hint_slice = "Vaccine 90% effective"; let output = create_output( &account_key.subaddress(0), - 10, + amount.clone(), &mut fixed_rng, Some(hint_slice), &logger, @@ -178,7 +213,7 @@ mod tests { let hint_slice = "Covid-19 Vaccine 90% Up to 90% Effective in Late-Stage Trials - LONDON — the University of Oxford added their vaccine candidate to a growing list of shots showing promising effectiveness against Covid-19 — setting in motion disparate regulatory and distribution tracks that executives and researchers hope will result in the start of widespread vaccinations by the end of the year."; let output = create_output( &account_key.subaddress(0), - 10, + amount.clone(), &mut fixed_rng, Some(hint_slice), &logger, @@ -192,7 +227,7 @@ mod tests { let hint_slice = ""; let output = create_output( &account_key.subaddress(0), - 10, + amount, &mut fixed_rng, Some(hint_slice), &logger, diff --git a/util/generate-sample-ledger/tests/bootstrap.rs b/util/generate-sample-ledger/tests/bootstrap.rs new file mode 100644 index 0000000000..a3493117eb --- /dev/null +++ b/util/generate-sample-ledger/tests/bootstrap.rs @@ -0,0 +1,41 @@ +use std::{ + env::{args, set_current_dir}, + path::PathBuf, + process::Command, +}; +use tempfile::TempDir; + +// Test that the bootstrap binary works with basic config +// +// Note: This test is needed mainly because if there is not an integration test +// in the `util-generate-sample-ledger` crate, then `cargo test` will not build +// the bootstrap binary, and this will cause the `bootstrap` test in +// `mc-fog-distribution` to fail. That test is there to confirm that bootstrap +// and fog distribution are working together in the way that they are used in CD +// and improve on iteration times. +// +// If cargo creates a way for one crate to have a dev dependency on a binary +// from another crate, that would be a way to avoid needing this test. (AFAIK +// this can't be done right now.) +#[test] +fn test_exercise_bootstrap() { + let me = PathBuf::from(args().next().unwrap()); + let bin = me.parent().unwrap().parent().unwrap(); + println!("bin = {:?}", bin); + + let dir = TempDir::new().unwrap(); + set_current_dir(dir.path()).unwrap(); + println!("dir = {:?}", dir); + + assert!(Command::new(bin.join("sample-keys")) + .args(["--num", "5"]) + .status() + .unwrap() + .success()); + + assert!(Command::new(bin.join("generate-sample-ledger")) + .args(["--txs", "10"]) + .status() + .unwrap() + .success()); +} From ce0671ff1ce8c4b4df390c158cc79a41de128aa0 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 10 Mar 2022 12:19:20 -0700 Subject: [PATCH 2/4] add notes and tests about keeping transaction core enum in sync with proto --- api/proto/external.proto | 3 +++ api/tests/tokens.rs | 22 ++++++++++++++++++++++ transaction/core/src/token.rs | 3 +++ 3 files changed, 28 insertions(+) create mode 100644 api/tests/tokens.rs diff --git a/api/proto/external.proto b/api/proto/external.proto index b11678741c..25d77bb7e5 100644 --- a/api/proto/external.proto +++ b/api/proto/external.proto @@ -131,6 +131,9 @@ message ViewKey { /// /// Note that this is not an exhaustive list and clients should gracefully handle /// the scenario that they receive a tx out with a token id they don't know about yet. +/// +/// If changing this, please keep it in sync with the list defined in +/// mc-transaction-core in the tokens module. enum KnownTokenId { MOB = 0; } diff --git a/api/tests/tokens.rs b/api/tests/tokens.rs new file mode 100644 index 0000000000..400514532d --- /dev/null +++ b/api/tests/tokens.rs @@ -0,0 +1,22 @@ +use mc_api::external::KnownTokenId; +use mc_transaction_core::{tokens, Token}; +use protobuf::ProtobufEnum; +use std::collections::HashMap; + +// Test that protobuf KnownTokens enum matches the tokens in mc-transaction-core +#[test] +fn test_known_tokens_enum_vs_mc_transaction_core_tokens() { + // Collect known tokens from proto + let mut known_tokens = HashMap::::default(); + + let descriptor = KnownTokenId::enum_descriptor_static(); + for value in KnownTokenId::values() { + known_tokens.insert( + descriptor.value_by_number(value.value()).name().to_string(), + value.value(), + ); + } + + assert_eq!(known_tokens.len(), 1); + assert_eq!(*known_tokens.get("MOB").unwrap() as u32, *tokens::Mob::ID); +} diff --git a/transaction/core/src/token.rs b/transaction/core/src/token.rs index c50cd98659..b751728c4a 100644 --- a/transaction/core/src/token.rs +++ b/transaction/core/src/token.rs @@ -67,6 +67,9 @@ pub trait Token { } /// Exports structures which expose constants related to tokens. +/// +/// If changing this, please keep it in sync with the enum defined in +/// external.proto pub mod tokens { use super::*; use crate::constants::MICROMOB_TO_PICOMOB; From 3bc7be3c0856b562f0090a719c9ee34f5dd97dba Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Thu, 10 Mar 2022 13:30:54 -0700 Subject: [PATCH 3/4] clippy --- fog/distribution/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/fog/distribution/src/main.rs b/fog/distribution/src/main.rs index c1742fc73a..c09217db5c 100755 --- a/fog/distribution/src/main.rs +++ b/fog/distribution/src/main.rs @@ -153,7 +153,6 @@ fn main() { let block_version = config .block_version - .clone() .unwrap_or_else(|| ledger_db.get_latest_block().unwrap().version); BLOCK_HEIGHT.store(ledger_db.num_blocks().unwrap(), Ordering::SeqCst); From 66237c1d43d7352e8ce5b6dba6e1a9c8eda3d746 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Fri, 11 Mar 2022 12:49:26 -0700 Subject: [PATCH 4/4] fix merge race --- transaction/std/src/transaction_builder.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/transaction/std/src/transaction_builder.rs b/transaction/std/src/transaction_builder.rs index c5407c06f7..b6bbe3f3b8 100644 --- a/transaction/std/src/transaction_builder.rs +++ b/transaction/std/src/transaction_builder.rs @@ -2497,10 +2497,16 @@ pub mod transaction_builder_tests { memo_builder.enable_destination_memo(); let mut transaction_builder = - TransactionBuilder::new(block_version, fog_resolver.clone(), memo_builder); + TransactionBuilder::new(block_version, Mob::ID, fog_resolver.clone(), memo_builder); - let input_credentials = - get_input_credentials(block_version, &sender, value, &fog_resolver, &mut rng); + let input_credentials = get_input_credentials( + block_version, + Mob::ID, + &sender, + value, + &fog_resolver, + &mut rng, + ); transaction_builder.add_input(input_credentials); let (burn_tx_out, _confirmation) = transaction_builder @@ -2546,7 +2552,7 @@ pub mod transaction_builder_tests { .amount .get_value(&shared_secret) .ok() - .map(|(num, _scalar)| num) + .map(|(amount, _scalar)| amount.value) } assert_eq!(