From 04ad0d16f578e69f3bdd9f9b3f8a937ab3300253 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Wed, 16 Apr 2025 17:58:39 +0300 Subject: [PATCH] sighash input commitments - WIP --- api-server/scanner-lib/src/sync/tests/mod.rs | 68 ++- .../stack-test-suite/tests/v2/address.rs | 14 +- .../tests/v2/address_all_utxos.rs | 10 +- .../tests/v2/address_spendable_utxos.rs | 10 +- .../tests/v2/address_token_authority.rs | 7 +- api-server/stack-test-suite/tests/v2/htlc.rs | 14 +- api-server/stack-test-suite/tests/v2/mod.rs | 2 +- api-server/stack-test-suite/tests/v2/nft.rs | 6 +- .../stack-test-suite/tests/v2/statistics.rs | 9 +- .../stack-test-suite/tests/v2/transaction.rs | 6 +- chainstate/src/detail/ban_score.rs | 12 + chainstate/src/detail/error_classification.rs | 26 +- .../test-framework/src/block_builder.rs | 22 +- chainstate/test-framework/src/framework.rs | 36 +- chainstate/test-framework/src/key_manager.rs | 40 +- .../test-framework/src/pos_block_builder.rs | 24 +- chainstate/test-framework/src/utils.rs | 94 +++- .../test-suite/src/tests/delegation_tests.rs | 9 +- .../src/tests/fungible_tokens_v1.rs | 135 +++--- chainstate/test-suite/src/tests/htlc.rs | 36 +- .../test-suite/src/tests/orders_tests.rs | 214 ++++----- .../src/tests/pos_processing_tests.rs | 26 +- .../test-suite/src/tests/processing_tests.rs | 14 +- .../test-suite/src/tests/signature_tests.rs | 103 ++-- .../test-suite/src/tests/stake_pool_tests.rs | 12 +- .../transaction_verifier/input_check/mod.rs | 303 +++++++++++- .../input_check/signature_only_check.rs | 30 +- .../src/transaction_verifier/mod.rs | 35 +- common/src/chain/config/builder.rs | 73 ++- .../src/chain/config/checkpoints_data/mod.rs | 1 + common/src/chain/config/mod.rs | 5 +- .../chain/transaction/output/output_value.rs | 15 + .../inputsig/arbitrary_message/tests.rs | 28 +- .../inputsig/authorize_pubkey_spend.rs | 97 ++-- .../inputsig/authorize_pubkeyhash_spend.rs | 96 ++-- .../transaction/signature/inputsig/htlc.rs | 13 +- .../signature/inputsig/standard_signature.rs | 63 +-- common/src/chain/transaction/signature/mod.rs | 49 +- .../transaction/signature/sighash/hashable.rs | 215 ++++++--- .../signature/sighash/input_commitment.rs | 394 ++++++++++++++++ .../transaction/signature/sighash/mod.rs | 36 +- .../signature/tests/mixed_sighash_types.rs | 19 +- .../chain/transaction/signature/tests/mod.rs | 226 +++++---- .../signature/tests/sign_and_mutate.rs | 443 +++++++++++------- .../signature/tests/sign_and_verify.rs | 62 ++- .../transaction/signature/tests/utils.rs | 170 +++++-- .../src/chain/upgrades/chainstate_upgrade.rs | 25 +- common/src/chain/upgrades/mod.rs | 4 +- .../src/chainstate_upgrade_builder.rs | 8 +- consensus/src/pos/input_data.rs | 9 +- mempool/src/error/ban_score.rs | 6 + mempool/src/pool/orphans/detect.rs | 11 +- mempool/src/pool/tx_pool/tests/accumulator.rs | 10 +- mintscript/src/checker/mod.rs | 2 +- mintscript/src/checker/signature.rs | 13 +- mintscript/src/tests/checkers.rs | 26 +- mintscript/src/tests/hashlock_checker.rs | 2 +- mintscript/src/tests/mod.rs | 2 +- mintscript/src/tests/utils.rs | 25 +- mintscript/src/translate.rs | 15 - orders-accounting/src/error.rs | 7 + orders-accounting/src/storage/in_memory.rs | 2 +- pos-accounting/src/error.rs | 6 + .../src/account/utxo_selector/output_group.rs | 15 +- wallet/src/signer/mod.rs | 8 + wallet/src/signer/software_signer/mod.rs | 210 +++++---- wallet/src/signer/software_signer/tests.rs | 277 +++++++---- wallet/src/signer/test_utils.rs | 38 ++ wallet/src/signer/trezor_signer/mod.rs | 28 +- wallet/src/signer/trezor_signer/tests.rs | 187 +++++--- wallet/src/wallet/mod.rs | 60 ++- wallet/src/wallet/tests.rs | 159 +++++-- .../types/src/partially_signed_transaction.rs | 62 ++- wallet/wallet-controller/src/helpers.rs | 85 ++-- wallet/wallet-controller/src/lib.rs | 119 +++-- .../src/synced_controller.rs | 38 +- wallet/wallet-rpc-lib/src/rpc/mod.rs | 2 +- wasm-wrappers/src/lib.rs | 50 +- 78 files changed, 3405 insertions(+), 1428 deletions(-) create mode 100644 common/src/chain/transaction/signature/sighash/input_commitment.rs create mode 100644 wallet/src/signer/test_utils.rs diff --git a/api-server/scanner-lib/src/sync/tests/mod.rs b/api-server/scanner-lib/src/sync/tests/mod.rs index 6b69c73189..c70a9bf801 100644 --- a/api-server/scanner-lib/src/sync/tests/mod.rs +++ b/api-server/scanner-lib/src/sync/tests/mod.rs @@ -23,6 +23,7 @@ use serialization::Encode; use super::*; use std::{ + borrow::Cow, convert::Infallible, sync::{Arc, Mutex}, time::Duration, @@ -46,7 +47,11 @@ use common::{ authorize_pubkey_spend::sign_public_key_spending, standard_signature::StandardInputSignature, InputWitness, }, - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::{ + input_commitment::{SighashInputCommitment, TrivialUtxoProvider}, + sighashtype::SigHashType, + signature_hash, + }, }, stakelock::StakePoolData, timelock::OutputTimeLock, @@ -306,6 +311,17 @@ async fn randomized(#[case] seed: Seed) { #[case(test_utils::random::Seed::from_entropy())] #[tokio::test] async fn compare_pool_rewards_with_chainstate_real_state(#[case] seed: Seed) { + use std::collections::BTreeMap; + + use common::chain::{ + signature::sighash::input_commitment::{ + make_sighash_input_commitments_for_transaction_inputs, OrderInfo, PoolInfo, + }, + OrderId, + }; + + logging::init_logging(); + let mut rng = make_seedable_rng(seed); let initial_pledge = 40_000 * CoinUnit::ATOMS_PER_COIN + rng.gen_range(10000..100000); @@ -546,25 +562,47 @@ async fn compare_pool_rewards_with_chainstate_real_state(#[case] seed: Seed) { sync_and_compare(&mut tf, block, &mut local_state, pool_id).await; let remaining_coins = remaining_coins - rng.gen_range(0..10); + let input1 = TxInput::from_utxo(OutPointSourceId::Transaction(prev_tx_id), 0); + let input2 = TxInput::from_utxo(OutPointSourceId::BlockReward(prev_block_hash.into()), 0); let transaction = TransactionBuilder::new() - .add_input( - TxInput::from_utxo(OutPointSourceId::Transaction(prev_tx_id), 0), - InputWitness::NoSignature(None), - ) - .add_input( - TxInput::from_utxo(OutPointSourceId::BlockReward(prev_block_hash.into()), 0), - InputWitness::NoSignature(None), - ) + .add_input(input1.clone(), InputWitness::NoSignature(None)) + .add_input(input2.clone(), InputWitness::NoSignature(None)) .add_output(TxOutput::Transfer( OutputValue::Coin(Amount::from_atoms(remaining_coins)), Destination::AnyoneCanSpend, )) .build(); + let utxos = [Some(coin_tx_out), Some(from_block_output)]; + let decommissioned_pool_staker_balance = local_state + .storage() + .transaction_ro() + .await + .unwrap() + .get_pool_data(pool_id) + .await + .unwrap() + .unwrap() + .staker_balance() + .unwrap(); + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + &[input1, input2], + &TrivialUtxoProvider(&utxos), + &BTreeMap::::from([( + pool_id, + PoolInfo { + staker_balance: decommissioned_pool_staker_balance, + }, + )]), + &BTreeMap::::new(), + &chain_config, + tf.next_block_height(), + ) + .unwrap(); let sighash = signature_hash( SigHashType::default(), transaction.transaction(), - &[Some(&coin_tx_out), Some(&from_block_output)], + &input_commitments, 1, ) .unwrap(); @@ -786,7 +824,10 @@ async fn reorg_locked_balance(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::default(), spend_transaction.transaction(), - &[Some(&lock_for_block_count), Some(&lock_until_height)], + &[ + SighashInputCommitment::Utxo(Cow::Borrowed(&lock_for_block_count)), + SighashInputCommitment::Utxo(Cow::Borrowed(&lock_until_height)), + ], idx, ) .unwrap(); @@ -864,7 +905,10 @@ async fn reorg_locked_balance(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::default(), spend_time_locked.transaction(), - &[Some(&lock_for_sec), Some(&lock_until_time)], + &[ + SighashInputCommitment::Utxo(Cow::Borrowed(&lock_for_sec)), + SighashInputCommitment::Utxo(Cow::Borrowed(&lock_until_time)), + ], idx, ) .unwrap(); diff --git a/api-server/stack-test-suite/tests/v2/address.rs b/api-server/stack-test-suite/tests/v2/address.rs index 6fe1549c70..63131fd16a 100644 --- a/api-server/stack-test-suite/tests/v2/address.rs +++ b/api-server/stack-test-suite/tests/v2/address.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::RwLock; +use std::{borrow::Cow, sync::RwLock}; use api_web_server::{api::json_helpers::amount_to_json, CachedValues}; use common::primitives::time::get_time; @@ -126,7 +126,7 @@ async fn multiple_outputs_to_single_address(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -198,7 +198,7 @@ async fn multiple_outputs_to_single_address(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -368,7 +368,7 @@ async fn test_unlocking_for_locked_utxos(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -440,7 +440,7 @@ async fn test_unlocking_for_locked_utxos(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -618,7 +618,7 @@ async fn ok(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -673,7 +673,7 @@ async fn ok(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) diff --git a/api-server/stack-test-suite/tests/v2/address_all_utxos.rs b/api-server/stack-test-suite/tests/v2/address_all_utxos.rs index 0ab2c6b18f..8ebc59faa5 100644 --- a/api-server/stack-test-suite/tests/v2/address_all_utxos.rs +++ b/api-server/stack-test-suite/tests/v2/address_all_utxos.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, sync::RwLock}; +use std::{borrow::Cow, collections::BTreeMap, sync::RwLock}; use api_web_server::{api::json_helpers::utxo_outpoint_to_json, CachedValues}; use common::{chain::UtxoOutPoint, primitives::time::get_time}; @@ -125,7 +125,7 @@ async fn multiple_utxos_to_single_address(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -201,7 +201,7 @@ async fn multiple_utxos_to_single_address(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -375,7 +375,7 @@ async fn ok(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -453,7 +453,7 @@ async fn ok(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) diff --git a/api-server/stack-test-suite/tests/v2/address_spendable_utxos.rs b/api-server/stack-test-suite/tests/v2/address_spendable_utxos.rs index 360f5b3059..c5b64e4891 100644 --- a/api-server/stack-test-suite/tests/v2/address_spendable_utxos.rs +++ b/api-server/stack-test-suite/tests/v2/address_spendable_utxos.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, sync::RwLock}; +use std::{borrow::Cow, collections::BTreeMap, sync::RwLock}; use api_web_server::{api::json_helpers::utxo_outpoint_to_json, CachedValues}; use common::{chain::UtxoOutPoint, primitives::time::get_time}; @@ -128,7 +128,7 @@ async fn multiple_utxos_to_single_address(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -203,7 +203,7 @@ async fn multiple_utxos_to_single_address(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -377,7 +377,7 @@ async fn ok(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -445,7 +445,7 @@ async fn ok(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) diff --git a/api-server/stack-test-suite/tests/v2/address_token_authority.rs b/api-server/stack-test-suite/tests/v2/address_token_authority.rs index 69f25a61c7..b4ea56a620 100644 --- a/api-server/stack-test-suite/tests/v2/address_token_authority.rs +++ b/api-server/stack-test-suite/tests/v2/address_token_authority.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use common::chain::{ tokens::{make_token_id, IsTokenFreezable, TokenIssuance, TokenIssuanceV1, TokenTotalSupply}, AccountNonce, @@ -161,7 +163,10 @@ async fn ok(#[case] seed: Seed) { SigHashType::all(), dest, &transaction, - &[Some(&input_utxo), None], + &[ + SighashInputCommitment::Utxo(Cow::Borrowed(&input_utxo)), + SighashInputCommitment::None, + ], 0, &mut rng, ) diff --git a/api-server/stack-test-suite/tests/v2/htlc.rs b/api-server/stack-test-suite/tests/v2/htlc.rs index 92cc416ce8..4c4913aea2 100644 --- a/api-server/stack-test-suite/tests/v2/htlc.rs +++ b/api-server/stack-test-suite/tests/v2/htlc.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use common::chain::{ classic_multisig::ClassicMultisigChallenge, htlc::HtlcSecret, @@ -129,7 +131,9 @@ async fn spend(#[case] seed: Seed) { SigHashType::all(), Destination::PublicKeyHash((&PublicKey::from_private_key(&bob_sk)).into()), &tx2, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, secret, &mut rng, @@ -284,7 +288,9 @@ async fn refund(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::all(), &tx2, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, ) .unwrap(); @@ -303,7 +309,9 @@ async fn refund(#[case] seed: Seed) { &authorization, SigHashType::all(), &tx2, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, ) .unwrap(); diff --git a/api-server/stack-test-suite/tests/v2/mod.rs b/api-server/stack-test-suite/tests/v2/mod.rs index 4264ee24d7..bb3546b791 100644 --- a/api-server/stack-test-suite/tests/v2/mod.rs +++ b/api-server/stack-test-suite/tests/v2/mod.rs @@ -65,7 +65,7 @@ use common::{ output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, - sighash::sighashtype::SigHashType, + sighash::{input_commitment::SighashInputCommitment, sighashtype::SigHashType}, }, transaction::output::timelock::OutputTimeLock, Destination, OutPointSourceId, SignedTransaction, Transaction, TxInput, TxOutput, diff --git a/api-server/stack-test-suite/tests/v2/nft.rs b/api-server/stack-test-suite/tests/v2/nft.rs index facc9c4abe..394d6262a3 100644 --- a/api-server/stack-test-suite/tests/v2/nft.rs +++ b/api-server/stack-test-suite/tests/v2/nft.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use api_server_common::storage::storage_api::NftWithOwner; use api_web_server::api::json_helpers::nft_with_owner_to_json; use common::{ @@ -127,7 +129,9 @@ async fn ok(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &tx, - &[issue_nft_tx.outputs().first()], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &issue_nft_tx.outputs()[0], + ))], 0, &mut rng, ) diff --git a/api-server/stack-test-suite/tests/v2/statistics.rs b/api-server/stack-test-suite/tests/v2/statistics.rs index a0f7588cd8..50e2f224c1 100644 --- a/api-server/stack-test-suite/tests/v2/statistics.rs +++ b/api-server/stack-test-suite/tests/v2/statistics.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use api_web_server::api::json_helpers::amount_to_json; use common::{ chain::{ @@ -164,7 +166,12 @@ async fn ok_tokens(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &mint_transaction, - &[Some(&issue_token_transaction.outputs()[0]), None], + &[ + SighashInputCommitment::Utxo(Cow::Borrowed( + &issue_token_transaction.outputs()[0], + )), + SighashInputCommitment::None, + ], 1, &mut rng, ) diff --git a/api-server/stack-test-suite/tests/v2/transaction.rs b/api-server/stack-test-suite/tests/v2/transaction.rs index 43ef4dec0c..36b09f43f2 100644 --- a/api-server/stack-test-suite/tests/v2/transaction.rs +++ b/api-server/stack-test-suite/tests/v2/transaction.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use api_web_server::api::json_helpers::tx_input_to_json; use super::*; @@ -105,7 +107,7 @@ async fn multiple_tx_in_same_block(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &signed_tx1, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) @@ -152,7 +154,7 @@ async fn multiple_tx_in_same_block(#[case] seed: Seed) { SigHashType::all(), alice_destination.clone(), &transaction2, - &[Some(&previous_tx_out)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&previous_tx_out))], 0, &mut rng, ) diff --git a/chainstate/src/detail/ban_score.rs b/chainstate/src/detail/ban_score.rs index 036b209eed..97ec3306bb 100644 --- a/chainstate/src/detail/ban_score.rs +++ b/chainstate/src/detail/ban_score.rs @@ -159,13 +159,25 @@ impl BanScore for tx_verifier::error::InputCheckErrorPayload { fn ban_score(&self) -> u32 { match self { Self::MissingUtxo(_) => 100, + Self::PoolNotFound(_) => 100, + Self::OrderNotFound(_) => 100, + Self::NonUtxoKernelInput(_) => 100, Self::UtxoView(e) => e.ban_score(), + Self::UtxoInfoProvider(e) => e.ban_score(), + Self::PoolInfoProvider(e) => e.ban_score(), + Self::OrderInfoProvider(e) => e.ban_score(), Self::Translation(e) => e.ban_score(), Self::Verification(e) => e.ban_score(), } } } +impl BanScore for chainstate_types::storage_result::Error { + fn ban_score(&self) -> u32 { + 0 + } +} + impl BanScore for mintscript::translate::TranslationError { fn ban_score(&self) -> u32 { match self { diff --git a/chainstate/src/detail/error_classification.rs b/chainstate/src/detail/error_classification.rs index a5162f9f9c..9d74e36a9d 100644 --- a/chainstate/src/detail/error_classification.rs +++ b/chainstate/src/detail/error_classification.rs @@ -334,14 +334,38 @@ impl BlockProcessingErrorClassification for tx_verifier::error::InputCheckError impl BlockProcessingErrorClassification for tx_verifier::error::InputCheckErrorPayload { fn classify(&self) -> BlockProcessingErrorClass { match self { - Self::MissingUtxo(_) => BlockProcessingErrorClass::BadBlock, + Self::MissingUtxo(_) + | Self::PoolNotFound(_) + | Self::OrderNotFound(_) + | Self::NonUtxoKernelInput(_) => BlockProcessingErrorClass::BadBlock, Self::UtxoView(e) => e.classify(), + Self::UtxoInfoProvider(e) => e.classify(), + Self::PoolInfoProvider(e) => e.classify(), + Self::OrderInfoProvider(e) => e.classify(), Self::Translation(e) => e.classify(), Self::Verification(e) => e.classify(), } } } +// impl BlockProcessingErrorClassification for SighashInputCommitmentCreationError { +// fn classify(&self) -> BlockProcessingErrorClass { +// match self { +// Self::UtxoProviderError(_) +// | Self::PoolInfoProviderError(_) +// | Self::OrderInfoProviderError(_) => { +// // FIXME need to propagate the call to the nested error (which is currently just String) +// BlockProcessingErrorClass::BadBlock +// } + +// Self::NonUtxoKernelInput(_, _) +// | Self::UtxoNotFound(_, _) +// | Self::PoolNotFound(_) +// | Self::OrderNotFound(_) => BlockProcessingErrorClass::BadBlock, +// } +// } +// } + impl BlockProcessingErrorClassification for mintscript::translate::TranslationError { fn classify(&self) -> BlockProcessingErrorClass { match self { diff --git a/chainstate/test-framework/src/block_builder.rs b/chainstate/test-framework/src/block_builder.rs index 82c9b3148d..9e12ad23de 100644 --- a/chainstate/test-framework/src/block_builder.rs +++ b/chainstate/test-framework/src/block_builder.rs @@ -48,6 +48,8 @@ pub struct BlockBuilder<'f> { framework: &'f mut TestFramework, transactions: Vec, prev_block_hash: Id, + // If true, prev_block_hash has been used in some way already and cannot be changed. + prev_block_hash_used: bool, timestamp: BlockTimestamp, consensus_data: ConsensusData, reward: BlockReward, @@ -109,6 +111,7 @@ impl<'f> BlockBuilder<'f> { framework, transactions, prev_block_hash, + prev_block_hash_used: false, timestamp, consensus_data, reward, @@ -134,6 +137,7 @@ impl<'f> BlockBuilder<'f> { self } + // FIXME this is unused; remove it? /// Adds a transaction that uses random utxos and accounts pub fn add_test_transaction(mut self, rng: &mut (impl Rng + CryptoRng)) -> Self { let utxo_set = self @@ -147,6 +151,7 @@ impl<'f> BlockBuilder<'f> { .filter(|(outpoint, _)| !self.used_utxo.contains(outpoint)) .collect(); let utxo_set = utxo::UtxosDBInMemoryImpl::new(self.prev_block_hash, utxo_set); + self.prev_block_hash_used = true; let account_nonce_getter = Box::new(|account: AccountType| -> Option { self.account_nonce_tracker.get(&account).copied().or_else(|| { @@ -180,13 +185,21 @@ impl<'f> BlockBuilder<'f> { let destination_getter = SignatureDestinationGetter::new_for_transaction( &tokens_db, &pos_db, &orders_db, &utxo_set, ); + let block_height = self + .framework + .gen_block_index(&self.prev_block_hash) + .block_height() + .next_height(); let witnesses = sign_witnesses( rng, &self.framework.key_manager, self.framework.chainstate.get_chain_config(), &tx, &utxo_set, + &pos_db, + &orders_db, destination_getter, + block_height, ); let tx = SignedTransaction::new(tx, witnesses).expect("invalid witness count"); @@ -296,14 +309,17 @@ impl<'f> BlockBuilder<'f> { /// Overrides the previous block hash that is deduced by default as the best block. pub fn with_parent(mut self, prev_block_hash: Id) -> Self { + assert!( + !self.prev_block_hash_used, + "The current builder state may depend on the previous value of prev_block_hash; consider re-ordering function calls" + ); self.prev_block_hash = prev_block_hash; self } /// Overrides the previous block hash by a random value making the resulting block an orphan. - pub fn make_orphan(mut self, rng: &mut impl Rng) -> Self { - self.prev_block_hash = Id::new(H256::random_using(rng)); - self + pub fn make_orphan(self, rng: &mut impl Rng) -> Self { + self.with_parent(Id::new(H256::random_using(rng))) } /// Overrides the timestamp that is equal to the current time by default. diff --git a/chainstate/test-framework/src/framework.rs b/chainstate/test-framework/src/framework.rs index 8081c82a7b..d67a00b206 100644 --- a/chainstate/test-framework/src/framework.rs +++ b/chainstate/test-framework/src/framework.rs @@ -18,7 +18,10 @@ use std::{collections::BTreeMap, sync::Arc}; use chainstate_storage::{ BlockchainStorageRead, BlockchainStorageWrite, TransactionRw, Transactional, }; +use orders_accounting::OrdersAccountingDB; +use pos_accounting::PoSAccountingDB; use rstest::rstest; +use utxo::UtxosDB; use crate::{ key_manager::KeyManager, @@ -28,18 +31,20 @@ use crate::{ utils::{ assert_block_index_opt_identical_to, assert_gen_block_index_identical_to, assert_gen_block_index_opt_identical_to, find_create_pool_tx_in_genesis, - outputs_from_block, outputs_from_genesis, + outputs_from_block, outputs_from_genesis, SighashInputCommitmentInfoProvider, }, BlockBuilder, TestChainstate, TestFrameworkBuilder, TestStore, }; use chainstate::{chainstate_interface::ChainstateInterface, BlockSource, ChainstateError}; use chainstate_types::{ pos_randomness::PoSRandomness, BlockIndex, BlockStatus, EpochStorageRead as _, GenBlockIndex, + TipStorageTag, }; use common::{ chain::{ - Block, ChainConfig, GenBlock, GenBlockId, Genesis, OutPointSourceId, PoolId, TxOutput, - UtxoOutPoint, + signature::sighash::{self, input_commitment::SighashInputCommitment}, + Block, ChainConfig, GenBlock, GenBlockId, Genesis, OutPointSourceId, PoolId, TxInput, + TxOutput, UtxoOutPoint, }, primitives::{id::WithId, time::Time, BlockHeight, Id, Idable}, time_getter::TimeGetter, @@ -575,6 +580,31 @@ impl TestFramework { }) .unwrap_or(PoSRandomness::new(chain_config.initial_randomness())) } + + pub fn next_block_height(&self) -> BlockHeight { + self.best_block_index().block_height().next_height() + } + + pub fn make_sighash_input_commitments_for_transaction_inputs( + &self, + inputs: &[TxInput], + block_height: BlockHeight, + ) -> Vec> { + let storage_tx = self.storage.transaction_ro().unwrap(); + let utxo_db = UtxosDB::new(&storage_tx); + let pos_db = PoSAccountingDB::<_, TipStorageTag>::new(&storage_tx); + let orders_db = OrdersAccountingDB::new(&storage_tx); + + sighash::input_commitment::make_sighash_input_commitments_for_transaction_inputs( + inputs, + &SighashInputCommitmentInfoProvider(&utxo_db), + &SighashInputCommitmentInfoProvider(&pos_db), + &SighashInputCommitmentInfoProvider(&orders_db), + self.chain_config(), + block_height, + ) + .unwrap() + } } #[rstest] diff --git a/chainstate/test-framework/src/key_manager.rs b/chainstate/test-framework/src/key_manager.rs index 81f12894ab..21d0f0149d 100644 --- a/chainstate/test-framework/src/key_manager.rs +++ b/chainstate/test-framework/src/key_manager.rs @@ -30,7 +30,9 @@ use common::{ standard_signature::StandardInputSignature, InputWitness, }, - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::{ + input_commitment::SighashInputCommitment, sighashtype::SigHashType, signature_hash, + }, }, ChainConfig, Destination, Transaction, TxOutput, }, @@ -149,7 +151,7 @@ impl KeyManager { destination: &Destination, chain_config: &ChainConfig, tx: &Transaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], input_num: usize, ) -> Option { match destination { @@ -163,7 +165,7 @@ impl KeyManager { sighash_type, destination.clone(), tx, - inputs_utxos, + input_commitments, input_num, rng, ) @@ -180,7 +182,7 @@ impl KeyManager { sighash_type, destination.clone(), tx, - inputs_utxos, + input_commitments, input_num, rng, ) @@ -195,7 +197,8 @@ impl KeyManager { AuthorizedClassicalMultisigSpend::new_empty(challenge.clone()); let sighash_type = SigHashType::all(); - let sighash = signature_hash(sighash_type, tx, inputs_utxos, input_num).unwrap(); + let sighash = + signature_hash(sighash_type, tx, input_commitments, input_num).unwrap(); for (key_index, (private_key, _pub_key)) in multisig.keys.iter().enumerate() { let res = sign_classical_multisig_spending( @@ -211,18 +214,21 @@ impl KeyManager { match res { ClassicalMultisigCompletionStatus::Complete(sigs) => { - let sig = if inputs_utxos[input_num].is_some_and(is_htlc_output) { - produce_classical_multisig_signature_for_htlc_input( - chain_config, - &sigs, - sighash_type, - tx, - inputs_utxos, - input_num, - ) - .unwrap() - } else { - StandardInputSignature::new(sighash_type, sigs.encode()) + let sig = match &input_commitments[input_num] { + SighashInputCommitment::Utxo(output) + if is_htlc_output(output.as_ref()) => + { + produce_classical_multisig_signature_for_htlc_input( + chain_config, + &sigs, + sighash_type, + tx, + input_commitments, + input_num, + ) + .unwrap() + } + _ => StandardInputSignature::new(sighash_type, sigs.encode()), }; return Some(InputWitness::Standard(sig)); diff --git a/chainstate/test-framework/src/pos_block_builder.rs b/chainstate/test-framework/src/pos_block_builder.rs index c2840720a2..09e07e982f 100644 --- a/chainstate/test-framework/src/pos_block_builder.rs +++ b/chainstate/test-framework/src/pos_block_builder.rs @@ -38,7 +38,7 @@ use common::{ AccountNonce, AccountType, Block, Destination, GenBlock, PoolId, RequiredConsensus, TxInput, TxOutput, UtxoOutPoint, }, - primitives::{Id, Idable, H256}, + primitives::{Id, Idable}, }; use crypto::{ key::{PrivateKey, PublicKey}, @@ -54,6 +54,8 @@ use tokens_accounting::{InMemoryTokensAccounting, TokensAccountingDB}; pub struct PoSBlockBuilder<'f> { framework: &'f mut TestFramework, prev_block_hash: Id, + // If true, prev_block_hash has been used in some way already and cannot be changed. + prev_block_hash_used: bool, timestamp: BlockTimestamp, consensus_data: Option, transactions: Vec, @@ -115,6 +117,7 @@ impl<'f> PoSBlockBuilder<'f> { framework, transactions, prev_block_hash, + prev_block_hash_used: false, timestamp, consensus_data: None, staking_pool: None, @@ -144,16 +147,14 @@ impl<'f> PoSBlockBuilder<'f> { /// Overrides the previous block hash that is deduced by default as the best block. pub fn with_parent(mut self, prev_block_hash: Id) -> Self { + assert!( + !self.prev_block_hash_used, + "The current builder state may depend on the previous value of prev_block_hash; consider re-ordering function calls" + ); self.prev_block_hash = prev_block_hash; self } - /// Overrides the previous block hash by a random value making the resulting block an orphan. - pub fn make_orphan(mut self, rng: &mut impl Rng) -> Self { - self.prev_block_hash = Id::new(H256::random_using(rng)); - self - } - /// Overrides the consensus data that is `ConsensusData::None` by default. pub fn with_consensus_data(mut self, data: PoSData) -> Self { self.consensus_data = Some(ConsensusData::PoS(Box::new(data))); @@ -376,6 +377,7 @@ impl<'f> PoSBlockBuilder<'f> { .filter(|(outpoint, _)| !self.used_utxo.contains(outpoint)) .collect(); let utxo_set = utxo::UtxosDBInMemoryImpl::new(self.prev_block_hash, utxo_set); + self.prev_block_hash_used = true; let account_nonce_getter = Box::new(|account: AccountType| -> Option { self.account_nonce_tracker.get(&account).copied().or_else(|| { @@ -409,13 +411,21 @@ impl<'f> PoSBlockBuilder<'f> { let destination_getter = SignatureDestinationGetter::new_for_transaction( &tokens_db, &pos_db, &orders_db, &utxo_set, ); + let block_height = self + .framework + .gen_block_index(&self.prev_block_hash) + .block_height() + .next_height(); let witnesses = sign_witnesses( rng, &self.framework.key_manager, self.framework.chainstate.get_chain_config(), &tx, &utxo_set, + &pos_db, + &orders_db, destination_getter, + block_height, ); let tx = SignedTransaction::new(tx, witnesses).expect("invalid witness count"); diff --git a/chainstate/test-framework/src/utils.rs b/chainstate/test-framework/src/utils.rs index 8fb4550799..ff30757910 100644 --- a/chainstate/test-framework/src/utils.rs +++ b/chainstate/test-framework/src/utils.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use crate::{ framework::BlockOutputs, key_manager::KeyManager, signature_destination_getter::SignatureDestinationGetter, TestFramework, @@ -27,12 +29,18 @@ use common::{ output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, - sighash::sighashtype::SigHashType, + sighash::{ + self, + input_commitment::{ + make_sighash_input_commitments_for_transaction_inputs, SighashInputCommitment, + }, + sighashtype::SigHashType, + }, }, stakelock::StakePoolData, Block, ChainConfig, CoinUnit, ConsensusUpgrade, Destination, GenBlock, Genesis, - NetUpgrades, OutPointSourceId, PoSChainConfig, PoSChainConfigBuilder, PoolId, TxInput, - TxOutput, UtxoOutPoint, + NetUpgrades, OrderId, OutPointSourceId, PoSChainConfig, PoSChainConfigBuilder, PoolId, + TxInput, TxOutput, UtxoOutPoint, }, primitives::{per_thousand::PerThousand, Amount, BlockHeight, Compact, Id, Idable, H256}, Uint256, @@ -42,6 +50,7 @@ use crypto::{ key::{KeyKind, PrivateKey, PublicKey}, vrf::{VRFPrivateKey, VRFPublicKey}, }; +use orders_accounting::OrdersAccountingView; use pos_accounting::{PoSAccountingDB, PoSAccountingView}; use randomness::{CryptoRng, Rng}; use utxo::UtxosView; @@ -267,7 +276,9 @@ pub fn produce_kernel_signature( SigHashType::default(), staking_destination, &block_reward_tx, - std::iter::once(Some(&utxo)).collect::>().as_slice(), + std::iter::once(SighashInputCommitment::Utxo(Cow::Borrowed(&utxo))) + .collect::>() + .as_slice(), 0, rng, ) @@ -363,7 +374,10 @@ pub fn sign_witnesses( chain_config: &ChainConfig, tx: &common::chain::Transaction, utxo_view: &impl UtxosView, + pos_accounting_view: &impl PoSAccountingView, + order_accounting_view: &impl OrdersAccountingView, destination_getter: SignatureDestinationGetter, + block_height: BlockHeight, ) -> Vec { let inputs_utxos = tx .inputs() @@ -377,7 +391,15 @@ pub fn sign_witnesses( | TxInput::OrderAccountCommand(..) => None, }) .collect::>(); - let input_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + tx.inputs(), + &sighash::input_commitment::TrivialUtxoProvider(&inputs_utxos), + &SighashInputCommitmentInfoProvider(&pos_accounting_view), + &SighashInputCommitmentInfoProvider(&order_accounting_view), + chain_config, + block_height, + ) + .unwrap(); let witnesses = tx .inputs() @@ -386,7 +408,7 @@ pub fn sign_witnesses( .map(|(idx, input)| { let dest = destination_getter.call(input).unwrap(); key_manager - .get_signature(rng, &dest, chain_config, tx, &input_utxos_refs, idx) + .get_signature(rng, &dest, chain_config, tx, &input_commitments, idx) .unwrap() }) .collect(); @@ -465,3 +487,63 @@ pub fn create_custom_genesis_with_stake_pool( vec![mint_output, initial_pool], ) } + +pub struct SighashInputCommitmentInfoProvider<'a, T>(pub &'a T); + +impl<'a, UV> sighash::input_commitment::UtxoProvider<'static> + for SighashInputCommitmentInfoProvider<'a, UV> +where + UV: UtxosView, +{ + type Error = std::convert::Infallible; + + fn get_utxo( + &self, + _tx_input_index: usize, + outpoint: &UtxoOutPoint, + ) -> Result>, Self::Error> { + Ok(self.0.utxo(outpoint).unwrap().map(|utxo| Cow::Owned(utxo.take_output()))) + } +} + +impl<'a, AV> sighash::input_commitment::PoolInfoProvider + for SighashInputCommitmentInfoProvider<'a, AV> +where + AV: pos_accounting::PoSAccountingView, +{ + type Error = std::convert::Infallible; + + fn get_pool_info( + &self, + pool_id: &PoolId, + ) -> Result, Self::Error> { + Ok(self.0.get_pool_data(*pool_id).unwrap().map(|pool_data| { + let staker_balance = pool_data.staker_balance().unwrap(); + sighash::input_commitment::PoolInfo { staker_balance } + })) + } +} + +impl<'a, OV> sighash::input_commitment::OrderInfoProvider + for SighashInputCommitmentInfoProvider<'a, OV> +where + OV: orders_accounting::OrdersAccountingView, +{ + type Error = std::convert::Infallible; + + fn get_order_info( + &self, + order_id: &OrderId, + ) -> Result, Self::Error> { + Ok(self.0.get_order_data(order_id).unwrap().map(|order_data| { + let ask_balance = self.0.get_ask_balance(order_id).unwrap(); + let give_balance = self.0.get_give_balance(order_id).unwrap(); + sighash::input_commitment::OrderInfo { + initially_asked: order_data.ask().clone(), + initially_given: order_data.give().clone(), + ask_balance, + give_balance, + } + })) + } +} diff --git a/chainstate/test-suite/src/tests/delegation_tests.rs b/chainstate/test-suite/src/tests/delegation_tests.rs index b14aa1d518..078021bcae 100644 --- a/chainstate/test-suite/src/tests/delegation_tests.rs +++ b/chainstate/test-suite/src/tests/delegation_tests.rs @@ -852,6 +852,8 @@ fn create_pool_and_delegation_and_delegate_same_block(#[case] seed: Seed) { #[trace] #[case(Seed::from_entropy())] fn check_signature_on_spend_share(#[case] seed: Seed) { + use common::chain::signature::sighash::input_commitment::SighashInputCommitment; + utils::concurrency::model(move || { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); @@ -920,9 +922,6 @@ fn check_signature_on_spend_share(#[case] seed: Seed) { )) ); - let inputs_utxos = [None]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); - // Try to spend share with wrong signature let tx = { let tx = spend_share_tx_no_signature.transaction().clone(); @@ -933,7 +932,7 @@ fn check_signature_on_spend_share(#[case] seed: Seed) { Default::default(), Destination::PublicKey(some_pk), &tx, - &inputs_utxos_refs, + &[SighashInputCommitment::None], 0, &mut rng, ) @@ -963,7 +962,7 @@ fn check_signature_on_spend_share(#[case] seed: Seed) { Default::default(), Destination::PublicKey(delegation_pk), &tx, - &inputs_utxos_refs, + &[SighashInputCommitment::None], 0, &mut rng, ) diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index 806ae2f95c..4025486f8a 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::{borrow::Cow, collections::BTreeMap}; use rstest::rstest; @@ -28,6 +28,7 @@ use common::{ output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, + sighash::input_commitment::SighashInputCommitment, DestinationSigError, }, timelock::OutputTimeLock, @@ -3282,11 +3283,9 @@ fn check_signature_on_mint(#[case] seed: Seed) { )) ); - let inputs_utxos = vec![ - None, - tf.chainstate.utxo(&utxo_with_change).unwrap().map(|utxo| utxo.output().clone()), - ]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let utxo = tf.chainstate.utxo(&utxo_with_change).unwrap().unwrap().take_output(); + let inputs_info_refs = + vec![SighashInputCommitment::None, SighashInputCommitment::Utxo(Cow::Borrowed(&utxo))]; // Try to mint with wrong signature let tx = { @@ -3298,7 +3297,7 @@ fn check_signature_on_mint(#[case] seed: Seed) { Default::default(), Destination::PublicKey(some_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -3332,7 +3331,7 @@ fn check_signature_on_mint(#[case] seed: Seed) { Default::default(), Destination::PublicKey(controller_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -3387,12 +3386,11 @@ fn check_signature_on_unmint(#[case] seed: Seed) { // Mint some tokens let amount_to_mint = Amount::from_atoms(rng.gen_range(2..100_000_000)); let mint_tx = { - let inputs_utxos = vec![ - None, - tf.chainstate.utxo(&utxo_with_change).unwrap().map(|utxo| utxo.output().clone()), + let utxo = tf.chainstate.utxo(&utxo_with_change).unwrap().unwrap().take_output(); + let inputs_info_refs = vec![ + SighashInputCommitment::None, + SighashInputCommitment::Utxo(Cow::Borrowed(&utxo)), ]; - let inputs_utxos_refs = - inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); let tx = TransactionBuilder::new() .add_input( @@ -3423,7 +3421,7 @@ fn check_signature_on_unmint(#[case] seed: Seed) { Default::default(), Destination::PublicKey(controller_pk.clone()), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -3477,17 +3475,22 @@ fn check_signature_on_unmint(#[case] seed: Seed) { ); let inputs_utxos = vec![ - None, tf.chainstate .utxo(&UtxoOutPoint::new(mint_tx_id.into(), 0)) .unwrap() - .map(|utxo| utxo.output().clone()), + .unwrap() + .take_output(), tf.chainstate .utxo(&UtxoOutPoint::new(mint_tx_id.into(), 1)) .unwrap() - .map(|utxo| utxo.output().clone()), + .unwrap() + .take_output(), + ]; + let inputs_info_refs = vec![ + SighashInputCommitment::None, + SighashInputCommitment::Utxo(Cow::Borrowed(&inputs_utxos[0])), + SighashInputCommitment::Utxo(Cow::Borrowed(&inputs_utxos[1])), ]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); // Try to unmint with wrong signature let tx = { @@ -3499,7 +3502,7 @@ fn check_signature_on_unmint(#[case] seed: Seed) { Default::default(), Destination::PublicKey(some_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -3537,7 +3540,7 @@ fn check_signature_on_unmint(#[case] seed: Seed) { Default::default(), Destination::PublicKey(controller_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -3621,11 +3624,9 @@ fn check_signature_on_lock_supply(#[case] seed: Seed) { )) ); - let inputs_utxos = vec![ - None, - tf.chainstate.utxo(&utxo_with_change).unwrap().map(|utxo| utxo.output().clone()), - ]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let utxo = tf.chainstate.utxo(&utxo_with_change).unwrap().unwrap().take_output(); + let inputs_info_refs = + vec![SighashInputCommitment::None, SighashInputCommitment::Utxo(Cow::Borrowed(&utxo))]; // Try to lock with wrong signature let tx = { @@ -3637,7 +3638,7 @@ fn check_signature_on_lock_supply(#[case] seed: Seed) { Default::default(), Destination::PublicKey(some_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -3671,7 +3672,7 @@ fn check_signature_on_lock_supply(#[case] seed: Seed) { Default::default(), Destination::PublicKey(controller_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -4820,19 +4821,17 @@ fn check_signature_on_freeze_unfreeze(#[case] seed: Seed) { )) ); - let inputs_utxos = vec![ - None, - tf.chainstate.utxo(&utxo_with_change).unwrap().map(|utxo| utxo.output().clone()), - ]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let utxo = tf.chainstate.utxo(&utxo_with_change).unwrap().unwrap().take_output(); + let inputs_info_refs = + vec![SighashInputCommitment::None, SighashInputCommitment::Utxo(Cow::Borrowed(&utxo))]; - let mut replace_signature_for_tx = |tx, sk, pk, inputs_utxos_refs| { + let mut replace_signature_for_tx = |tx, sk, pk, inputs_info_refs| { let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( &sk, Default::default(), Destination::PublicKey(pk), &tx, - inputs_utxos_refs, + inputs_info_refs, 0, &mut rng3, ) @@ -4851,7 +4850,7 @@ fn check_signature_on_freeze_unfreeze(#[case] seed: Seed) { freeze_tx_no_signatures.transaction().clone(), random_sk, random_pk, - &inputs_utxos_refs, + &inputs_info_refs, ); let result = tf.make_block_builder().add_transaction(signed_tx).build_and_process(&mut rng); @@ -4870,7 +4869,7 @@ fn check_signature_on_freeze_unfreeze(#[case] seed: Seed) { freeze_tx_no_signatures.transaction().clone(), controller_sk.clone(), controller_pk.clone(), - &inputs_utxos_refs, + &inputs_info_refs, ); tf.make_block_builder() .add_transaction(signed_tx) @@ -4907,14 +4906,14 @@ fn check_signature_on_freeze_unfreeze(#[case] seed: Seed) { )) ); - let inputs_utxos = vec![ - None, - tf.chainstate - .utxo(&UtxoOutPoint::new(freeze_tx_id.into(), 0)) - .unwrap() - .map(|utxo| utxo.output().clone()), - ]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let utxo = tf + .chainstate + .utxo(&UtxoOutPoint::new(freeze_tx_id.into(), 0)) + .unwrap() + .unwrap() + .take_output(); + let inputs_info_refs = + vec![SighashInputCommitment::None, SighashInputCommitment::Utxo(Cow::Borrowed(&utxo))]; // Try unfreeze with random signature let (random_sk, random_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); @@ -4922,7 +4921,7 @@ fn check_signature_on_freeze_unfreeze(#[case] seed: Seed) { unfreeze_tx_no_signatures.transaction().clone(), random_sk, random_pk, - &inputs_utxos_refs, + &inputs_info_refs, ); let result = tf.make_block_builder().add_transaction(signed_tx).build_and_process(&mut rng); @@ -4941,7 +4940,7 @@ fn check_signature_on_freeze_unfreeze(#[case] seed: Seed) { unfreeze_tx_no_signatures.transaction().clone(), controller_sk, controller_pk, - &inputs_utxos_refs, + &inputs_info_refs, ); tf.make_block_builder() .add_transaction(signed_tx) @@ -5022,11 +5021,9 @@ fn check_signature_on_change_authority(#[case] seed: Seed) { )) ); - let inputs_utxos = vec![ - None, - tf.chainstate.utxo(&utxo_with_change).unwrap().map(|utxo| utxo.output().clone()), - ]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let utxo = tf.chainstate.utxo(&utxo_with_change).unwrap().unwrap().take_output(); + let inputs_info_refs = + vec![SighashInputCommitment::None, SighashInputCommitment::Utxo(Cow::Borrowed(&utxo))]; // Try to change authority with wrong signature let tx = { @@ -5038,7 +5035,7 @@ fn check_signature_on_change_authority(#[case] seed: Seed) { Default::default(), Destination::PublicKey(some_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -5072,7 +5069,7 @@ fn check_signature_on_change_authority(#[case] seed: Seed) { Default::default(), Destination::PublicKey(original_pk.clone()), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -5089,14 +5086,14 @@ fn check_signature_on_change_authority(#[case] seed: Seed) { tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); // Now try to change authority once more with original key - let inputs_utxos = vec![ - None, - tf.chainstate - .utxo(&UtxoOutPoint::new(tx_1_id.into(), 0)) - .unwrap() - .map(|utxo| utxo.output().clone()), - ]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let utxo = tf + .chainstate + .utxo(&UtxoOutPoint::new(tx_1_id.into(), 0)) + .unwrap() + .unwrap() + .take_output(); + let inputs_info_refs = + vec![SighashInputCommitment::None, SighashInputCommitment::Utxo(Cow::Borrowed(&utxo))]; let tx_2_no_signatures = TransactionBuilder::new() .add_input( @@ -5120,7 +5117,7 @@ fn check_signature_on_change_authority(#[case] seed: Seed) { Default::default(), Destination::PublicKey(original_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -5153,7 +5150,7 @@ fn check_signature_on_change_authority(#[case] seed: Seed) { Default::default(), Destination::PublicKey(new_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -6230,11 +6227,9 @@ fn only_authority_can_change_metadata_uri(#[case] seed: Seed) { )) ); - let inputs_utxos = vec![ - None, - tf.chainstate.utxo(&utxo_with_change).unwrap().map(|utxo| utxo.output().clone()), - ]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let utxo = tf.chainstate.utxo(&utxo_with_change).unwrap().unwrap().take_output(); + let inputs_info_refs = + vec![SighashInputCommitment::None, SighashInputCommitment::Utxo(Cow::Borrowed(&utxo))]; // Try to change metadata with wrong signature let tx = { @@ -6246,7 +6241,7 @@ fn only_authority_can_change_metadata_uri(#[case] seed: Seed) { Default::default(), Destination::PublicKey(some_pk), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) @@ -6280,7 +6275,7 @@ fn only_authority_can_change_metadata_uri(#[case] seed: Seed) { Default::default(), Destination::PublicKey(original_pk.clone()), &tx, - &inputs_utxos_refs, + &inputs_info_refs, 0, &mut rng, ) diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index 901286ad20..da7ff2fe24 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use chainstate::{BlockError, ChainstateError, ConnectTransactionError}; use chainstate_test_framework::{TestFramework, TransactionBuilder}; use common::{ @@ -31,7 +33,9 @@ use common::{ }, standard_signature::StandardInputSignature, }, - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::{ + input_commitment::SighashInputCommitment, sighashtype::SigHashType, signature_hash, + }, DestinationSigError, }, signed_transaction::SignedTransaction, @@ -149,7 +153,7 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { (&PublicKey::from_private_key(&test_fixture.alice_sk)).into(), ), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, test_fixture.secret.clone(), &mut rng, @@ -196,7 +200,7 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { (&PublicKey::from_private_key(&test_fixture.bob_sk)).into(), ), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, &mut rng, ) @@ -244,7 +248,7 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { (&PublicKey::from_private_key(&test_fixture.bob_sk)).into(), ), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, random_secret, &mut rng, @@ -286,7 +290,7 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { SigHashType::all(), Destination::PublicKeyHash((&PublicKey::from_private_key(&test_fixture.bob_sk)).into()), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, test_fixture.secret, &mut rng, @@ -354,7 +358,9 @@ fn refund_htlc(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, ) .unwrap(); @@ -373,7 +379,7 @@ fn refund_htlc(#[case] seed: Seed) { &authorization, SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, ) .unwrap(); @@ -425,7 +431,9 @@ fn refund_htlc(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, ) .unwrap(); @@ -480,7 +488,9 @@ fn refund_htlc(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, ) .unwrap(); @@ -533,7 +543,7 @@ fn refund_htlc(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, ) .unwrap(); @@ -552,7 +562,7 @@ fn refund_htlc(#[case] seed: Seed) { &authorization, SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, ) .unwrap(); @@ -814,7 +824,9 @@ fn spend_tokens(#[case] seed: Seed) { SigHashType::all(), Destination::PublicKeyHash((&PublicKey::from_private_key(&test_fixture.bob_sk)).into()), &tx, - &[Some(&mint_token_v1_tx.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &mint_token_v1_tx.transaction().outputs()[0], + ))], 0, test_fixture.secret, &mut rng, diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index 6785e3b239..7a30dd2678 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -45,7 +45,7 @@ use test_utils::{ random::{make_seedable_rng, Seed}, random_ascii_alphanumeric_string, }; -use tx_verifier::error::{InputCheckError, ScriptError, TranslationError}; +use tx_verifier::error::{InputCheckError, InputCheckErrorPayload, ScriptError}; use crate::tests::helpers::{issue_token_from_block, mint_tokens_in_block}; @@ -1336,13 +1336,15 @@ fn conclude_order_check_signature(#[case] seed: Seed, #[case] version: OrdersVer let tokens_circulating_supply = tf.chainstate.get_token_circulating_supply(&token_id).unwrap().unwrap(); - let ask_amount = Amount::from_atoms(rng.gen_range(1u128..1000)); - let give_amount = - Amount::from_atoms(rng.gen_range(1u128..=tokens_circulating_supply.into_atoms())); + let initially_asked = OutputValue::Coin(Amount::from_atoms(rng.gen_range(1u128..1000))); + let initially_given = OutputValue::TokenV1( + token_id, + Amount::from_atoms(rng.gen_range(1u128..=tokens_circulating_supply.into_atoms())), + ); let order_data = OrderData::new( Destination::PublicKey(order_pk.clone()), - OutputValue::Coin(ask_amount), - OutputValue::TokenV1(token_id, give_amount), + initially_asked.clone(), + initially_given.clone(), ); let order_id = make_order_id(&tokens_outpoint); @@ -1352,25 +1354,32 @@ fn conclude_order_check_signature(#[case] seed: Seed, #[case] version: OrdersVer .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + let conclude_input = match version { + OrdersVersion::V0 => TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + OrdersVersion::V1 => { + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) + } + }; + let tx = TransactionBuilder::new() + .add_input(conclude_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::Transfer( + initially_given.clone(), + Destination::AnyoneCanSpend, + )) + .build(); + + let input_commitments = tf.make_sighash_input_commitments_for_transaction_inputs( + tx.inputs(), + tf.next_block_height(), + ); + // try conclude without signature { - let conclude_input = match version { - OrdersVersion::V0 => TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), - OrdersVersion::V1 => { - TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) - } - }; - let tx = TransactionBuilder::new() - .add_input(conclude_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::Transfer( - OutputValue::TokenV1(token_id, give_amount), - Destination::AnyoneCanSpend, - )) - .build(); - let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + let result = + tf.make_block_builder().add_transaction(tx.clone()).build_and_process(&mut rng); assert_eq!( result.unwrap_err(), @@ -1387,40 +1396,20 @@ fn conclude_order_check_signature(#[case] seed: Seed, #[case] version: OrdersVer // try conclude with wrong signature { - let conclude_input = match version { - OrdersVersion::V0 => TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), - OrdersVersion::V1 => { - TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) - } - }; - let tx = TransactionBuilder::new() - .add_input(conclude_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::Transfer( - OutputValue::TokenV1(token_id, give_amount), - Destination::AnyoneCanSpend, - )) - .build(); - - let inputs_utxos: Vec> = vec![None]; - let inputs_utxos_refs = - inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); let (some_sk, some_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( &some_sk, Default::default(), Destination::PublicKey(some_pk), &tx, - &inputs_utxos_refs, + &input_commitments, 0, &mut rng, ) .unwrap(); let tx = SignedTransaction::new( - tx.take_transaction(), + tx.transaction().clone(), vec![InputWitness::Standard(account_sig)], ) .unwrap(); @@ -1442,42 +1431,25 @@ fn conclude_order_check_signature(#[case] seed: Seed, #[case] version: OrdersVer } // valid case - let conclude_input = match version { - OrdersVersion::V0 => TxInput::AccountCommand( - AccountNonce::new(0), - AccountCommand::ConcludeOrder(order_id), - ), - OrdersVersion::V1 => { - TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(order_id)) - } - }; - let tx = TransactionBuilder::new() - .add_input(conclude_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::Transfer( - OutputValue::TokenV1(token_id, give_amount), - Destination::AnyoneCanSpend, - )) - .build(); - - let inputs_utxos: Vec> = vec![None]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); - let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( - &order_sk, - Default::default(), - Destination::PublicKey(order_pk), - &tx, - &inputs_utxos_refs, - 0, - &mut rng, - ) - .unwrap(); + { + let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( + &order_sk, + Default::default(), + Destination::PublicKey(order_pk), + &tx, + &input_commitments, + 0, + &mut rng, + ) + .unwrap(); - let tx = SignedTransaction::new( - tx.take_transaction(), - vec![InputWitness::Standard(account_sig)], - ) - .unwrap(); - tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + let tx = SignedTransaction::new( + tx.take_transaction(), + vec![InputWitness::Standard(account_sig)], + ) + .unwrap(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + } }); } @@ -2954,7 +2926,7 @@ fn freeze_order_check_storage(#[case] seed: Seed, #[case] version: OrdersVersion chainstate::BlockError::StateUpdateFailed( ConnectTransactionError::InputCheck(InputCheckError::new( 0, - TranslationError::OrderNotFound(random_order_id) + InputCheckErrorPayload::OrderNotFound(random_order_id) )) ) ) @@ -3041,15 +3013,22 @@ fn freeze_order_check_signature(#[case] seed: Seed) { .build(); tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + let tx = TransactionBuilder::new() + .add_input( + TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order_id)), + InputWitness::NoSignature(None), + ) + .build(); + + let input_commitments = tf.make_sighash_input_commitments_for_transaction_inputs( + tx.inputs(), + tf.next_block_height(), + ); + // try freeze without signature { - let tx = TransactionBuilder::new() - .add_input( - TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order_id)), - InputWitness::NoSignature(None), - ) - .build(); - let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + let result = + tf.make_block_builder().add_transaction(tx.clone()).build_and_process(&mut rng); assert_eq!( result.unwrap_err(), @@ -3066,30 +3045,20 @@ fn freeze_order_check_signature(#[case] seed: Seed) { // try conclude with wrong signature { - let tx = TransactionBuilder::new() - .add_input( - TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order_id)), - InputWitness::NoSignature(None), - ) - .build(); - - let inputs_utxos: Vec> = vec![None]; - let inputs_utxos_refs = - inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); let (some_sk, some_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( &some_sk, Default::default(), Destination::PublicKey(some_pk), &tx, - &inputs_utxos_refs, + &input_commitments, 0, &mut rng, ) .unwrap(); let tx = SignedTransaction::new( - tx.take_transaction(), + tx.transaction().clone(), vec![InputWitness::Standard(account_sig)], ) .unwrap(); @@ -3111,32 +3080,25 @@ fn freeze_order_check_signature(#[case] seed: Seed) { } // valid case - let tx = TransactionBuilder::new() - .add_input( - TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(order_id)), - InputWitness::NoSignature(None), + { + let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( + &order_sk, + Default::default(), + Destination::PublicKey(order_pk), + &tx, + &input_commitments, + 0, + &mut rng, ) - .build(); - - let inputs_utxos: Vec> = vec![None]; - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); - let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( - &order_sk, - Default::default(), - Destination::PublicKey(order_pk), - &tx, - &inputs_utxos_refs, - 0, - &mut rng, - ) - .unwrap(); + .unwrap(); - let tx = SignedTransaction::new( - tx.take_transaction(), - vec![InputWitness::Standard(account_sig)], - ) - .unwrap(); - tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + let tx = SignedTransaction::new( + tx.take_transaction(), + vec![InputWitness::Standard(account_sig)], + ) + .unwrap(); + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + } }); } @@ -3291,7 +3253,7 @@ fn fill_freeze_conclude_order(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - //Try freezing concluded order + // Try freezing concluded order { let result = tf .make_block_builder() @@ -3311,7 +3273,7 @@ fn fill_freeze_conclude_order(#[case] seed: Seed) { result.unwrap_err(), chainstate::ChainstateError::ProcessBlockError( chainstate::BlockError::StateUpdateFailed(ConnectTransactionError::InputCheck( - InputCheckError::new(0, TranslationError::OrderNotFound(order_id)) + InputCheckError::new(0, InputCheckErrorPayload::OrderNotFound(order_id)) )) ) ); diff --git a/chainstate/test-suite/src/tests/pos_processing_tests.rs b/chainstate/test-suite/src/tests/pos_processing_tests.rs index f54345f8cf..120d87f7da 100644 --- a/chainstate/test-suite/src/tests/pos_processing_tests.rs +++ b/chainstate/test-suite/src/tests/pos_processing_tests.rs @@ -41,7 +41,10 @@ use common::{ output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, - sighash::sighashtype::SigHashType, + sighash::{ + self, input_commitment::make_sighash_input_commitments_for_kernel_inputs, + sighashtype::SigHashType, + }, }, stakelock::StakePoolData, timelock::OutputTimeLock, @@ -282,6 +285,13 @@ fn produce_kernel_signature( let kernel_inputs = vec![kernel_outpoint.into()]; + let utxos = [Some(utxo.clone())]; + let input_commitments = make_sighash_input_commitments_for_kernel_inputs( + &kernel_inputs, + &sighash::input_commitment::TrivialUtxoProvider(&utxos), + ) + .unwrap(); + let block_reward_tx = BlockRewardTransactable::new(Some(kernel_inputs.as_slice()), Some(reward_outputs), None); StandardInputSignature::produce_uniparty_signature_for_input( @@ -289,7 +299,7 @@ fn produce_kernel_signature( SigHashType::default(), staking_destination, &block_reward_tx, - std::iter::once(Some(utxo)).collect::>().as_slice(), + &input_commitments, 0, rng, ) @@ -2132,18 +2142,16 @@ fn pos_decommission_genesis_pool(#[case] seed: Seed) { .transaction() .clone(); - let input_utxo = tf - .chainstate - .utxo(&UtxoOutPoint::new(tf.best_block_id().into(), 0)) - .unwrap() - .unwrap(); - + let input_commitments = tf.make_sighash_input_commitments_for_transaction_inputs( + tx.inputs(), + tf.next_block_height(), + ); let input_sign = StandardInputSignature::produce_uniparty_signature_for_input( &genesis_staking_sk, SigHashType::all(), Destination::PublicKey(genesis_staking_pk), &tx, - &[Some(input_utxo.output())], + &input_commitments, 0, &mut rng, ) diff --git a/chainstate/test-suite/src/tests/processing_tests.rs b/chainstate/test-suite/src/tests/processing_tests.rs index b5150d2f20..9dcd3c12e7 100644 --- a/chainstate/test-suite/src/tests/processing_tests.rs +++ b/chainstate/test-suite/src/tests/processing_tests.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use chainstate::{ chainstate_interface::ChainstateInterface, make_chainstate, BlockError, @@ -36,7 +36,7 @@ use common::{ output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, - sighash::sighashtype::SigHashType, + sighash::{input_commitment::SighashInputCommitment, sighashtype::SigHashType}, DestinationSigError, }, signed_transaction::SignedTransaction, @@ -1461,7 +1461,9 @@ fn spend_timelocked_signed_output(#[case] seed: Seed) { SigHashType::all(), Destination::PublicKey(public_key.clone()), &tx_2, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, &mut rng, ) @@ -1515,7 +1517,9 @@ fn spend_timelocked_signed_output(#[case] seed: Seed) { SigHashType::all(), Destination::PublicKey(random_public_key), &tx_2, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, &mut rng, ) @@ -1543,7 +1547,7 @@ fn spend_timelocked_signed_output(#[case] seed: Seed) { SigHashType::all(), Destination::PublicKey(public_key), &tx_2, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, &mut rng, ) diff --git a/chainstate/test-suite/src/tests/signature_tests.rs b/chainstate/test-suite/src/tests/signature_tests.rs index dee5b2339a..080042a7c5 100644 --- a/chainstate/test-suite/src/tests/signature_tests.rs +++ b/chainstate/test-suite/src/tests/signature_tests.rs @@ -13,39 +13,38 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::{borrow::Cow, num::NonZeroU8}; + use chainstate::ConnectTransactionError; -use common::address::pubkeyhash::PublicKeyHash; -use common::chain; -use common::chain::classic_multisig::ClassicMultisigChallenge; -use common::chain::signature::inputsig::classical_multisig::authorize_classical_multisig::AuthorizedClassicalMultisigSpend; -use common::chain::signature::DestinationSigError; -use common::chain::signed_transaction::SignedTransaction; -use common::chain::ConsensusUpgrade; -use common::chain::NetUpgrades; -use common::primitives::BlockHeight; -use common::primitives::Idable; +use chainstate_test_framework::{ + anyonecanspend_address, empty_witness, TestFramework, TransactionBuilder, +}; use common::{ + address::pubkeyhash::PublicKeyHash, chain::{ + classic_multisig::ClassicMultisigChallenge, output_value::OutputValue, - signature::{inputsig::InputWitness, sighash::sighashtype::SigHashType}, - Destination, OutPointSourceId, TxInput, TxOutput, + signature::{ + inputsig::{ + classical_multisig::authorize_classical_multisig::AuthorizedClassicalMultisigSpend, + standard_signature::StandardInputSignature, InputWitness, + }, + sighash::{ + input_commitment::SighashInputCommitment, sighashtype::SigHashType, signature_hash, + }, + DestinationSigError, + }, + signed_transaction::SignedTransaction, + ConsensusUpgrade, Destination, NetUpgrades, OutPointSourceId, TxInput, TxOutput, }, - primitives::Amount, + primitives::{Amount, BlockHeight, Idable}, }; use crypto::key::{KeyKind, PrivateKey}; - -use chainstate_test_framework::TransactionBuilder; -use chainstate_test_framework::{anyonecanspend_address, empty_witness, TestFramework}; -use common::chain::signature::inputsig::standard_signature::StandardInputSignature; -use common::chain::signature::sighash::signature_hash; use randomness::{Rng, SliceRandom}; use rstest::rstest; use serialization::Encode; -use std::num::NonZeroU8; -use test_utils::random::gen_random_bytes; -use test_utils::random::Seed; -use tx_verifier::error::InputCheckError; -use tx_verifier::error::ScriptError; +use test_utils::random::{gen_random_bytes, Seed}; +use tx_verifier::error::{InputCheckError, ScriptError}; #[rstest] #[trace] @@ -157,7 +156,9 @@ fn signed_tx(#[case] seed: Seed) { SigHashType::all(), Destination::PublicKey(public_key), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, &mut rng, ) @@ -244,7 +245,7 @@ fn signed_classical_multisig_tx(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, ) .unwrap(); @@ -324,7 +325,9 @@ fn signed_classical_multisig_tx(#[case] seed: Seed) { &authorization, SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, ) .unwrap(); @@ -413,7 +416,7 @@ fn signed_classical_multisig_tx_missing_sigs(#[case] seed: Seed) { let sighash = signature_hash( SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx_1.transaction().outputs()[0]))], 0, ) .unwrap(); @@ -435,7 +438,9 @@ fn signed_classical_multisig_tx_missing_sigs(#[case] seed: Seed) { &authorization, SigHashType::all(), &tx, - &[Some(&tx_1.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed( + &tx_1.transaction().outputs()[0], + ))], 0, ) .unwrap(); @@ -571,17 +576,18 @@ fn no_sig_data_not_allowed( ) { utils::concurrency::model(move || { let mut rng = test_utils::random::make_seedable_rng(seed); - let chain_config = chain::config::Builder::new(chain::config::ChainType::Regtest) - .data_in_no_signature_witness_allowed(data_allowed) - .consensus_upgrades( - NetUpgrades::initialize(vec![( - BlockHeight::zero(), - ConsensusUpgrade::IgnoreConsensus, - )]) - .unwrap(), - ) - .genesis_unittest(Destination::AnyoneCanSpend) - .build(); + let chain_config = + common::chain::config::Builder::new(common::chain::config::ChainType::Regtest) + .data_in_no_signature_witness_allowed(data_allowed) + .consensus_upgrades( + NetUpgrades::initialize(vec![( + BlockHeight::zero(), + ConsensusUpgrade::IgnoreConsensus, + )]) + .unwrap(), + ) + .genesis_unittest(Destination::AnyoneCanSpend) + .build(); let mut tf = TestFramework::builder(&mut rng).with_chain_config(chain_config).build(); @@ -642,15 +648,16 @@ fn no_sig_data_not_allowed( fn try_to_spend_with_no_signature_on_mainnet(#[case] seed: Seed) { utils::concurrency::model(move || { let mut rng = test_utils::random::make_seedable_rng(seed); - let chain_config = chain::config::Builder::new(chain::config::ChainType::Mainnet) - .consensus_upgrades( - NetUpgrades::initialize(vec![( - BlockHeight::zero(), - ConsensusUpgrade::IgnoreConsensus, - )]) - .unwrap(), - ) - .build(); + let chain_config = + common::chain::config::Builder::new(common::chain::config::ChainType::Mainnet) + .consensus_upgrades( + NetUpgrades::initialize(vec![( + BlockHeight::zero(), + ConsensusUpgrade::IgnoreConsensus, + )]) + .unwrap(), + ) + .build(); let mut tf = TestFramework::builder(&mut rng).with_chain_config(chain_config).build(); diff --git a/chainstate/test-suite/src/tests/stake_pool_tests.rs b/chainstate/test-suite/src/tests/stake_pool_tests.rs index f94f6f2356..9db17f4617 100644 --- a/chainstate/test-suite/src/tests/stake_pool_tests.rs +++ b/chainstate/test-suite/src/tests/stake_pool_tests.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use chainstate::{BlockError, ChainstateError, ConnectTransactionError, IOPolicyError}; use chainstate::{BlockSource, CheckBlockError}; use chainstate_test_framework::{ @@ -26,6 +28,7 @@ use common::{ output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, + sighash::input_commitment::SighashInputCommitment, DestinationSigError, }, stakelock::StakePoolData, @@ -734,7 +737,10 @@ fn decommission_from_stake_pool_with_staker_key(#[case] seed: Seed) { let (best_block_source_id, best_block_utxos) = tf.outputs_from_genblock(tf.best_block_id()).into_iter().next().unwrap(); - let inputs_utxos = best_block_utxos.iter().map(Some).collect::>(); + let inputs_info_refs = best_block_utxos + .iter() + .map(|u| SighashInputCommitment::Utxo(Cow::Borrowed(u))) + .collect::>(); { // sign with staking key @@ -758,7 +764,7 @@ fn decommission_from_stake_pool_with_staker_key(#[case] seed: Seed) { Default::default(), Destination::PublicKey(staking_pk), &tx, - &inputs_utxos, + &inputs_info_refs, 0, &mut rng, ) @@ -804,7 +810,7 @@ fn decommission_from_stake_pool_with_staker_key(#[case] seed: Seed) { Default::default(), Destination::PublicKey(decommission_pk), &tx, - &inputs_utxos, + &inputs_info_refs, 0, &mut rng, ) diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs index a6f3014f7e..938c125755 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs @@ -13,15 +13,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::convert::Infallible; +use std::{borrow::Cow, convert::Infallible}; use chainstate_types::block_index_ancestor_getter; use common::{ chain::{ - block::timestamp::BlockTimestamp, - signature::{inputsig::InputWitness, DestinationSigError, Transactable}, + block::{timestamp::BlockTimestamp, BlockRewardTransactable}, + signature::{ + inputsig::InputWitness, + sighash::{ + self, + input_commitment::{ + make_sighash_input_commitments_for_kernel_inputs, + make_sighash_input_commitments_for_transaction_inputs, SighashInputCommitment, + SighashInputCommitmentCreationError, + }, + }, + DestinationSigError, Signable, Transactable, + }, tokens::TokenId, - ChainConfig, DelegationId, Destination, GenBlock, PoolId, TxInput, TxOutput, + ChainConfig, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, + TxInput, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, Id}, }; @@ -29,6 +41,8 @@ use mintscript::{ translate::InputInfoProvider, InputInfo, SignatureContext, TimelockContext, TranslateInput, WitnessScript, }; +use utils::debug_assert_or_log; +use utxo::UtxosView; use crate::TransactionVerifierStorageRef; @@ -46,11 +60,29 @@ pub enum InputCheckErrorPayload { #[error("Utxo {0:?} missing or spent")] MissingUtxo(common::chain::UtxoOutPoint), + #[error("Pool not found: {0}")] + PoolNotFound(PoolId), + + #[error("Order not found: {0}")] + OrderNotFound(OrderId), + + #[error("Non-utxo kernel input: {0:?}")] + NonUtxoKernelInput(TxInput), + #[error("Utxo view error: {0}")] UtxoView(#[from] utxo::Error), + #[error("Utxo info provider error: {0}")] + UtxoInfoProvider(chainstate_types::storage_result::Error), + + #[error("Pool info provider error: {0}")] + PoolInfoProvider(pos_accounting::Error), + + #[error("Orders info provider error: {0}")] + OrderInfoProvider(orders_accounting::Error), + #[error(transparent)] - Translation(#[from] mintscript::translate::TranslationError), + Translation(mintscript::translate::TranslationError), #[error(transparent)] Verification(#[from] ScriptError), @@ -80,6 +112,19 @@ impl From for InputCheckErrorPayload { + fn from(value: mintscript::translate::TranslationError) -> Self { + match value { + mintscript::translate::TranslationError::OrderNotFound(id) => Self::OrderNotFound(id), + _ => Self::Translation(value), + } + } +} + #[derive(PartialEq, Eq, Clone, thiserror::Error, Debug)] #[error("Error verifying input #{input_num}: {error}")] pub struct InputCheckError { @@ -87,6 +132,49 @@ pub struct InputCheckError { error: InputCheckErrorPayload, } +impl From> for InputCheckError +where + UPE: std::error::Error, + PIPE: std::error::Error, + OIPE: std::error::Error, + chainstate_types::storage_result::Error: From, + pos_accounting::Error: From, + orders_accounting::Error: From, +{ + fn from(value: SighashInputCommitmentCreationError) -> Self { + let (error, input_index) = match value { + SighashInputCommitmentCreationError::UtxoProviderError(err, input_index) => ( + InputCheckErrorPayload::UtxoInfoProvider(err.into()), + input_index, + ), + SighashInputCommitmentCreationError::PoolInfoProviderError(err, input_index) => ( + InputCheckErrorPayload::PoolInfoProvider(err.into()), + input_index, + ), + SighashInputCommitmentCreationError::OrderInfoProviderError(err, input_index) => ( + InputCheckErrorPayload::OrderInfoProvider(err.into()), + input_index, + ), + SighashInputCommitmentCreationError::NonUtxoKernelInput(tx_input, input_index) => ( + InputCheckErrorPayload::NonUtxoKernelInput(tx_input), + input_index, + ), + SighashInputCommitmentCreationError::UtxoNotFound(utxo_out_point, input_index) => ( + InputCheckErrorPayload::MissingUtxo(utxo_out_point), + input_index, + ), + SighashInputCommitmentCreationError::PoolNotFound(id, input_index) => { + (InputCheckErrorPayload::PoolNotFound(id), input_index) + } + SighashInputCommitmentCreationError::OrderNotFound(id, input_index) => { + (InputCheckErrorPayload::OrderNotFound(id), input_index) + } + }; + + InputCheckError::new(input_index, error) + } +} + impl InputCheckError { pub fn new(input_num: usize, error: impl Into) -> Self { let error = error.into(); @@ -299,6 +387,110 @@ impl<'a> CoreContext<'a> { } } +impl<'a, 'b> sighash::input_commitment::UtxoProvider<'a> for &'a CoreContext<'b> { + type Error = std::convert::Infallible; + + fn get_utxo( + &self, + tx_input_index: usize, + outpoint: &common::chain::UtxoOutPoint, + ) -> Result>, Self::Error> { + let utxo = + self.inputs_and_sigs.get(tx_input_index).and_then(|input_data| { + match &input_data.input { + InputInfo::Utxo { + outpoint: actual_outpoint, + utxo, + utxo_source: _, + } => { + debug_assert_or_log!( + *actual_outpoint == outpoint, + "actual_outpoint = {actual_outpoint:?}, outpoint = {outpoint:?}" + ); + Some(Cow::Borrowed(utxo)) + } + InputInfo::Account { .. } + | InputInfo::AccountCommand { .. } + | InputInfo::OrderAccountCommand { .. } => None, + } + }); + + Ok(utxo) + } +} + +/// A wrapper that implements the "InfoProvider" traits from sighash::input_commitment; +/// this is needed to work around Rust's "orphan rule". +// FIXME should it be separate wrappers each in the corresponding accounting crate? +pub struct SighashInputCommitmentInfoProvidersImplementor<'a, T>(pub &'a T); + +impl<'a, UV> sighash::input_commitment::UtxoProvider<'static> + for SighashInputCommitmentInfoProvidersImplementor<'a, UV> +where + UV: UtxosView, + chainstate_types::storage_result::Error: From<::Error>, +{ + type Error = chainstate_types::storage_result::Error; + + fn get_utxo( + &self, + _tx_input_index: usize, + outpoint: &UtxoOutPoint, + ) -> Result>, Self::Error> { + Ok(self.0.utxo(outpoint)?.map(|utxo| Cow::Owned(utxo.take_output()))) + } +} + +impl<'a, AV> sighash::input_commitment::PoolInfoProvider + for SighashInputCommitmentInfoProvidersImplementor<'a, AV> +where + AV: pos_accounting::PoSAccountingView, + pos_accounting::Error: From<::Error>, +{ + type Error = pos_accounting::Error; + + fn get_pool_info( + &self, + pool_id: &PoolId, + ) -> Result, Self::Error> { + self.0 + .get_pool_data(*pool_id)? + .map(|pool_data| { + let staker_balance = pool_data.staker_balance()?; + Ok(sighash::input_commitment::PoolInfo { staker_balance }) + }) + .transpose() + } +} + +impl<'a, OV> sighash::input_commitment::OrderInfoProvider + for SighashInputCommitmentInfoProvidersImplementor<'a, OV> +where + OV: orders_accounting::OrdersAccountingView, + orders_accounting::Error: From<::Error>, +{ + type Error = orders_accounting::Error; + + fn get_order_info( + &self, + order_id: &OrderId, + ) -> Result, Self::Error> { + self.0 + .get_order_data(order_id)? + .map(|order_data| { + let ask_balance = self.0.get_ask_balance(order_id)?; + let give_balance = self.0.get_give_balance(order_id)?; + Ok(sighash::input_commitment::OrderInfo { + initially_asked: order_data.ask().clone(), + initially_given: order_data.give().clone(), + ask_balance, + give_balance, + }) + }) + .transpose() + } +} + // Context shared between timelock and full verification. struct VerifyContextTimelock<'a, S> { // Information about the chain @@ -442,20 +634,21 @@ impl TimelockContext for InputVerifyContextTim struct VerifyContextFull<'a, T, S> { // Here we need more information about the transaction transaction: &'a T, - spent_utxos: Vec>, + input_commitments: Vec>, // And the part of the context that's shared with timelock verification sub_ctx: &'a VerifyContextTimelock<'a, S>, } impl<'a, T, S> VerifyContextFull<'a, T, S> { - fn new(transaction: &'a T, sub_ctx: &'a VerifyContextTimelock<'a, S>) -> Self { - let inp_iter = sub_ctx.core_ctx.inputs_and_sigs.iter(); - let spent_utxos = inp_iter.map(|d| d.input_info().as_utxo_output()).collect(); - + fn new( + transaction: &'a T, + input_commitments: Vec>, + sub_ctx: &'a VerifyContextTimelock<'a, S>, + ) -> Self { Self { transaction, - spent_utxos, + input_commitments, sub_ctx, } } @@ -507,8 +700,8 @@ impl SignatureContext for InputVerifyContextFull<'_, T, S> { self.ctx.transaction } - fn input_utxos(&self) -> &[Option<&TxOutput>] { - &self.ctx.spent_utxos + fn input_commitments(&self) -> &[SighashInputCommitment] { + &self.ctx.input_commitments } fn input_num(&self) -> usize { @@ -516,20 +709,86 @@ impl SignatureContext for InputVerifyContextFull<'_, T, S> { } } +pub trait SighashInputCommitmentSource { + fn get_input_commitments<'a, AV, OV>( + &self, + core_ctx: &'a CoreContext, + pos_accounting: &'a AV, + orders_accounting: &'a OV, + chain_config: &ChainConfig, + block_height: BlockHeight, + ) -> Result>, InputCheckError> + where + AV: pos_accounting::PoSAccountingView, + OV: orders_accounting::OrdersAccountingView; +} + +impl SighashInputCommitmentSource for SignedTransaction { + fn get_input_commitments<'a, AV, OV>( + &self, + core_ctx: &'a CoreContext, + pos_accounting: &'a AV, + orders_accounting: &'a OV, + chain_config: &ChainConfig, + block_height: BlockHeight, + ) -> Result>, InputCheckError> + where + AV: pos_accounting::PoSAccountingView, + OV: orders_accounting::OrdersAccountingView, + { + Ok(make_sighash_input_commitments_for_transaction_inputs( + self.inputs(), + &core_ctx, + &SighashInputCommitmentInfoProvidersImplementor(pos_accounting), + &SighashInputCommitmentInfoProvidersImplementor(orders_accounting), + chain_config, + block_height, + )?) + } +} + +impl<'b> SighashInputCommitmentSource for BlockRewardTransactable<'b> { + fn get_input_commitments<'a, AV, OV>( + &self, + core_ctx: &'a CoreContext, + _pos_accounting: &'a AV, + _orders_accounting: &'a OV, + _chain_config: &ChainConfig, + _block_height: BlockHeight, + ) -> Result>, InputCheckError> + where + AV: pos_accounting::PoSAccountingView, + OV: orders_accounting::OrdersAccountingView, + { + if let Some(kernel_inputs) = self.inputs() { + let commitments = + make_sighash_input_commitments_for_kernel_inputs(kernel_inputs, &core_ctx)?; + + Ok(commitments) + } else { + Ok(Vec::new()) + } + } +} + pub trait FullyVerifiable: - Transactable + for<'a> TranslateInput> + Transactable + + for<'a> TranslateInput> + + SighashInputCommitmentSource { } impl FullyVerifiable for T where - T: Transactable + for<'a> TranslateInput> + T: Transactable + + for<'a> TranslateInput> + + SighashInputCommitmentSource { } /// Perform full verification of given input. #[allow(clippy::too_many_arguments)] -pub fn verify_full( - transaction: &T, +pub fn verify_full<'a, T, S, UV, AV, TV, OV>( + transaction: &'a T, chain_config: &ChainConfig, utxos_view: &UV, pos_accounting: &AV, @@ -538,6 +797,7 @@ pub fn verify_full( storage: &S, tx_source: &TransactionSourceForConnect, spending_time: BlockTimestamp, + height: BlockHeight, ) -> Result<(), InputCheckError> where T: FullyVerifiable, @@ -555,7 +815,14 @@ where spending_time, &core_ctx, ); - let ctx = VerifyContextFull::new(transaction, &tl_ctx); + let input_commitments = transaction.get_input_commitments( + &core_ctx, + pos_accounting, + orders_accounting, + chain_config, + height, + )?; + let ctx = VerifyContextFull::new(transaction, input_commitments, &tl_ctx); for (n, inp) in core_ctx.inputs_iter() { let script = diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs b/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs index b9e4f8c3d9..b5908a22bb 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs @@ -16,7 +16,10 @@ use std::convert::Infallible; use common::chain::{ - signature::{inputsig::InputWitness, DestinationSigError, Transactable}, + signature::{ + inputsig::InputWitness, sighash::input_commitment::SighashInputCommitment, + DestinationSigError, Transactable, + }, tokens::TokenId, ChainConfig, DelegationId, Destination, PoolId, SignedTransaction, TxInput, TxOutput, }; @@ -31,7 +34,7 @@ struct InputVerifyContextSignature<'a, T> { chain_config: &'a ChainConfig, tx: &'a T, outpoint_destination: &'a Destination, - inputs_utxos: &'a [Option<&'a TxOutput>], + input_commitments: &'a [SighashInputCommitment<'a>], input_num: usize, input_data: PerInputData<'a>, } @@ -47,8 +50,8 @@ impl SignatureContext for InputVerifyContextSignature<'_, T> { self.tx } - fn input_utxos(&self) -> &[Option<&TxOutput>] { - self.inputs_utxos + fn input_commitments(&self) -> &[SighashInputCommitment] { + self.input_commitments } fn input_num(&self) -> usize { @@ -106,8 +109,9 @@ pub fn verify_tx_signature( chain_config: &ChainConfig, outpoint_destination: &Destination, tx: &T, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], input_num: usize, + input_utxo: Option, ) -> Result<(), InputCheckError> { let map_sig_err = |e: DestinationSigError| { InputCheckError::new( @@ -129,21 +133,19 @@ pub fn verify_tx_signature( .map_err(map_sig_err)?; ensure!( - inputs.len() == inputs_utxos.len(), + inputs.len() == input_commitments.len(), map_sig_err(DestinationSigError::InvalidUtxoCountVsInputs( - inputs_utxos.len(), + input_commitments.len(), inputs.len() )) ); let input_info = match input { TxInput::Utxo(outpoint) => { - let utxo = inputs_utxos[input_num] - .ok_or(InputCheckError::new( - input_num, - InputCheckErrorPayload::MissingUtxo(outpoint.clone()), - ))? - .clone(); + let utxo = input_utxo.ok_or(InputCheckError::new( + input_num, + InputCheckErrorPayload::MissingUtxo(outpoint.clone()), + ))?; InputInfo::Utxo { outpoint, utxo, @@ -164,7 +166,7 @@ pub fn verify_tx_signature( chain_config, tx, outpoint_destination, - inputs_utxos, + input_commitments, input_num, input_data, }; diff --git a/chainstate/tx-verifier/src/transaction_verifier/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/mod.rs index 97a47484d2..f69c77e0fb 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/mod.rs @@ -991,7 +991,12 @@ where &self.utxo_cache, )?; - self.verify_inputs(tx, tx_source, *median_time_past)?; + self.verify_inputs( + tx, + tx_source, + *median_time_past, + tx_source.expected_block_height(), + )?; self.connect_pos_accounting_outputs(tx_source, tx.transaction())?; @@ -1022,23 +1027,25 @@ where total_fees: Fee, median_time_past: BlockTimestamp, ) -> Result<(), ConnectTransactionError> { + let block_height = block_index.block_height(); + let block_id = *block_index.block_id(); + // TODO: test spending block rewards from chains outside the mainchain if reward_transactable.inputs().is_some() { let tx_source = TransactionSourceForConnect::for_chain(block_index); - self.verify_inputs(&reward_transactable, &tx_source, median_time_past)?; + self.verify_inputs( + &reward_transactable, + &tx_source, + median_time_past, + block_height, + )?; } - let block_id = *block_index.block_id(); - // spend inputs of the block reward // if block reward has no inputs then only outputs will be added to the utxo set let reward_undo = self .utxo_cache - .connect_block_transactable( - &reward_transactable, - &block_id.into(), - block_index.block_height(), - ) + .connect_block_transactable(&reward_transactable, &block_id.into(), block_height) .map_err(ConnectTransactionError::from)?; if let Some(reward_undo) = reward_undo { @@ -1052,7 +1059,7 @@ where ConsensusData::PoS(pos_data) => { // distribute reward among staker and delegators let block_subsidy = - self.chain_config.as_ref().block_subsidy_at_height(&block_index.block_height()); + self.chain_config.as_ref().block_subsidy_at_height(&block_height); let total_reward = (block_subsidy + total_fees.0) .ok_or(ConnectTransactionError::RewardAdditionError(block_id))?; @@ -1064,7 +1071,7 @@ where .chain_config .as_ref() .chainstate_upgrades() - .version_at_height(block_index.block_height()) + .version_at_height(block_height) .1 .reward_distribution_version(); @@ -1209,11 +1216,12 @@ where self.utxo_cache.set_best_block(id); } - pub fn verify_inputs( + pub fn verify_inputs<'a, Tx>( &self, - tx: &Tx, + tx: &'a Tx, tx_source: &TransactionSourceForConnect, median_time_past: BlockTimestamp, + height: BlockHeight, ) -> Result<(), input_check::InputCheckError> where Tx: input_check::FullyVerifiable< @@ -1232,6 +1240,7 @@ where &self.storage, tx_source, median_time_past, + height, ) } diff --git a/common/src/chain/config/builder.rs b/common/src/chain/config/builder.rs index 4ba359b510..f7eeb420d7 100644 --- a/common/src/chain/config/builder.rs +++ b/common/src/chain/config/builder.rs @@ -32,7 +32,8 @@ use crate::{ DataDepositFeeVersion, Destination, FrozenTokensValidationVersion, GenBlock, Genesis, HtlcActivated, NetUpgrades, OrdersActivated, OrdersVersion, PoSChainConfig, PoSConsensusVersion, PoWChainConfig, RewardDistributionVersion, - StakerDestinationUpdateForbidden, TokenIssuanceVersion, TokensFeeVersion, + SighashInputCommitmentVersion, StakerDestinationUpdateForbidden, TokenIssuanceVersion, + TokensFeeVersion, }, primitives::{ id::WithId, per_thousand::PerThousand, semver::SemVer, Amount, BlockCount, BlockDistance, @@ -53,24 +54,38 @@ use super::{ // the `StakerDestinationUpdateForbidden` upgrade can be completely removed, as if this ability has // never existed. -// The fork at which we upgrade consensus to dis-incentivize large pools + enable tokens v1 +// The fork at which we upgrade consensus to dis-incentivize large pools + enable tokens v1. const TESTNET_FORK_HEIGHT_1_TOKENS_V1_AND_CONSENSUS_UPGRADE: BlockHeight = BlockHeight::new(78_440); // The fork at which we upgrade chainstate to distribute reward to staker proportionally to their balance -// and change various tokens fees +// and change various tokens fees. const TESTNET_FORK_HEIGHT_2_STAKER_REWARD_AND_TOKENS_FEE: BlockHeight = BlockHeight::new(138_244); -// The fork at which txs with htlc outputs become valid, data deposit fee and size, max future block time offset changed +// The fork at which txs with htlc outputs become valid, data deposit fee and size, max future block time offset changed. const TESTNET_FORK_HEIGHT_3_HTLC_AND_DATA_DEPOSIT_FEE: BlockHeight = BlockHeight::new(297_550); -// The fork at which order outputs become valid +// The fork at which order outputs become valid. const TESTNET_FORK_HEIGHT_4_ORDERS: BlockHeight = BlockHeight::new(325_180); -// The fork at which we enable orders v1 and prohibit updating the staker destination in ProduceBlockFromStake. +// The fork at which we: +// * enable orders v1; +// * enable sighash input commitment V1; +// * FIXME enable token id calculation v1; +// * prohibit updating the staker destination in ProduceBlockFromStake. const TESTNET_FORK_HEIGHT_5_ORDERS_V1_AND_STAKER_DESTINATION_UPDATE_PROHIBITION: BlockHeight = BlockHeight::new(999_999_999); +// The fork at which we switch to sighash input commitments v1. +const TESTNET_FORK_HEIGHT_6_SIGHASH_INPUT_COMMITMENTS_V1: BlockHeight = + BlockHeight::new(999_999_999); -// The fork at which txs with htlc and orders outputs become valid +// The fork at which txs with htlc and orders outputs become valid. const MAINNET_FORK_HEIGHT_1_HTLC_AND_ORDERS: BlockHeight = BlockHeight::new(254_740); -// The fork at which we enable orders v1 and prohibit updating the staker destination in ProduceBlockFromStake. +// The fork at which we: +// * enable orders v1; +// * enable sighash input commitment V1; +// * FIXME enable token id calculation v1; +// * prohibit updating the staker destination in ProduceBlockFromStake. const MAINNET_FORK_HEIGHT_2_ORDERS_V1_AND_STAKER_DESTINATION_UPDATE_PROHIBITION: BlockHeight = BlockHeight::new(999_999_999); +// The fork at which we switch to sighash input commitments v1. +const MAINNET_FORK_HEIGHT_3_SIGHASH_INPUT_COMMITMENTS_V1: BlockHeight = + BlockHeight::new(999_999_999); impl ChainType { fn default_genesis_init(&self) -> GenesisBlockInit { @@ -191,6 +206,7 @@ impl ChainType { OrdersActivated::No, OrdersVersion::V0, StakerDestinationUpdateForbidden::No, + SighashInputCommitmentVersion::V0, ), ), ( @@ -206,6 +222,7 @@ impl ChainType { OrdersActivated::Yes, OrdersVersion::V0, StakerDestinationUpdateForbidden::No, + SighashInputCommitmentVersion::V0, ), ), ( @@ -221,6 +238,23 @@ impl ChainType { OrdersActivated::Yes, OrdersVersion::V1, StakerDestinationUpdateForbidden::Yes, + SighashInputCommitmentVersion::V0, + ), + ), + ( + MAINNET_FORK_HEIGHT_3_SIGHASH_INPUT_COMMITMENTS_V1, + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, + HtlcActivated::Yes, + OrdersActivated::Yes, + OrdersVersion::V1, + StakerDestinationUpdateForbidden::Yes, + SighashInputCommitmentVersion::V1, ), ), ]; @@ -240,6 +274,7 @@ impl ChainType { OrdersActivated::Yes, OrdersVersion::V1, StakerDestinationUpdateForbidden::Yes, + SighashInputCommitmentVersion::V1, ), )]; NetUpgrades::initialize(upgrades).expect("net upgrades") @@ -259,6 +294,7 @@ impl ChainType { OrdersActivated::No, OrdersVersion::V0, StakerDestinationUpdateForbidden::No, + SighashInputCommitmentVersion::V0, ), ), ( @@ -274,6 +310,7 @@ impl ChainType { OrdersActivated::No, OrdersVersion::V0, StakerDestinationUpdateForbidden::No, + SighashInputCommitmentVersion::V0, ), ), ( @@ -289,6 +326,7 @@ impl ChainType { OrdersActivated::No, OrdersVersion::V0, StakerDestinationUpdateForbidden::No, + SighashInputCommitmentVersion::V0, ), ), ( @@ -304,6 +342,7 @@ impl ChainType { OrdersActivated::No, OrdersVersion::V0, StakerDestinationUpdateForbidden::No, + SighashInputCommitmentVersion::V0, ), ), ( @@ -319,6 +358,7 @@ impl ChainType { OrdersActivated::Yes, OrdersVersion::V0, StakerDestinationUpdateForbidden::No, + SighashInputCommitmentVersion::V0, ), ), ( @@ -334,6 +374,23 @@ impl ChainType { OrdersActivated::Yes, OrdersVersion::V1, StakerDestinationUpdateForbidden::Yes, + SighashInputCommitmentVersion::V0, + ), + ), + ( + TESTNET_FORK_HEIGHT_6_SIGHASH_INPUT_COMMITMENTS_V1, + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, + FrozenTokensValidationVersion::V1, + HtlcActivated::Yes, + OrdersActivated::Yes, + OrdersVersion::V1, + StakerDestinationUpdateForbidden::Yes, + SighashInputCommitmentVersion::V1, ), ), ]; diff --git a/common/src/chain/config/checkpoints_data/mod.rs b/common/src/chain/config/checkpoints_data/mod.rs index 4e499b0668..9cc87e28c7 100644 --- a/common/src/chain/config/checkpoints_data/mod.rs +++ b/common/src/chain/config/checkpoints_data/mod.rs @@ -40,6 +40,7 @@ pub fn print_block_heights_ids_as_checkpoints_data( fmt().expect("Writing to string must not fail") } +// FIXME a) make the maps "lazy_static"; b) the corresponding field in chain config must be Cow. pub fn make_mainnet_checkpoints() -> BTreeMap> { make_checkpoints(mainnet::CHECKPOINTS_DATA).expect("corrupted mainnet checkpoints data") } diff --git a/common/src/chain/config/mod.rs b/common/src/chain/config/mod.rs index 2dd51b7ed3..ac1de6bb19 100644 --- a/common/src/chain/config/mod.rs +++ b/common/src/chain/config/mod.rs @@ -58,8 +58,8 @@ use super::{ output_value::OutputValue, stakelock::StakePoolData, ChainstateUpgrade, ChangeTokenMetadataUriActivated, ConsensusUpgrade, DataDepositFeeVersion, DestinationTag, FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, OrdersVersion, - RequiredConsensus, RewardDistributionVersion, StakerDestinationUpdateForbidden, - TokenIssuanceVersion, TokensFeeVersion, + RequiredConsensus, RewardDistributionVersion, SighashInputCommitmentVersion, + StakerDestinationUpdateForbidden, TokenIssuanceVersion, TokensFeeVersion, }; use self::{ @@ -918,6 +918,7 @@ pub fn create_unit_test_config_builder() -> Builder { OrdersActivated::Yes, OrdersVersion::V1, StakerDestinationUpdateForbidden::Yes, + SighashInputCommitmentVersion::V1, ), )]) .expect("cannot fail"), diff --git a/common/src/chain/transaction/output/output_value.rs b/common/src/chain/transaction/output/output_value.rs index 55e275f734..deaab7f5e7 100644 --- a/common/src/chain/transaction/output/output_value.rs +++ b/common/src/chain/transaction/output/output_value.rs @@ -48,6 +48,21 @@ impl OutputValue { OutputValue::TokenV0(_) | OutputValue::TokenV1(_, _) => None, } } + + pub fn amount(&self) -> Amount { + match self { + OutputValue::Coin(amount) => *amount, + OutputValue::TokenV0(token_data) => { + let token_data = token_data.as_ref(); + match token_data { + TokenData::TokenTransfer(transfer) => transfer.amount, + TokenData::TokenIssuance(issuance) => issuance.amount_to_issue, + TokenData::NftIssuance(_) => Amount::from_atoms(1), + } + } + OutputValue::TokenV1(_, amount) => *amount, + } + } } impl From for OutputValue { diff --git a/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs b/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs index 78e8e5b8f7..18bbfc3483 100644 --- a/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs +++ b/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs @@ -354,8 +354,12 @@ fn verify_corrupted_signature(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn signing_transactions_shouldnt_work(#[case] seed: Seed) { - use crate::chain::signature::tests::utils::verify_signature; +fn signing_transactions_should_not_work(#[case] seed: Seed) { + use std::borrow::Cow; + + use crate::chain::signature::{ + sighash::input_commitment::SighashInputCommitment, tests::utils::verify_signature, + }; let mut rng = test_utils::random::make_seedable_rng(seed); @@ -367,13 +371,7 @@ fn signing_transactions_shouldnt_work(#[case] seed: Seed) { let sighash_type = SigHashType::try_from(SigHashType::NONE | SigHashType::ANYONECANPAY).unwrap(); - let tx = generate_unsigned_tx( - &mut rng, - &Destination::AnyoneCanSpend, - &[Some(input_utxo.clone())], - 1, - ) - .unwrap(); + let tx = generate_unsigned_tx(&mut rng, &Destination::AnyoneCanSpend, 1, 1).unwrap(); let unhashed_tx_data_to_sign = { let mut data = Vec::::new(); @@ -394,7 +392,13 @@ fn signing_transactions_shouldnt_work(#[case] seed: Seed) { // Sanity check - if we sign tx_data_hash normally, it will actually produce a correct // transaction signature. { - let expected_hash = signature_hash(sighash_type, &tx, &[Some(&input_utxo)], 0).unwrap(); + let expected_hash = signature_hash( + sighash_type, + &tx, + &[SighashInputCommitment::Utxo(Cow::Borrowed(&input_utxo))], + 0, + ) + .unwrap(); assert_eq!(tx_data_hash, expected_hash); let raw_sig = sign_public_key_spending(&private_key, &public_key, &tx_data_hash, &mut rng) @@ -410,7 +414,7 @@ fn signing_transactions_shouldnt_work(#[case] seed: Seed) { &destination, &signed_tx, &signed_tx.signatures()[0], - &[Some(&input_utxo)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&input_utxo))], 0, ) .unwrap(); @@ -437,7 +441,7 @@ fn signing_transactions_shouldnt_work(#[case] seed: Seed) { &destination, &signed_tx, &signed_tx.signatures()[0], - &[Some(&input_utxo)], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&input_utxo))], 0, ) .unwrap_err(); diff --git a/common/src/chain/transaction/signature/inputsig/authorize_pubkey_spend.rs b/common/src/chain/transaction/signature/inputsig/authorize_pubkey_spend.rs index 91b2f2b0bb..d47ca95c6c 100644 --- a/common/src/chain/transaction/signature/inputsig/authorize_pubkey_spend.rs +++ b/common/src/chain/transaction/signature/inputsig/authorize_pubkey_spend.rs @@ -68,24 +68,27 @@ pub fn sign_public_key_spending( #[cfg(test)] mod test { - use super::*; - use crate::chain::signature::sighash::signature_hash; - use crate::chain::signature::tests::utils::generate_inputs_utxos; + use itertools::Itertools as _; + use rstest::rstest; + use crate::{ address::pubkeyhash::PublicKeyHash, chain::{ - signature::inputsig::StandardInputSignature, - transaction::signature::tests::utils::{generate_unsigned_tx, sig_hash_types}, + signature::{sighash::signature_hash, StandardInputSignature}, + transaction::signature::tests::utils::{ + generate_input_commitments, generate_unsigned_tx, sig_hash_types, + }, Destination, }, }; use crypto::key::{KeyKind, PrivateKey}; use randomness::Rng; - use rstest::rstest; use test_utils::random::Seed; - const INPUTS: usize = 10; - const OUTPUTS: usize = 10; + use super::*; + + const INPUTS_COUNT: usize = 10; + const OUTPUTS_COUNT: usize = 10; // Try to produce a signature for a non-existent input. #[rstest] @@ -98,10 +101,10 @@ mod test { PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 1); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 1); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, 2).unwrap(); + let tx = generate_unsigned_tx(&mut rng, &destination, input_commitments.len(), 2).unwrap(); for sighash_type in sig_hash_types() { let res = StandardInputSignature::produce_uniparty_signature_for_input( @@ -109,7 +112,7 @@ mod test { sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, 1, &mut rng, ); @@ -128,10 +131,16 @@ mod test { PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKeyHash(PublicKeyHash::from(&public_key)); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, OUTPUTS).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + OUTPUTS_COUNT, + ) + .unwrap(); for sighash_type in sig_hash_types() { let witness = StandardInputSignature::produce_uniparty_signature_for_input( @@ -139,8 +148,8 @@ mod test { sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, - rng.gen_range(0..inputs_utxos.len()), + &input_commitments_refs, + rng.gen_range(0..INPUTS_COUNT), &mut rng, ) .unwrap(); @@ -163,10 +172,16 @@ mod test { PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, OUTPUTS).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + OUTPUTS_COUNT, + ) + .unwrap(); for sighash_type in sig_hash_types() { let witness = StandardInputSignature::produce_uniparty_signature_for_input( @@ -174,8 +189,8 @@ mod test { sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, - rng.gen_range(0..INPUTS), + &input_commitments_refs, + rng.gen_range(0..INPUTS_COUNT), &mut rng, ) .unwrap(); @@ -204,19 +219,25 @@ mod test { PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key.clone()); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, OUTPUTS).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + OUTPUTS_COUNT, + ) + .unwrap(); for sighash_type in sig_hash_types() { - let input = rng.gen_range(0..inputs_utxos.len()); + let input = rng.gen_range(0..INPUTS_COUNT); let witness = StandardInputSignature::produce_uniparty_signature_for_input( &private_key, sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, input, &mut rng, ) @@ -224,7 +245,8 @@ mod test { let spender_signature = AuthorizedPublicKeySpend::from_data(witness.raw_signature()).unwrap(); let sighash = - signature_hash(witness.sighash_type(), &tx, &inputs_utxos_refs, input).unwrap(); + signature_hash(witness.sighash_type(), &tx, &input_commitments_refs, input) + .unwrap(); verify_public_key_spending(&public_key, &spender_signature, &sighash) .unwrap_or_else(|_| panic!("{sighash_type:X?}")); } @@ -240,25 +262,32 @@ mod test { PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key.clone()); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, OUTPUTS).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + OUTPUTS_COUNT, + ) + .unwrap(); for sighash_type in sig_hash_types() { - let input = rng.gen_range(0..inputs_utxos.len()); + let input = rng.gen_range(0..INPUTS_COUNT); let witness = StandardInputSignature::produce_uniparty_signature_for_input( &private_key, sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, input, &mut rng, ) .unwrap(); let sighash = - signature_hash(witness.sighash_type(), &tx, &inputs_utxos_refs, input).unwrap(); + signature_hash(witness.sighash_type(), &tx, &input_commitments_refs, input) + .unwrap(); sign_public_key_spending(&private_key, &public_key, &sighash, &mut rng) .unwrap_or_else(|_| panic!("{sighash_type:X?}")); } diff --git a/common/src/chain/transaction/signature/inputsig/authorize_pubkeyhash_spend.rs b/common/src/chain/transaction/signature/inputsig/authorize_pubkeyhash_spend.rs index 693339c5e7..f429eb4f1d 100644 --- a/common/src/chain/transaction/signature/inputsig/authorize_pubkeyhash_spend.rs +++ b/common/src/chain/transaction/signature/inputsig/authorize_pubkeyhash_spend.rs @@ -99,20 +99,24 @@ fn sign_public_key_hash_spending_impl( #[cfg(test)] mod test { - use super::*; - use crate::chain::signature::tests::utils::generate_inputs_utxos; + use itertools::Itertools as _; + use rstest::rstest; + use crate::chain::{ - signature::{inputsig::StandardInputSignature, sighash::signature_hash}, - transaction::signature::tests::utils::{generate_unsigned_tx, sig_hash_types}, + signature::{sighash::signature_hash, StandardInputSignature}, + transaction::signature::tests::utils::{ + generate_input_commitments, generate_unsigned_tx, sig_hash_types, + }, Destination, }; use crypto::key::{KeyKind, PrivateKey}; use randomness::Rng; - use rstest::rstest; use test_utils::random::Seed; - const INPUTS: usize = 10; - const OUTPUTS: usize = 10; + use super::*; + + const INPUTS_COUNT: usize = 10; + const OUTPUTS_COUNT: usize = 10; // Try to produce a signature for a non-existent input. #[rstest] @@ -126,10 +130,10 @@ mod test { let pubkey_hash = PublicKeyHash::from(&public_key); let destination = Destination::PublicKeyHash(pubkey_hash); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 1); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 1); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, 2).unwrap(); + let tx = generate_unsigned_tx(&mut rng, &destination, input_commitments.len(), 2).unwrap(); for sighash_type in sig_hash_types() { let res = StandardInputSignature::produce_uniparty_signature_for_input( @@ -137,7 +141,7 @@ mod test { sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, 1, &mut rng, ); @@ -156,10 +160,16 @@ mod test { PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, OUTPUTS).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + OUTPUTS_COUNT, + ) + .unwrap(); for sighash_type in sig_hash_types() { let witness = StandardInputSignature::produce_uniparty_signature_for_input( @@ -167,8 +177,8 @@ mod test { sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, - rng.gen_range(0..inputs_utxos.len()), + &input_commitments_refs, + rng.gen_range(0..INPUTS_COUNT), &mut rng, ) .unwrap(); @@ -193,10 +203,16 @@ mod test { let pubkey_hash = PublicKeyHash::from(&public_key); let destination = Destination::PublicKeyHash(pubkey_hash); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, OUTPUTS).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + OUTPUTS_COUNT, + ) + .unwrap(); for sighash_type in sig_hash_types() { let witness = StandardInputSignature::produce_uniparty_signature_for_input( @@ -204,8 +220,8 @@ mod test { sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, - rng.gen_range(0..inputs_utxos.len()), + &input_commitments_refs, + rng.gen_range(0..INPUTS_COUNT), &mut rng, ) .unwrap(); @@ -237,19 +253,25 @@ mod test { let pubkey_hash = PublicKeyHash::from(&public_key); let destination = Destination::PublicKeyHash(pubkey_hash); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, OUTPUTS).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + OUTPUTS_COUNT, + ) + .unwrap(); for sighash_type in sig_hash_types() { - let input = rng.gen_range(0..INPUTS); + let input = rng.gen_range(0..INPUTS_COUNT); let witness = StandardInputSignature::produce_uniparty_signature_for_input( &private_key, sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, input, &mut rng, ) @@ -257,7 +279,8 @@ mod test { let spender_signature = AuthorizedPublicKeyHashSpend::from_data(witness.raw_signature()).unwrap(); let sighash = - signature_hash(witness.sighash_type(), &tx, &inputs_utxos_refs, input).unwrap(); + signature_hash(witness.sighash_type(), &tx, &input_commitments_refs, input) + .unwrap(); verify_public_key_hash_spending(&pubkey_hash, &spender_signature, &sighash) .unwrap_or_else(|_| panic!("{sighash_type:X?}")); @@ -275,25 +298,32 @@ mod test { let destination = Destination::PublicKey(public_key.clone()); let pubkey_hash = PublicKeyHash::from(&public_key); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, OUTPUTS).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + OUTPUTS_COUNT, + ) + .unwrap(); for sighash_type in sig_hash_types() { - let input = rng.gen_range(0..inputs_utxos.len()); + let input = rng.gen_range(0..INPUTS_COUNT); let witness = StandardInputSignature::produce_uniparty_signature_for_input( &private_key, sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, input, &mut rng, ) .unwrap(); let sighash = - signature_hash(witness.sighash_type(), &tx, &inputs_utxos_refs, input).unwrap(); + signature_hash(witness.sighash_type(), &tx, &input_commitments_refs, input) + .unwrap(); sign_public_key_hash_spending(&private_key, &pubkey_hash, &sighash, &mut rng) .unwrap_or_else(|_| panic!("{sighash_type:X?}")); diff --git a/common/src/chain/transaction/signature/inputsig/htlc.rs b/common/src/chain/transaction/signature/inputsig/htlc.rs index b5ece5ca88..3041ac801f 100644 --- a/common/src/chain/transaction/signature/inputsig/htlc.rs +++ b/common/src/chain/transaction/signature/inputsig/htlc.rs @@ -18,7 +18,10 @@ use serialization::Encode; use standard_signature::StandardInputSignature; -use crate::chain::{htlc::HtlcSecret, ChainConfig, Destination, Transaction, TxOutput}; +use crate::chain::{ + htlc::HtlcSecret, signature::sighash::input_commitment::SighashInputCommitment, ChainConfig, + Destination, Transaction, +}; use super::{ super::sighash::sighashtype::SigHashType, @@ -33,7 +36,7 @@ pub fn produce_uniparty_signature_for_htlc_input], + input_commitments: &[SighashInputCommitment], input_num: usize, htlc_secret: HtlcSecret, rng: R, @@ -43,7 +46,7 @@ pub fn produce_uniparty_signature_for_htlc_input], + input_commitments: &[SighashInputCommitment], input_num: usize, ) -> Result { let sig = StandardInputSignature::produce_classical_multisig_signature_for_input( @@ -71,7 +74,7 @@ pub fn produce_classical_multisig_signature_for_htlc_input( authorization, sighash_type, tx, - inputs_utxos, + input_commitments, input_num, )?; diff --git a/common/src/chain/transaction/signature/inputsig/standard_signature.rs b/common/src/chain/transaction/signature/inputsig/standard_signature.rs index 9cd3865038..effc2a6c35 100644 --- a/common/src/chain/transaction/signature/inputsig/standard_signature.rs +++ b/common/src/chain/transaction/signature/inputsig/standard_signature.rs @@ -21,10 +21,12 @@ use serialization::{Decode, DecodeAll, Encode}; use crate::{ chain::{ signature::{ - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::{ + input_commitment::SighashInputCommitment, sighashtype::SigHashType, signature_hash, + }, DestinationSigError, Signable, }, - ChainConfig, Destination, Transaction, TxOutput, + ChainConfig, Destination, Transaction, }, primitives::H256, }; @@ -105,11 +107,11 @@ impl StandardInputSignature { sighash_type: SigHashType, outpoint_destination: Destination, tx: &T, - inputs_utxos: &[Option<&TxOutput>], - input_num: usize, + input_commitments: &[SighashInputCommitment], + input_index: usize, rng: R, ) -> Result { - let sighash = signature_hash(sighash_type, tx, inputs_utxos, input_num)?; + let sighash = signature_hash(sighash_type, tx, input_commitments, input_index)?; let serialized_sig = match outpoint_destination { Destination::PublicKeyHash(ref addr) => { let sig = sign_public_key_hash_spending(private_key, addr, &sighash, rng)?; @@ -141,12 +143,12 @@ impl StandardInputSignature { authorization: &AuthorizedClassicalMultisigSpend, sighash_type: SigHashType, tx: &Transaction, - inputs_utxos: &[Option<&TxOutput>], - input_num: usize, + input_commitments: &[SighashInputCommitment], + input_index: usize, ) -> Result { use super::classical_multisig::multisig_partial_signature::SigsVerifyResult; - let sighash = signature_hash(sighash_type, tx, inputs_utxos, input_num)?; + let sighash = signature_hash(sighash_type, tx, input_commitments, input_index)?; let message = sighash.encode(); let verifier = @@ -223,14 +225,15 @@ mod test { address::pubkeyhash::PublicKeyHash, chain::{ config::create_mainnet, - transaction::signature::tests::utils::{generate_unsigned_tx, sig_hash_types}, + signature::{sighash::signature_hash, DestinationSigError}, + transaction::signature::tests::utils::{ + generate_input_commitments, generate_unsigned_tx, sig_hash_types, + }, + Destination, }, }; use super::*; - use crate::chain::signature::tests::utils::generate_inputs_utxos; - use crate::chain::signature::{sighash::signature_hash, DestinationSigError}; - use crate::chain::Destination; use crypto::key::{KeyKind, PrivateKey}; use itertools::Itertools; use rstest::rstest; @@ -248,10 +251,10 @@ mod test { let (_, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKeyHash(PublicKeyHash::from(&public_key)); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 1); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 1); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, 2).unwrap(); + let tx = generate_unsigned_tx(&mut rng, &destination, input_commitments.len(), 2).unwrap(); for sighash_type in sig_hash_types() { assert_eq!( @@ -261,7 +264,7 @@ mod test { sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, INPUT_NUM, &mut rng, ), @@ -280,10 +283,10 @@ mod test { let (_, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 1); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 1); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, 2).unwrap(); + let tx = generate_unsigned_tx(&mut rng, &destination, input_commitments.len(), 2).unwrap(); for sighash_type in sig_hash_types() { assert_eq!( @@ -293,7 +296,7 @@ mod test { sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, INPUT_NUM, &mut rng ), @@ -319,24 +322,30 @@ mod test { for (sighash_type, destination) in sig_hash_types().cartesian_product(outpoints.into_iter()) { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 1); - let inputs_utxos_refs = - inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 1); + let input_commitments_refs = + input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, 2).unwrap(); + let tx = + generate_unsigned_tx(&mut rng, &destination, input_commitments.len(), 2).unwrap(); let witness = StandardInputSignature::produce_uniparty_signature_for_input( &private_key, sighash_type, destination.clone(), &tx, - &inputs_utxos_refs, + &input_commitments_refs, INPUT_NUM, &mut rng, ) .unwrap(); - let sighash = - signature_hash(witness.sighash_type(), &tx, &inputs_utxos_refs, INPUT_NUM).unwrap(); + let sighash = signature_hash( + witness.sighash_type(), + &tx, + &input_commitments_refs, + INPUT_NUM, + ) + .unwrap(); witness .verify_signature(&chain_config, &destination, &sighash) .unwrap_or_else(|_| panic!("{sighash_type:X?} {destination:?}")); diff --git a/common/src/chain/transaction/signature/mod.rs b/common/src/chain/transaction/signature/mod.rs index 7f3fce2cc9..e10fe0fbfb 100644 --- a/common/src/chain/transaction/signature/mod.rs +++ b/common/src/chain/transaction/signature/mod.rs @@ -13,6 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use thiserror::Error; + +use serialization::{Decode, Encode}; +use utils::ensure; + use crate::chain::{ChainConfig, TxInput}; use self::{ @@ -24,18 +29,14 @@ use self::{ standard_signature::StandardInputSignature, InputWitness, }, - sighash::signature_hash, + sighash::{input_commitment::SighashInputCommitment, signature_hash}, }; +use super::{Destination, TxOutput}; + pub mod inputsig; pub mod sighash; -use serialization::{Decode, Encode}; -use thiserror::Error; -use utils::ensure; - -use super::{Destination, TxOutput}; - #[derive(Error, Debug, PartialEq, Eq, Clone)] pub enum DestinationSigError { #[error("Invalid sighash value provided")] @@ -128,13 +129,13 @@ pub fn verify_signature( outpoint_destination: &Destination, tx: &T, input_witness: &EvaluatedInputWitness, - inputs_utxos: &[Option<&TxOutput>], - input_num: usize, + input_commitments: &[SighashInputCommitment], + input_index: usize, ) -> Result<(), DestinationSigError> { let inputs = tx.inputs().ok_or(DestinationSigError::SignatureVerificationWithoutInputs)?; ensure!( - input_num < inputs.len(), - DestinationSigError::InvalidSignatureIndex(input_num, inputs.len(),) + input_index < inputs.len(), + DestinationSigError::InvalidSignatureIndex(input_index, inputs.len(),) ); match input_witness { @@ -147,30 +148,14 @@ pub fn verify_signature( } Destination::AnyoneCanSpend => {} }, - EvaluatedInputWitness::Standard(witness) => verify_standard_input_signature( - chain_config, - outpoint_destination, - witness, - tx, - inputs_utxos, - input_num, - )?, + EvaluatedInputWitness::Standard(witness) => { + let sighash = + signature_hash(witness.sighash_type(), tx, input_commitments, input_index)?; + witness.verify_signature(chain_config, outpoint_destination, &sighash)?; + } } Ok(()) } -fn verify_standard_input_signature( - chain_config: &ChainConfig, - outpoint_destination: &Destination, - witness: &StandardInputSignature, - tx: &T, - inputs_utxos: &[Option<&TxOutput>], - input_num: usize, -) -> Result<(), DestinationSigError> { - let sighash = signature_hash(witness.sighash_type(), tx, inputs_utxos, input_num)?; - witness.verify_signature(chain_config, outpoint_destination, &sighash)?; - Ok(()) -} - #[cfg(test)] mod tests; diff --git a/common/src/chain/transaction/signature/sighash/hashable.rs b/common/src/chain/transaction/signature/sighash/hashable.rs index 0e31a83090..bf644b7d3d 100644 --- a/common/src/chain/transaction/signature/sighash/hashable.rs +++ b/common/src/chain/transaction/signature/sighash/hashable.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use utils::ensure; + use crate::{ chain::{ signature::{sighash::sighashtype, DestinationSigError}, @@ -21,6 +23,8 @@ use crate::{ primitives::id::{hash_encoded_to, DefaultHashAlgoStream}, }; +use super::SighashInputCommitment; + pub trait SignatureHashableElement { fn signature_hash( &self, @@ -58,26 +62,22 @@ impl SignatureHashableElement for &[TxOutput] { #[derive(Debug, Clone)] pub struct SignatureHashableInputs<'a> { inputs: &'a [TxInput], - /// Include utxos of the inputs to make it possible to verify the inputs scripts and amounts without downloading the full transactions - /// It can be None which means that input spends from an account not utxo - inputs_utxos: &'a [Option<&'a TxOutput>], + input_commitments: &'a [SighashInputCommitment<'a>], } impl<'a> SignatureHashableInputs<'a> { pub fn new( inputs: &'a [TxInput], - inputs_utxos: &'a [Option<&'a TxOutput>], + input_commitments: &'a [SighashInputCommitment<'a>], ) -> Result { - if inputs.len() != inputs_utxos.len() { - return Err(DestinationSigError::InvalidUtxoCountVsInputs( - inputs_utxos.len(), - inputs.len(), - )); - } + ensure!( + input_commitments.len() == inputs.len(), + DestinationSigError::InvalidUtxoCountVsInputs(input_commitments.len(), inputs.len()) + ); let result = Self { inputs, - inputs_utxos, + input_commitments, }; Ok(result) @@ -90,40 +90,36 @@ impl SignatureHashableElement for SignatureHashableInputs<'_> { stream: &mut DefaultHashAlgoStream, mode: sighashtype::SigHashType, target_input: &TxInput, - target_input_num: usize, + target_input_index: usize, ) -> Result<(), DestinationSigError> { - if target_input_num >= self.inputs.len() { - return Err(DestinationSigError::InvalidInputIndex( - target_input_num, - self.inputs.len(), - )); - } + ensure!( + target_input_index < self.inputs.len(), + DestinationSigError::InvalidInputIndex(target_input_index, self.inputs.len(),) + ); match mode.inputs_mode() { sighashtype::InputsMode::CommitWhoPays => { { // Commit inputs - let inputs = self.inputs; - hash_encoded_to(&(inputs.len() as u32), stream); - for input in inputs { + hash_encoded_to(&(self.inputs.len() as u32), stream); + for input in self.inputs { hash_encoded_to(&input, stream); } } { - // Commit inputs' utxos - let inputs_utxos = self.inputs_utxos; - hash_encoded_to(&(inputs_utxos.len() as u32), stream); - for input_utxo in inputs_utxos { - hash_encoded_to(&input_utxo, stream); + // Commit the extra commitments + hash_encoded_to(&(self.input_commitments.len() as u32), stream); + for input_info in self.input_commitments { + hash_encoded_to(&input_info, stream); } } } sighashtype::InputsMode::AnyoneCanPay => { // Commit the input being signed (target input) hash_encoded_to(&target_input, stream); - // Commit the utxo of the input being signed (target input) - hash_encoded_to(&self.inputs_utxos[target_input_num], stream); + // Commit the extra commitment of the input being signed (target input) + hash_encoded_to(&self.input_commitments[target_input_index], stream); } } Ok(()) @@ -132,24 +128,79 @@ impl SignatureHashableElement for SignatureHashableInputs<'_> { #[cfg(test)] mod tests { - use crypto::key::{KeyKind, PrivateKey}; + use itertools::Itertools as _; use randomness::{CryptoRng, Rng}; use rstest::rstest; use test_utils::random::{make_seedable_rng, Seed}; use crate::{ chain::{ - output_value::OutputValue, signature::sighash::sighashtype::SigHashType, Destination, + signature::{ + sighash::{sighashtype::SigHashType, SighashInputCommitment}, + tests::utils::{generate_input_commitments, generate_inputs_utxos}, + }, OutPointSourceId, }, - primitives::{Amount, Id, H256}, + primitives::{Id, H256}, }; use crypto::hash::StreamHasher; use super::*; - fn generate_random_invalid_input(rng: &mut impl Rng) -> TxInput { - let outpoint = if rng.next_u32() % 2 == 0 { + #[derive(Debug, Clone)] + pub struct SignatureHashableInputsDeprecated<'a> { + inputs: &'a [TxInput], + inputs_utxos: &'a [Option<&'a TxOutput>], + } + + impl SignatureHashableElement for SignatureHashableInputsDeprecated<'_> { + fn signature_hash( + &self, + stream: &mut DefaultHashAlgoStream, + mode: sighashtype::SigHashType, + target_input: &TxInput, + target_input_num: usize, + ) -> Result<(), DestinationSigError> { + if target_input_num >= self.inputs.len() { + return Err(DestinationSigError::InvalidInputIndex( + target_input_num, + self.inputs.len(), + )); + } + + match mode.inputs_mode() { + sighashtype::InputsMode::CommitWhoPays => { + { + // Commit inputs + let inputs = self.inputs; + hash_encoded_to(&(inputs.len() as u32), stream); + for input in inputs { + hash_encoded_to(&input, stream); + } + } + + { + // Commit inputs' utxos + let inputs_utxos = self.inputs_utxos; + hash_encoded_to(&(inputs_utxos.len() as u32), stream); + for input_utxo in inputs_utxos { + hash_encoded_to(&input_utxo, stream); + } + } + } + sighashtype::InputsMode::AnyoneCanPay => { + // Commit the input being signed (target input) + hash_encoded_to(&target_input, stream); + // Commit the utxo of the input being signed (target input) + hash_encoded_to(&self.inputs_utxos[target_input_num], stream); + } + } + Ok(()) + } + } + + fn generate_random_input(rng: &mut impl Rng) -> TxInput { + let outpoint = if rng.gen::() { OutPointSourceId::Transaction(Id::new(H256::random_using(rng))) } else { OutPointSourceId::BlockReward(Id::new(H256::random_using(rng))) @@ -158,39 +209,25 @@ mod tests { TxInput::from_utxo(outpoint, rng.next_u32()) } - fn generate_random_invalid_output(rng: &mut (impl Rng + CryptoRng)) -> TxOutput { - let (_, pub_key) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); - TxOutput::Transfer( - OutputValue::Coin(Amount::from_atoms(rng.next_u64() as u128)), - Destination::PublicKey(pub_key), - ) - } - fn do_test_hashable_inputs( inputs_count: usize, inputs_utxos_count: usize, rng: &mut (impl Rng + CryptoRng), ) { - let inputs = (0..inputs_count) - .map(|_| generate_random_invalid_input(rng)) - .collect::>(); - - let inputs_utxos = (0..inputs_utxos_count) - .map(|_| generate_random_invalid_output(rng)) - .collect::>(); + let inputs = (0..inputs_count).map(|_| generate_random_input(rng)).collect::>(); - let inputs_utxos = inputs_utxos.iter().map(Some).collect::>(); + let input_commitment_vals = generate_input_commitments(rng, inputs_utxos_count); + let input_commitments = input_commitment_vals.iter().map(|comm| comm.into()).collect_vec(); - let hashable_inputs_result = SignatureHashableInputs::new(&inputs, &inputs_utxos); - - if inputs_count == 0 { - return; - } + let hashable_inputs_result = SignatureHashableInputs::new(&inputs, &input_commitments); if inputs_count != inputs_utxos_count { assert_eq!( hashable_inputs_result.unwrap_err(), - DestinationSigError::InvalidUtxoCountVsInputs(inputs_utxos.len(), inputs.len(),) + DestinationSigError::InvalidUtxoCountVsInputs( + input_commitments.len(), + inputs.len(), + ) ); } else { assert!(hashable_inputs_result.is_ok()); @@ -209,7 +246,7 @@ mod tests { &inputs[index_to_hash], index_to_hash, ) - .is_ok(),); + .is_ok()); // Valid case assert_eq!( @@ -230,10 +267,10 @@ mod tests { #[rstest] #[trace] #[case(Seed::from_entropy())] - fn signature_hashable_inputs(#[case] seed: Seed) { + fn signature_hashable_inputs_utxo_refs(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); - let inputs_count = rng.gen_range(0..100); + let inputs_count = rng.gen_range(1..100); let inputs_utxos_count = rng.gen_range(0..100); // invalid case @@ -242,4 +279,66 @@ mod tests { // valid case do_test_hashable_inputs(inputs_count, inputs_count, &mut rng); } + + // Make sure that new signature_hash produces the same hash as the old one + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn signature_hash_backward_compatibility(#[case] seed: Seed) { + use std::borrow::Cow; + + use crate::chain::signature::tests::utils::sig_hash_types; + + let mut rng = make_seedable_rng(seed); + + for sighash_type in sig_hash_types() { + let inputs_count = rng.gen_range(1..100); + let inputs = + (0..inputs_count).map(|_| generate_random_input(&mut rng)).collect::>(); + let (inputs_utxos, _) = generate_inputs_utxos(&mut rng, inputs_count); + + let inputs_utxos_refs = inputs_utxos.iter().map(|u| u.as_ref()).collect::>(); + + let hashable_inputs_1 = SignatureHashableInputsDeprecated { + inputs: &inputs, + inputs_utxos: &inputs_utxos_refs, + }; + + let input_index = rng.gen_range(0..inputs_count); + + let mut stream1 = DefaultHashAlgoStream::new(); + hashable_inputs_1 + .signature_hash( + &mut stream1, + sighash_type, + &inputs[input_index], + input_index, + ) + .unwrap(); + let hash1: H256 = stream1.finalize().into(); + + let inputs_info = inputs_utxos_refs + .iter() + .map(|utxo| { + utxo.map_or(SighashInputCommitment::None, |utxo| { + SighashInputCommitment::Utxo(Cow::Borrowed(utxo)) + }) + }) + .collect::>(); + let hashable_inputs_2 = SignatureHashableInputs::new(&inputs, &inputs_info).unwrap(); + + let mut stream2 = DefaultHashAlgoStream::new(); + hashable_inputs_2 + .signature_hash( + &mut stream2, + sighash_type, + &inputs[input_index], + input_index, + ) + .unwrap(); + let hash2: H256 = stream2.finalize().into(); + + assert_eq!(hash1, hash2); + } + } } diff --git a/common/src/chain/transaction/signature/sighash/input_commitment.rs b/common/src/chain/transaction/signature/sighash/input_commitment.rs new file mode 100644 index 0000000000..d917eb1ae6 --- /dev/null +++ b/common/src/chain/transaction/signature/sighash/input_commitment.rs @@ -0,0 +1,394 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{borrow::Cow, collections::BTreeMap}; + +use serialization::Encode; + +use crate::{ + chain::{ + output_value::OutputValue, AccountCommand, ChainConfig, OrderAccountCommand, OrderId, + PoolId, SighashInputCommitmentVersion, TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{Amount, BlockHeight}, +}; + +/// Extra data related to an input to which we commit when signing a transaction. +/// +/// This allows to verify input amounts when the input transactions and account states are not available +/// (e.g. in a hardware wallet). +/// +/// Note: +/// 1) Originally, this was `Option<&TxOutput>`, but `SighashInputCommitment` is encode-compatible +/// with the `Option`, provided that only `None` and `Utxo` variants are used. +/// 2) The `ProduceBlockFromStakeUtxo`, `FillOrderAccountCommand` and `ConcludeOrderAccountCommand` +/// commitments are enabled since `SighashInputCommitmentVersion::V1`. +#[derive(Clone, Debug, Encode)] +pub enum SighashInputCommitment<'a> { + /// No extra commitment. + /// + /// In V0 this is used for all account-based inputs. + /// In V1, it's used for all account-based inputs except for FillOrder and ConcludeOrder. + #[codec(index = 0)] + None, + + /// Commit to the entire UTXO. + /// + /// In V0, this is used for all UTXO-based inputs (which is redundant in many cases, + /// but this is how it was implemented initially, for simplicity). + /// In V1, this is used for all UTXO-based inputs except for ProduceBlockFromStake. + #[codec(index = 1)] + Utxo(Cow<'a, TxOutput>), + + /// Below are the V1-specific commitments. + + /// When decommissioning a pool, we want to commit to the resulting staker balance. + /// + /// Note: the utxos consumed during pool decommissioning are `TxOutput::CreateStakePool` and + /// `ProduceBlockFromStake`. If the former is consumed, then the pool hasn't staked yet, + /// so committing to `TxOutput` is enough, which is covered by the `Utxo` case. + #[codec(index = 2)] + ProduceBlockFromStakeUtxo { staker_balance: Amount }, + + /// For FillOrder we want to commit to the initial balances, which are used to calculate + /// the exchange ratio in orders V1. + /// + /// Note that we do NOT want to commit to the current balances here, because this would make + /// FillOrder transactions non-commutative. + #[codec(index = 3)] + FillOrderAccountCommand { + initially_asked: OutputValue, + initially_given: OutputValue, + }, + + /// For ConcludeOrder we want to commit to the current balances. + #[codec(index = 4)] + ConcludeOrderAccountCommand { + ask_balance: Amount, + give_balance: Amount, + }, + // Note: we don't commit to anything for FreezeOrder: + // a) Committing to the initial balances would be redundant. + // b) Committing to the current balances would be WRONG, because this would allow a malicious + // actor to disrupt the order owner's FreezeOrder txs by constantly sending FillOrder txs + // for some small amounts (note that such a disruption is possible for ConcludeOrder if the + // order is not frozen; FreezeOrder's purpose is to prevent situations like this). +} + +// FIXME: move infos and providers into a separate submodule. + +#[derive(Debug, Clone)] +pub struct PoolInfo { + pub staker_balance: Amount, +} + +#[derive(Debug, Clone)] +pub struct OrderInfo { + pub initially_asked: OutputValue, + pub initially_given: OutputValue, + pub ask_balance: Amount, + pub give_balance: Amount, +} + +pub trait UtxoProvider<'a> { + type Error: std::error::Error; + + fn get_utxo( + &self, + tx_input_index: usize, + outpoint: &UtxoOutPoint, + ) -> Result>, Self::Error>; +} + +pub struct TrivialUtxoProvider<'a>(pub &'a [Option]); + +impl<'a> UtxoProvider<'a> for TrivialUtxoProvider<'a> { + type Error = std::convert::Infallible; + + fn get_utxo( + &self, + tx_input_index: usize, + _outpoint: &UtxoOutPoint, + ) -> Result>, Self::Error> { + Ok(self.0.get(tx_input_index).and_then(|utxo| utxo.as_ref().map(Cow::Borrowed))) + } +} + +pub trait PoolInfoProvider { + type Error: std::error::Error; + + fn get_pool_info(&self, pool_id: &PoolId) -> Result, Self::Error>; +} + +impl PoolInfoProvider for BTreeMap { + type Error = std::convert::Infallible; + + fn get_pool_info(&self, pool_id: &PoolId) -> Result, Self::Error> { + Ok(self.get(pool_id).cloned()) + } +} + +pub trait OrderInfoProvider { + type Error: std::error::Error; + + fn get_order_info(&self, order_id: &OrderId) -> Result, Self::Error>; +} + +impl OrderInfoProvider for BTreeMap { + type Error = std::convert::Infallible; + + fn get_order_info(&self, order_id: &OrderId) -> Result, Self::Error> { + Ok(self.get(order_id).cloned()) + } +} + +pub fn make_sighash_input_commitments_for_kernel_input_utxos( + kernel_input_utxos: &'_ [TxOutput], +) -> Vec> { + kernel_input_utxos + .iter() + .map(|utxo| SighashInputCommitment::Utxo(Cow::Borrowed(utxo))) + .collect() +} + +pub fn make_sighash_input_commitments_for_kernel_inputs<'a, UP>( + kernel_inputs: &[TxInput], + utxo_provider: &UP, +) -> Result< + Vec>, + SighashInputCommitmentCreationError< + >::Error, + std::convert::Infallible, + std::convert::Infallible, + >, +> +where + UP: UtxoProvider<'a>, +{ + kernel_inputs + .iter() + .enumerate() + .map(|(idx, input)| match input { + TxInput::Utxo(outpoint) => { + let utxo = utxo_provider + .get_utxo(idx, outpoint) + .map_err(|err| { + SighashInputCommitmentCreationError::UtxoProviderError(err, idx) + })? + .ok_or_else(|| { + SighashInputCommitmentCreationError::UtxoNotFound(outpoint.clone(), idx) + })?; + Ok(SighashInputCommitment::Utxo(utxo)) + } + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => Err( + SighashInputCommitmentCreationError::NonUtxoKernelInput(input.clone(), idx), + ), + }) + .collect::>() +} + +#[allow(clippy::type_complexity)] +pub fn make_sighash_input_commitments_for_transaction_inputs<'a, UP, PP, OP>( + tx_inputs: &[TxInput], + utxo_provider: &UP, + pool_info_provider: &PP, + order_info_provider: &OP, + chain_config: &ChainConfig, + block_height: BlockHeight, +) -> Result< + Vec>, + SighashInputCommitmentCreationError< + >::Error, + ::Error, + ::Error, + >, +> +where + UP: UtxoProvider<'a>, + PP: PoolInfoProvider, + OP: OrderInfoProvider, +{ + let input_commitment_version = chain_config + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .sighash_input_commitment_version(); + + let commitments = match input_commitment_version { + SighashInputCommitmentVersion::V0 => tx_inputs + .iter() + .enumerate() + .map(|(idx, input)| match input { + TxInput::Utxo(outpoint) => { + let utxo = utxo_provider + .get_utxo(idx, outpoint) + .map_err(|err| { + SighashInputCommitmentCreationError::UtxoProviderError(err, idx) + })? + .ok_or_else(|| { + SighashInputCommitmentCreationError::UtxoNotFound(outpoint.clone(), idx) + })?; + Ok(SighashInputCommitment::Utxo(utxo)) + } + TxInput::Account(_) + | TxInput::AccountCommand(_, _) + | TxInput::OrderAccountCommand(_) => Ok(SighashInputCommitment::None), + }) + .collect::>()?, + SighashInputCommitmentVersion::V1 => { + let get_order_info = |order_id, input_idx| { + order_info_provider + .get_order_info(order_id) + .map_err(|err| { + SighashInputCommitmentCreationError::OrderInfoProviderError(err, input_idx) + })? + .ok_or_else(|| { + SighashInputCommitmentCreationError::OrderNotFound(*order_id, input_idx) + }) + }; + + tx_inputs + .iter() + .enumerate() + .map(|(idx, input)| match input { + TxInput::Utxo(outpoint) => { + let utxo = utxo_provider + .get_utxo(idx, outpoint) + .map_err(|err| { + SighashInputCommitmentCreationError::UtxoProviderError(err, idx) + })? + .ok_or_else(|| { + SighashInputCommitmentCreationError::UtxoNotFound( + outpoint.clone(), + idx, + ) + })?; + match utxo.as_ref() { + TxOutput::ProduceBlockFromStake(_, pool_id) => { + let pool_info = pool_info_provider + .get_pool_info(pool_id) + .map_err(|err| { + SighashInputCommitmentCreationError::PoolInfoProviderError( + err, idx, + ) + })? + .ok_or_else(|| { + SighashInputCommitmentCreationError::PoolNotFound( + *pool_id, idx, + ) + })?; + + Ok(SighashInputCommitment::ProduceBlockFromStakeUtxo { + staker_balance: pool_info.staker_balance, + }) + } + TxOutput::Transfer(_, _) + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::Burn(_) + | TxOutput::CreateStakePool(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) + | TxOutput::CreateOrder(_) => Ok(SighashInputCommitment::Utxo(utxo)), + } + } + TxInput::Account(_) => Ok(SighashInputCommitment::None), + TxInput::AccountCommand(_, command) => match command { + AccountCommand::ConcludeOrder(order_id) => { + let order_info = get_order_info(order_id, idx)?; + + Ok(SighashInputCommitment::ConcludeOrderAccountCommand { + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }) + } + AccountCommand::FillOrder(order_id, _, _) => { + let order_info = get_order_info(order_id, idx)?; + + Ok(SighashInputCommitment::FillOrderAccountCommand { + initially_asked: order_info.initially_asked, + initially_given: order_info.initially_given, + }) + } + AccountCommand::MintTokens(_, _) + | AccountCommand::UnmintTokens(_) + | AccountCommand::LockTokenSupply(_) + | AccountCommand::FreezeToken(_, _) + | AccountCommand::UnfreezeToken(_) + | AccountCommand::ChangeTokenAuthority(_, _) + | AccountCommand::ChangeTokenMetadataUri(_, _) => { + Ok(SighashInputCommitment::None) + } + }, + | TxInput::OrderAccountCommand(command) => match command { + OrderAccountCommand::FillOrder(order_id, _, _) => { + let order_info = get_order_info(order_id, idx)?; + + Ok(SighashInputCommitment::FillOrderAccountCommand { + initially_asked: order_info.initially_asked, + initially_given: order_info.initially_given, + }) + } + OrderAccountCommand::ConcludeOrder(order_id) => { + let order_info = get_order_info(order_id, idx)?; + + Ok(SighashInputCommitment::ConcludeOrderAccountCommand { + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }) + } + | OrderAccountCommand::FreezeOrder(_) => Ok(SighashInputCommitment::None), + }, + }) + .collect::>()? + } + }; + + Ok(commitments) +} + +#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)] +pub enum SighashInputCommitmentCreationError +where + UPE: std::error::Error, + PIPE: std::error::Error, + OIPE: std::error::Error, +{ + #[error("Utxo provider error: {0} (input index: {1})")] + UtxoProviderError(UPE, /*input index*/ usize), + + #[error("Pool info provider error: {0} (input index: {1})")] + PoolInfoProviderError(PIPE, /*input index*/ usize), + + #[error("Order info provider error: {0} (input index: {1})")] + OrderInfoProviderError(OIPE, /*input index*/ usize), + + #[error("Non-utxo kernel input: {0:?} (input index: {1})")] + NonUtxoKernelInput(TxInput, /*input index*/ usize), + + #[error("Utxo not found: {0:?} (input index: {1})")] + UtxoNotFound(UtxoOutPoint, /*input index*/ usize), + + #[error("Pool not found: {0} (input index: {1})")] + PoolNotFound(PoolId, /*input index*/ usize), + + #[error("Order not found: {0} (input index: {1})")] + OrderNotFound(OrderId, /*input index*/ usize), +} diff --git a/common/src/chain/transaction/signature/sighash/mod.rs b/common/src/chain/transaction/signature/sighash/mod.rs index c8e932edc4..7e4fd36357 100644 --- a/common/src/chain/transaction/signature/sighash/mod.rs +++ b/common/src/chain/transaction/signature/sighash/mod.rs @@ -14,17 +14,16 @@ // limitations under the License. use crypto::hash::StreamHasher; +use input_commitment::SighashInputCommitment; use serialization::Encode; mod hashable; +pub mod input_commitment; pub mod sighashtype; -use crate::{ - chain::TxOutput, - primitives::{ - id::{hash_encoded_to, DefaultHashAlgoStream}, - H256, - }, +use crate::primitives::{ + id::{hash_encoded_to, DefaultHashAlgoStream}, + H256, }; use self::hashable::{SignatureHashableElement, SignatureHashableInputs}; @@ -39,18 +38,15 @@ fn hash_encoded_if_some(val: &Option, stream: &mut DefaultHashAlgo fn stream_signature_hash( tx: &T, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], stream: &mut DefaultHashAlgoStream, mode: sighashtype::SigHashType, - target_input_num: usize, + target_input_index: usize, ) -> Result<(), DestinationSigError> { - let inputs = match tx.inputs() { - Some(ins) => ins, - None => return Err(DestinationSigError::SigHashRequestWithoutInputs), - }; + let inputs = tx.inputs().ok_or(DestinationSigError::SigHashRequestWithoutInputs)?; - let target_input = inputs.get(target_input_num).ok_or( - DestinationSigError::InvalidInputIndex(target_input_num, inputs.len()), + let target_input = inputs.get(target_input_index).ok_or( + DestinationSigError::InvalidInputIndex(target_input_index, inputs.len()), )?; hash_encoded_to(&mode.get(), stream); @@ -58,11 +54,11 @@ fn stream_signature_hash( hash_encoded_if_some(&tx.version_byte(), stream); hash_encoded_if_some(&tx.flags(), stream); - let inputs_hashable = SignatureHashableInputs::new(inputs, inputs_utxos)?; - inputs_hashable.signature_hash(stream, mode, target_input, target_input_num)?; + let inputs_hashable = SignatureHashableInputs::new(inputs, input_commitments)?; + inputs_hashable.signature_hash(stream, mode, target_input, target_input_index)?; let outputs_hashable = tx.outputs().unwrap_or_default(); - outputs_hashable.signature_hash(stream, mode, target_input, target_input_num)?; + outputs_hashable.signature_hash(stream, mode, target_input, target_input_index)?; Ok(()) } @@ -70,12 +66,12 @@ fn stream_signature_hash( pub fn signature_hash( mode: sighashtype::SigHashType, tx: &T, - inputs_utxos: &[Option<&TxOutput>], - input_num: usize, + input_commitments: &[SighashInputCommitment], + input_index: usize, ) -> Result { let mut stream = DefaultHashAlgoStream::new(); - stream_signature_hash(tx, inputs_utxos, &mut stream, mode, input_num)?; + stream_signature_hash(tx, input_commitments, &mut stream, mode, input_index)?; let result = stream.finalize().into(); Ok(result) diff --git a/common/src/chain/transaction/signature/tests/mixed_sighash_types.rs b/common/src/chain/transaction/signature/tests/mixed_sighash_types.rs index e658c46d68..1c8b02cd03 100644 --- a/common/src/chain/transaction/signature/tests/mixed_sighash_types.rs +++ b/common/src/chain/transaction/signature/tests/mixed_sighash_types.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use itertools::iproduct; +use itertools::{iproduct, Itertools as _}; use crypto::key::{KeyKind, PrivateKey}; use rstest::rstest; @@ -46,9 +46,9 @@ fn mixed_sighash_types(#[case] seed: Seed) { sig_hash_types(), sig_hash_types() ) { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 6); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, 6).unwrap(); + let input_commitments = generate_input_commitments(&mut rng, 6); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); + let tx = generate_unsigned_tx(&mut rng, &destination, input_commitments.len(), 6).unwrap(); let sigs = [ sighash_types.0, @@ -65,7 +65,7 @@ fn mixed_sighash_types(#[case] seed: Seed) { make_signature( &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, input, &private_key, sighash_type, @@ -78,7 +78,12 @@ fn mixed_sighash_types(#[case] seed: Seed) { let signed_tx = tx.with_signatures(sigs).unwrap(); - verify_signed_tx(&chain_config, &signed_tx, &inputs_utxos_refs, &destination) - .expect("Signature verification failed") + verify_signed_tx( + &chain_config, + &signed_tx, + &input_commitments_refs, + &destination, + ) + .expect("Signature verification failed") } } diff --git a/common/src/chain/transaction/signature/tests/mod.rs b/common/src/chain/transaction/signature/tests/mod.rs index 2816518a88..59f7a7db2c 100644 --- a/common/src/chain/transaction/signature/tests/mod.rs +++ b/common/src/chain/transaction/signature/tests/mod.rs @@ -13,13 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::vec; +use std::{borrow::Cow, vec}; use itertools::Itertools; use rstest::rstest; +use serialization::{DecodeAll, Encode}; use self::utils::*; -use super::{inputsig::InputWitness, sighash::sighashtype::SigHashType}; +use super::{ + inputsig::InputWitness, + sighash::{input_commitment::SighashInputCommitment, sighashtype::SigHashType}, +}; use crate::{ chain::{ config::create_mainnet, @@ -41,6 +45,47 @@ mod sign_and_verify; pub mod utils; +// Check that encoding SighashInputCommitment::Utxo(utxo) is the same as encoding Some(utxo), +// and encoding SighashInputCommitment::None is the same as encoding None. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn encode_decode_utxo_refs_roundtrip(#[case] seed: Seed) { + use crate::chain::signature::sighash::input_commitment::SighashInputCommitment; + + let mut rng = test_utils::random::make_seedable_rng(seed); + + // Utxo + { + // FIXME generate_input_utxo should generate all kinds of utxos + let (utxo, _) = generate_input_utxo(&mut rng); + + let commitment = SighashInputCommitment::Utxo(Cow::Borrowed(&utxo)); + let encoded_commitment = commitment.encode(); + let opt = Some(&utxo); + let encoded_opt = opt.encode(); + + assert_eq!(encoded_commitment, encoded_opt); + + // Sanity check + let decoded_commitment = + Option::::decode_all(&mut encoded_commitment.as_slice()).unwrap(); + assert_eq!(decoded_commitment.as_ref(), Some(&utxo)); + } + + // None + { + let encoded_commitment = SighashInputCommitment::None.encode(); + let encoded_opt = Option::<&TxOutput>::None.encode(); + + assert_eq!(encoded_commitment, encoded_opt); + + let decoded_commitment = + Option::::decode_all(&mut encoded_commitment.as_slice()).unwrap(); + assert_eq!(decoded_commitment.as_ref(), None); + } +} + #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -53,20 +98,20 @@ fn sign_and_verify_different_sighash_types(#[case] seed: Seed) { let destination = Destination::PublicKey(public_key); for sighash_type in sig_hash_types() { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &destination, - &inputs_utxos, + &input_commitments_refs, 3, &private_key, sighash_type, ) .unwrap(); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); assert_eq!( - verify_signed_tx(&chain_config, &tx, &inputs_utxos_refs, &destination), + verify_signed_tx(&chain_config, &tx, &input_commitments_refs, &destination), Ok(()), "{sighash_type:?}" ); @@ -87,9 +132,9 @@ fn verify_no_signature(#[case] seed: Seed) { for destination in destinations(&mut rng, public_key).filter(|d| d != &Destination::AnyoneCanSpend) { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, 3).unwrap(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); + let tx = generate_unsigned_tx(&mut rng, &destination, input_commitments.len(), 3).unwrap(); let witnesses = (0..tx.inputs().len()) .map(|_| InputWitness::NoSignature(Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]))) .collect_vec(); @@ -100,7 +145,7 @@ fn verify_no_signature(#[case] seed: Seed) { &destination, &signed_tx, &signed_tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, 0 ), Err(DestinationSigError::SignatureNotFound), @@ -126,9 +171,9 @@ fn verify_invalid_signature(#[case] seed: Seed) { for (sighash_type, raw_signature) in sig_hash_types().cartesian_product([empty_signature, invalid_signature]) { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, 3).unwrap(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); + let tx = generate_unsigned_tx(&mut rng, &destination, input_commitments.len(), 3).unwrap(); let witnesses = (0..tx.inputs().len()) .map(|_| { InputWitness::Standard(StandardInputSignature::new( @@ -145,7 +190,7 @@ fn verify_invalid_signature(#[case] seed: Seed) { &destination, &signed_tx, &signed_tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, 0 ), Err(DestinationSigError::InvalidSignatureEncoding), @@ -168,13 +213,13 @@ fn verify_signature_invalid_signature_index(#[case] seed: Seed) { let destination = Destination::PublicKey(public_key); for sighash_type in sig_hash_types() { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &destination, - &inputs_utxos, + &input_commitments_refs, 3, &private_key, sighash_type, @@ -186,7 +231,7 @@ fn verify_signature_invalid_signature_index(#[case] seed: Seed) { &destination, &tx, &tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, INVALID_SIGNATURE_INDEX ), Err(DestinationSigError::InvalidSignatureIndex( @@ -213,13 +258,13 @@ fn verify_signature_wrong_destination(#[case] seed: Seed) { let different_outpoint = Destination::PublicKey(public_key_2); for sighash_type in sig_hash_types() { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &outpoint, - &inputs_utxos, + &input_commitments_refs, 3, &private_key, sighash_type, @@ -231,7 +276,7 @@ fn verify_signature_wrong_destination(#[case] seed: Seed) { &different_outpoint, &tx, &tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, 0 ), Err(DestinationSigError::SignatureVerificationFailed), @@ -252,12 +297,12 @@ fn mutate_all(#[case] seed: Seed) { let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let outpoint_dest = Destination::PublicKey(public_key); let sighash_type = SigHashType::all(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let original_tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &outpoint_dest, @@ -267,7 +312,7 @@ fn mutate_all(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -275,7 +320,7 @@ fn mutate_all(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -283,14 +328,14 @@ fn mutate_all(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); check_mutate_output( &chain_config, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -308,12 +353,12 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let outpoint_dest = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::ALL | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let original_tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &outpoint_dest, @@ -323,7 +368,7 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); @@ -331,7 +376,7 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -339,14 +384,14 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); check_mutate_output( &chain_config, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -364,12 +409,12 @@ fn mutate_none(#[case] seed: Seed) { let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let outpoint_dest = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::NONE).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let original_tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &outpoint_dest, @@ -379,7 +424,7 @@ fn mutate_none(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -387,7 +432,7 @@ fn mutate_none(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -395,14 +440,14 @@ fn mutate_none(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); check_mutate_output( &chain_config, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); @@ -421,12 +466,12 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { let outpoint_dest = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::NONE | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let original_tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &outpoint_dest, @@ -436,7 +481,7 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); @@ -444,7 +489,7 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -452,14 +497,14 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); check_mutate_output( &chain_config, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); @@ -477,12 +522,12 @@ fn mutate_single(#[case] seed: Seed) { let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let outpoint_dest = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::SINGLE).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let original_tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &outpoint_dest, @@ -492,7 +537,7 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -500,7 +545,7 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -508,14 +553,14 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); check_mutate_output( &chain_config, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -534,12 +579,12 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { let outpoint_dest = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::SINGLE | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let original_tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &outpoint_dest, @@ -549,7 +594,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); @@ -557,7 +602,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -565,14 +610,14 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, false, ); check_mutate_output( &chain_config, &original_tx, - &inputs_utxos_refs, + &input_commitments_refs, &outpoint_dest, true, ); @@ -581,7 +626,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { fn sign_mutate_then_verify( chain_config: &ChainConfig, rng: &mut (impl Rng + CryptoRng), - inputs_utxos: &[Option], + input_commitments: &[SighashInputCommitment], private_key: &PrivateKey, sighash_type: SigHashType, destination: &Destination, @@ -591,28 +636,27 @@ fn sign_mutate_then_verify( chain_config, rng, destination, - inputs_utxos, + input_commitments, 3, private_key, sighash_type, ) .unwrap(); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); assert_eq!( - verify_signed_tx(chain_config, &original_tx, &inputs_utxos_refs, destination), + verify_signed_tx(chain_config, &original_tx, input_commitments, destination), Ok(()) ); - check_change_flags(chain_config, &original_tx, &inputs_utxos_refs, destination); - check_mutate_witness(chain_config, &original_tx, &inputs_utxos_refs, destination); - check_mutate_inputs_utxos(chain_config, &original_tx, &inputs_utxos_refs, destination); + check_change_flags(chain_config, &original_tx, input_commitments, destination); + check_mutate_witness(chain_config, &original_tx, input_commitments, destination); + check_mutate_input_commitments(chain_config, &original_tx, input_commitments, destination); original_tx } fn check_change_flags( chain_config: &ChainConfig, original_tx: &SignedTransaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], destination: &Destination, ) { let mut tx_updater = MutableTransaction::from(original_tx); @@ -625,7 +669,7 @@ fn check_change_flags( destination, &tx, &tx.signatures()[input_num], - inputs_utxos, + input_commitments, input_num ), Err(DestinationSigError::SignatureVerificationFailed) @@ -637,7 +681,7 @@ fn check_insert_input( chain_config: &ChainConfig, rng: &mut (impl Rng + CryptoRng), original_tx: &SignedTransaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], destination: &Destination, should_fail: bool, ) { @@ -649,8 +693,8 @@ fn check_insert_input( OutputValue::Coin(Amount::from_atoms(123)), Destination::AnyoneCanSpend, ); - let mut inputs_utxos = inputs_utxos.to_vec(); - inputs_utxos.push(Some(&inputs_utxo)); + let mut input_commitments = input_commitments.to_vec(); + input_commitments.push(SighashInputCommitment::Utxo(Cow::Borrowed(&inputs_utxo))); tx_updater.inputs.push(TxInput::from_utxo(outpoint_source_id, 1)); tx_updater.witness.push(InputWitness::NoSignature(Some(vec![1, 2, 3]))); @@ -660,7 +704,7 @@ fn check_insert_input( destination, &tx, &tx.signatures()[0], - &inputs_utxos, + &input_commitments, 0, ); if should_fail { @@ -674,7 +718,7 @@ fn check_insert_input( fn check_mutate_witness( chain_config: &ChainConfig, original_tx: &SignedTransaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], outpoint_dest: &Destination, ) { let mut tx_updater = MutableTransaction::from(original_tx); @@ -695,7 +739,7 @@ fn check_mutate_witness( outpoint_dest, &tx, &tx.signatures()[input], - inputs_utxos, + input_commitments, input ), Err(DestinationSigError::SignatureVerificationFailed @@ -708,7 +752,7 @@ fn check_insert_output( chain_config: &ChainConfig, rng: &mut (impl Rng + CryptoRng), original_tx: &SignedTransaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], destination: &Destination, should_fail: bool, ) { @@ -724,7 +768,7 @@ fn check_insert_output( destination, &tx, &tx.signatures()[0], - inputs_utxos, + input_commitments, 0, ); if should_fail { @@ -747,7 +791,7 @@ fn add_value(output_value: OutputValue) -> OutputValue { fn check_mutate_output( chain_config: &ChainConfig, original_tx: &SignedTransaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], destination: &Destination, should_fail: bool, ) { @@ -774,7 +818,7 @@ fn check_mutate_output( destination, &tx, &tx.signatures()[0], - inputs_utxos, + input_commitments, 0, ); if should_fail { @@ -788,7 +832,7 @@ fn check_mutate_input( chain_config: &ChainConfig, rng: &mut impl Rng, original_tx: &SignedTransaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], destination: &Destination, should_fail: bool, ) { @@ -804,7 +848,7 @@ fn check_mutate_input( destination, &tx, &tx.signatures()[0], - inputs_utxos, + input_commitments, 0, ); if should_fail { @@ -815,19 +859,19 @@ fn check_mutate_input( } // An input UTXO mutation should result in signature verification error. -fn check_mutate_inputs_utxos( +fn check_mutate_input_commitments( chain_config: &ChainConfig, original_tx: &SignedTransaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], outpoint_dest: &Destination, ) { - for input in 0..inputs_utxos.len() { + for input in 0..input_commitments.len() { let inputs_utxo = TxOutput::Transfer( OutputValue::Coin(Amount::from_atoms(123456789012345)), Destination::AnyoneCanSpend, ); - let mut inputs_utxos = inputs_utxos.to_owned(); - inputs_utxos[input] = Some(&inputs_utxo); + let mut input_commitments = input_commitments.to_owned(); + input_commitments[input] = SighashInputCommitment::Utxo(Cow::Borrowed(&inputs_utxo)); assert!(matches!( verify_signature( @@ -835,7 +879,7 @@ fn check_mutate_inputs_utxos( outpoint_dest, original_tx, &original_tx.signatures()[input], - &inputs_utxos, + &input_commitments, input ), Err(DestinationSigError::SignatureVerificationFailed) diff --git a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs index 29a1ba0313..b8dae406d3 100644 --- a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs +++ b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs @@ -38,11 +38,11 @@ use crate::{ primitives::{Amount, Id, H256}, }; use crypto::key::{KeyKind, PrivateKey}; -use randomness::Rng; +use randomness::{CryptoRng, Rng}; -const INPUTS: usize = 15; -const OUTPUTS: usize = 15; -const INVALID_INPUT: usize = 1235466; +const INPUTS_COUNT: usize = 15; +const OUTPUTS_COUNT: usize = 15; +const INVALID_INPUT_INDEX: usize = 1235466; // Create a transaction, sign it, modify and try to verify the signature. Modifications include // changing flags and lock time. @@ -60,8 +60,8 @@ fn test_mutate_tx_internal_data(#[case] seed: Seed) { (0, 31, Ok(())), (31, 0, Err(DestinationSigError::SignatureVerificationFailed)), ( - INPUTS, - OUTPUTS, + INPUTS_COUNT, + OUTPUTS_COUNT, Err(DestinationSigError::SignatureVerificationFailed), ), ( @@ -71,19 +71,25 @@ fn test_mutate_tx_internal_data(#[case] seed: Seed) { ), ]; - for ((destination, sighash_type), (inputs, outputs, expected)) in + for ((destination, sighash_type), (inputs_count, outputs_count, expected)) in destinations(&mut rng, public_key) .cartesian_product(sig_hash_types()) .cartesian_product(test_data) { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, inputs); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, inputs_count); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, outputs).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + outputs_count, + ) + .unwrap(); match sign_whole_tx( &mut rng, tx, - &inputs_utxos_refs, + &input_commitments_refs, &private_key, sighash_type, &destination, @@ -92,7 +98,12 @@ fn test_mutate_tx_internal_data(#[case] seed: Seed) { // Test flags change. let updated_tx = change_flags(&mut rng, &signed_tx, 1234567890); assert_eq!( - verify_signed_tx(&chain_config, &updated_tx, &inputs_utxos_refs, &destination), + verify_signed_tx( + &chain_config, + &updated_tx, + &input_commitments_refs, + &destination + ), expected ); } @@ -124,12 +135,13 @@ fn modify_and_verify(#[case] seed: Seed) { { let sighash_type = SigHashType::all(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + // FIXME or reverse the naming? input_commitments -> input_commitment_vals, input_commitments_refs -> input_commitments + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &destination, @@ -138,7 +150,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -146,7 +158,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -154,22 +166,28 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, + &destination, + true, + ); + check_mutate_output( + &chain_config, + &tx, + &input_commitments_refs, &destination, true, ); - check_mutate_output(&chain_config, &tx, &inputs_utxos_refs, &destination, true); } { let sighash_type = SigHashType::try_from(SigHashType::ALL | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &destination, @@ -178,7 +196,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, false, ); @@ -186,7 +204,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -194,21 +212,27 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, + &destination, + true, + ); + check_mutate_output( + &chain_config, + &tx, + &input_commitments_refs, &destination, true, ); - check_mutate_output(&chain_config, &tx, &inputs_utxos_refs, &destination, true); } { let sighash_type = SigHashType::try_from(SigHashType::NONE).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &destination, @@ -217,7 +241,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -225,7 +249,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -233,22 +257,28 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, + &destination, + false, + ); + check_mutate_output( + &chain_config, + &tx, + &input_commitments_refs, &destination, false, ); - check_mutate_output(&chain_config, &tx, &inputs_utxos_refs, &destination, false); } { let sighash_type = SigHashType::try_from(SigHashType::NONE | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &destination, @@ -257,7 +287,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, false, ); @@ -265,7 +295,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -273,21 +303,27 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, + &destination, + false, + ); + check_mutate_output( + &chain_config, + &tx, + &input_commitments_refs, &destination, false, ); - check_mutate_output(&chain_config, &tx, &inputs_utxos_refs, &destination, false); } { let sighash_type = SigHashType::try_from(SigHashType::SINGLE).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &destination, @@ -296,7 +332,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -304,7 +340,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -312,22 +348,28 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, false, ); - check_mutate_output(&chain_config, &tx, &inputs_utxos_refs, &destination, true); + check_mutate_output( + &chain_config, + &tx, + &input_commitments_refs, + &destination, + true, + ); } { let sighash_type = SigHashType::try_from(SigHashType::SINGLE | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, 3); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, 3); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = sign_mutate_then_verify( &chain_config, &mut rng, - &inputs_utxos, + &input_commitments_refs, &private_key, sighash_type, &destination, @@ -336,7 +378,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, false, ); @@ -344,7 +386,7 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, true, ); @@ -352,11 +394,17 @@ fn modify_and_verify(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos_refs, + &input_commitments_refs, &destination, false, ); - check_mutate_output(&chain_config, &tx, &inputs_utxos_refs, &destination, true); + check_mutate_output( + &chain_config, + &tx, + &input_commitments_refs, + &destination, + true, + ); } } @@ -373,13 +421,14 @@ fn mutate_all(#[case] seed: Seed) { let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key); let sighash_type = SigHashType::all(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &destination, - &inputs_utxos, - OUTPUTS, + &input_commitments_refs, + OUTPUTS_COUNT, &private_key, sighash_type, ) @@ -396,12 +445,13 @@ fn mutate_all(#[case] seed: Seed) { remove_first_output, remove_middle_output, remove_last_output, + mutate_input_commitment, ]; check_mutations( &chain_config, &mut rng, &tx, - &inputs_utxos, + &input_commitments, &destination, mutations, Err(DestinationSigError::SignatureVerificationFailed), @@ -420,14 +470,14 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::ALL | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &destination, - &inputs_utxos, - OUTPUTS, + &input_commitments_refs, + OUTPUTS_COUNT, &private_key, sighash_type, ) @@ -444,16 +494,18 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos, + &input_commitments, &destination, mutations, Err(DestinationSigError::SignatureVerificationFailed), ); + // FIXME this makes no sense (also, input_commitments_refs should probably come from mutated tx, just in case). + // Just add mutate_first_input to the above "mutations" list. { - let tx = SignedTransactionWithUtxo { + let tx = SignedTransactionWithInputCommitments { tx: tx.clone(), - inputs_utxos: inputs_utxos.clone(), + input_commitments: input_commitments.clone(), }; let tx = mutate_first_input(&mut rng, &tx); assert_eq!( @@ -462,7 +514,7 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, 0 ), Err(DestinationSigError::SignatureVerificationFailed), @@ -474,7 +526,7 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos, + &input_commitments, &destination, mutations, Ok(()), @@ -492,14 +544,15 @@ fn mutate_none(#[case] seed: Seed) { let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::NONE).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &destination, - &inputs_utxos, - OUTPUTS, + &input_commitments_refs, + OUTPUTS_COUNT, &private_key, sighash_type, ) @@ -516,7 +569,7 @@ fn mutate_none(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos, + &input_commitments, &destination, mutations, Err(DestinationSigError::SignatureVerificationFailed), @@ -533,7 +586,7 @@ fn mutate_none(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos, + &input_commitments, &destination, mutations, Ok(()), @@ -552,23 +605,23 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { let destination = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::NONE | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &destination, - &inputs_utxos, - OUTPUTS, + &input_commitments_refs, + OUTPUTS_COUNT, &private_key, sighash_type, ) .unwrap(); { - let tx = SignedTransactionWithUtxo { + let tx = SignedTransactionWithInputCommitments { tx: tx.clone(), - inputs_utxos: inputs_utxos.clone(), + input_commitments: input_commitments.clone(), }; let tx = mutate_first_input(&mut rng, &tx); let inputs = tx.tx.inputs().len(); @@ -579,7 +632,7 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, 0 ), Err(DestinationSigError::SignatureVerificationFailed), @@ -591,7 +644,7 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[input], - &inputs_utxos_refs, + &input_commitments_refs, input ), Ok(()) @@ -604,7 +657,7 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos, + &input_commitments, &destination, mutations, Err(DestinationSigError::SignatureVerificationFailed), @@ -625,7 +678,7 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { &chain_config, &mut rng, &tx, - &inputs_utxos, + &input_commitments, &destination, mutations, Ok(()), @@ -643,14 +696,14 @@ fn mutate_single(#[case] seed: Seed) { let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let destination = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::SINGLE).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &destination, - &inputs_utxos, - OUTPUTS, + &input_commitments_refs, + OUTPUTS_COUNT, &private_key, sighash_type, ) @@ -664,15 +717,15 @@ fn mutate_single(#[case] seed: Seed) { remove_last_input, remove_first_output, ]; - let tx = SignedTransactionWithUtxo { + let tx = SignedTransactionWithInputCommitments { tx, - inputs_utxos: inputs_utxos.clone(), + input_commitments: input_commitments.clone(), }; for mutate in mutations.into_iter() { let tx = mutate(&mut rng, &tx); let total_inputs = tx.tx.inputs().len(); - let inputs_utxos_refs = - tx.inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments_refs = + tx.input_commitments.iter().map(|info| info.into()).collect::>(); // Mutations make the last input number invalid, so verifying the signature for it should // result in the different error. @@ -683,7 +736,7 @@ fn mutate_single(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[input], - &inputs_utxos_refs, + &input_commitments_refs, input ), Err(DestinationSigError::SignatureVerificationFailed) @@ -695,7 +748,7 @@ fn mutate_single(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, total_inputs ), Err(DestinationSigError::InvalidSignatureIndex( @@ -719,7 +772,7 @@ fn mutate_single(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[input], - &inputs_utxos_refs, + &input_commitments_refs, input ), Ok(()) @@ -731,7 +784,7 @@ fn mutate_single(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, inputs ), Err(DestinationSigError::InvalidSignatureIndex(inputs, inputs)), @@ -741,8 +794,8 @@ fn mutate_single(#[case] seed: Seed) { { let tx = mutate_output(&mut rng, &tx); let total_inputs = tx.tx.inputs().len(); - let inputs_utxos_refs = - tx.inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments_refs = + tx.input_commitments.iter().map(|info| info.into()).collect::>(); // Mutation of the first output makes signature invalid. assert_eq!( @@ -751,7 +804,7 @@ fn mutate_single(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, 0 ), Err(DestinationSigError::SignatureVerificationFailed), @@ -763,7 +816,7 @@ fn mutate_single(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[input], - &inputs_utxos_refs, + &input_commitments_refs, input ), Ok(()) @@ -775,7 +828,7 @@ fn mutate_single(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, total_inputs ), Err(DestinationSigError::InvalidSignatureIndex( @@ -798,28 +851,29 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { let destination = Destination::PublicKey(public_key); let sighash_type = SigHashType::try_from(SigHashType::SINGLE | SigHashType::ANYONECANPAY).unwrap(); - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, INPUTS); + let input_commitments = generate_input_commitments(&mut rng, INPUTS_COUNT); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); let tx = generate_and_sign_tx( &chain_config, &mut rng, &destination, - &inputs_utxos, - OUTPUTS, + &input_commitments_refs, + OUTPUTS_COUNT, &private_key, sighash_type, ) .unwrap(); let mutations = [add_input, remove_last_input, add_output, remove_last_output]; - let tx = SignedTransactionWithUtxo { + let tx = SignedTransactionWithInputCommitments { tx, - inputs_utxos: inputs_utxos.clone(), + input_commitments: input_commitments.clone(), }; for mutate in mutations.into_iter() { let tx = mutate(&mut rng, &tx); let total_inputs = tx.tx.inputs().len(); - let inputs_utxos_refs = - tx.inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments_refs = + tx.input_commitments.iter().map(|info| info.into()).collect::>(); // Mutations make the last input number invalid, so verifying the signature for it should // result in the `InvalidInputIndex` error. @@ -830,7 +884,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[input], - &inputs_utxos_refs, + &input_commitments_refs, input ), Ok(()), @@ -843,7 +897,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, total_inputs ), Err(DestinationSigError::InvalidSignatureIndex( @@ -857,8 +911,8 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { for mutate in mutations.into_iter() { let tx = mutate(&mut rng, &tx); let total_inputs = tx.tx.inputs().len(); - let inputs_utxos_refs = - tx.inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments_refs = + tx.input_commitments.iter().map(|info| info.into()).collect::>(); assert_eq!( verify_signature( @@ -866,7 +920,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, 0 ), Err(DestinationSigError::SignatureVerificationFailed), @@ -878,7 +932,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[input], - &inputs_utxos_refs, + &input_commitments_refs, input ), Ok(()), @@ -891,7 +945,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, total_inputs ), Err(DestinationSigError::InvalidSignatureIndex( @@ -905,8 +959,8 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { for mutate in mutations.into_iter() { let tx = mutate(&mut rng, &tx); let total_inputs = tx.tx.inputs().len(); - let inputs_utxos_refs = - tx.inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments_refs = + tx.input_commitments.iter().map(|info| info.into()).collect::>(); // Mutations make the last input number invalid, so verifying the signature for it should // result in the `InvalidInputIndex` error. @@ -917,7 +971,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[input], - &inputs_utxos_refs, + &input_commitments_refs, input ), Err(DestinationSigError::SignatureVerificationFailed), @@ -930,7 +984,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, + &input_commitments_refs, total_inputs ), Err(DestinationSigError::InvalidSignatureIndex( @@ -946,24 +1000,29 @@ fn check_mutations( chain_config: &ChainConfig, rng: &mut R, tx: &SignedTransaction, - inputs_utxos: &[Option], + input_commitments: &[InputCommitmentVal], destination: &Destination, mutations: M, expected: Result<(), DestinationSigError>, ) where R: Rng, - M: IntoIterator SignedTransactionWithUtxo>, + M: IntoIterator< + Item = fn( + &mut R, + &SignedTransactionWithInputCommitments, + ) -> SignedTransactionWithInputCommitments, + >, { - let tx = SignedTransactionWithUtxo { + let tx = SignedTransactionWithInputCommitments { tx: tx.clone(), - inputs_utxos: inputs_utxos.to_vec(), + input_commitments: input_commitments.to_vec(), }; for mutate in mutations.into_iter() { let tx = mutate(rng, &tx); // The number of inputs can be changed by the `mutate` function. let inputs = tx.tx.inputs().len(); - let inputs_utxos_refs = - tx.inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments_refs = + tx.input_commitments.iter().map(|info| info.into()).collect::>(); assert_eq!( verify_signature( @@ -971,11 +1030,11 @@ fn check_mutations( destination, &tx.tx, &tx.tx.signatures()[0], - &inputs_utxos_refs, - INVALID_INPUT + &input_commitments_refs, + INVALID_INPUT_INDEX ), Err(DestinationSigError::InvalidSignatureIndex( - INVALID_INPUT, + INVALID_INPUT_INDEX, inputs )) ); @@ -986,7 +1045,7 @@ fn check_mutations( destination, &tx.tx, &tx.tx.signatures()[input], - &inputs_utxos_refs, + &input_commitments_refs, input ), expected @@ -995,27 +1054,33 @@ fn check_mutations( } } -struct SignedTransactionWithUtxo { +struct SignedTransactionWithInputCommitments { tx: SignedTransaction, - inputs_utxos: Vec>, + input_commitments: Vec, } -fn add_input(_rng: &mut impl Rng, tx: &SignedTransactionWithUtxo) -> SignedTransactionWithUtxo { +fn add_input( + _rng: &mut impl Rng, + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); updater.inputs.push(updater.inputs[0].clone()); updater.witness.push(updater.witness[0].clone()); - let mut inputs_utxos = tx.inputs_utxos.clone(); - inputs_utxos.push(inputs_utxos[0].clone()); - SignedTransactionWithUtxo { + + let mut input_commitments = tx.input_commitments.clone(); + input_commitments.push(input_commitments[0].clone()); + + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos, + input_commitments, } } +// FIXME choose input randomly fn mutate_first_input( rng: &mut impl Rng, - tx: &SignedTransactionWithUtxo, -) -> SignedTransactionWithUtxo { + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); let mutated_input = match updater.inputs.first().unwrap() { @@ -1078,68 +1143,82 @@ fn mutate_first_input( }; updater.inputs[0] = mutated_input; - SignedTransactionWithUtxo { + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos: tx.inputs_utxos.clone(), + input_commitments: tx.input_commitments.clone(), } } +// FIXME all input remove's should be merged into one, same for outputs. fn remove_first_input( _rng: &mut impl Rng, - tx: &SignedTransactionWithUtxo, -) -> SignedTransactionWithUtxo { + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); updater.inputs.remove(0); updater.witness.remove(0); - let mut inputs_utxos = tx.inputs_utxos.clone(); - inputs_utxos.remove(0); - SignedTransactionWithUtxo { + + let mut input_commitments = tx.input_commitments.clone(); + input_commitments.remove(0); + + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos, + input_commitments, } } fn remove_middle_input( _rng: &mut impl Rng, - tx: &SignedTransactionWithUtxo, -) -> SignedTransactionWithUtxo { + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); assert!(updater.inputs.len() > 8); updater.inputs.remove(7); updater.witness.remove(7); - let mut inputs_utxos = tx.inputs_utxos.clone(); - inputs_utxos.remove(7); - SignedTransactionWithUtxo { + + let mut input_commitments = tx.input_commitments.clone(); + input_commitments.remove(7); + + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos, + input_commitments, } } fn remove_last_input( _rng: &mut impl Rng, - tx: &SignedTransactionWithUtxo, -) -> SignedTransactionWithUtxo { + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); updater.inputs.pop().expect("Unexpected empty inputs"); updater.witness.pop().expect("Unexpected empty witness"); - let mut inputs_utxos = tx.inputs_utxos.clone(); - inputs_utxos.pop().expect("Unexpected empty witness"); - SignedTransactionWithUtxo { + + let mut input_commitments = tx.input_commitments.clone(); + input_commitments.pop().expect("Unexpected empty witness"); + + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos, + input_commitments, } } -fn add_output(_rng: &mut impl Rng, tx: &SignedTransactionWithUtxo) -> SignedTransactionWithUtxo { +fn add_output( + _rng: &mut impl Rng, + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); updater.outputs.push(updater.outputs[0].clone()); - SignedTransactionWithUtxo { + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos: tx.inputs_utxos.clone(), + input_commitments: tx.input_commitments.clone(), } } -fn mutate_output(_rng: &mut impl Rng, tx: &SignedTransactionWithUtxo) -> SignedTransactionWithUtxo { +// FIXME choose output randomly +fn mutate_output( + _rng: &mut impl Rng, + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); // Should fail due to change in output value updater.outputs[0] = match updater.outputs[0].clone() { @@ -1156,46 +1235,62 @@ fn mutate_output(_rng: &mut impl Rng, tx: &SignedTransactionWithUtxo) -> SignedT TxOutput::Htlc(_, _) => unreachable!(), TxOutput::CreateOrder(_) => unreachable!(), }; - SignedTransactionWithUtxo { + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos: tx.inputs_utxos.clone(), + input_commitments: tx.input_commitments.clone(), } } fn remove_first_output( _rng: &mut impl Rng, - tx: &SignedTransactionWithUtxo, -) -> SignedTransactionWithUtxo { + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); updater.outputs.remove(0); - SignedTransactionWithUtxo { + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos: tx.inputs_utxos.clone(), + input_commitments: tx.input_commitments.clone(), } } fn remove_middle_output( _rng: &mut impl Rng, - tx: &SignedTransactionWithUtxo, -) -> SignedTransactionWithUtxo { + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); assert!(updater.outputs.len() > 8); updater.outputs.remove(7); - SignedTransactionWithUtxo { + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos: tx.inputs_utxos.clone(), + input_commitments: tx.input_commitments.clone(), } } fn remove_last_output( _rng: &mut impl Rng, - tx: &SignedTransactionWithUtxo, -) -> SignedTransactionWithUtxo { + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { let mut updater = MutableTransaction::from(&tx.tx); updater.outputs.pop().expect("Unexpected empty outputs"); - SignedTransactionWithUtxo { + SignedTransactionWithInputCommitments { tx: updater.generate_tx().unwrap(), - inputs_utxos: tx.inputs_utxos.clone(), + input_commitments: tx.input_commitments.clone(), + } +} + +// FIXME add this to tests +// BUT mod.rs also has stuff related to mutations +fn mutate_input_commitment( + rng: &mut (impl Rng + CryptoRng), + tx: &SignedTransactionWithInputCommitments, +) -> SignedTransactionWithInputCommitments { + let index = rng.gen_range(0..tx.input_commitments.len()); + let mut input_commitments = tx.input_commitments.clone(); + input_commitments[index] = generate_input_commitment(rng); + + SignedTransactionWithInputCommitments { + tx: tx.tx.clone(), + input_commitments, } } diff --git a/common/src/chain/transaction/signature/tests/sign_and_verify.rs b/common/src/chain/transaction/signature/tests/sign_and_verify.rs index 2c531d0e58..833909fa0f 100644 --- a/common/src/chain/transaction/signature/tests/sign_and_verify.rs +++ b/common/src/chain/transaction/signature/tests/sign_and_verify.rs @@ -48,35 +48,47 @@ fn sign_and_verify_all_and_none(#[case] seed: Seed) { let test_data = [(0, 31), (31, 0), (20, 3), (3, 20)]; let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); - for ((destination, sighash_type), (inputs, outputs)) in destinations(&mut rng, public_key) - .cartesian_product(sig_hash_types().filter(|t| t.outputs_mode() != OutputsMode::Single)) - .cartesian_product(test_data) + for ((destination, sighash_type), (inputs_count, outputs_count)) in + destinations(&mut rng, public_key) + .cartesian_product(sig_hash_types().filter(|t| t.outputs_mode() != OutputsMode::Single)) + .cartesian_product(test_data) { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, inputs); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + let input_commitments = generate_input_commitments(&mut rng, inputs_count); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, outputs).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + outputs_count, + ) + .unwrap(); let signed_tx = sign_whole_tx( &mut rng, tx, - &inputs_utxos_refs, + &input_commitments_refs, &private_key, sighash_type, &destination, ); // `sign_whole_tx` does nothing if there no inputs. - if destination == Destination::AnyoneCanSpend && inputs > 0 { + if destination == Destination::AnyoneCanSpend && inputs_count > 0 { assert_eq!( signed_tx, Err(DestinationSigError::AttemptedToProduceSignatureForAnyoneCanSpend) ); - } else if matches!(destination, Destination::ScriptHash(_)) && inputs > 0 { + } else if matches!(destination, Destination::ScriptHash(_)) && inputs_count > 0 { // This should be updated after ScriptHash support is implemented. assert_eq!(signed_tx, Err(DestinationSigError::Unsupported)); } else { let signed_tx = signed_tx.expect("{sighash_type:?} {destination:?}"); - verify_signed_tx(&chain_config, &signed_tx, &inputs_utxos_refs, &destination) - .expect("{sighash_type:?} {destination:?}") + verify_signed_tx( + &chain_config, + &signed_tx, + &input_commitments_refs, + &destination, + ) + .expect("{sighash_type:?} {destination:?}") } } } @@ -245,23 +257,33 @@ fn sign_and_verify_single(#[case] seed: Seed) { ), ]; - for (destination, sighash_type, inputs, outputs, expected) in test_data.into_iter() { - let (inputs_utxos, _priv_keys) = generate_inputs_utxos(&mut rng, inputs); - let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + for (destination, sighash_type, inputs_count, outputs_count, expected) in test_data.into_iter() + { + let input_commitments = generate_input_commitments(&mut rng, inputs_count); + let input_commitments_refs = input_commitments.iter().map(|comm| comm.into()).collect_vec(); - let tx = generate_unsigned_tx(&mut rng, &destination, &inputs_utxos, outputs).unwrap(); + let tx = generate_unsigned_tx( + &mut rng, + &destination, + input_commitments.len(), + outputs_count, + ) + .unwrap(); match sign_whole_tx( &mut rng, tx, - &inputs_utxos_refs, + &input_commitments_refs, &private_key, sighash_type, &destination, ) { - Ok(signed_tx) => { - verify_signed_tx(&chain_config, &signed_tx, &inputs_utxos_refs, &destination) - .expect("{sighash_type:X?}, {destination:?}") - } + Ok(signed_tx) => verify_signed_tx( + &chain_config, + &signed_tx, + &input_commitments_refs, + &destination, + ) + .expect("{sighash_type:X?}, {destination:?}"), Err(err) => assert_eq!(Err(err), expected, "{sighash_type:X?}, {destination:?}"), } } diff --git a/common/src/chain/transaction/signature/tests/utils.rs b/common/src/chain/transaction/signature/tests/utils.rs index 1efa93fb81..58cefc8fc9 100644 --- a/common/src/chain/transaction/signature/tests/utils.rs +++ b/common/src/chain/transaction/signature/tests/utils.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use itertools::Itertools; use crypto::key::{KeyKind, PrivateKey, PublicKey}; @@ -26,7 +28,7 @@ use crate::{ output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, - sighash::sighashtype::SigHashType, + sighash::{input_commitment::SighashInputCommitment, sighashtype::SigHashType}, DestinationSigError, EvaluatedInputWitness, Signable, }, signed_transaction::SignedTransaction, @@ -36,14 +38,108 @@ use crate::{ primitives::{amount::UnsignedIntType, Amount, Id, H256}, }; +fn make_random_value(rng: &mut (impl Rng + CryptoRng)) -> OutputValue { + if rng.gen::() { + OutputValue::Coin(Amount::from_atoms(rng.gen())) + } else { + OutputValue::TokenV1(H256(rng.gen()).into(), Amount::from_atoms(rng.gen())) + } +} + pub fn generate_input_utxo( rng: &mut (impl Rng + CryptoRng), ) -> (TxOutput, crypto::key::PrivateKey) { - let (private_key, public_key) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); - let destination = Destination::PublicKey(public_key); + let (sk, pk) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + let destination = Destination::PublicKey(pk); let output_value = OutputValue::Coin(Amount::from_atoms(rng.next_u64() as u128)); let utxo = TxOutput::Transfer(output_value, destination); - (utxo, private_key) + (utxo, sk) +} + +// FIXME: remove and use SighashInputCommitment directly instead? +#[derive(Clone, Debug)] +pub enum InputCommitmentVal { + None, + Utxo(TxOutput), + ProduceBlockFromStakeUtxo { + staker_balance: Amount, + }, + FillOrderAccountCommand { + initially_asked: OutputValue, + initially_given: OutputValue, + }, + ConcludeOrderAccountCommand { + ask_balance: Amount, + give_balance: Amount, + }, +} + +impl<'a> Into> for &'a InputCommitmentVal { + fn into(self) -> SighashInputCommitment<'a> { + match self { + InputCommitmentVal::None => SighashInputCommitment::None, + InputCommitmentVal::Utxo(utxo) => SighashInputCommitment::Utxo(Cow::Borrowed(utxo)), + InputCommitmentVal::ProduceBlockFromStakeUtxo { staker_balance } => { + SighashInputCommitment::ProduceBlockFromStakeUtxo { + staker_balance: *staker_balance, + } + } + InputCommitmentVal::FillOrderAccountCommand { + initially_asked, + initially_given, + } => SighashInputCommitment::FillOrderAccountCommand { + initially_asked: initially_asked.clone(), + initially_given: initially_given.clone(), + }, + InputCommitmentVal::ConcludeOrderAccountCommand { + ask_balance, + give_balance, + } => SighashInputCommitment::ConcludeOrderAccountCommand { + ask_balance: *ask_balance, + give_balance: *give_balance, + }, + } + } +} + +pub fn generate_input_commitment(rng: &mut (impl Rng + CryptoRng)) -> InputCommitmentVal { + match rng.gen_range(0..5) { + 0 => InputCommitmentVal::None, + 1 => { + let (utxo, _) = generate_input_utxo(rng); + InputCommitmentVal::Utxo(utxo) + } + 2 => { + let staker_balance = Amount::from_atoms(rng.gen::()); + InputCommitmentVal::ProduceBlockFromStakeUtxo { staker_balance } + } + 3 => { + let initially_asked = make_random_value(rng); + let initially_given = make_random_value(rng); + + InputCommitmentVal::FillOrderAccountCommand { + initially_asked, + initially_given, + } + } + 4 => { + let ask_balance = Amount::from_atoms(rng.gen()); + let give_balance = Amount::from_atoms(rng.gen()); + + InputCommitmentVal::ConcludeOrderAccountCommand { + ask_balance, + give_balance, + } + } + _ => unreachable!(), + } +} + +pub fn generate_input_commitments( + rng: &mut (impl Rng + CryptoRng), + input_count: usize, +) -> Vec { + (0..input_count).map(|_| generate_input_commitment(rng)).collect() } pub fn generate_inputs_utxos( @@ -94,23 +190,25 @@ impl MutableTransaction { pub fn generate_unsigned_tx( rng: &mut (impl Rng + CryptoRng), destination: &Destination, - inputs_utxos: &[Option], + inputs_count: usize, outputs_count: usize, ) -> Result { - let inputs = inputs_utxos - .iter() - .map(|utxo| match utxo { - Some(_) => TxInput::from_utxo( - Id::::new(H256::random_using(rng)).into(), - rng.gen(), - ), - None => TxInput::from_account( - AccountNonce::new(rng.gen()), - AccountSpending::DelegationBalance( - DelegationId::new(H256::random_using(rng)), - Amount::from_atoms(rng.gen()), - ), - ), + let inputs = (0..inputs_count) + .map(|_| { + if rng.gen_bool(0.5) { + TxInput::from_utxo( + Id::::new(H256::random_using(rng)).into(), + rng.gen(), + ) + } else { + TxInput::from_account( + AccountNonce::new(rng.gen()), + AccountSpending::DelegationBalance( + DelegationId::new(H256::random_using(rng)), + Amount::from_atoms(rng.gen()), + ), + ) + } }) .collect(); @@ -130,7 +228,7 @@ pub fn generate_unsigned_tx( pub fn sign_whole_tx( rng: &mut (impl Rng + CryptoRng), tx: Transaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], private_key: &PrivateKey, sighash_type: SigHashType, destination: &Destination, @@ -143,7 +241,7 @@ pub fn sign_whole_tx( make_signature( rng, &tx, - inputs_utxos, + input_commitments, i, private_key, sighash_type, @@ -160,29 +258,23 @@ pub fn generate_and_sign_tx( chain_config: &ChainConfig, rng: &mut (impl Rng + CryptoRng), destination: &Destination, - inputs_utxos: &[Option], + input_commitments: &[SighashInputCommitment], outputs: usize, private_key: &PrivateKey, sighash_type: SigHashType, ) -> Result { - let tx = generate_unsigned_tx(rng, destination, inputs_utxos, outputs).unwrap(); - let inputs_utxos_refs = inputs_utxos.iter().map(|i| i.as_ref()).collect::>(); + let tx = generate_unsigned_tx(rng, destination, input_commitments.len(), outputs).unwrap(); let signed_tx = sign_whole_tx( rng, tx, - inputs_utxos_refs.as_slice(), + input_commitments, private_key, sighash_type, destination, ) .unwrap(); assert_eq!( - verify_signed_tx( - chain_config, - &signed_tx, - inputs_utxos_refs.as_slice(), - destination - ), + verify_signed_tx(chain_config, &signed_tx, input_commitments, destination), Ok(()) ); Ok(signed_tx) @@ -191,7 +283,7 @@ pub fn generate_and_sign_tx( pub fn make_signature( rng: &mut (impl Rng + CryptoRng), tx: &Transaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], input_num: usize, private_key: &PrivateKey, sighash_type: SigHashType, @@ -202,7 +294,7 @@ pub fn make_signature( sighash_type, outpoint_dest, tx, - inputs_utxos, + input_commitments, input_num, rng, )?; @@ -212,7 +304,7 @@ pub fn make_signature( pub fn verify_signed_tx( chain_config: &ChainConfig, tx: &SignedTransaction, - inputs_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], destination: &Destination, ) -> Result<(), DestinationSigError> { for i in 0..tx.inputs().len() { @@ -221,7 +313,7 @@ pub fn verify_signed_tx( destination, tx, &tx.signatures()[i], - inputs_utxos, + input_commitments, i, )? } @@ -262,8 +354,8 @@ pub fn verify_signature( outpoint_destination: &Destination, tx: &T, input_witness: &InputWitness, - inputs_utxos: &[Option<&TxOutput>], - input_num: usize, + input_commitments: &[SighashInputCommitment], + input_index: usize, ) -> Result<(), DestinationSigError> { let eval_witness = match input_witness.clone() { InputWitness::NoSignature(d) => EvaluatedInputWitness::NoSignature(d), @@ -274,7 +366,7 @@ pub fn verify_signature( outpoint_destination, tx, &eval_witness, - inputs_utxos, - input_num, + input_commitments, + input_index, ) } diff --git a/common/src/chain/upgrades/chainstate_upgrade.rs b/common/src/chain/upgrades/chainstate_upgrade.rs index 31b450ddc1..e4676cc9ac 100644 --- a/common/src/chain/upgrades/chainstate_upgrade.rs +++ b/common/src/chain/upgrades/chainstate_upgrade.rs @@ -49,12 +49,6 @@ pub enum OrdersActivated { No, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] -pub enum StakerDestinationUpdateForbidden { - Yes, - No, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] pub enum DataDepositFeeVersion { V0, @@ -81,6 +75,18 @@ pub enum OrdersVersion { V1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum StakerDestinationUpdateForbidden { + Yes, + No, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum SighashInputCommitmentVersion { + V0, + V1, +} + #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct ChainstateUpgrade { token_issuance_version: TokenIssuanceVersion, @@ -93,6 +99,7 @@ pub struct ChainstateUpgrade { orders_activated: OrdersActivated, orders_version: OrdersVersion, staker_destination_update_forbidden: StakerDestinationUpdateForbidden, + sighash_input_commitment_version: SighashInputCommitmentVersion, } impl ChainstateUpgrade { @@ -108,6 +115,7 @@ impl ChainstateUpgrade { orders_activated: OrdersActivated, orders_version: OrdersVersion, staker_destination_update_forbidden: StakerDestinationUpdateForbidden, + sighash_input_commitment_version: SighashInputCommitmentVersion, ) -> Self { Self { token_issuance_version, @@ -120,6 +128,7 @@ impl ChainstateUpgrade { orders_activated, orders_version, staker_destination_update_forbidden, + sighash_input_commitment_version, } } @@ -162,4 +171,8 @@ impl ChainstateUpgrade { pub fn orders_version(&self) -> OrdersVersion { self.orders_version } + + pub fn sighash_input_commitment_version(&self) -> SighashInputCommitmentVersion { + self.sighash_input_commitment_version + } } diff --git a/common/src/chain/upgrades/mod.rs b/common/src/chain/upgrades/mod.rs index fdbec12522..fad1c5a3a4 100644 --- a/common/src/chain/upgrades/mod.rs +++ b/common/src/chain/upgrades/mod.rs @@ -20,8 +20,8 @@ mod netupgrade; pub use chainstate_upgrade::{ ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, OrdersVersion, - RewardDistributionVersion, StakerDestinationUpdateForbidden, TokenIssuanceVersion, - TokensFeeVersion, + RewardDistributionVersion, SighashInputCommitmentVersion, StakerDestinationUpdateForbidden, + TokenIssuanceVersion, TokensFeeVersion, }; pub use consensus_upgrade::{ConsensusUpgrade, PoSStatus, PoWStatus, RequiredConsensus}; pub use netupgrade::NetUpgrades; diff --git a/common/test-helpers/src/chainstate_upgrade_builder.rs b/common/test-helpers/src/chainstate_upgrade_builder.rs index 7683d4e5e3..c28a260b0a 100644 --- a/common/test-helpers/src/chainstate_upgrade_builder.rs +++ b/common/test-helpers/src/chainstate_upgrade_builder.rs @@ -16,8 +16,8 @@ use common::chain::{ ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, FrozenTokensValidationVersion, HtlcActivated, OrdersActivated, OrdersVersion, - RewardDistributionVersion, StakerDestinationUpdateForbidden, TokenIssuanceVersion, - TokensFeeVersion, + RewardDistributionVersion, SighashInputCommitmentVersion, StakerDestinationUpdateForbidden, + TokenIssuanceVersion, TokensFeeVersion, }; #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] @@ -32,6 +32,7 @@ pub struct ChainstateUpgradeBuilder { orders_activated: OrdersActivated, orders_version: OrdersVersion, staker_destination_update_forbidden: StakerDestinationUpdateForbidden, + sighash_input_commitment_version: SighashInputCommitmentVersion, } macro_rules! builder_method { @@ -58,6 +59,7 @@ impl ChainstateUpgradeBuilder { orders_activated: OrdersActivated::Yes, orders_version: OrdersVersion::V1, staker_destination_update_forbidden: StakerDestinationUpdateForbidden::Yes, + sighash_input_commitment_version: SighashInputCommitmentVersion::V1, } } @@ -73,6 +75,7 @@ impl ChainstateUpgradeBuilder { self.orders_activated, self.orders_version, self.staker_destination_update_forbidden, + self.sighash_input_commitment_version, ) } @@ -82,4 +85,5 @@ impl ChainstateUpgradeBuilder { builder_method!(orders_activated: OrdersActivated); builder_method!(orders_version: OrdersVersion); builder_method!(staker_destination_update_forbidden: StakerDestinationUpdateForbidden); + builder_method!(sighash_input_commitment_version: SighashInputCommitmentVersion); } diff --git a/consensus/src/pos/input_data.rs b/consensus/src/pos/input_data.rs index 324c7e3872..3e32fa1347 100644 --- a/consensus/src/pos/input_data.rs +++ b/consensus/src/pos/input_data.rs @@ -32,7 +32,10 @@ use common::{ authorize_pubkey_spend::sign_public_key_spending, standard_signature::StandardInputSignature, InputWitness, }, - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::{ + input_commitment::make_sighash_input_commitments_for_kernel_input_utxos, + sighashtype::SigHashType, signature_hash, + }, }, ChainConfig, Destination, PoSStatus, PoolId, TxInput, TxOutput, }, @@ -241,10 +244,12 @@ where None, ); + let input_commitments = + make_sighash_input_commitments_for_kernel_input_utxos(pos_input_data.kernel_input_utxos()); let sighash = signature_hash( SigHashType::default(), &block_reward_transactable, - &pos_input_data.kernel_input_utxos().iter().map(Some).collect::>(), + &input_commitments, 0, ) .map_err(|_| ConsensusPoSError::FailedToSignKernel)?; diff --git a/mempool/src/error/ban_score.rs b/mempool/src/error/ban_score.rs index a54c00f000..23a098f4f8 100644 --- a/mempool/src/error/ban_score.rs +++ b/mempool/src/error/ban_score.rs @@ -183,7 +183,13 @@ impl MempoolBanScore for tx_verifier::error::InputCheckErrorPayload { fn mempool_ban_score(&self) -> u32 { match self { Self::MissingUtxo(_) => 100, + Self::PoolNotFound(_) => 100, + Self::OrderNotFound(_) => 100, + Self::NonUtxoKernelInput(_) => 100, Self::UtxoView(e) => e.ban_score(), + Self::UtxoInfoProvider(e) => e.ban_score(), + Self::PoolInfoProvider(e) => e.ban_score(), + Self::OrderInfoProvider(e) => e.ban_score(), Self::Translation(e) => e.ban_score(), Self::Verification(e) => e.ban_score(), } diff --git a/mempool/src/pool/orphans/detect.rs b/mempool/src/pool/orphans/detect.rs index e065254862..cabd641f7b 100644 --- a/mempool/src/pool/orphans/detect.rs +++ b/mempool/src/pool/orphans/detect.rs @@ -39,7 +39,16 @@ impl OrphanType { CTE::InputCheck(e) => match e.error() { // Missing UTXO signifies a possible orphan ICE::MissingUtxo(_) => Ok(Self::MissingUtxo), - ICE::UtxoView(_) | ICE::Translation(_) | ICE::Verification(_) => Err(err), + + ICE::PoolNotFound(_) + | ICE::OrderNotFound(_) + | ICE::NonUtxoKernelInput(_) + | ICE::UtxoView(_) + | ICE::UtxoInfoProvider(_) + | ICE::PoolInfoProvider(_) + | ICE::OrderInfoProvider(_) + | ICE::Translation(_) + | ICE::Verification(_) => Err(err), }, // These do not diff --git a/mempool/src/pool/tx_pool/tests/accumulator.rs b/mempool/src/pool/tx_pool/tests/accumulator.rs index 72fc9cb070..03d8361ecc 100644 --- a/mempool/src/pool/tx_pool/tests/accumulator.rs +++ b/mempool/src/pool/tx_pool/tests/accumulator.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use common::{ address::pubkeyhash::PublicKeyHash, chain::{ @@ -23,7 +25,9 @@ use common::{ classical_multisig::authorize_classical_multisig::AuthorizedClassicalMultisigSpend, htlc::produce_classical_multisig_signature_for_htlc_input, }, - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::{ + input_commitment::SighashInputCommitment, sighashtype::SigHashType, signature_hash, + }, }, timelock::OutputTimeLock, }, @@ -546,7 +550,7 @@ async fn timelocked_htlc_refund( let sighash = signature_hash( SigHashType::all(), &tx, - &[Some(&tx0.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx0.transaction().outputs()[0]))], 0, ) .unwrap(); @@ -565,7 +569,7 @@ async fn timelocked_htlc_refund( &authorization, SigHashType::all(), &tx, - &[Some(&tx0.transaction().outputs()[0])], + &[SighashInputCommitment::Utxo(Cow::Borrowed(&tx0.transaction().outputs()[0]))], 0, ) .unwrap(); diff --git a/mintscript/src/checker/mod.rs b/mintscript/src/checker/mod.rs index a6c45d29ed..6955fdcecb 100644 --- a/mintscript/src/checker/mod.rs +++ b/mintscript/src/checker/mod.rs @@ -43,7 +43,7 @@ pub struct ScriptChecker { pub type TimelockOnlyScriptChecker = ScriptChecker; -/// Script checker only verifying signtures. +/// Script checker only verifying signatures. pub type SignatureOnlyScriptChecker = ScriptChecker; diff --git a/mintscript/src/checker/signature.rs b/mintscript/src/checker/signature.rs index 4910de0153..6a756c2e3b 100644 --- a/mintscript/src/checker/signature.rs +++ b/mintscript/src/checker/signature.rs @@ -14,8 +14,11 @@ // limitations under the License. use common::chain::{ - signature::{DestinationSigError, EvaluatedInputWitness, Transactable}, - ChainConfig, Destination, TxOutput, + signature::{ + sighash::input_commitment::SighashInputCommitment, DestinationSigError, + EvaluatedInputWitness, Transactable, + }, + ChainConfig, Destination, }; pub trait SignatureChecker { @@ -55,8 +58,8 @@ pub trait SignatureContext { /// Get the transaction being signed fn transaction(&self) -> &Self::Tx; - /// Get the list of input utxos - fn input_utxos(&self) -> &[Option<&TxOutput>]; + /// Get the list of input commitments + fn input_commitments(&self) -> &[SighashInputCommitment]; /// Get the input number fn input_num(&self) -> usize; @@ -83,7 +86,7 @@ impl SignatureChecker for StandardSignatureChecker { destination, tx, witness, - ctx.input_utxos(), + ctx.input_commitments(), input_num, ) } diff --git a/mintscript/src/tests/checkers.rs b/mintscript/src/tests/checkers.rs index 9febafaa64..c9efc98b30 100644 --- a/mintscript/src/tests/checkers.rs +++ b/mintscript/src/tests/checkers.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; + use super::*; fn make_dummy_tx( @@ -33,8 +35,10 @@ fn make_dummy_tx( let utxos: Vec<_> = (0_usize..n_inputs) .map(|_| TxOutput::Transfer(gen_value(), Destination::AnyoneCanSpend)) .collect(); - - let utxo_refs: Vec<_> = utxos.iter().map(Some).collect(); + let input_commitments = utxos + .iter() + .map(|utxo| SighashInputCommitment::Utxo(Cow::Borrowed(utxo))) + .collect::>(); let transaction = { let output = TxOutput::Burn(gen_value()); @@ -53,7 +57,7 @@ fn make_dummy_tx( SigHashType::default(), Destination::PublicKey(PublicKey::from_private_key(private_key)), &tx, - &utxo_refs, + &input_commitments, input_num, &mut rng, ) @@ -84,6 +88,11 @@ fn check_sig(#[case] seed: Seed) { let pubkey0 = &keypairs[0].1; let (utxos, transaction) = make_dummy_tx(&mut rng, &privkeys); + // FIXME: reconsider + let input_commitments = utxos + .iter() + .map(|utxo| SighashInputCommitment::Utxo(Cow::Borrowed(utxo))) + .collect::>(); let sig0 = &transaction.signatures()[0]; let eval_witness = match sig0.clone() { @@ -93,7 +102,7 @@ fn check_sig(#[case] seed: Seed) { let script = WitnessScript::signature(Destination::PublicKey(pubkey0.clone()), eval_witness); // Test a successful check - let mut checker = MockContext::new(0, &transaction, &utxos).into_checker(); + let mut checker = MockContext::new(0, &transaction, input_commitments.clone()).into_checker(); script.verify(&mut checker).expect("Check to succeed"); // Test checks which mutate the original transaction, the signature check should fail @@ -103,7 +112,7 @@ fn check_sig(#[case] seed: Seed) { Err(_) => continue, }; - let mut checker = MockContext::new(0, &bad_tx, &utxos).into_checker(); + let mut checker = MockContext::new(0, &bad_tx, input_commitments.clone()).into_checker(); match script.verify(&mut checker).expect_err("this should fail") { ScriptError::Signature(_e) => (), e @ (ScriptError::Timelock(_) @@ -132,11 +141,16 @@ fn check_timelocks( .collect(); let privkeys: Vec<_> = keypairs.iter().map(|(priv_k, _pub_k)| priv_k).collect(); let (utxos, transaction) = make_dummy_tx(rng, &privkeys); + // FIXME: reconsider + let input_commitments = utxos + .iter() + .map(|utxo| SighashInputCommitment::Utxo(Cow::Borrowed(utxo))) + .collect::>(); let script = WitnessScript::timelock(timelock); // Test a successful check - let mut checker = MockContext::new(0, &transaction, &utxos) + let mut checker = MockContext::new(0, &transaction, input_commitments) .with_block_heights(utxo_height, spend_height) .with_timestamps(utxo_time, spend_time) .into_checker(); diff --git a/mintscript/src/tests/hashlock_checker.rs b/mintscript/src/tests/hashlock_checker.rs index 14f1763864..ca4d9eeef6 100644 --- a/mintscript/src/tests/hashlock_checker.rs +++ b/mintscript/src/tests/hashlock_checker.rs @@ -30,7 +30,7 @@ impl crate::SignatureContext for EmptyContext { unreachable!() } - fn input_utxos(&self) -> &[Option<&common::chain::TxOutput>] { + fn input_commitments(&self) -> &[SighashInputCommitment] { unreachable!() } diff --git a/mintscript/src/tests/mod.rs b/mintscript/src/tests/mod.rs index 75ef8cee0c..0f2b7119b5 100644 --- a/mintscript/src/tests/mod.rs +++ b/mintscript/src/tests/mod.rs @@ -19,7 +19,7 @@ use common::{ output_value::OutputValue, signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, - sighash::sighashtype::SigHashType, + sighash::{input_commitment::SighashInputCommitment, sighashtype::SigHashType}, }, timelock::OutputTimeLock, tokens::TokenId, diff --git a/mintscript/src/tests/utils.rs b/mintscript/src/tests/utils.rs index e1bcec860f..791c3dee44 100644 --- a/mintscript/src/tests/utils.rs +++ b/mintscript/src/tests/utils.rs @@ -19,7 +19,7 @@ pub struct MockContext<'a, C> { chain_config: C, input_num: usize, transaction: &'a SignedTransaction, - utxos: Vec>, + input_commitments: Vec>, source_height: BlockHeight, spending_height: BlockHeight, source_time: BlockTimestamp, @@ -27,25 +27,16 @@ pub struct MockContext<'a, C> { } impl<'a> MockContext<'a, ChainConfig> { - /// New mock context, all inputs have utxos. + /// New mock context pub fn new( input_num: usize, transaction: &'a SignedTransaction, - utxos: impl IntoIterator, - ) -> Self { - Self::new_some_utxos(input_num, transaction, utxos.into_iter().map(Some)) - } - - /// New mock context, not all inputs have utxos. - pub fn new_some_utxos( - input_num: usize, - transaction: &'a SignedTransaction, - utxos: impl IntoIterator>, + input_commitments: Vec>, ) -> Self { Self { input_num, transaction, - utxos: utxos.into_iter().collect(), + input_commitments, source_height: BlockHeight::zero(), spending_height: BlockHeight::one(), source_time: BlockTimestamp::from_int_seconds(0), @@ -74,7 +65,7 @@ impl<'a, C> MockContext<'a, C> { chain_config: _, input_num, transaction, - utxos, + input_commitments, source_height, spending_height, source_time, @@ -85,7 +76,7 @@ impl<'a, C> MockContext<'a, C> { chain_config, input_num, transaction, - utxos, + input_commitments, source_height, spending_height, source_time, @@ -115,8 +106,8 @@ impl> crate::SignatureContext for MockContext<'_, C> { self.transaction } - fn input_utxos(&self) -> &[Option<&common::chain::TxOutput>] { - self.utxos.as_slice() + fn input_commitments(&self) -> &[SighashInputCommitment] { + self.input_commitments.as_slice() } fn input_num(&self) -> usize { diff --git a/mintscript/src/translate.rs b/mintscript/src/translate.rs index d14f3d52ef..9479fa9217 100644 --- a/mintscript/src/translate.rs +++ b/mintscript/src/translate.rs @@ -85,21 +85,6 @@ pub enum InputInfo<'a> { }, } -impl InputInfo<'_> { - pub fn as_utxo_output(&self) -> Option<&TxOutput> { - match self { - InputInfo::Utxo { - outpoint: _, - utxo, - utxo_source: _, - } => Some(utxo), - InputInfo::Account { .. } - | InputInfo::AccountCommand { .. } - | InputInfo::OrderAccountCommand { .. } => None, - } - } -} - pub trait InputInfoProvider { fn input_info(&self) -> &InputInfo; fn witness(&self) -> &InputWitness; diff --git a/orders-accounting/src/error.rs b/orders-accounting/src/error.rs index 0fcf0a5dce..12406fbd93 100644 --- a/orders-accounting/src/error.rs +++ b/orders-accounting/src/error.rs @@ -15,6 +15,7 @@ use common::{chain::OrderId, primitives::Amount}; +// FIXME StorageError is unused and it's the only thing that requires the chainstate-types dependency. #[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)] pub enum Error { #[error("Accounting storage error")] @@ -71,3 +72,9 @@ pub enum Error { } pub type Result = core::result::Result; + +impl From for Error { + fn from(value: std::convert::Infallible) -> Self { + match value {} + } +} diff --git a/orders-accounting/src/storage/in_memory.rs b/orders-accounting/src/storage/in_memory.rs index f4479ccf7f..417cb02a4a 100644 --- a/orders-accounting/src/storage/in_memory.rs +++ b/orders-accounting/src/storage/in_memory.rs @@ -64,7 +64,7 @@ impl InMemoryOrdersAccounting { } impl OrdersAccountingStorageRead for InMemoryOrdersAccounting { - type Error = chainstate_types::storage_result::Error; + type Error = crate::Error; fn get_order_data(&self, id: &OrderId) -> Result, Self::Error> { Ok(self.orders_data.get(id).cloned()) diff --git a/pos-accounting/src/error.rs b/pos-accounting/src/error.rs index 5608723d2c..bf38126bd8 100644 --- a/pos-accounting/src/error.rs +++ b/pos-accounting/src/error.rs @@ -105,3 +105,9 @@ pub enum Error { } pub type Result = core::result::Result; + +impl From for Error { + fn from(value: std::convert::Infallible) -> Self { + match value {} + } +} diff --git a/wallet/src/account/utxo_selector/output_group.rs b/wallet/src/account/utxo_selector/output_group.rs index bfc21ff194..12c7ff27f3 100644 --- a/wallet/src/account/utxo_selector/output_group.rs +++ b/wallet/src/account/utxo_selector/output_group.rs @@ -14,7 +14,7 @@ // limitations under the License. use common::{ - chain::{output_value::OutputValue, tokens::TokenData, TxInput, TxOutput}, + chain::{output_value::OutputValue, TxInput, TxOutput}, primitives::Amount, }; @@ -74,18 +74,7 @@ impl OutputGroup { ))) } }; - let value = match output_value { - OutputValue::Coin(output_amount) => output_amount, - OutputValue::TokenV0(token_data) => { - let token_data = token_data.as_ref(); - match token_data { - TokenData::TokenTransfer(token_transfer) => token_transfer.amount, - TokenData::TokenIssuance(token_issuance) => token_issuance.amount_to_issue, - TokenData::NftIssuance(_) => Amount::from_atoms(1), - } - } - OutputValue::TokenV1(_, output_amount) => output_amount, - }; + let value = output_value.amount(); Ok(Self { outputs: vec![output], diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index 837f799479..1b4431e51b 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -28,6 +28,7 @@ use common::{ ChainConfig, Destination, SignedTransactionIntent, SignedTransactionIntentError, Transaction, }, + primitives::BlockHeight, }; use crypto::key::hdkd::{derivable::DerivationError, u31::U31}; use wallet_storage::{ @@ -40,6 +41,7 @@ use wallet_types::{ use crate::{ key_chain::{AccountKeyChains, KeyChainError}, + wallet::SighashInputCommitmentCreationError, Account, WalletResult, }; @@ -47,6 +49,9 @@ pub mod software_signer; #[cfg(feature = "trezor")] pub mod trezor_signer; +#[cfg(test)] +mod test_utils; + #[cfg(feature = "trezor")] use self::trezor_signer::TrezorError; @@ -90,6 +95,8 @@ pub enum SignerError { AddressError(#[from] AddressError), #[error("Order was filled more than the available balance")] OrderFillUnderflow, + #[error("Error creating sighash input commitment")] + SighashInputCommitmentCreationError(#[from] SighashInputCommitmentCreationError), } type SignerResult = Result; @@ -103,6 +110,7 @@ pub trait Signer { tx: PartiallySignedTransaction, key_chain: &impl AccountKeyChains, db_tx: &impl WalletStorageReadUnlocked, + block_height: BlockHeight, ) -> SignerResult<( PartiallySignedTransaction, Vec, diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index c733c3e30e..1ea541ef16 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -15,33 +15,44 @@ use std::sync::Arc; -use common::chain::{ - htlc::HtlcSecret, - signature::{ - inputsig::{ - arbitrary_message::ArbitraryMessageSignature, - classical_multisig::{ - authorize_classical_multisig::{ - sign_classical_multisig_spending, AuthorizedClassicalMultisigSpend, - ClassicalMultisigCompletionStatus, +use itertools::Itertools; + +use common::{ + chain::{ + htlc::HtlcSecret, + signature::{ + inputsig::{ + arbitrary_message::ArbitraryMessageSignature, + classical_multisig::{ + authorize_classical_multisig::{ + sign_classical_multisig_spending, AuthorizedClassicalMultisigSpend, + ClassicalMultisigCompletionStatus, + }, + encode_decode_multisig_spend::{decode_multisig_spend, encode_multisig_spend}, + }, + htlc::produce_uniparty_signature_for_htlc_input, + standard_signature::StandardInputSignature, + InputWitness, + }, + sighash::{ + self, + input_commitment::{ + make_sighash_input_commitments_for_transaction_inputs, SighashInputCommitment, }, - encode_decode_multisig_spend::{decode_multisig_spend, encode_multisig_spend}, + sighashtype::SigHashType, + signature_hash, }, - htlc::produce_uniparty_signature_for_htlc_input, - standard_signature::StandardInputSignature, - InputWitness, + DestinationSigError, }, - sighash::{sighashtype::SigHashType, signature_hash}, - DestinationSigError, + ChainConfig, Destination, SignedTransactionIntent, Transaction, TxOutput, }, - ChainConfig, Destination, SignedTransactionIntent, Transaction, TxOutput, + primitives::BlockHeight, }; use crypto::key::{ extended::{ExtendedPrivateKey, ExtendedPublicKey}, hdkd::{derivable::Derivable, u31::U31}, PrivateKey, }; -use itertools::Itertools; use randomness::make_true_rng; use wallet_storage::{ StoreTxRwUnlocked, WalletStorageReadLocked, WalletStorageReadUnlocked, @@ -110,7 +121,8 @@ impl SoftwareSigner { tx: &Transaction, destination: &Destination, input_index: usize, - inputs_utxo_refs: &[Option<&TxOutput>], + input_utxo: Option<&TxOutput>, + input_commitments: &[SighashInputCommitment], key_chain: &impl AccountKeyChains, htlc_secret: &Option, db_tx: &impl WalletStorageReadUnlocked, @@ -131,7 +143,7 @@ impl SoftwareSigner { sighash_type, destination.clone(), tx, - inputs_utxo_refs, + input_commitments, input_index, htlc_secret.clone(), make_true_rng(), @@ -143,7 +155,7 @@ impl SoftwareSigner { sighash_type, destination.clone(), tx, - inputs_utxo_refs, + input_commitments, input_index, make_true_rng(), ) @@ -167,13 +179,13 @@ impl SoftwareSigner { let (sig, _, status) = self.sign_multisig_input( tx, input_index, - inputs_utxo_refs, + input_commitments, current_signatures, key_chain, db_tx, )?; - let signature = encode_multisig_spend(&sig, inputs_utxo_refs[input_index]); + let signature = encode_multisig_spend(&sig, input_utxo); return Ok((Some(InputWitness::Standard(signature)), status)); } @@ -188,7 +200,7 @@ impl SoftwareSigner { &self, tx: &Transaction, input_index: usize, - input_utxos: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], mut current_signatures: AuthorizedClassicalMultisigSpend, key_chain: &impl AccountKeyChains, db_tx: &impl WalletStorageReadUnlocked, @@ -200,7 +212,7 @@ impl SoftwareSigner { let sighash_type = SigHashType::all(); let challenge = current_signatures.challenge().clone(); - let sighash = signature_hash(sighash_type, tx, input_utxos, input_index)?; + let sighash = signature_hash(sighash_type, tx, input_commitments, input_index)?; let required_signatures = challenge.min_required_signatures(); let previous_status = SignatureStatus::PartialMultisig { @@ -258,12 +270,20 @@ impl Signer for SoftwareSigner { ptx: PartiallySignedTransaction, key_chain: &impl AccountKeyChains, db_tx: &impl WalletStorageReadUnlocked, + block_height: BlockHeight, ) -> SignerResult<( PartiallySignedTransaction, Vec, Vec, )> { - let inputs_utxo_refs: Vec<_> = ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + ptx.tx().inputs(), + &sighash::input_commitment::TrivialUtxoProvider(ptx.input_utxos()), + ptx.additional_info(), + ptx.additional_info(), + &self.chain_config, + block_height, + )?; let (witnesses, prev_statuses, new_statuses) = ptx .witnesses() @@ -271,84 +291,90 @@ impl Signer for SoftwareSigner { .enumerate() .zip(ptx.destinations()) .zip(ptx.htlc_secrets()) - .map(|(((i, witness), destination), htlc_secret)| match witness { - Some(w) => match w { - InputWitness::NoSignature(_) => Ok(( - Some(w.clone()), - SignatureStatus::FullySigned, - SignatureStatus::FullySigned, - )), - InputWitness::Standard(sig) => match destination { - Some(destination) => { - let sig_verified = + .map(|(((i, witness), destination), htlc_secret)| { + let input_utxo = &ptx.input_utxos()[i]; + + match witness { + Some(w) => match w { + InputWitness::NoSignature(_) => Ok(( + Some(w.clone()), + SignatureStatus::FullySigned, + SignatureStatus::FullySigned, + )), + InputWitness::Standard(sig) => match destination { + Some(destination) => { + let sig_verified = tx_verifier::input_check::signature_only_check::verify_tx_signature( &self.chain_config, destination, &ptx, - &inputs_utxo_refs, + &input_commitments, i, + input_utxo.clone() ) .is_ok(); - if sig_verified { - Ok(( - Some(w.clone()), - SignatureStatus::FullySigned, - SignatureStatus::FullySigned, - )) - } else if let Destination::ClassicMultisig(_) = destination { - let sig_components = - decode_multisig_spend(sig, inputs_utxo_refs[i]) - .map_err(SignerError::SigningError)?; - - let (sig_component, previous_status, final_status) = self - .sign_multisig_input( - ptx.tx(), - i, - &inputs_utxo_refs, - sig_components, - key_chain, - db_tx, - )?; - - let signature = - encode_multisig_spend(&sig_component, inputs_utxo_refs[i]); - - Ok(( - Some(InputWitness::Standard(signature)), - previous_status, - final_status, - )) - } else { - Ok(( - None, - SignatureStatus::InvalidSignature, - SignatureStatus::NotSigned, - )) + if sig_verified { + Ok(( + Some(w.clone()), + SignatureStatus::FullySigned, + SignatureStatus::FullySigned, + )) + } else if let Destination::ClassicMultisig(_) = destination { + let sig_components = + decode_multisig_spend(sig, input_utxo.as_ref()) + .map_err(SignerError::SigningError)?; + + let (sig_component, previous_status, final_status) = self + .sign_multisig_input( + ptx.tx(), + i, + &input_commitments, + sig_components, + key_chain, + db_tx, + )?; + + let signature = + encode_multisig_spend(&sig_component, input_utxo.as_ref()); + + Ok(( + Some(InputWitness::Standard(signature)), + previous_status, + final_status, + )) + } else { + Ok(( + None, + SignatureStatus::InvalidSignature, + SignatureStatus::NotSigned, + )) + } } + None => Ok(( + Some(w.clone()), + SignatureStatus::UnknownSignature, + SignatureStatus::UnknownSignature, + )), + }, + }, + None => match destination { + Some(destination) => { + let (sig, status) = self.sign_input( + &ptx.tx(), + destination, + i, + input_utxo.as_ref(), + &input_commitments, + key_chain, + htlc_secret, + db_tx, + )?; + Ok((sig, SignatureStatus::NotSigned, status)) } - None => Ok(( - Some(w.clone()), - SignatureStatus::UnknownSignature, - SignatureStatus::UnknownSignature, - )), + None => Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)), }, - }, - None => match destination { - Some(destination) => { - let (sig, status) = self.sign_input( - ptx.tx(), - destination, - i, - &inputs_utxo_refs, - key_chain, - htlc_secret, - db_tx, - )?; - Ok((sig, SignatureStatus::NotSigned, status)) - } - None => Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)), - }, + } }) .collect::, SignerError>>()? .into_iter() diff --git a/wallet/src/signer/software_signer/tests.rs b/wallet/src/signer/software_signer/tests.rs index 75f3509601..909c4ef6e0 100644 --- a/wallet/src/signer/software_signer/tests.rs +++ b/wallet/src/signer/software_signer/tests.rs @@ -13,38 +13,65 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::ops::{Add, Div, Mul, Sub}; - -use super::*; -use crate::key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}; -use crate::{Account, SendRequest}; -use common::chain::block::timestamp::BlockTimestamp; -use common::chain::config::create_regtest; -use common::chain::htlc::HashedTimelockContract; -use common::chain::output_value::OutputValue; -use common::chain::signature::inputsig::arbitrary_message::produce_message_challenge; -use common::chain::signature::inputsig::authorize_pubkeyhash_spend::AuthorizedPublicKeyHashSpend; -use common::chain::stakelock::StakePoolData; -use common::chain::timelock::OutputTimeLock; -use common::chain::tokens::{NftIssuance, NftIssuanceV0, TokenId, TokenIssuance}; -use common::chain::{ - AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, GenBlock, - OrderData, OutPointSourceId, PoolId, TxInput, +use std::{ + num::NonZeroU8, + ops::{Add, Div, Mul, Sub}, }; -use common::primitives::per_thousand::PerThousand; -use common::primitives::{Amount, BlockHeight, Id, H256}; -use crypto::key::secp256k1::Secp256k1PublicKey; -use crypto::key::{KeyKind, PublicKey, Signature}; + use itertools::izip; -use randomness::{Rng, RngCore}; use rstest::rstest; -use serialization::Encode; + +use common::{ + chain::{ + self, + block::timestamp::BlockTimestamp, + classic_multisig::ClassicMultisigChallenge, + config::{create_regtest, ChainType}, + htlc::HashedTimelockContract, + output_value::OutputValue, + signature::inputsig::{ + arbitrary_message::produce_message_challenge, + authorize_pubkeyhash_spend::AuthorizedPublicKeyHashSpend, + }, + stakelock::StakePoolData, + timelock::OutputTimeLock, + tokens::{ + IsTokenUnfreezable, Metadata, NftIssuance, NftIssuanceV0, TokenId, TokenIssuance, + TokenIssuanceV1, + }, + AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, GenBlock, + NetUpgrades, OrderAccountCommand, OrderData, OrderId, OutPointSourceId, PoolId, + SighashInputCommitmentVersion, TxInput, TxOutput, + }, + primitives::{ + amount::UnsignedIntType, per_thousand::PerThousand, Amount, BlockHeight, Id, Idable, H256, + }, +}; +use common_test_helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; +use crypto::{ + key::{secp256k1::Secp256k1PublicKey, KeyKind, PublicKey, Signature}, + vrf::VRFPrivateKey, +}; +use randomness::{Rng, RngCore}; +use serialization::{extras::non_empty_vec::DataOrNoVec, Encode}; use test_utils::random::{make_seedable_rng, Seed}; use wallet_storage::{DefaultBackend, Store, Transactional}; -use wallet_types::account_info::DEFAULT_ACCOUNT_INDEX; -use wallet_types::partially_signed_transaction::TxAdditionalInfo; -use wallet_types::seed_phrase::StoreSeedPhrase; -use wallet_types::KeyPurpose::{Change, ReceiveFunds}; +use wallet_types::{ + account_info::DEFAULT_ACCOUNT_INDEX, + partially_signed_transaction::{OrderAdditionalInfo, TxAdditionalInfo}, + seed_phrase::StoreSeedPhrase, + Currency, + KeyPurpose::{Change, ReceiveFunds}, +}; + +use crate::{ + key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}, + Account, SendRequest, +}; + +use super::*; + +use super::super::test_utils::random_order_info; const MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; @@ -88,8 +115,6 @@ fn sign_message(#[case] seed: Seed) { #[trace] #[case(Seed::from_entropy())] fn sign_transaction_intent(#[case] seed: Seed) { - use common::primitives::Idable; - let mut rng = make_seedable_rng(seed); let config = Arc::new(create_regtest()); @@ -156,29 +181,30 @@ fn sign_transaction_intent(#[case] seed: Seed) { #[trace] #[case(Seed::from_entropy())] fn sign_transaction(#[case] seed: Seed) { - use std::num::NonZeroU8; - - use common::{ - chain::{ - classic_multisig::ClassicMultisigChallenge, - htlc::HashedTimelockContract, - stakelock::StakePoolData, - tokens::{ - IsTokenUnfreezable, Metadata, NftIssuance, NftIssuanceV0, TokenId, TokenIssuance, - TokenIssuanceV1, - }, - AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, - OrderData, OrderId, PoolId, - }, - primitives::amount::UnsignedIntType, - }; - use crypto::vrf::VRFPrivateKey; - use itertools::izip; - use serialization::extras::non_empty_vec::DataOrNoVec; - let mut rng = make_seedable_rng(seed); - let chain_config = Arc::new(create_regtest()); + let sighash_input_commitment_version_fork_height = BlockHeight::new(rng.gen_range(1..100_000)); + let chain_config = Arc::new( + chain::config::Builder::new(ChainType::Regtest) + .chainstate_upgrades( + NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V0) + .build(), + ), + ( + sighash_input_commitment_version_fork_height, + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V1) + .build(), + ), + ]) + .unwrap(), + ) + .build(), + ); let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); @@ -202,6 +228,7 @@ fn sign_transaction(#[case] seed: Seed) { let total_amount = amounts.iter().fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); + // FIXME add ProduceBlockFromStake to inputs let utxos: Vec = amounts .iter() .map(|a| { @@ -277,6 +304,11 @@ fn sign_transaction(#[case] seed: Seed) { Box::new(hash_lock.clone()), ); + let filled_order1_id = OrderId::new(H256::random_using(&mut rng)); + let filled_order2_id = OrderId::new(H256::random_using(&mut rng)); + let concluded_order1_id = OrderId::new(H256::random_using(&mut rng)); + let concluded_order2_id = OrderId::new(H256::random_using(&mut rng)); + let frozen_order_id = OrderId::new(H256::random_using(&mut rng)); let acc_inputs = vec![ TxInput::Account(AccountOutPoint::new( AccountNonce::new(0), @@ -320,12 +352,12 @@ fn sign_transaction(#[case] seed: Seed) { ), TxInput::AccountCommand( AccountNonce::new(rng.next_u64()), - AccountCommand::ConcludeOrder(OrderId::new(H256::random_using(&mut rng))), + AccountCommand::ConcludeOrder(concluded_order1_id), ), TxInput::AccountCommand( AccountNonce::new(rng.next_u64()), AccountCommand::FillOrder( - OrderId::new(H256::random_using(&mut rng)), + filled_order1_id, Amount::from_atoms(123), Destination::AnyoneCanSpend, ), @@ -337,6 +369,13 @@ fn sign_transaction(#[case] seed: Seed) { "http://uri".as_bytes().to_vec(), ), ), + TxInput::OrderAccountCommand(OrderAccountCommand::FillOrder( + filled_order2_id, + Amount::from_atoms(123), + Destination::AnyoneCanSpend, + )), + TxInput::OrderAccountCommand(OrderAccountCommand::ConcludeOrder(concluded_order2_id)), + TxInput::OrderAccountCommand(OrderAccountCommand::FreezeOrder(frozen_order_id)), ]; let acc_dests: Vec = acc_inputs .iter() @@ -363,7 +402,7 @@ fn sign_transaction(#[case] seed: Seed) { let (_dest_prv, dest_pub) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); let (_, vrf_public_key) = VRFPrivateKey::new_from_entropy(crypto::vrf::VRFKeyKind::Schnorrkel); - let pool_id = PoolId::new(H256::random()); + let created_pool_id = PoolId::new(H256::random()); let delegation_id = DelegationId::new(H256::random()); let pool_data = StakePoolData::new( Amount::from_atoms(5000000), @@ -413,7 +452,7 @@ fn sign_transaction(#[case] seed: Seed) { OutputTimeLock::ForSeconds(rng.next_u64()), ), TxOutput::Burn(OutputValue::Coin(burn_amount)), - TxOutput::CreateStakePool(pool_id, Box::new(pool_data)), + TxOutput::CreateStakePool(created_pool_id, Box::new(pool_data)), TxOutput::CreateDelegationId( Destination::AnyoneCanSpend, PoolId::new(H256::random_using(&mut rng)), @@ -453,11 +492,42 @@ fn sign_transaction(#[case] seed: Seed) { .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) .with_outputs(outputs); let destinations = req.destinations().to_vec(); - let additional_info = TxAdditionalInfo::new(); + let additional_info = TxAdditionalInfo::new() + .with_order_info( + filled_order1_id, + random_order_info(&Currency::Coin, &Currency::Coin, &mut rng), + ) + .with_order_info( + filled_order2_id, + random_order_info(&Currency::Coin, &Currency::Coin, &mut rng), + ) + .with_order_info( + concluded_order1_id, + random_order_info(&Currency::Coin, &Currency::Coin, &mut rng), + ) + .with_order_info( + concluded_order2_id, + random_order_info(&Currency::Coin, &Currency::Coin, &mut rng), + ) + .with_order_info( + frozen_order_id, + random_order_info(&Currency::Coin, &Currency::Coin, &mut rng), + ); let ptx = req.into_partially_signed_tx(additional_info).unwrap(); + let block_height_for_input_commitments = BlockHeight::new( + rng.gen_range(0..sighash_input_commitment_version_fork_height.into_int() * 2), + ); + let mut signer = SoftwareSigner::new(chain_config.clone(), DEFAULT_ACCOUNT_INDEX); - let (ptx, _, _) = signer.sign_tx(ptx, account.key_chain(), &db_tx).unwrap(); + let (ptx, _, _) = signer + .sign_tx( + ptx, + account.key_chain(), + &db_tx, + block_height_for_input_commitments, + ) + .unwrap(); eprintln!("num inputs in tx: {} {:?}", inputs.len(), ptx.witnesses()); for (i, w) in ptx.witnesses().iter().enumerate() { @@ -465,7 +535,17 @@ fn sign_transaction(#[case] seed: Seed) { } assert!(ptx.all_signatures_available()); - let utxos_ref = utxos + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + ptx.tx().inputs(), + &sighash::input_commitment::TrivialUtxoProvider(ptx.input_utxos()), + ptx.additional_info(), + ptx.additional_info(), + &chain_config, + block_height_for_input_commitments, + ) + .unwrap(); + + let all_utxos = utxos .iter() .map(Some) .chain([Some(&htlc_utxo), Some(&multisig_utxo)]) @@ -477,26 +557,42 @@ fn sign_transaction(#[case] seed: Seed) { &chain_config, dest, &ptx, - &utxos_ref, + &input_commitments, i, + all_utxos[i].cloned(), ) .unwrap(); } } -#[test] -fn fixed_signatures() { - use std::num::NonZeroU8; - - use common::chain::{ - classic_multisig::ClassicMultisigChallenge, - tokens::{IsTokenUnfreezable, Metadata, TokenIssuanceV1}, - OrderId, - }; - use crypto::vrf::VRFPrivateKey; - use serialization::extras::non_empty_vec::DataOrNoVec; +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fixed_signatures(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); - let chain_config = Arc::new(create_regtest()); + let sighash_input_commitment_version_fork_height = BlockHeight::new(rng.gen_range(1..100_000)); + let chain_config = Arc::new( + chain::config::Builder::new(ChainType::Regtest) + .chainstate_upgrades( + NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V0) + .build(), + ), + ( + sighash_input_commitment_version_fork_height, + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V1) + .build(), + ), + ]) + .unwrap(), + ) + .build(), + ); let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); @@ -560,10 +656,18 @@ fn fixed_signatures() { let order_id = OrderId::new(H256::zero()); let pool_id = PoolId::new(H256::zero()); + let order_info = OrderAdditionalInfo { + initially_asked: OutputValue::TokenV1(token_id, Amount::from_atoms(10)), + initially_given: OutputValue::Coin(Amount::from_atoms(20)), + ask_balance: Amount::from_atoms(5), + give_balance: Amount::from_atoms(10), + }; + let uri = "http://uri.com".as_bytes().to_vec(); let wallet_dest0 = Destination::PublicKeyHash((&wallet_pk0).into()); + // FIXME add ProduceBlockFromStake to inputs let utxos = [TxOutput::Transfer(OutputValue::Coin(amount_1), wallet_dest0.clone())]; let inputs = [TxInput::from_utxo( @@ -609,6 +713,7 @@ fn fixed_signatures() { AccountNonce::new(0), AccountCommand::ChangeTokenMetadataUri(token_id, uri.clone()), ), + // FIXME add new order commands ]; let acc_dests: Vec = vec![wallet_dest0.clone(); acc_inputs.len()]; @@ -709,16 +814,9 @@ fn fixed_signatures() { .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) .with_outputs(outputs); let destinations = req.destinations().to_vec(); - let additional_info = TxAdditionalInfo::new(); + let additional_info = TxAdditionalInfo::new().with_order_info(order_id, order_info); let ptx = req.into_partially_signed_tx(additional_info).unwrap(); - let utxos_ref = utxos - .iter() - .map(Some) - .chain([Some(&multisig_utxo)]) - .chain(acc_dests.iter().map(|_| None)) - .collect::>(); - let multisig_signatures = [ (0, "7a99714dc6cc917faa2afded8028159a5048caf6f8382f67e6b61623fbe62c60423f8f7983f88f40c6f42924594f3de492a232e9e703b241c3b17b130f8daa59"), (2, "0a0a17d71bc98fa5c24ea611c856d0d08c6a765ea3c4c8f068668e0bdff710b82b0d2eead83d059386cd3cbdf4308418d4b81e6f52c001a9141ec477a8d24845"), @@ -750,13 +848,34 @@ fn fixed_signatures() { assert!(ptx.all_signatures_available()); + // The height doesn't matter as long as it's below the fork. + // FIXME: need a separate "fixed signature" test with the height above the form. + let block_height_for_input_commitments = + BlockHeight::new(rng.gen_range(0..sighash_input_commitment_version_fork_height.into_int())); + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + ptx.tx().inputs(), + &sighash::input_commitment::TrivialUtxoProvider(ptx.input_utxos()), + ptx.additional_info(), + ptx.additional_info(), + &chain_config, + block_height_for_input_commitments, + ) + .unwrap(); + let all_utxos = utxos + .iter() + .map(Some) + .chain([Some(&multisig_utxo)]) + .chain(acc_dests.iter().map(|_| None)) + .collect::>(); + for (i, dest) in destinations.iter().enumerate() { tx_verifier::input_check::signature_only_check::verify_tx_signature( &chain_config, dest, &ptx, - &utxos_ref, + &input_commitments, i, + all_utxos[i].cloned(), ) .unwrap(); } diff --git a/wallet/src/signer/test_utils.rs b/wallet/src/signer/test_utils.rs new file mode 100644 index 0000000000..d9b1c3789d --- /dev/null +++ b/wallet/src/signer/test_utils.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use common::{chain::output_value::OutputValue, primitives::Amount}; +use randomness::Rng; +use wallet_types::{partially_signed_transaction::OrderAdditionalInfo, Currency}; + +pub fn random_order_info( + ask_currency: &Currency, + give_currency: &Currency, + rng: &mut impl Rng, +) -> OrderAdditionalInfo { + OrderAdditionalInfo { + initially_asked: random_output_value(ask_currency, rng), + initially_given: random_output_value(give_currency, rng), + ask_balance: Amount::from_atoms(rng.gen()), + give_balance: Amount::from_atoms(rng.gen()), + } +} + +pub fn random_output_value(currency: &Currency, rng: &mut impl Rng) -> OutputValue { + match currency { + Currency::Coin => OutputValue::Coin(Amount::from_atoms(rng.gen())), + Currency::Token(id) => OutputValue::TokenV1(*id, Amount::from_atoms(rng.gen())), + } +} diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index 68fde28cf2..d7b7ae46de 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -36,7 +36,10 @@ use common::{ standard_signature::StandardInputSignature, InputWitness, }, - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::{ + self, input_commitment::make_sighash_input_commitments_for_transaction_inputs, + sighashtype::SigHashType, signature_hash, + }, DestinationSigError, }, timelock::OutputTimeLock, @@ -44,7 +47,7 @@ use common::{ AccountCommand, AccountSpending, ChainConfig, Destination, OutPointSourceId, SignedTransactionIntent, Transaction, TxInput, TxOutput, }, - primitives::{Amount, Idable, H256}, + primitives::{Amount, BlockHeight, Idable, H256}, }; use crypto::key::{ extended::ExtendedPublicKey, @@ -283,6 +286,7 @@ impl Signer for TrezorSigner { ptx: PartiallySignedTransaction, key_chain: &impl AccountKeyChains, _db_tx: &impl WalletStorageReadUnlocked, + block_height: BlockHeight, ) -> SignerResult<( PartiallySignedTransaction, Vec, @@ -293,6 +297,8 @@ impl Signer for TrezorSigner { let utxos = to_trezor_utxo_msgs(&ptx, &self.chain_config)?; let chain_type = to_trezor_chain_type(&self.chain_config); + // FIXME: mintlayer_sign_tx should either accept a height, so that it can calculate the input + // commitments correctly OR it may accept the commitments themselves and just verify their contents. let new_signatures = self .client .lock() @@ -300,7 +306,14 @@ impl Signer for TrezorSigner { .mintlayer_sign_tx(chain_type, inputs, outputs, utxos) .map_err(|err| TrezorError::DeviceError(err.to_string()))?; - let inputs_utxo_refs: Vec<_> = ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + ptx.tx().inputs(), + &sighash::input_commitment::TrivialUtxoProvider(ptx.input_utxos()), + ptx.additional_info(), + ptx.additional_info(), + &self.chain_config, + block_height, + )?; let (witnesses, prev_statuses, new_statuses) = ptx .witnesses() @@ -326,6 +339,8 @@ impl Signer for TrezorSigner { InputWitness::Standard(sig) }; + let input_utxo = &ptx.input_utxos()[i]; + match witness { Some(w) => match w { InputWitness::NoSignature(_) => Ok(( @@ -339,8 +354,9 @@ impl Signer for TrezorSigner { &self.chain_config, destination, &ptx, - &inputs_utxo_refs, + &input_commitments, i, + input_utxo.clone() ) .is_ok() { @@ -353,7 +369,7 @@ impl Signer for TrezorSigner { let sighash = signature_hash( sig.sighash_type(), ptx.tx(), - &inputs_utxo_refs, + &input_commitments, i, )?; @@ -405,7 +421,7 @@ impl Signer for TrezorSigner { None => match (destination, new_signatures.get(i)) { (Some(destination), Some(sig)) => { let sighash_type = SigHashType::all(); - let sighash = signature_hash(sighash_type, ptx.tx(), &inputs_utxo_refs, i)?; + let sighash = signature_hash(sighash_type, ptx.tx(), &input_commitments, i)?; let (sig, status) = self.make_signature( sig, destination, diff --git a/wallet/src/signer/trezor_signer/tests.rs b/wallet/src/signer/trezor_signer/tests.rs index b75cbb234a..a863dbd133 100644 --- a/wallet/src/signer/trezor_signer/tests.rs +++ b/wallet/src/signer/trezor_signer/tests.rs @@ -14,32 +14,42 @@ // limitations under the License. use core::panic; -use std::ops::{Add, Div, Sub}; +use std::{ + num::NonZeroU8, + ops::{Add, Div, Sub}, +}; -use super::*; -use crate::{ - key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}, - Account, SendRequest, +use itertools::izip; +use rstest::rstest; +use trezor_client::find_devices; + +use common::{ + chain::{ + self, + classic_multisig::ClassicMultisigChallenge, + config::create_regtest, + htlc::{HashedTimelockContract, HtlcSecret}, + output_value::OutputValue, + signature::inputsig::arbitrary_message::produce_message_challenge, + stakelock::StakePoolData, + timelock::OutputTimeLock, + tokens::{IsTokenUnfreezable, Metadata, NftIssuanceV0, TokenId, TokenIssuanceV1}, + AccountNonce, AccountOutPoint, DelegationId, Destination, GenBlock, NetUpgrades, OrderData, + OrderId, PoolId, SighashInputCommitmentVersion, Transaction, TxInput, + }, + primitives::{ + amount::UnsignedIntType, per_thousand::PerThousand, Amount, BlockHeight, Id, Idable, H256, + }, }; -use common::chain::{ - config::create_regtest, - htlc::{HashedTimelockContract, HtlcSecret}, - output_value::OutputValue, - signature::inputsig::arbitrary_message::produce_message_challenge, - stakelock::StakePoolData, - timelock::OutputTimeLock, - tokens::{NftIssuanceV0, TokenId}, - AccountNonce, AccountOutPoint, DelegationId, Destination, GenBlock, OrderData, PoolId, - Transaction, TxInput, +use common_test_helpers::chainstate_upgrade_builder::ChainstateUpgradeBuilder; +use crypto::{ + key::{KeyKind, PrivateKey}, + vrf::VRFPrivateKey, }; -use common::primitives::{per_thousand::PerThousand, Amount, BlockHeight, Id, H256}; -use crypto::key::{KeyKind, PrivateKey}; -use itertools::izip; use randomness::{Rng, RngCore}; -use rstest::rstest; use serial_test::serial; +use serialization::extras::non_empty_vec::DataOrNoVec; use test_utils::random::{make_seedable_rng, Seed}; -use trezor_client::find_devices; use wallet_storage::{DefaultBackend, Store, Transactional}; use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, @@ -47,6 +57,13 @@ use wallet_types::{ KeyPurpose::{Change, ReceiveFunds}, }; +use crate::{ + key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}, + Account, SendRequest, +}; + +use super::*; + const MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; @@ -57,7 +74,28 @@ const MNEMONIC: &str = fn sign_message(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); - let chain_config = Arc::new(create_regtest()); + let sighash_input_commitment_version_fork_height = BlockHeight::new(rng.gen_range(1..100_000)); + let chain_config = Arc::new( + chain::config::Builder::new(ChainType::Regtest) + .chainstate_upgrades( + NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V0) + .build(), + ), + ( + sighash_input_commitment_version_fork_height, + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V1) + .build(), + ), + ]) + .unwrap(), + ) + .build(), + ); let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); @@ -103,8 +141,6 @@ fn sign_message(#[case] seed: Seed) { #[case(Seed::from_entropy())] #[serial] fn sign_transaction_intent(#[case] seed: Seed) { - use common::primitives::Idable; - let mut rng = make_seedable_rng(seed); let config = Arc::new(create_regtest()); @@ -187,22 +223,30 @@ fn sign_transaction_intent(#[case] seed: Seed) { #[case(Seed::from_entropy())] #[serial] fn sign_transaction(#[case] seed: Seed) { - use std::num::NonZeroU8; - - use common::{ - chain::{ - classic_multisig::ClassicMultisigChallenge, - tokens::{IsTokenUnfreezable, Metadata, TokenIssuanceV1}, - OrderId, - }, - primitives::amount::UnsignedIntType, - }; - use crypto::vrf::VRFPrivateKey; - use serialization::extras::non_empty_vec::DataOrNoVec; - let mut rng = make_seedable_rng(seed); - let chain_config = Arc::new(create_regtest()); + let sighash_input_commitment_version_fork_height = BlockHeight::new(rng.gen_range(1..100_000)); + let chain_config = Arc::new( + chain::config::Builder::new(ChainType::Regtest) + .chainstate_upgrades( + NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V0) + .build(), + ), + ( + sighash_input_commitment_version_fork_height, + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V1) + .build(), + ), + ]) + .unwrap(), + ) + .build(), + ); let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); @@ -227,6 +271,7 @@ fn sign_transaction(#[case] seed: Seed) { let total_amount = amounts.iter().fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); eprintln!("total utxo amounts: {total_amount:?}"); + // FIXME add ProduceBlockFromStake let utxos: Vec = amounts .iter() .map(|a| { @@ -364,6 +409,7 @@ fn sign_transaction(#[case] seed: Seed) { "http://uri".as_bytes().to_vec(), ), ), + // FIXME use new order-related inputs ]; let acc_dests: Vec = acc_inputs .iter() @@ -471,46 +517,63 @@ fn sign_transaction(#[case] seed: Seed) { .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) .with_outputs(outputs); let destinations = req.destinations().to_vec(); - let additional_info = TxAdditionalInfo::with_token_info( - token_id, - TokenAdditionalInfo { - num_decimals: 1, - ticker: "TKN".as_bytes().to_vec(), - }, - ) - .join(TxAdditionalInfo::with_order_info( - order_id, - OrderAdditionalInfo { - ask_balance: Amount::from_atoms(10), - give_balance: Amount::from_atoms(100), - initially_asked: OutputValue::Coin(Amount::from_atoms(20)), - initially_given: OutputValue::TokenV1(token_id, Amount::from_atoms(200)), - }, - )); + let additional_info = TxAdditionalInfo::new() + .with_token_info( + token_id, + TokenAdditionalInfo { + num_decimals: 1, + ticker: "TKN".as_bytes().to_vec(), + }, + ) + .join(TxAdditionalInfo::new().with_order_info( + order_id, + OrderAdditionalInfo { + ask_balance: Amount::from_atoms(10), + give_balance: Amount::from_atoms(100), + initially_asked: OutputValue::Coin(Amount::from_atoms(20)), + initially_given: OutputValue::TokenV1(token_id, Amount::from_atoms(200)), + }, + )); let ptx = req.into_partially_signed_tx(additional_info).unwrap(); let client = find_test_device(); + // FIXME: same as for the SoftwareSigner, this should be `sighash_input_commitment_version_fork_height * 2`, + // so that both versions are tested. + let block_height_for_input_commitments = + BlockHeight::new(rng.gen_range(0..sighash_input_commitment_version_fork_height.into_int())); + let mut signer = TrezorSigner::new(chain_config.clone(), Arc::new(Mutex::new(client))); - let (ptx, _, _) = signer.sign_tx(ptx, account.key_chain(), &db_tx).unwrap(); + let (ptx, _, _) = signer + .sign_tx( + ptx, + account.key_chain(), + &db_tx, + block_height_for_input_commitments, + ) + .unwrap(); eprintln!("num inputs in tx: {} {:?}", inputs.len(), ptx.witnesses()); assert!(ptx.all_signatures_available()); - let utxos_ref = utxos - .iter() - .map(Some) - .chain([Some(&htlc_utxo), Some(&multisig_utxo)]) - .chain(acc_dests.iter().map(|_| None)) - .collect::>(); + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + ptx.tx().inputs(), + &sighash::input_commitment::TrivialUtxoProvider(ptx.input_utxos()), + ptx.additional_info(), + ptx.additional_info(), + &chain_config, + block_height_for_input_commitments, + ) + .unwrap(); for (i, dest) in destinations.iter().enumerate() { tx_verifier::input_check::signature_only_check::verify_tx_signature( &chain_config, dest, &ptx, - &utxos_ref, + &input_commitments, i, + Some(utxos[i].clone()), ) .unwrap(); } @@ -538,3 +601,5 @@ fn find_test_device() -> Trezor { .connect() .unwrap() } + +// FIXME: add a fixed_signatures test as well? diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 15d52d3298..836cef244f 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -41,7 +41,8 @@ use common::chain::output_value::OutputValue; use common::chain::signature::inputsig::arbitrary_message::{ ArbitraryMessageSignature, SignArbitraryMessageError, }; -use common::chain::signature::DestinationSigError; +use common::chain::signature::sighash::input_commitment::make_sighash_input_commitments_for_transaction_inputs; +use common::chain::signature::{sighash, DestinationSigError}; use common::chain::tokens::{ make_token_id, IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance, }; @@ -289,8 +290,17 @@ pub enum WalletError { UnsupportedHardwareWalletOperation, #[error("Transaction from {0:?} is confirmed and among unconfirmed descendants")] ConfirmedTxAmongUnconfirmedDescendants(OutPointSourceId), + #[error("Error creating sighash input commitment: {0}")] + SighashInputCommitmentCreationError(#[from] SighashInputCommitmentCreationError), } +pub type SighashInputCommitmentCreationError = + sighash::input_commitment::SighashInputCommitmentCreationError< + std::convert::Infallible, + std::convert::Infallible, + std::convert::Infallible, + >; + /// Result type used for the wallet pub type WalletResult = Result; @@ -1078,7 +1088,8 @@ where ) -> WalletResult<(SendRequest, AddlData)>, error_mapper: impl FnOnce(WalletError) -> WalletError, ) -> WalletResult<(SignedTransaction, AddlData)> { - let (_, block_height) = self.get_best_block_for_account(account_index)?; + let (_, best_block_height) = self.get_best_block_for_account(account_index)?; + let next_block_height = best_block_height.next_height(); self.for_account_rw_unlocked( account_index, @@ -1089,21 +1100,33 @@ where let mut signer = signer_provider.provide(Arc::new(chain_config.clone()), account_index); - let ptx = signer.sign_tx(ptx, account.key_chain(), db_tx).map(|(ptx, _, _)| ptx)?; + let ptx = signer + .sign_tx(ptx, account.key_chain(), db_tx, next_block_height) + .map(|(ptx, _, _)| ptx)?; + + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + ptx.tx().inputs(), + &sighash::input_commitment::TrivialUtxoProvider(ptx.input_utxos()), + ptx.additional_info(), + ptx.additional_info(), + &chain_config, + next_block_height, + )?; - let inputs_utxo_refs: Vec<_> = - ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); let is_fully_signed = ptx.destinations().iter().enumerate().zip(ptx.witnesses()).all( |((i, destination), witness)| match (witness, destination) { (None | Some(_), None) | (None, Some(_)) => false, (Some(_), Some(destination)) => { + let input_utxo = ptx.input_utxos()[i].clone(); + tx_verifier::input_check::signature_only_check::verify_tx_signature( chain_config, destination, &ptx, - &inputs_utxo_refs, + &input_commitments, i, + input_utxo, ) .is_ok() } @@ -1120,7 +1143,7 @@ where error_mapper(WalletError::PartiallySignedTransactionCreation(e)) })?; - check_transaction(chain_config, block_height.next_height(), &tx)?; + check_transaction(chain_config, next_block_height, &tx)?; Ok((tx, additional_data)) }, ) @@ -1975,7 +1998,7 @@ where current_fee_rate: FeeRate, ) -> WalletResult { let additional_info = - TxAdditionalInfo::with_pool_info(pool_id, PoolAdditionalInfo { staker_balance }); + TxAdditionalInfo::new().with_pool_info(pool_id, PoolAdditionalInfo { staker_balance }); Ok(self .for_account_rw_unlocked_and_check_tx_generic( account_index, @@ -2005,8 +2028,11 @@ where output_address: Option, current_fee_rate: FeeRate, ) -> WalletResult { + let (_, best_block_height) = self.get_best_block_for_account(account_index)?; + let next_block_height = best_block_height.next_height(); + let additional_info = - TxAdditionalInfo::with_pool_info(pool_id, PoolAdditionalInfo { staker_balance }); + TxAdditionalInfo::new().with_pool_info(pool_id, PoolAdditionalInfo { staker_balance }); self.for_account_rw_unlocked( account_index, |account, db_tx, chain_config, signer_provider| { @@ -2022,7 +2048,9 @@ where let mut signer = signer_provider.provide(Arc::new(chain_config.clone()), account_index); - let ptx = signer.sign_tx(ptx, account.key_chain(), db_tx).map(|(ptx, _, _)| ptx)?; + let ptx = signer + .sign_tx(ptx, account.key_chain(), db_tx, next_block_height) + .map(|(ptx, _, _)| ptx)?; if ptx.all_signatures_available() { return Err(WalletError::FullySignedTransactionInDecommissionReq); @@ -2202,13 +2230,21 @@ where Vec, Vec, )> { + let (_, best_block_height) = self.get_best_block_for_account(account_index)?; + let next_block_height = best_block_height.next_height(); + + // FIXME: need to ensure that the provided ptx always contains all the necessary info. + // And since it comes from RPC, it's better to ensure that ANY PartiallySignedTransaction + // contains ALL the necessary info (and we'd better explicitly define what "necessary info" + // means exactly and encapsulate its retrieval in one module). + self.for_account_rw_unlocked( account_index, |account, db_tx, chain_config, signer_provider| { let mut signer = signer_provider.provide(Arc::new(chain_config.clone()), account_index); - let res = signer.sign_tx(ptx, account.key_chain(), db_tx)?; + let res = signer.sign_tx(ptx, account.key_chain(), db_tx, next_block_height)?; Ok(res) }, ) @@ -2400,7 +2436,7 @@ where } fn to_token_additional_info(token_info: &UnconfirmedTokenInfo) -> TxAdditionalInfo { - TxAdditionalInfo::with_token_info( + TxAdditionalInfo::new().with_token_info( token_info.token_id(), TokenAdditionalInfo { num_decimals: token_info.num_decimals(), diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 6c4f084a0e..a5608a6744 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -55,7 +55,8 @@ use wallet_storage::{schema, WalletStorageEncryptionRead}; use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, partially_signed_transaction::{ - PartiallySignedTransaction, PartiallySignedTransactionCreationError, TxAdditionalInfo, + OrderAdditionalInfo, PartiallySignedTransaction, PartiallySignedTransactionCreationError, + TxAdditionalInfo, }, seed_phrase::{PassPhrase, StoreSeedPhrase}, utxo_types::{UtxoState, UtxoType}, @@ -2700,7 +2701,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { Destination::PublicKeyHash(some_other_address), ); - let additional_info = TxAdditionalInfo::with_token_info( + let additional_info = TxAdditionalInfo::new().with_token_info( *token_id, TokenAdditionalInfo { num_decimals: number_of_decimals, @@ -3051,7 +3052,7 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { Destination::PublicKeyHash(some_other_address), ); - let additional_info = TxAdditionalInfo::with_token_info( + let additional_info = TxAdditionalInfo::new().with_token_info( issued_token_id, TokenAdditionalInfo { num_decimals: unconfirmed_token_info.num_decimals(), @@ -5649,7 +5650,7 @@ fn create_order(#[case] seed: Seed) { // Create an order selling tokens for coins let ask_value = OutputValue::Coin(Amount::from_atoms(111)); let give_value = OutputValue::TokenV1(issued_token_id, token_amount_to_mint); - let additional_info = TxAdditionalInfo::with_token_info( + let additional_info = TxAdditionalInfo::new().with_token_info( issued_token_id, TokenAdditionalInfo { num_decimals: unconfirmed_token_info.num_decimals(), @@ -5775,7 +5776,7 @@ fn create_order_and_conclude(#[case] seed: Seed) { // Create an order selling tokens for coins let ask_value = OutputValue::Coin(Amount::from_atoms(111)); let give_value = OutputValue::TokenV1(issued_token_id, token_amount_to_mint); - let additional_info = TxAdditionalInfo::with_token_info( + let additional_info = TxAdditionalInfo::new().with_token_info( issued_token_id, TokenAdditionalInfo { num_decimals: unconfirmed_token_info.num_decimals(), @@ -5819,13 +5820,23 @@ fn create_order_and_conclude(#[case] seed: Seed) { assert_eq!(coin_balance, expected_balance); assert!(token_balances.is_empty()); - let additional_info = TxAdditionalInfo::with_token_info( - issued_token_id, - TokenAdditionalInfo { - num_decimals: unconfirmed_token_info.num_decimals(), - ticker: unconfirmed_token_info.token_ticker().to_vec(), - }, - ); + let additional_info = TxAdditionalInfo::new() + .with_token_info( + issued_token_id, + TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }, + ) + .with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.clone().into(), + initially_given: order_info.initially_given.clone().into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + ); let conclude_order_tx = wallet .create_conclude_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5964,7 +5975,7 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { let ask_value = OutputValue::TokenV1(issued_token_id, token_amount_to_mint); let sell_amount = Amount::from_atoms(1000); let give_value = OutputValue::Coin(sell_amount); - let additional_info = TxAdditionalInfo::with_token_info( + let additional_info = TxAdditionalInfo::new().with_token_info( issued_token_id, TokenAdditionalInfo { num_decimals: unconfirmed_token_info.num_decimals(), @@ -6022,13 +6033,23 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { Some(&(issued_token_id, token_amount_to_mint)) ); } - let additional_info = TxAdditionalInfo::with_token_info( - issued_token_id, - TokenAdditionalInfo { - num_decimals: unconfirmed_token_info.num_decimals(), - ticker: unconfirmed_token_info.token_ticker().to_vec(), - }, - ); + let additional_info = TxAdditionalInfo::new() + .with_token_info( + issued_token_id, + TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }, + ) + .with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.clone().into(), + initially_given: order_info.initially_given.clone().into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + ); // Fill order partially let fill_order_tx_1 = wallet2 @@ -6088,13 +6109,23 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { nonce: Some(AccountNonce::new(0)), }; - let additional_info = TxAdditionalInfo::with_token_info( - issued_token_id, - TokenAdditionalInfo { - num_decimals: unconfirmed_token_info.num_decimals(), - ticker: unconfirmed_token_info.token_ticker().to_vec(), - }, - ); + let additional_info = TxAdditionalInfo::new() + .with_token_info( + issued_token_id, + TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }, + ) + .with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.clone().into(), + initially_given: order_info.initially_given.clone().into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + ); let fill_order_tx_2 = wallet2 .create_fill_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -6145,13 +6176,23 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { ask_balance: Amount::ZERO, nonce: Some(AccountNonce::new(1)), }; - let additional_info = TxAdditionalInfo::with_token_info( - issued_token_id, - TokenAdditionalInfo { - num_decimals: unconfirmed_token_info.num_decimals(), - ticker: unconfirmed_token_info.token_ticker().to_vec(), - }, - ); + let additional_info = TxAdditionalInfo::new() + .with_token_info( + issued_token_id, + TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }, + ) + .with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.clone().into(), + initially_given: order_info.initially_given.clone().into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + ); let conclude_order_tx = wallet1 .create_conclude_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -6301,7 +6342,7 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { let ask_value = OutputValue::TokenV1(issued_token_id, token_amount_to_mint); let sell_amount = Amount::from_atoms(1000); let give_value = OutputValue::Coin(sell_amount); - let additional_info = TxAdditionalInfo::with_token_info( + let additional_info = TxAdditionalInfo::new().with_token_info( issued_token_id, TokenAdditionalInfo { num_decimals: unconfirmed_token_info.num_decimals(), @@ -6360,13 +6401,23 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { ); } - let additional_info = TxAdditionalInfo::with_token_info( - issued_token_id, - TokenAdditionalInfo { - num_decimals: unconfirmed_token_info.num_decimals(), - ticker: unconfirmed_token_info.token_ticker().to_vec(), - }, - ); + let additional_info = TxAdditionalInfo::new() + .with_token_info( + issued_token_id, + TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }, + ) + .with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.clone().into(), + initially_given: order_info.initially_given.clone().into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + ); // Fill order partially let fill_order_tx_1 = wallet2 .create_fill_order_tx( @@ -6425,13 +6476,23 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { nonce: Some(AccountNonce::new(0)), }; - let additional_info = TxAdditionalInfo::with_token_info( - issued_token_id, - TokenAdditionalInfo { - num_decimals: unconfirmed_token_info.num_decimals(), - ticker: unconfirmed_token_info.token_ticker().to_vec(), - }, - ); + let additional_info = TxAdditionalInfo::new() + .with_token_info( + issued_token_id, + TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }, + ) + .with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.clone().into(), + initially_given: order_info.initially_given.clone().into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + ); let conclude_order_tx = wallet1 .create_conclude_order_tx( DEFAULT_ACCOUNT_INDEX, diff --git a/wallet/types/src/partially_signed_transaction.rs b/wallet/types/src/partially_signed_transaction.rs index eab3a58e1b..48b24375f7 100644 --- a/wallet/types/src/partially_signed_transaction.rs +++ b/wallet/types/src/partially_signed_transaction.rs @@ -19,7 +19,7 @@ use common::{ chain::{ htlc::HtlcSecret, output_value::OutputValue, - signature::{inputsig::InputWitness, Signable, Transactable}, + signature::{inputsig::InputWitness, sighash, Signable, Transactable}, tokens::TokenId, Destination, OrderId, PoolId, SignedTransaction, Transaction, TransactionCreationError, TxInput, TxOutput, @@ -82,28 +82,19 @@ impl TxAdditionalInfo { } } - pub fn with_token_info(token_id: TokenId, info: TokenAdditionalInfo) -> Self { - Self { - token_info: BTreeMap::from([(token_id, info)]), - pool_info: BTreeMap::new(), - order_info: BTreeMap::new(), - } + pub fn with_token_info(mut self, token_id: TokenId, info: TokenAdditionalInfo) -> Self { + self.token_info.insert(token_id, info); + self } - pub fn with_pool_info(pool_id: PoolId, info: PoolAdditionalInfo) -> Self { - Self { - token_info: BTreeMap::new(), - pool_info: BTreeMap::from([(pool_id, info)]), - order_info: BTreeMap::new(), - } + pub fn with_pool_info(mut self, pool_id: PoolId, info: PoolAdditionalInfo) -> Self { + self.pool_info.insert(pool_id, info); + self } - pub fn with_order_info(order_id: OrderId, info: OrderAdditionalInfo) -> Self { - Self { - token_info: BTreeMap::new(), - pool_info: BTreeMap::new(), - order_info: BTreeMap::from([(order_id, info)]), - } + pub fn with_order_info(mut self, order_id: OrderId, info: OrderAdditionalInfo) -> Self { + self.order_info.insert(order_id, info); + self } pub fn add_token_info(&mut self, token_id: TokenId, info: TokenAdditionalInfo) { @@ -134,6 +125,39 @@ impl TxAdditionalInfo { } } +impl sighash::input_commitment::PoolInfoProvider for TxAdditionalInfo { + type Error = std::convert::Infallible; + + fn get_pool_info( + &self, + pool_id: &PoolId, + ) -> Result, Self::Error> { + Ok( + self.pool_info.get(pool_id).map(|info| sighash::input_commitment::PoolInfo { + staker_balance: info.staker_balance, + }), + ) + } +} + +impl sighash::input_commitment::OrderInfoProvider for TxAdditionalInfo { + type Error = std::convert::Infallible; + + fn get_order_info( + &self, + order_id: &OrderId, + ) -> Result, Self::Error> { + Ok( + self.order_info.get(order_id).map(|info| sighash::input_commitment::OrderInfo { + initially_asked: info.initially_asked.clone(), + initially_given: info.initially_given.clone(), + ask_balance: info.ask_balance, + give_balance: info.give_balance, + }), + ) + } +} + #[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] pub struct PartiallySignedTransaction { tx: Transaction, diff --git a/wallet/wallet-controller/src/helpers.rs b/wallet/wallet-controller/src/helpers.rs index 5bc971b880..b5975d6df4 100644 --- a/wallet/wallet-controller/src/helpers.rs +++ b/wallet/wallet-controller/src/helpers.rs @@ -143,7 +143,7 @@ where OutputValue::Coin(_) | OutputValue::TokenV0(_) => Ok(TxAdditionalInfo::new()), OutputValue::TokenV1(token_id, _) => { let info = fetch_token_info(rpc_client, *token_id).await?; - Ok(TxAdditionalInfo::with_token_info( + Ok(TxAdditionalInfo::new().with_token_info( *token_id, TokenAdditionalInfo { num_decimals: info.token_number_of_decimals(), @@ -154,6 +154,16 @@ where } } +// FIXME: need tests in the wallet for sign_raw_transaction & compose_transaction that have order/pool decomm inputs, +// before and after the fork (at least sign and verify, but also maybe do a low-level check for commitments or their absence). + +// FIXME: +// a) rename to just fetch_utxo_extra_info; +// b) handle ALL output types; +// c) write unit tests (need 3 traits for getting order/token infos and pool staker balance); +// Probably add a submodule extra_info_retrieval (or _fetching). +// Probably fix the TODO with rpc calls optimization. "fetch" functions may return both TxAdditionalInfo (for things that are available right away) and +// a "set" of ids - tokens/orders/pools - to retrieve the info for. pub async fn fetch_utxo_extra_info_for_hw_wallet( rpc_client: &T, utxo: TxOutput, @@ -181,10 +191,8 @@ where .await .map_err(ControllerError::NodeCallError)? .map(|staker_balance| { - TxAdditionalInfo::with_pool_info( - *pool_id, - PoolAdditionalInfo { staker_balance }, - ) + TxAdditionalInfo::new() + .with_pool_info(*pool_id, PoolAdditionalInfo { staker_balance }) }) .ok_or(WalletError::UnknownPoolId(*pool_id))?; Ok((utxo, additional_infos)) @@ -227,26 +235,14 @@ pub async fn into_balances( } // TODO: optimize RPC calls to the Node +// FIXME: this function shouldn't assume None for htlc_secrets, they should be passed from the outside. pub async fn tx_to_partially_signed_tx( rpc_client: &T, wallet: &RuntimeWallet, tx: Transaction, ) -> Result> { - let tasks: FuturesOrdered<_> = tx - .inputs() - .iter() - .map(|inp| into_utxo_and_destination(rpc_client, wallet, inp)) - .collect(); let (input_utxos, additional_infos, destinations) = - tasks.try_collect::>().await?.into_iter().fold( - (Vec::new(), TxAdditionalInfo::new(), Vec::new()), - |(mut input_utxos, additional_info, mut destinations), (x, y, z)| { - input_utxos.push(x); - let additional_info = additional_info.join(y); - destinations.push(z); - (input_utxos, additional_info, destinations) - }, - ); + fetch_input_infos(rpc_client, wallet, tx.inputs()).await?; let num_inputs = tx.inputs().len(); @@ -273,6 +269,36 @@ pub async fn tx_to_partially_signed_tx( Ok(ptx) } +pub async fn fetch_input_infos( + rpc_client: &T, + wallet: &RuntimeWallet, + inputs: &[TxInput], +) -> Result< + ( + Vec>, + TxAdditionalInfo, + Vec>, + ), + ControllerError, +> { + let tasks: FuturesOrdered<_> = inputs + .iter() + .map(|inp| into_utxo_and_destination(rpc_client, wallet, inp)) + .collect(); + let (input_utxos, additional_infos, destinations) = + tasks.try_collect::>().await?.into_iter().fold( + (Vec::new(), TxAdditionalInfo::new(), Vec::new()), + |(mut input_utxos, additional_info, mut destinations), (x, y, z)| { + input_utxos.push(x); + let additional_info = additional_info.join(y); + destinations.push(z); + (input_utxos, additional_info, destinations) + }, + ); + + Ok((input_utxos, additional_infos, destinations)) +} + async fn into_utxo_and_destination( rpc_client: &T, wallet: &RuntimeWallet, @@ -348,14 +374,17 @@ async fn fetch_order_additional_info( ) .await?; - let result = ask_token_info.join(give_token_info).join(TxAdditionalInfo::with_order_info( - order_id, - OrderAdditionalInfo { - initially_asked: order_info.initially_asked.into(), - initially_given: order_info.initially_given.into(), - ask_balance: order_info.ask_balance, - give_balance: order_info.give_balance, - }, - )); + let result = + ask_token_info + .join(give_token_info) + .join(TxAdditionalInfo::new().with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.into(), + initially_given: order_info.initially_given.into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + )); Ok(result) } diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 0a2b5b8800..f9ac698add 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -31,7 +31,11 @@ use chainstate::tx_verifier::{ self, error::ScriptError, input_check::signature_only_check::SignatureOnlyVerifiable, }; use futures::{never::Never, stream::FuturesOrdered, TryStreamExt}; -use helpers::{fetch_token_info, fetch_utxo, fetch_utxo_extra_info_for_hw_wallet, into_balances}; +use helpers::{ + fetch_input_infos, fetch_token_info, fetch_utxo, fetch_utxo_extra_info_for_hw_wallet, + into_balances, +}; +use itertools::Itertools as _; use node_comm::rpc_client::ColdWalletClient; use runtime_wallet::RuntimeWallet; use std::{ @@ -58,7 +62,16 @@ use common::{ chain::{ block::timestamp::BlockTimestamp, htlc::HtlcSecret, - signature::{inputsig::InputWitness, DestinationSigError, Transactable}, + signature::{ + inputsig::InputWitness, + sighash::{ + self, + input_commitment::{ + make_sighash_input_commitments_for_transaction_inputs, SighashInputCommitment, + }, + }, + DestinationSigError, Transactable, + }, tokens::{RPCTokenInfo, TokenId}, Block, ChainConfig, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, @@ -90,7 +103,7 @@ use wallet::{ }, destination_getters::{get_tx_output_destination, HtlcSpendingCondition}, signer::software_signer::SoftwareSignerProvider, - wallet::WalletPoolsFilter, + wallet::{SighashInputCommitmentCreationError, WalletPoolsFilter}, wallet_events::WalletEvents, WalletError, WalletResult, }; @@ -151,6 +164,8 @@ pub enum ControllerError { InvalidTxOutput(GenericCurrencyTransferToTxOutputConversionError), #[error("The specified token {0} is not a fungible token")] NotFungibleToken(TokenId), + #[error("Error creating sighash input commitment")] + SighashInputCommitmentCreationError(#[from] SighashInputCommitmentCreationError), } #[derive(Clone, Copy)] @@ -873,22 +888,37 @@ where &self, stx: &SignedTransaction, ) -> Result<(Balances, Vec), ControllerError> { - let tasks: FuturesOrdered<_> = - stx.inputs().iter().map(|input| self.fetch_opt_utxo(input)).collect(); - let input_utxos: Vec> = tasks.try_collect().await?; - let only_input_utxos: Vec<_> = input_utxos.clone().into_iter().flatten().collect(); + let (input_utxos, additional_infos, _destinations) = + fetch_input_infos(&self.rpc_client, &self.wallet, stx.inputs()).await?; + + let only_input_utxos = input_utxos.iter().flatten().cloned().collect_vec(); let fees = self.get_fees(&only_input_utxos, stx.outputs()).await?; - let inputs_utxos_refs: Vec<_> = input_utxos.iter().map(|out| out.as_ref()).collect(); - let destinations = inputs_utxos_refs + + // FIXME: is this correct? + let block_height_for_input_commitments = self.best_block().1.next_height(); + + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + stx.inputs(), + &sighash::input_commitment::TrivialUtxoProvider(&input_utxos), + &additional_infos, + &additional_infos, + &self.chain_config, + block_height_for_input_commitments, + )?; + + // FIXME: what's the difference between these destinations and the ones returned from fetch_input_infos? + // (the latter returns destinations for accounts as well) + let destinations = input_utxos .iter() .map(|txo| { - txo.map(|txo| { - get_tx_output_destination(txo, &|_| None, HtlcSpendingCondition::Skip) - .ok_or_else(|| { - WalletError::UnsupportedTransactionOutput(Box::new(txo.clone())) - }) - }) - .transpose() + txo.as_ref() + .map(|txo| { + get_tx_output_destination(txo, &|_| None, HtlcSpendingCondition::Skip) + .ok_or_else(|| { + WalletError::UnsupportedTransactionOutput(Box::new(txo.clone())) + }) + }) + .transpose() }) .collect::, WalletError>>() .map_err(ControllerError::WalletError)?; @@ -901,9 +931,13 @@ where (InputWitness::NoSignature(_), None) => SignatureStatus::FullySigned, (InputWitness::NoSignature(_), Some(_)) => SignatureStatus::NotSigned, (InputWitness::Standard(_), None) => SignatureStatus::InvalidSignature, - (InputWitness::Standard(_), Some(dest)) => { - self.verify_tx_signature(stx, &inputs_utxos_refs, input_num, &dest) - } + (InputWitness::Standard(_), Some(dest)) => self.verify_tx_signature( + stx, + &input_commitments, + input_num, + input_utxos[input_num].clone(), + &dest, + ), }) .collect(); Ok((fees, signature_statuses)) @@ -915,7 +949,17 @@ where ) -> Result> { let input_utxos: Vec<_> = ptx.input_utxos().iter().flatten().cloned().collect(); let fees = self.get_fees(&input_utxos, ptx.tx().outputs()).await?; - let inputs_utxos_refs: Vec<_> = ptx.input_utxos().iter().map(|out| out.as_ref()).collect(); + + // FIXME: is this correct? + let block_height_for_input_commitments = self.best_block().1.next_height(); + let input_commitments = make_sighash_input_commitments_for_transaction_inputs( + ptx.tx().inputs(), + &sighash::input_commitment::TrivialUtxoProvider(ptx.input_utxos()), + ptx.additional_info(), + ptx.additional_info(), + &self.chain_config, + block_height_for_input_commitments, + )?; let signature_statuses: Vec<_> = ptx .witnesses() .iter() @@ -925,9 +969,13 @@ where (Some(InputWitness::NoSignature(_)), None) => SignatureStatus::FullySigned, (Some(InputWitness::NoSignature(_)), Some(_)) => SignatureStatus::InvalidSignature, (Some(InputWitness::Standard(_)), None) => SignatureStatus::UnknownSignature, - (Some(InputWitness::Standard(_)), Some(dest)) => { - self.verify_tx_signature(&ptx, &inputs_utxos_refs, input_num, dest) - } + (Some(InputWitness::Standard(_)), Some(dest)) => self.verify_tx_signature( + &ptx, + &input_commitments, + input_num, + ptx.input_utxos()[input_num].clone(), + dest, + ), (None, _) => SignatureStatus::NotSigned, }) .collect(); @@ -977,27 +1025,23 @@ where fn verify_tx_signature( &self, tx: &(impl Transactable + SignatureOnlyVerifiable), - inputs_utxos_refs: &[Option<&TxOutput>], + input_commitments: &[SighashInputCommitment], input_num: usize, + input_utxo: Option, dest: &Destination, ) -> SignatureStatus { let valid = tx_verifier::input_check::signature_only_check::verify_tx_signature( &self.chain_config, dest, tx, - inputs_utxos_refs, + input_commitments, input_num, + input_utxo, ); match valid { Ok(_) => SignatureStatus::FullySigned, Err(e) => match e.error() { - tx_verifier::error::InputCheckErrorPayload::MissingUtxo(_) - | tx_verifier::error::InputCheckErrorPayload::UtxoView(_) - | tx_verifier::error::InputCheckErrorPayload::Translation(_) => { - SignatureStatus::InvalidSignature - } - tx_verifier::error::InputCheckErrorPayload::Verification( ScriptError::Signature( DestinationSigError::IncompleteClassicalMultisigSignature( @@ -1010,7 +1054,18 @@ where num_signatures: *num_signatures, }, - _ => SignatureStatus::InvalidSignature, + tx_verifier::error::InputCheckErrorPayload::MissingUtxo(_) + | tx_verifier::error::InputCheckErrorPayload::PoolNotFound(_) + | tx_verifier::error::InputCheckErrorPayload::OrderNotFound(_) + | tx_verifier::error::InputCheckErrorPayload::NonUtxoKernelInput(_) + | tx_verifier::error::InputCheckErrorPayload::UtxoView(_) + | tx_verifier::error::InputCheckErrorPayload::UtxoInfoProvider(_) + | tx_verifier::error::InputCheckErrorPayload::PoolInfoProvider(_) + | tx_verifier::error::InputCheckErrorPayload::OrderInfoProvider(_) + | tx_verifier::error::InputCheckErrorPayload::Translation(_) + | tx_verifier::error::InputCheckErrorPayload::Verification(_) => { + SignatureStatus::InvalidSignature + } }, } } diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 35361960f7..a429a9da65 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -57,7 +57,7 @@ use wallet::{ }; use wallet_types::{ partially_signed_transaction::{ - PartiallySignedTransaction, TokenAdditionalInfo, TxAdditionalInfo, + OrderAdditionalInfo, PartiallySignedTransaction, TokenAdditionalInfo, TxAdditionalInfo, }, signature_status::SignatureStatus, utxo_types::{UtxoState, UtxoType}, @@ -953,7 +953,7 @@ where account_index: U31, token_info: &UnconfirmedTokenInfo| { token_info.check_can_be_used()?; - let additional_info = TxAdditionalInfo::with_token_info( + let additional_info = TxAdditionalInfo::new().with_token_info( token_info.token_id(), TokenAdditionalInfo { num_decimals: token_info.num_decimals(), @@ -991,7 +991,7 @@ where account_index: U31, token_info: &UnconfirmedTokenInfo| { token_info.check_can_be_used()?; - let additional_info = TxAdditionalInfo::with_token_info( + let additional_info = TxAdditionalInfo::new().with_token_info( token_info.token_id(), TokenAdditionalInfo { num_decimals: token_info.num_decimals(), @@ -1052,6 +1052,10 @@ where pool_id: PoolId, output_address: Option, ) -> Result> { + // FIXME: the additional_info with the staker balance is created inside the corresponding method of the Wallet + // struct, which is inconsistent with other methods, where additional_info is created by SyncedController. + // It's better to unify this, so that additional_info is always crated in a uniform, way on the same level. + // (Note: same applies to decommission_stake_pool_request) let staker_balance = self .rpc_client .get_staker_balance(pool_id) @@ -1106,6 +1110,8 @@ where .map_err(ControllerError::WalletError) } + // FIXME: collect additional_info inside this method, just like other methods in this struct do, + // instead of accepting it from the outside. pub async fn create_htlc_tx( &mut self, output_value: OutputValue, @@ -1161,7 +1167,15 @@ where output_address: Option, token_infos: Vec, ) -> Result> { - let additional_info = self.additional_token_info(token_infos)?; + let additional_info = self.additional_token_info(token_infos)?.with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.clone().into(), + initially_given: order_info.initially_given.clone().into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + ); self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, @@ -1181,6 +1195,8 @@ where .await } + // FIXME: here and in other places - can we move collecting token_infos inside SyncedController? + // Same for other infos, like order_info. pub async fn fill_order( &mut self, order_id: OrderId, @@ -1189,7 +1205,15 @@ where output_address: Option, token_infos: Vec, ) -> Result> { - let additional_info = self.additional_token_info(token_infos)?; + let additional_info = self.additional_token_info(token_infos)?.with_order_info( + order_id, + OrderAdditionalInfo { + initially_asked: order_info.initially_asked.clone().into(), + initially_given: order_info.initially_given.clone().into(), + ask_balance: order_info.ask_balance, + give_balance: order_info.give_balance, + }, + ); self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, @@ -1215,6 +1239,9 @@ where order_id: OrderId, order_info: RpcOrderInfo, ) -> Result> { + // FIXME: though freeze_order technically doesn't need additional_info, this function should + // not depend on this fact (because e.g. we could change tx input commitments in the future again). + // Need to collect additional_info in a uniform way on every tx creation method call. self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, @@ -1239,6 +1266,7 @@ where token_infos .into_iter() .try_fold(TxAdditionalInfo::new(), |mut acc, token_info| { + // FIXME: what does "unconfirmed token info" mean? let token_info = self.unconfiremd_token_info(token_info)?; acc.add_token_info( diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index de13498148..89a449456a 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -1531,7 +1531,7 @@ where .ok_or(RpcError::InvalidCoinAmount)?; ( OutputValue::TokenV1(token_id, amount), - TxAdditionalInfo::with_token_info( + TxAdditionalInfo::new().with_token_info( token_id, TokenAdditionalInfo { num_decimals: token_info.token_number_of_decimals(), diff --git a/wasm-wrappers/src/lib.rs b/wasm-wrappers/src/lib.rs index f16215a96a..b6ee3e7b1c 100644 --- a/wasm-wrappers/src/lib.rs +++ b/wasm-wrappers/src/lib.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{num::NonZeroU8, str::FromStr}; +use std::{borrow::Cow, num::NonZeroU8, str::FromStr}; use bip39::Language; use gloo_utils::format::JsValueSerdeExt as _; @@ -38,7 +38,9 @@ use common::{ standard_signature::StandardInputSignature, InputWitness, }, - sighash::{sighashtype::SigHashType, signature_hash}, + sighash::{ + input_commitment::SighashInputCommitment, sighashtype::SigHashType, signature_hash, + }, DestinationSigError, }, stakelock::StakePoolData, @@ -1128,6 +1130,11 @@ pub fn encode_witness_no_signature() -> Vec { InputWitness::NoSignature(None).encode() } +// FIXME: this and other "encode_witness" functions should accept "input_commitments" instead of "inputs". +// Which means that in addition to each "encode_input" function we should also have the corresponding "encode_input_commitment" one??? +// Alternatively, encode_witness could accept the input utxos, like it does now, and also an "additional info" object, +// which would be used to construct the "input_commitments". But it's not clear how such an object can be represented here. +// /// Given a private key, inputs and an input number to sign, and the destination that owns that output (through the utxo), /// and a network type (mainnet, testnet, etc), this function returns a witness to be used in a signed transaction, as bytes. #[wasm_bindgen] @@ -1156,14 +1163,22 @@ pub fn encode_witness( input_utxos.push(utxo); } - let utxos = input_utxos.iter().map(Option::as_ref).collect::>(); + // FIXME + let input_commitments = input_utxos + .iter() + .map(|output| { + output.as_ref().map_or(SighashInputCommitment::None, |output| { + SighashInputCommitment::Utxo(Cow::Borrowed(output)) + }) + }) + .collect::>(); let witness = StandardInputSignature::produce_uniparty_signature_for_input( &private_key, sighashtype.into(), destination, &tx, - &utxos, + &input_commitments, input_num as usize, randomness::make_true_rng(), ) @@ -1203,7 +1218,15 @@ pub fn encode_witness_htlc_secret( input_utxos.push(utxo); } - let utxos = input_utxos.iter().map(Option::as_ref).collect::>(); + // FIXME + let input_commitments = input_utxos + .iter() + .map(|output| { + output.as_ref().map_or(SighashInputCommitment::None, |output| { + SighashInputCommitment::Utxo(Cow::Borrowed(output)) + }) + }) + .collect::>(); let secret = HtlcSecret::decode_all(&mut secret).map_err(|_| Error::InvalidHtlcSecret)?; @@ -1212,7 +1235,7 @@ pub fn encode_witness_htlc_secret( sighashtype.into(), destination, &tx, - &utxos, + &input_commitments, input_num as usize, secret, randomness::make_true_rng(), @@ -1281,9 +1304,17 @@ pub fn encode_witness_htlc_multisig( input_utxos.push(utxo); } - let utxos = input_utxos.iter().map(Option::as_ref).collect::>(); + // FIXME + let input_commitments = input_utxos + .iter() + .map(|output| { + output.as_ref().map_or(SighashInputCommitment::None, |output| { + SighashInputCommitment::Utxo(Cow::Borrowed(output)) + }) + }) + .collect::>(); let sighashtype = sighashtype.into(); - let sighash = signature_hash(sighashtype, &tx, &utxos, input_num as usize) + let sighash = signature_hash(sighashtype, &tx, &input_commitments, input_num as usize) .map_err(|_| Error::InvalidSignatureEncoding)?; let mut rng = randomness::make_true_rng(); @@ -1541,6 +1572,9 @@ pub fn encode_create_order_output( Ok(output.encode()) } +// FIXME freeze order +// FIXME use newer OrderAccountCommand + /// Given an amount to fill an order (which is described in terms of ask currency) and a destination /// for result outputs create an input that fills the order. #[wasm_bindgen]