From 15cd2bd1e8f8ee1105d1f3e0cbc1b86eff486a13 Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Wed, 3 Apr 2024 17:47:57 +0300 Subject: [PATCH 01/12] Base implementation --- .../scanner-lib/src/blockchain_state/mod.rs | 18 +- .../scanner-lib/src/sync/tests/simulation.rs | 10 +- api-server/web-server/src/api/json_helpers.rs | 12 + .../src/constraints_accumulator.rs | 32 +- chainstate/src/detail/ban_score.rs | 1 + .../src/detail/chainstateref/epoch_seal.rs | 3 +- chainstate/src/detail/chainstateref/mod.rs | 6 +- chainstate/src/detail/error_classification.rs | 5 +- chainstate/src/detail/mod.rs | 3 +- chainstate/src/detail/query.rs | 3 +- .../interface/chainstate_interface_impl.rs | 7 +- chainstate/src/rpc/types/output.rs | 36 +- .../test-framework/src/random_tx_maker.rs | 7 +- .../src/signature_destination_getter.rs | 1 + chainstate/test-framework/src/utils.rs | 13 +- .../src/tests/chainstate_storage_tests.rs | 8 +- .../test-suite/src/tests/fungible_tokens.rs | 8 +- chainstate/test-suite/src/tests/htlc.rs | 827 ++++++++++++++++++ chainstate/test-suite/src/tests/mod.rs | 1 + chainstate/test-suite/src/tests/nft_burn.rs | 3 +- .../test-suite/src/tests/nft_issuance.rs | 6 +- .../test-suite/src/tests/nft_transfer.rs | 5 +- chainstate/test-suite/src/tests/tx_fee.rs | 5 +- .../transaction_verifier/check_transaction.rs | 50 +- .../input_output_policy/mod.rs | 6 +- .../input_output_policy/purposes_check.rs | 15 +- .../tests/constraints_tests.rs | 6 +- .../tests/outputs_utils.rs | 40 +- .../tests/purpose_tests.rs | 9 +- .../src/transaction_verifier/mod.rs | 12 +- .../token_issuance_cache.rs | 3 +- common/src/chain/config/builder.rs | 48 +- common/src/chain/config/mod.rs | 3 +- common/src/chain/tokens/tokens_utils.rs | 6 +- common/src/chain/transaction/output/htlc.rs | 138 +++ common/src/chain/transaction/output/mod.rs | 32 +- .../chain/transaction/output/output_value.rs | 3 + ...uthorize_hashed_timelock_contract_spend.rs | 32 + .../transaction/signature/inputsig/htlc.rs | 85 ++ .../transaction/signature/inputsig/mod.rs | 4 + .../signature/inputsig/standard_signature.rs | 10 +- common/src/chain/transaction/signature/mod.rs | 47 +- .../chain/transaction/signature/tests/mod.rs | 1 + .../signature/tests/sign_and_mutate.rs | 1 + .../chain/transaction/signed_transaction.rs | 33 +- .../src/chain/upgrades/chainstate_upgrade.rs | 13 + common/src/chain/upgrades/mod.rs | 3 +- common/src/size_estimation/mod.rs | 1 + consensus/src/pos/block_sig.rs | 3 +- consensus/src/pos/mod.rs | 3 +- mempool/src/error/ban_score.rs | 1 + mempool/src/pool/tx_pool/store/mem_usage.rs | 5 +- mintscript/src/translate.rs | 5 +- utxo/src/cache.rs | 3 +- wallet/src/account/currency_grouper/mod.rs | 36 +- wallet/src/account/mod.rs | 119 +-- wallet/src/account/output_cache/mod.rs | 19 +- .../account/partially_signed_transaction.rs | 151 ++++ wallet/src/account/transaction_list/mod.rs | 1 + .../src/account/utxo_selector/output_group.rs | 4 +- wallet/src/send_request/mod.rs | 1 + wallet/src/wallet/mod.rs | 2 +- wallet/types/src/utxo_types.rs | 1 + wallet/wallet-controller/src/lib.rs | 60 +- 64 files changed, 1740 insertions(+), 295 deletions(-) create mode 100644 chainstate/test-suite/src/tests/htlc.rs create mode 100644 common/src/chain/transaction/output/htlc.rs create mode 100644 common/src/chain/transaction/signature/inputsig/authorize_hashed_timelock_contract_spend.rs create mode 100644 common/src/chain/transaction/signature/inputsig/htlc.rs create mode 100644 wallet/src/account/partially_signed_transaction.rs diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index 0e490bd129..770ebe6a4a 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -395,7 +395,8 @@ async fn update_tables_from_block_reward( | TxOutput::DataDeposit(_) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::IssueNft(_, _, _) => {} + | TxOutput::IssueNft(_, _, _) + | TxOutput::Htlc(_, _) => {} TxOutput::ProduceBlockFromStake(_, _) => { set_utxo( outpoint, @@ -569,7 +570,8 @@ async fn calculate_fees( | TxOutput::DataDeposit(_) | TxOutput::DelegateStaking(_, _) | TxOutput::CreateDelegationId(_, _) - | TxOutput::ProduceBlockFromStake(_, _) => None, + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::Htlc(_, _) => None, }) }) .collect(); @@ -601,7 +603,8 @@ async fn calculate_fees( | TxOutput::DelegateStaking(_, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::ProduceBlockFromStake(_, _) => None, + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::Htlc(_, _) => None, }, TxInput::Account(_) => None, TxInput::AccountCommand(_, cmd) => match cmd { @@ -726,7 +729,8 @@ async fn prefetch_pool_amounts( | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueNft(_, _, _) - | TxOutput::IssueFungibleToken(_), + | TxOutput::IssueFungibleToken(_) + | TxOutput::Htlc(_, _), ) => {} None => {} } @@ -1103,7 +1107,8 @@ async fn update_tables_from_transaction_inputs( | TxOutput::DataDeposit(_) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) => {} + | TxOutput::IssueFungibleToken(_) + | TxOutput::Htlc(_, _) => {} TxOutput::CreateStakePool(pool_id, _) | TxOutput::ProduceBlockFromStake(_, pool_id) => { let pool_data = db_tx @@ -1196,6 +1201,7 @@ async fn update_tables_from_transaction_inputs( ) .await; } + TxOutput::Htlc(_, _) => {} // TODO: support htlc TxOutput::LockThenTransfer(output_value, destination, _) | TxOutput::Transfer(output_value, destination) => { let address = Address::::new(&chain_config, destination) @@ -1610,6 +1616,7 @@ async fn update_tables_from_transaction_outputs( .expect("Unable to set locked utxo"); } } + TxOutput::Htlc(_, _) => {} // TODO: support htlc } } @@ -1809,5 +1816,6 @@ fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> { | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) => None, + TxOutput::Htlc(_, _) => None, // TODO: support htlc } } diff --git a/api-server/scanner-lib/src/sync/tests/simulation.rs b/api-server/scanner-lib/src/sync/tests/simulation.rs index 27b2d4394e..6e4b62ae1d 100644 --- a/api-server/scanner-lib/src/sync/tests/simulation.rs +++ b/api-server/scanner-lib/src/sync/tests/simulation.rs @@ -259,7 +259,8 @@ async fn simulation( | TxOutput::CreateDelegationId(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::IssueNft(_, _, _) => None, + | TxOutput::IssueNft(_, _, _) + | TxOutput::Htlc(_, _) => None, }); staking_pools.extend(new_pools); @@ -277,7 +278,8 @@ async fn simulation( | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::IssueNft(_, _, _) => None, + | TxOutput::IssueNft(_, _, _) + | TxOutput::Htlc(_, _) => None, }); delegations.extend(new_delegations); @@ -292,7 +294,8 @@ async fn simulation( | TxOutput::DataDeposit(_) | TxOutput::DelegateStaking(_, _) | TxOutput::CreateDelegationId(_, _) - | TxOutput::ProduceBlockFromStake(_, _) => None, + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::Htlc(_, _) => None, }); token_ids.extend(new_tokens); @@ -332,6 +335,7 @@ async fn simulation( .and_modify(|amount| *amount = (*amount + *to_stake).unwrap()) .or_insert(*to_stake); } + TxOutput::Htlc(_, _) => todo!(), | TxOutput::CreateDelegationId(_, _) | TxOutput::Transfer(_, _) | TxOutput::LockThenTransfer(_, _, _) diff --git a/api-server/web-server/src/api/json_helpers.rs b/api-server/web-server/src/api/json_helpers.rs index 7a9032018a..cf77b95a7a 100644 --- a/api-server/web-server/src/api/json_helpers.rs +++ b/api-server/web-server/src/api/json_helpers.rs @@ -185,6 +185,18 @@ pub fn txoutput_to_json( "destination": Address::new(chain_config, dest.clone()).expect("no error").as_str(), }) } + TxOutput::Htlc(value, htlc) => { + json!({ + "type": "Htlc", + "value": outputvalue_to_json(value, chain_config, token_decimals), + "htlc": { + "secret_hash": to_json_string(htlc.secret_hash.as_bytes()), + "spend_key": Address::new(chain_config, htlc.spend_key.clone()).expect("no error").as_str(), + "refund_timelock": htlc.refund_timelock, + "refund_key": Address::new(chain_config, htlc.refund_key.clone()).expect("no error").as_str(), + }, + }) + } } } diff --git a/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs b/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs index 08d7c89c56..484d429023 100644 --- a/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs +++ b/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs @@ -133,7 +133,9 @@ impl ConstrainedValueAccumulator { StakerBalanceGetterFn: Fn(PoolId) -> Result, Error>, { match input_utxo { - TxOutput::Transfer(value, _) | TxOutput::LockThenTransfer(value, _, _) => { + TxOutput::Transfer(value, _) + | TxOutput::LockThenTransfer(value, _, _) + | TxOutput::Htlc(value, _) => { match value { OutputValue::Coin(amount) => insert_or_increase( &mut self.unconstrained_value, @@ -269,19 +271,21 @@ impl ConstrainedValueAccumulator { for output in outputs { match output { - TxOutput::Transfer(value, _) | TxOutput::Burn(value) => match value { - OutputValue::Coin(amount) => insert_or_increase( - &mut accumulator.unconstrained_value, - CoinOrTokenId::Coin, - *amount, - )?, - OutputValue::TokenV0(_) => { /* ignore */ } - OutputValue::TokenV1(token_id, amount) => insert_or_increase( - &mut accumulator.unconstrained_value, - CoinOrTokenId::TokenId(*token_id), - *amount, - )?, - }, + TxOutput::Transfer(value, _) | TxOutput::Burn(value) | TxOutput::Htlc(value, _) => { + match value { + OutputValue::Coin(amount) => insert_or_increase( + &mut accumulator.unconstrained_value, + CoinOrTokenId::Coin, + *amount, + )?, + OutputValue::TokenV0(_) => { /* ignore */ } + OutputValue::TokenV1(token_id, amount) => insert_or_increase( + &mut accumulator.unconstrained_value, + CoinOrTokenId::TokenId(*token_id), + *amount, + )?, + } + } TxOutput::DelegateStaking(coins, _) => insert_or_increase( &mut accumulator.unconstrained_value, CoinOrTokenId::Coin, diff --git a/chainstate/src/detail/ban_score.rs b/chainstate/src/detail/ban_score.rs index 1e476230a1..f0699d3253 100644 --- a/chainstate/src/detail/ban_score.rs +++ b/chainstate/src/detail/ban_score.rs @@ -351,6 +351,7 @@ impl BanScore for CheckTransactionError { CheckTransactionError::DataDepositMaxSizeExceeded(_, _, _) => 100, CheckTransactionError::TxSizeTooLarge(_, _, _) => 100, CheckTransactionError::DeprecatedTokenOperationVersion(_, _) => 100, + CheckTransactionError::HtlcsAreNotActivated => 100, } } } diff --git a/chainstate/src/detail/chainstateref/epoch_seal.rs b/chainstate/src/detail/chainstateref/epoch_seal.rs index 2ef0e254d4..c1d25c56c8 100644 --- a/chainstate/src/detail/chainstateref/epoch_seal.rs +++ b/chainstate/src/detail/chainstateref/epoch_seal.rs @@ -174,7 +174,8 @@ where | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => { + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => { return Err(EpochSealError::SpendStakeError( SpendStakeError::InvalidBlockRewardOutputType, )); diff --git a/chainstate/src/detail/chainstateref/mod.rs b/chainstate/src/detail/chainstateref/mod.rs index d0c8f26284..f0fb4182d4 100644 --- a/chainstate/src/detail/chainstateref/mod.rs +++ b/chainstate/src/detail/chainstateref/mod.rs @@ -697,7 +697,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => Err( + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => Err( CheckBlockError::InvalidBlockRewardOutputType(block.get_id()), ), }, @@ -713,7 +714,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => Err( + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => Err( CheckBlockError::InvalidBlockRewardOutputType(block.get_id()), ), } diff --git a/chainstate/src/detail/error_classification.rs b/chainstate/src/detail/error_classification.rs index b10c90c659..cdcb40b3a2 100644 --- a/chainstate/src/detail/error_classification.rs +++ b/chainstate/src/detail/error_classification.rs @@ -786,9 +786,8 @@ impl BlockProcessingErrorClassification for CheckTransactionError { | CheckTransactionError::NoSignatureDataNotAllowed(_) | CheckTransactionError::DataDepositMaxSizeExceeded(_, _, _) | CheckTransactionError::TxSizeTooLarge(_, _, _) - | CheckTransactionError::DeprecatedTokenOperationVersion(_, _) => { - BlockProcessingErrorClass::BadBlock - } + | CheckTransactionError::DeprecatedTokenOperationVersion(_, _) + | CheckTransactionError::HtlcsAreNotActivated => BlockProcessingErrorClass::BadBlock, CheckTransactionError::PropertyQueryError(err) => err.classify(), CheckTransactionError::TokensError(err) => err.classify(), diff --git a/chainstate/src/detail/mod.rs b/chainstate/src/detail/mod.rs index 2bf4dae62f..75dad6467f 100644 --- a/chainstate/src/detail/mod.rs +++ b/chainstate/src/detail/mod.rs @@ -723,7 +723,8 @@ impl Chainstate | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => { /* do nothing */ } + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => { /* do nothing */ } | TxOutput::CreateStakePool(pool_id, data) => { let _ = db .create_pool(*pool_id, data.as_ref().clone().into()) diff --git a/chainstate/src/detail/query.rs b/chainstate/src/detail/query.rs index 51e33920f7..3338a15c20 100644 --- a/chainstate/src/detail/query.rs +++ b/chainstate/src/detail/query.rs @@ -348,7 +348,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, TxOutput::IssueNft(_, issuance, _) => match issuance.as_ref() { NftIssuance::V0(nft) => { Some(RPCTokenInfo::new_nonfungible(RPCNonFungibleTokenInfo::new( diff --git a/chainstate/src/interface/chainstate_interface_impl.rs b/chainstate/src/interface/chainstate_interface_impl.rs index b6c9bae4dc..f618300572 100644 --- a/chainstate/src/interface/chainstate_interface_impl.rs +++ b/chainstate/src/interface/chainstate_interface_impl.rs @@ -776,9 +776,10 @@ fn get_output_coin_amount( output: &TxOutput, ) -> Result, ChainstateError> { let amount = match output { - TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) | TxOutput::Burn(v) => { - v.coin_amount() - } + TxOutput::Transfer(v, _) + | TxOutput::LockThenTransfer(v, _, _) + | TxOutput::Burn(v) + | TxOutput::Htlc(v, _) => v.coin_amount(), TxOutput::CreateStakePool(_, data) => Some(data.pledge()), TxOutput::ProduceBlockFromStake(_, pool_id) => { let pledge_amount = pos_accounting_view diff --git a/chainstate/src/rpc/types/output.rs b/chainstate/src/rpc/types/output.rs index 94fd67d0d4..fa0d7f0bed 100644 --- a/chainstate/src/rpc/types/output.rs +++ b/chainstate/src/rpc/types/output.rs @@ -16,8 +16,9 @@ use common::{ address::{AddressError, RpcAddress}, chain::{ - output_value::OutputValue, stakelock::StakePoolData, timelock::OutputTimeLock, - tokens::TokenId, ChainConfig, DelegationId, Destination, PoolId, TxOutput, + htlc::HashedTimelockContract, output_value::OutputValue, stakelock::StakePoolData, + timelock::OutputTimeLock, tokens::TokenId, ChainConfig, DelegationId, Destination, PoolId, + TxOutput, }, primitives::amount::RpcAmountOut, }; @@ -81,6 +82,29 @@ impl RpcStakePoolData { } } +#[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] +pub struct RpcHashedTimelockContract { + secret_hash: RpcHexString, + spend_key: RpcAddress, + refund_timelock: OutputTimeLock, + refund_key: RpcAddress, +} + +impl RpcHashedTimelockContract { + fn new( + chain_config: &ChainConfig, + htlc: &HashedTimelockContract, + ) -> Result { + let result = Self { + secret_hash: RpcHexString::from_bytes(htlc.secret_hash.as_bytes().to_owned()), + spend_key: RpcAddress::new(chain_config, htlc.spend_key.clone())?, + refund_timelock: htlc.refund_timelock, + refund_key: RpcAddress::new(chain_config, htlc.refund_key.clone())?, + }; + Ok(result) + } +} + #[derive(Debug, Clone, serde::Serialize, rpc_description::HasValueHint)] #[serde(tag = "type", content = "content")] pub enum RpcTxOutput { @@ -123,6 +147,10 @@ pub enum RpcTxOutput { DataDeposit { data: RpcHexString, }, + Htlc { + value: RpcOutputValue, + htlc: RpcHashedTimelockContract, + }, } impl RpcTxOutput { @@ -139,6 +167,10 @@ impl RpcTxOutput { timelock, } } + TxOutput::Htlc(value, htlc) => RpcTxOutput::Htlc { + value: RpcOutputValue::new(chain_config, value)?, + htlc: RpcHashedTimelockContract::new(chain_config, &htlc)?, + }, TxOutput::Burn(value) => RpcTxOutput::Burn { value: RpcOutputValue::new(chain_config, value)?, }, diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index 742da7c3ad..b299faed04 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -240,7 +240,8 @@ impl<'a> RandomTxMaker<'a> { | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => { /* do nothing */ } + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => { /* do nothing */ } TxOutput::CreateStakePool(pool_id, _) => { let (staker_sk, vrf_sk) = new_staking_pools.get(pool_id).unwrap(); staking_pools_observer.on_pool_created( @@ -666,6 +667,7 @@ impl<'a> RandomTxMaker<'a> { | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::DataDeposit(_) => unreachable!(), + TxOutput::Htlc(_, _) => unimplemented!(), }; result_inputs.extend(new_inputs); @@ -1005,7 +1007,8 @@ impl<'a> RandomTxMaker<'a> { | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::DataDeposit(_) => Some(output), + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => Some(output), TxOutput::CreateStakePool(dummy_pool_id, pool_data) => { let pool_id = make_pool_id(inputs[0].utxo_outpoint().unwrap()); let (vrf_sk, vrf_pk) = VRFPrivateKey::new_from_rng(rng, VRFKeyKind::Schnorrkel); diff --git a/chainstate/test-framework/src/signature_destination_getter.rs b/chainstate/test-framework/src/signature_destination_getter.rs index 71d10ee4c0..4ae6313853 100644 --- a/chainstate/test-framework/src/signature_destination_getter.rs +++ b/chainstate/test-framework/src/signature_destination_getter.rs @@ -116,6 +116,7 @@ impl<'a> SignatureDestinationGetter<'a> { // but this is just a double-check. Err(SignatureDestinationGetterError::SigVerifyOfNotSpendableOutput) } + TxOutput::Htlc(_, _) => todo!(), } } TxInput::Account(outpoint) => match outpoint.account() { diff --git a/chainstate/test-framework/src/utils.rs b/chainstate/test-framework/src/utils.rs index d64f7dde0c..bbc85f952d 100644 --- a/chainstate/test-framework/src/utils.rs +++ b/chainstate/test-framework/src/utils.rs @@ -59,9 +59,10 @@ pub fn anyonecanspend_address() -> Destination { pub fn get_output_value(output: &TxOutput) -> Option { match output { - TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) | TxOutput::Burn(v) => { - Some(v.clone()) - } + TxOutput::Transfer(v, _) + | TxOutput::LockThenTransfer(v, _, _) + | TxOutput::Burn(v) + | TxOutput::Htlc(v, _) => Some(v.clone()), TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::CreateDelegationId(_, _) @@ -127,7 +128,8 @@ pub fn create_utxo_data( | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, } } @@ -383,7 +385,8 @@ pub fn find_create_pool_tx_in_genesis(genesis: &Genesis, pool_id: &PoolId) -> Op | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => false, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => false, TxOutput::CreateStakePool(genesis_pool_id, _) => genesis_pool_id == pool_id, }); diff --git a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs index 9344dfc89c..72101923cb 100644 --- a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs +++ b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs @@ -24,8 +24,9 @@ use common::{ chain::{ output_value::OutputValue, tokens::{make_token_id, NftIssuance, TokenAuxiliaryData, TokenIssuanceV0}, - ChainstateUpgrade, Destination, NetUpgrades, OutPointSourceId, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, Transaction, TxInput, TxOutput, UtxoOutPoint, + ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, OutPointSourceId, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, Transaction, TxInput, + TxOutput, UtxoOutPoint, }, primitives::{Amount, Id, Idable}, }; @@ -118,6 +119,7 @@ fn store_fungible_token_v0(#[case] seed: Seed) { TokenIssuanceVersion::V0, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), @@ -196,6 +198,7 @@ fn store_nft_v0(#[case] seed: Seed) { TokenIssuanceVersion::V0, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), @@ -505,6 +508,7 @@ fn store_aux_data_from_issue_nft(#[case] seed: Seed) { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/fungible_tokens.rs b/chainstate/test-suite/src/tests/fungible_tokens.rs index 9b88f843a0..7e0ea18085 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens.rs @@ -28,8 +28,8 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{make_token_id, TokenData, TokenId}, - ChainstateUpgrade, Destination, OutPointSourceId, TokenIssuanceVersion, TokensFeeVersion, - TxInput, TxOutput, + ChainstateUpgrade, Destination, HtlcActivated, OutPointSourceId, TokenIssuanceVersion, + TokensFeeVersion, TxInput, TxOutput, }, primitives::{Amount, Idable}, }; @@ -56,6 +56,7 @@ fn make_test_framework_with_v0(rng: &mut (impl Rng + CryptoRng)) -> TestFramewor TokenIssuanceVersion::V0, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), @@ -959,6 +960,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), @@ -1021,6 +1023,7 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { TokenIssuanceVersion::V0, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), ), ( @@ -1029,6 +1032,7 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), ), ]) diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs new file mode 100644 index 0000000000..e6a033fe5b --- /dev/null +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -0,0 +1,827 @@ +// Copyright (c) 2024 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 super::*; +use chainstate::{BlockError, ChainstateError, ConnectTransactionError}; +use chainstate_test_framework::{TestFramework, TransactionBuilder}; +use common::{ + address::pubkeyhash::PublicKeyHash, + chain::{ + classic_multisig::ClassicMultisigChallenge, + htlc::{self, HashedTimelockContract, HtlcSecret, HtlcSecretHash}, + output_value::OutputValue, + signature::{ + inputsig::{ + authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, + classical_multisig::authorize_classical_multisig::AuthorizedClassicalMultisigSpend, + htlc::{ + produce_classical_multisig_signature_for_htlc_input, + produce_uniparty_signature_for_htlc_input, + }, + standard_signature::StandardInputSignature, + }, + sighash::{sighashtype::SigHashType, signature_hash}, + DestinationSigError, + }, + signed_transaction::SignedTransaction, + timelock::OutputTimeLock, + tokens::{make_token_id, TokenData, TokenIssuance, TokenTransfer}, + AccountCommand, AccountNonce, ChainConfig, ChainstateUpgrade, Destination, HtlcActivated, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + UtxoOutPoint, + }, + primitives::{Amount, Idable}, +}; +use crypto::key::{KeyKind, PrivateKey, PublicKey}; +use randomness::CryptoRng; +use serialization::Encode; +use test_utils::nft_utils::{random_token_issuance, random_token_issuance_v1}; + +struct TestFixture { + alice_sk: PrivateKey, + bob_sk: PrivateKey, + secret: HtlcSecret, +} + +impl TestFixture { + fn new(rng: &mut (impl Rng + CryptoRng)) -> Self { + let secret = HtlcSecret::new_from_rng(rng); + + let (alice_sk, _) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + let (bob_sk, _) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + + Self { + alice_sk, + bob_sk, + secret, + } + } + + fn create_htlc( + &self, + chain_config: &ChainConfig, + ) -> (HashedTimelockContract, ClassicMultisigChallenge) { + let alice_pk = PublicKey::from_private_key(&self.alice_sk); + let bob_pk = PublicKey::from_private_key(&self.bob_sk); + + let refund_challenge = ClassicMultisigChallenge::new( + chain_config, + utils::const_nz_u8!(2), + vec![alice_pk.clone(), bob_pk.clone()], + ) + .unwrap(); + let destination_multisig: PublicKeyHash = (&refund_challenge).into(); + + let htlc = HashedTimelockContract { + secret_hash: self.secret.hashed(htlc::HashType::HASH160), + spend_key: Destination::PublicKeyHash((&bob_pk).into()), + refund_timelock: OutputTimeLock::ForSeconds(200), + refund_key: Destination::ClassicMultisig(destination_multisig), + }; + (htlc, refund_challenge) + } +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn spend_htlc_with_secret(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = test_utils::random::make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + let chain_config = tf.chainstate.get_chain_config().clone(); + + let test_fixture = TestFixture::new(&mut rng); + + let (htlc, _) = test_fixture.create_htlc(&chain_config); + let tx_1 = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(chain_config.genesis_block_id().into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Htlc( + OutputValue::Coin(Amount::from_atoms(100)), + htlc, + )) + .build(); + let tx_1_id = tx_1.transaction().get_id(); + + tf.make_block_builder() + .add_transaction(tx_1.clone()) + .build_and_process(&mut rng) + .unwrap(); + + // Alice can't spend the output even though she knows the secret + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let input_sign = produce_uniparty_signature_for_htlc_input( + &test_fixture.alice_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + Destination::PublicKeyHash( + (&PublicKey::from_private_key(&test_fixture.alice_sk)).into(), + ), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + test_fixture.secret.clone(), + &mut rng, + ) + .unwrap(); + + let result = tf + .make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng); + // FIXME: is it still valid? + //assert_eq!( + // result.unwrap_err(), + // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + // ConnectTransactionError::SignatureVerificationFailed( + // DestinationSigError::PublicKeyToAddressMismatch + // ) + // )) + //); + } + + // Bob can't spend the output without the secret + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let input_sign = StandardInputSignature::produce_uniparty_signature_for_input( + &test_fixture.bob_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + Destination::PublicKeyHash( + (&PublicKey::from_private_key(&test_fixture.bob_sk)).into(), + ), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + &mut rng, + ) + .unwrap(); + + let result = tf + .make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng); + // FIXME: is it still valid? + //assert_eq!( + // result.unwrap_err(), + // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + // ConnectTransactionError::SignatureVerificationFailed( + // DestinationSigError::InvalidSignatureEncoding + // ) + // )) + //); + } + + // Bob can't spend the output with random secret + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let random_secret = HtlcSecret::new_from_rng(&mut rng); + + let input_sign = produce_uniparty_signature_for_htlc_input( + &test_fixture.bob_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + Destination::PublicKeyHash( + (&PublicKey::from_private_key(&test_fixture.bob_sk)).into(), + ), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + random_secret, + &mut rng, + ) + .unwrap(); + + let result = tf + .make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng); + todo!(); + //assert_eq!( + // result.unwrap_err(), + // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + // ConnectTransactionError::SecretHashMismatch(UtxoOutPoint::new( + // tx_1_id.into(), + // 0 + // )) + // )) + //); + } + + // Success case + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let input_sign = produce_uniparty_signature_for_htlc_input( + &test_fixture.bob_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + Destination::PublicKeyHash((&PublicKey::from_private_key(&test_fixture.bob_sk)).into()), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + test_fixture.secret, + &mut rng, + ) + .unwrap(); + + tf.make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn refund_htlc(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = test_utils::random::make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + let chain_config = tf.chainstate.get_chain_config().clone(); + + let test_fixture = TestFixture::new(&mut rng); + + let (htlc, refund_challenge) = test_fixture.create_htlc(&chain_config); + let tx_1 = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(chain_config.genesis_block_id().into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Htlc( + OutputValue::Coin(Amount::from_atoms(100)), + htlc, + )) + .build(); + let tx_1_id = tx_1.transaction().get_id(); + + tf.make_block_builder() + .add_transaction(tx_1.clone()) + .build_and_process(&mut rng) + .unwrap(); + + // Refund can't be spent until timelock is passed + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let authorization = { + let mut authorization = + AuthorizedClassicalMultisigSpend::new_empty(refund_challenge.clone()); + + let sighash = signature_hash( + SigHashType::try_from(SigHashType::ALL).unwrap(), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + ) + .unwrap(); + let sighash = sighash.encode(); + + let signature = test_fixture.alice_sk.sign_message(&sighash, &mut rng).unwrap(); + authorization.add_signature(0, signature); + let signature = test_fixture.bob_sk.sign_message(&sighash, &mut rng).unwrap(); + authorization.add_signature(1, signature); + + authorization + }; + + let input_sign = produce_classical_multisig_signature_for_htlc_input( + &chain_config, + &authorization, + SigHashType::try_from(SigHashType::ALL).unwrap(), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + ) + .unwrap(); + + let result = tf + .make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng); + // FIXME: fix + //assert_eq!( + // result.unwrap_err(), + // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + // ConnectTransactionError::InputCheck(()) + // )) + //); + } + + tf.progress_time_seconds_since_epoch(200); + // Produce empty blocks to move MTP forward + tf.make_block_builder().build_and_process(&mut rng).unwrap(); + tf.make_block_builder().build_and_process(&mut rng).unwrap(); + + // Alice can't spend output alone + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let authorization = { + let mut authorization = + AuthorizedClassicalMultisigSpend::new_empty(refund_challenge.clone()); + + let sighash = signature_hash( + SigHashType::try_from(SigHashType::ALL).unwrap(), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + ) + .unwrap(); + let sighash = sighash.encode(); + + let signature = test_fixture.alice_sk.sign_message(&sighash, &mut rng).unwrap(); + authorization.add_signature(0, signature); + + AuthorizedHashedTimelockContractSpend::Multisig(authorization.encode()) + }; + + let input_sign = StandardInputSignature::new( + SigHashType::try_from(SigHashType::ALL).unwrap(), + authorization.encode(), + ); + + let result = tf + .make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng); + // FIXME: is it still valid? + //assert_eq!( + // result.unwrap_err(), + // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + // ConnectTransactionError::SignatureVerificationFailed( + // DestinationSigError::IncompleteClassicalMultisigSignature(2, 1) + // ) + // )) + //); + } + + // Bob can't spend output alone + { + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let authorization = { + let mut authorization = + AuthorizedClassicalMultisigSpend::new_empty(refund_challenge.clone()); + + let sighash = signature_hash( + SigHashType::try_from(SigHashType::ALL).unwrap(), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + ) + .unwrap(); + let sighash = sighash.encode(); + + let signature = test_fixture.bob_sk.sign_message(&sighash, &mut rng).unwrap(); + authorization.add_signature(1, signature); + + AuthorizedHashedTimelockContractSpend::Multisig(authorization.encode()) + }; + + let input_sign = StandardInputSignature::new( + SigHashType::try_from(SigHashType::ALL).unwrap(), + authorization.encode(), + ); + + let result = tf + .make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng); + // FIXME: is it still valid? + //assert_eq!( + // result.unwrap_err(), + // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + // ConnectTransactionError::SignatureVerificationFailed( + // DestinationSigError::IncompleteClassicalMultisigSignature(2, 1) + // ) + // )) + //); + } + + // Success case + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(tx_1_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100)), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let authorization = { + let mut authorization = AuthorizedClassicalMultisigSpend::new_empty(refund_challenge); + + let sighash = signature_hash( + SigHashType::try_from(SigHashType::ALL).unwrap(), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + ) + .unwrap(); + let sighash = sighash.encode(); + + let signature = test_fixture.alice_sk.sign_message(&sighash, &mut rng).unwrap(); + authorization.add_signature(0, signature); + let signature = test_fixture.bob_sk.sign_message(&sighash, &mut rng).unwrap(); + authorization.add_signature(1, signature); + + authorization + }; + + let input_sign = produce_classical_multisig_signature_for_htlc_input( + &chain_config, + &authorization, + SigHashType::try_from(SigHashType::ALL).unwrap(), + &tx, + &[Some(&tx_1.transaction().outputs()[0])], + 0, + ) + .unwrap(); + + tf.make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn fork_activation(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = test_utils::random::make_seedable_rng(seed); + // activate htlc at height 2 + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgrade::new( + TokenIssuanceVersion::V0, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + HtlcActivated::No, + ), + ), + ( + BlockHeight::new(2), + ChainstateUpgrade::new( + TokenIssuanceVersion::V0, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + HtlcActivated::Yes, + ), + ), + ]) + .unwrap(), + ) + .genesis_unittest(Destination::AnyoneCanSpend) + .build(), + ) + .build(); + let chain_config = tf.chainstate.get_chain_config().clone(); + + let htlc = HashedTimelockContract { + secret_hash: HtlcSecretHash::zero(), + spend_key: Destination::AnyoneCanSpend, + refund_timelock: OutputTimeLock::ForSeconds(200), + refund_key: Destination::AnyoneCanSpend, + }; + + // Try to produce htlc output before fork activation, check an error + let result = tf + .make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_utxo(chain_config.genesis_block_id().into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Htlc( + OutputValue::Coin(Amount::from_atoms(100)), + htlc.clone(), + )) + .build(), + ) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::HtlcsAreNotActivated + ) + ) + )) + ); + + // produce an empty block + tf.make_block_builder().build_and_process(&mut rng).unwrap(); + + // now it should be possible to use htlc output + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_utxo(chain_config.genesis_block_id().into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Htlc( + OutputValue::Coin(Amount::from_atoms(100)), + htlc, + )) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn spend_tokens(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = test_utils::random::make_seedable_rng(seed); + + // deprecate tokens v0 at height 2 + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgrade::new( + TokenIssuanceVersion::V0, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + HtlcActivated::Yes, + ), + ), + ( + BlockHeight::new(2), + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + HtlcActivated::Yes, + ), + ), + ]) + .unwrap(), + ) + .genesis_unittest(Destination::AnyoneCanSpend) + .build(), + ) + .build(); + let chain_config = tf.chainstate.get_chain_config().clone(); + let token_issuance_fee = tf.chainstate.get_chain_config().fungible_token_issuance_fee(); + let token_mint_fee = + tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::new(0)); + + let test_fixture = TestFixture::new(&mut rng); + let (htlc, _) = test_fixture.create_htlc(&chain_config); + + // issue token v0 + let token_v0_issuance_tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(chain_config.genesis_block_id().into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + random_token_issuance(&chain_config, &mut rng).into(), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Burn(OutputValue::Coin(token_issuance_fee))) + .add_output(TxOutput::Transfer( + OutputValue::Coin((token_issuance_fee + token_mint_fee).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let token_v0_id = make_token_id(token_v0_issuance_tx.inputs()).unwrap(); + let token_v0_issuance_tx_id = token_v0_issuance_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(token_v0_issuance_tx) + .build_and_process(&mut rng) + .unwrap(); + + // try to produce htlc output with tokens v0, check an error + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(token_v0_issuance_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Htlc( + OutputValue::TokenV0(Box::new(TokenData::TokenTransfer(TokenTransfer { + token_id: token_v0_id, + amount: Amount::from_atoms(1), + }))), + htlc.clone(), + )) + .build(); + let tx_id = tx.transaction().get_id(); + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::DeprecatedTokenOperationVersion( + TokenIssuanceVersion::V0, + tx_id, + ) + ) + ) + )) + ); + + // issue a token v1 + let token_v1_issuance_tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(token_v0_issuance_tx_id.into(), 2), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::IssueFungibleToken(Box::new(TokenIssuance::V1( + random_token_issuance_v1(&chain_config, Destination::AnyoneCanSpend, &mut rng), + )))) + .add_output(TxOutput::Transfer( + OutputValue::Coin(token_mint_fee), + Destination::AnyoneCanSpend, + )) + .build(); + let token_v1_id = make_token_id(token_v1_issuance_tx.inputs()).unwrap(); + let token_v1_issuance_tx_id = token_v1_issuance_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(token_v1_issuance_tx) + .build_and_process(&mut rng) + .unwrap(); + + // mint v1 tokens and lock them with htlc output + let amount_to_mint = Amount::from_atoms(1); + let mint_token_v1_tx = TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::MintTokens(token_v1_id, amount_to_mint), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(token_v1_issuance_tx_id.into(), 1), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Htlc( + OutputValue::TokenV1(token_v1_id, amount_to_mint), + htlc, + )) + .build(); + let mint_token_v1_tx_id = mint_token_v1_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(mint_token_v1_tx.clone()) + .build_and_process(&mut rng) + .unwrap(); + + // Spend tokens from htlc + let tx = TransactionBuilder::new() + .add_input( + TxInput::from_utxo(mint_token_v1_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::TokenV1(token_v1_id, amount_to_mint), + Destination::AnyoneCanSpend, + )) + .build() + .take_transaction(); + + let input_sign = produce_uniparty_signature_for_htlc_input( + &test_fixture.bob_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + Destination::PublicKeyHash((&PublicKey::from_private_key(&test_fixture.bob_sk)).into()), + &tx, + &[Some(&mint_token_v1_tx.transaction().outputs()[0])], + 0, + test_fixture.secret, + &mut rng, + ) + .unwrap(); + + tf.make_block_builder() + .add_transaction( + SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} diff --git a/chainstate/test-suite/src/tests/mod.rs b/chainstate/test-suite/src/tests/mod.rs index d64c2a4464..c3051369bd 100644 --- a/chainstate/test-suite/src/tests/mod.rs +++ b/chainstate/test-suite/src/tests/mod.rs @@ -42,6 +42,7 @@ mod fungible_tokens_v1; mod get_stake_pool_balances_at_heights; mod history_iteration; mod homomorphism; +mod htlc; mod initialization; mod mempool_output_timelock; mod nft_burn; diff --git a/chainstate/test-suite/src/tests/nft_burn.rs b/chainstate/test-suite/src/tests/nft_burn.rs index 18142bb8f3..57e7135c31 100644 --- a/chainstate/test-suite/src/tests/nft_burn.rs +++ b/chainstate/test-suite/src/tests/nft_burn.rs @@ -17,7 +17,7 @@ use chainstate::{BlockError, ChainstateError, ConnectTransactionError}; use chainstate_test_framework::{TestFramework, TransactionBuilder}; use common::chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::make_token_id, - ChainstateUpgrade, Destination, RewardDistributionVersion, TokenIssuanceVersion, + ChainstateUpgrade, Destination, HtlcActivated, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }; use common::chain::{OutPointSourceId, UtxoOutPoint}; @@ -215,6 +215,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/nft_issuance.rs b/chainstate/test-suite/src/tests/nft_issuance.rs index b911f4ca0d..3810f9f6c7 100644 --- a/chainstate/test-suite/src/tests/nft_issuance.rs +++ b/chainstate/test-suite/src/tests/nft_issuance.rs @@ -22,8 +22,8 @@ use common::chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{is_rfc3986_valid_symbol, make_token_id, Metadata, NftIssuance, NftIssuanceV0}, - Block, ChainstateUpgrade, Destination, OutPointSourceId, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + Block, ChainstateUpgrade, Destination, HtlcActivated, OutPointSourceId, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }; use common::primitives::{BlockHeight, Idable}; use randomness::{CryptoRng, Rng}; @@ -1651,6 +1651,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), @@ -1713,6 +1714,7 @@ fn only_ascii_alphanumeric_after_v1(#[case] seed: Seed) { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/nft_transfer.rs b/chainstate/test-suite/src/tests/nft_transfer.rs index 4dd126bf0d..e0b1b4e9b7 100644 --- a/chainstate/test-suite/src/tests/nft_transfer.rs +++ b/chainstate/test-suite/src/tests/nft_transfer.rs @@ -21,8 +21,8 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{make_token_id, NftIssuance, TokenId}, - ChainstateUpgrade, Destination, NetUpgrades, OutPointSourceId, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, OutPointSourceId, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }, primitives::{Amount, BlockHeight, CoinOrTokenId}, }; @@ -369,6 +369,7 @@ fn ensure_nft_cannot_be_printed_from_tokens_op(#[case] seed: Seed) { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/test-suite/src/tests/tx_fee.rs b/chainstate/test-suite/src/tests/tx_fee.rs index 612113c9a3..5fbdfd2604 100644 --- a/chainstate/test-suite/src/tests/tx_fee.rs +++ b/chainstate/test-suite/src/tests/tx_fee.rs @@ -33,8 +33,8 @@ use common::{ make_token_id, IsTokenFreezable, TokenIssuance, TokenIssuanceV0, TokenIssuanceV1, TokenTotalSupply, }, - ChainConfig, ChainstateUpgrade, Destination, NetUpgrades, TokenIssuanceVersion, - TokensFeeVersion, TxInput, TxOutput, UtxoOutPoint, + ChainConfig, ChainstateUpgrade, Destination, HtlcActivated, NetUpgrades, + TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Fee, Idable}, }; @@ -576,6 +576,7 @@ fn issue_fungible_token_v0(#[case] seed: Seed) { TokenIssuanceVersion::V0, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .unwrap(), diff --git a/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs b/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs index 592af3fa30..bd5e53e0ed 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs @@ -21,8 +21,8 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{get_tokens_issuance_count, NftIssuance}, - ChainConfig, SignedTransaction, TokenIssuanceVersion, Transaction, TransactionSize, - TxOutput, + ChainConfig, HtlcActivated, SignedTransaction, TokenIssuanceVersion, Transaction, + TransactionSize, TxOutput, }, primitives::{BlockHeight, Id, Idable}, }; @@ -56,6 +56,8 @@ pub enum CheckTransactionError { TxSizeTooLarge(Id, usize, usize), #[error("Token version {0:?} from tx {1} is deprecated")] DeprecatedTokenOperationVersion(TokenIssuanceVersion, Id), + #[error("Htlcs are not activated yet")] + HtlcsAreNotActivated, } pub fn check_transaction( @@ -69,6 +71,7 @@ pub fn check_transaction( check_tokens_tx(chain_config, block_height, tx)?; check_no_signature_size(chain_config, tx)?; check_data_deposit_outputs(chain_config, tx)?; + check_htlc_outputs(chain_config, block_height, tx)?; Ok(()) } @@ -148,7 +151,8 @@ fn check_tokens_tx( let has_tokens_v0_op = tx.outputs().iter().any(|output| match output { TxOutput::Transfer(output_value, _) | TxOutput::Burn(output_value) - | TxOutput::LockThenTransfer(output_value, _, _) => match output_value { + | TxOutput::LockThenTransfer(output_value, _, _) + | TxOutput::Htlc(output_value, _) => match output_value { OutputValue::Coin(_) | OutputValue::TokenV1(_, _) => false, OutputValue::TokenV0(_) => true, }, @@ -196,7 +200,8 @@ fn check_tokens_tx( | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::DataDeposit(_) => Ok(()), + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => Ok(()), }) .map_err(CheckTransactionError::TokensError)?; @@ -248,7 +253,8 @@ fn check_data_deposit_outputs( | TxOutput::CreateDelegationId(..) | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) - | TxOutput::IssueNft(..) => { /* Do nothing */ } + | TxOutput::IssueNft(..) + | TxOutput::Htlc(_, _) => { /* Do nothing */ } TxOutput::DataDeposit(v) => { // Ensure the size of the data doesn't exceed the max allowed if v.len() > chain_config.data_deposit_max_size() { @@ -264,3 +270,37 @@ fn check_data_deposit_outputs( Ok(()) } + +fn check_htlc_outputs( + chain_config: &ChainConfig, + block_height: BlockHeight, + tx: &SignedTransaction, +) -> Result<(), CheckTransactionError> { + match chain_config + .as_ref() + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .htlc_activated() + { + HtlcActivated::Yes => { /* do nothing */ } + HtlcActivated::No => { + let htlc_output = tx.outputs().iter().any(|output| match output { + TxOutput::Transfer(_, _) + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::Burn(_) + | TxOutput::CreateStakePool(_, _) + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::DataDeposit(_) => false, + TxOutput::Htlc(_, _) => true, + }); + + ensure!(!htlc_output, CheckTransactionError::HtlcsAreNotActivated); + } + }; + Ok(()) +} diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs index 1dcd97159f..76db7eea87 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/mod.rs @@ -74,7 +74,8 @@ pub fn calculate_tokens_burned_in_outputs( | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, }) .sum::>() .ok_or(ConnectTransactionError::BurnAmountSumError(tx.get_id())) @@ -241,7 +242,8 @@ fn check_issuance_fee_burn_v0( | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) | TxOutput::DataDeposit(_) - | TxOutput::DelegateStaking(_, _) => None, + | TxOutput::DelegateStaking(_, _) + | TxOutput::Htlc(_, _) => None, }) .sum::>() .ok_or_else(|| ConnectTransactionError::BurnAmountSumError(tx.get_id()))?; diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs index eb4d28d7e2..eeb3a98f58 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/purposes_check.rs @@ -63,7 +63,8 @@ pub fn check_reward_inputs_outputs_purposes( | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) - | TxOutput::DataDeposit(..) => Err(ConnectTransactionError::IOPolicyError( + | TxOutput::DataDeposit(..) + | TxOutput::Htlc(..) => Err(ConnectTransactionError::IOPolicyError( IOPolicyError::InvalidInputTypeInReward, block_id.into(), )), @@ -86,7 +87,8 @@ pub fn check_reward_inputs_outputs_purposes( | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) - | TxOutput::DataDeposit(..) => { + | TxOutput::DataDeposit(..) + | TxOutput::Htlc(..) => { Err(ConnectTransactionError::IOPolicyError( IOPolicyError::InvalidOutputTypeInReward, block_id.into(), @@ -136,7 +138,8 @@ pub fn check_reward_inputs_outputs_purposes( | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) - | TxOutput::DataDeposit(..) => false, + | TxOutput::DataDeposit(..) + | TxOutput::Htlc(..) => false, }); ensure!( all_lock_then_transfer, @@ -161,7 +164,8 @@ pub fn check_tx_inputs_outputs_purposes( | TxOutput::LockThenTransfer(..) | TxOutput::CreateStakePool(..) | TxOutput::ProduceBlockFromStake(..) - | TxOutput::IssueNft(..) => true, + | TxOutput::IssueNft(..) + | TxOutput::Htlc(..) => true, TxOutput::Burn(..) | TxOutput::CreateDelegationId(..) | TxOutput::DelegateStaking(..) @@ -197,7 +201,8 @@ pub fn check_tx_inputs_outputs_purposes( | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) - | TxOutput::DataDeposit(..) => { /* do nothing */ } + | TxOutput::DataDeposit(..) + | TxOutput::Htlc(..) => { /* do nothing */ } TxOutput::CreateStakePool(..) => { stake_pool_outputs_count += 1; } diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/constraints_tests.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/constraints_tests.rs index c244093405..28d20fe0f8 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/constraints_tests.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/constraints_tests.rs @@ -75,8 +75,8 @@ fn create_stake_pool_data(rng: &mut (impl Rng + CryptoRng), atoms_to_stake: u128 #[trace] #[case(Seed::from_entropy())] fn timelock_constraints_on_decommission_in_tx(#[case] seed: Seed) { - let source_inputs = [lock_then_transfer(), transfer()]; - let source_outputs = [lock_then_transfer(), transfer(), burn(), delegate_staking()]; + let source_inputs = [lock_then_transfer(), transfer(), htlc()]; + let source_outputs = [lock_then_transfer(), transfer(), htlc(), burn(), delegate_staking()]; let chain_config = common::chain::config::Builder::new(ChainType::Mainnet) .consensus_upgrades(NetUpgrades::regtest_with_pos()) @@ -214,7 +214,7 @@ fn timelock_constraints_on_decommission_in_tx(#[case] seed: Seed) { #[trace] #[case(Seed::from_entropy())] fn timelock_constraints_on_spend_share_in_tx(#[case] seed: Seed) { - let source_outputs = [lock_then_transfer(), transfer(), burn(), delegate_staking()]; + let source_outputs = [lock_then_transfer(), transfer(), htlc(), burn(), delegate_staking()]; let chain_config = common::chain::config::Builder::new(ChainType::Mainnet) .consensus_upgrades(NetUpgrades::regtest_with_pos()) diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs index fbdf21ffd1..b3513a60fd 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs @@ -15,6 +15,7 @@ use common::{ chain::{ + htlc::{HashedTimelockContract, HtlcSecretHash}, output_value::OutputValue, stakelock::StakePoolData, timelock::OutputTimeLock, @@ -45,12 +46,14 @@ fn update_functions_below_if_new_outputs_were_added(output: TxOutput) { TxOutput::IssueFungibleToken(_) => unimplemented!(), TxOutput::IssueNft(_, _, _) => unimplemented!(), TxOutput::DataDeposit(_) => unimplemented!(), + TxOutput::Htlc(_, _) => unimplemented!(), } } -pub fn all_outputs() -> [TxOutput; 10] { +pub fn all_outputs() -> [TxOutput; 11] { [ transfer(), + htlc(), burn(), lock_then_transfer(), stake_pool(), @@ -63,9 +66,10 @@ pub fn all_outputs() -> [TxOutput; 10] { ] } -pub fn valid_tx_outputs() -> [TxOutput; 9] { +pub fn valid_tx_outputs() -> [TxOutput; 10] { [ transfer(), + htlc(), burn(), lock_then_transfer(), stake_pool(), @@ -77,17 +81,25 @@ pub fn valid_tx_outputs() -> [TxOutput; 9] { ] } -pub fn valid_tx_inputs_utxos() -> [TxOutput; 5] { - [transfer(), lock_then_transfer(), stake_pool(), produce_block(), issue_nft()] +pub fn valid_tx_inputs_utxos() -> [TxOutput; 6] { + [ + transfer(), + htlc(), + lock_then_transfer(), + stake_pool(), + produce_block(), + issue_nft(), + ] } pub fn invalid_tx_inputs_utxos() -> [TxOutput; 5] { [burn(), delegate_staking(), create_delegation(), issue_tokens(), data_deposit()] } -pub fn invalid_block_reward_for_pow() -> [TxOutput; 9] { +pub fn invalid_block_reward_for_pow() -> [TxOutput; 10] { [ transfer(), + htlc(), burn(), stake_pool(), produce_block(), @@ -136,6 +148,18 @@ pub fn transfer() -> TxOutput { TxOutput::Transfer(OutputValue::Coin(Amount::ZERO), Destination::AnyoneCanSpend) } +pub fn htlc() -> TxOutput { + TxOutput::Htlc( + OutputValue::Coin(Amount::ZERO), + HashedTimelockContract { + secret_hash: HtlcSecretHash::zero(), + spend_key: Destination::AnyoneCanSpend, + refund_timelock: OutputTimeLock::ForSeconds(1), + refund_key: Destination::AnyoneCanSpend, + }, + ) +} + pub fn burn() -> TxOutput { TxOutput::Burn(OutputValue::Coin(Amount::ZERO)) } @@ -219,7 +243,8 @@ pub fn is_stake_pool(output: &TxOutput) -> bool { | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) - | TxOutput::DataDeposit(..) => false, + | TxOutput::DataDeposit(..) + | TxOutput::Htlc(..) => false, TxOutput::CreateStakePool(..) => true, } } @@ -234,7 +259,8 @@ pub fn is_produce_block(output: &TxOutput) -> bool { | TxOutput::DelegateStaking(..) | TxOutput::IssueFungibleToken(..) | TxOutput::IssueNft(..) - | TxOutput::DataDeposit(..) => false, + | TxOutput::DataDeposit(..) + | TxOutput::Htlc(..) => false, TxOutput::ProduceBlockFromStake(..) => true, } } diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs index 02a58cda30..dbd56ade49 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/purpose_tests.rs @@ -37,7 +37,8 @@ fn tx_stake_multiple_pools(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let source_inputs = super::outputs_utils::valid_tx_inputs_utxos(); - let source_valid_outputs = [lock_then_transfer(), transfer(), burn(), delegate_staking()]; + let source_valid_outputs = + [lock_then_transfer(), transfer(), htlc(), burn(), delegate_staking()]; let source_invalid_outputs = [stake_pool()]; let inputs = get_random_outputs_combination(&mut rng, &source_inputs, 1); @@ -71,7 +72,8 @@ fn tx_create_multiple_delegations(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let source_inputs = super::outputs_utils::valid_tx_inputs_utxos(); - let source_valid_outputs = [lock_then_transfer(), transfer(), burn(), delegate_staking()]; + let source_valid_outputs = + [lock_then_transfer(), transfer(), htlc(), burn(), delegate_staking()]; let source_invalid_outputs = [create_delegation()]; let inputs = get_random_outputs_combination(&mut rng, &source_inputs, 1); @@ -111,6 +113,7 @@ fn tx_many_to_many_valid(#[case] seed: Seed) { let valid_outputs = [ lock_then_transfer(), transfer(), + htlc(), burn(), delegate_staking(), issue_tokens(), @@ -208,7 +211,7 @@ fn produce_block_in_tx_output(#[case] seed: Seed) { #[trace] #[case(Seed::from_entropy())] fn tx_create_pool_and_delegation_same_tx(#[case] seed: Seed) { - let source_inputs = [transfer(), lock_then_transfer()]; + let source_inputs = [transfer(), htlc(), lock_then_transfer()]; let outputs = [stake_pool(), create_delegation()]; let mut rng = make_seedable_rng(seed); diff --git a/chainstate/tx-verifier/src/transaction_verifier/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/mod.rs index 11b0fefc7e..2e8e8339ba 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/mod.rs @@ -302,7 +302,8 @@ where | TxOutput::Burn(_) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => Ok(None), + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => Ok(None), } } @@ -421,7 +422,8 @@ where | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, }) .collect::, _>>()?; @@ -597,7 +599,8 @@ where | TxOutput::DelegateStaking(_, _) | TxOutput::LockThenTransfer(_, _, _) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, TxOutput::IssueFungibleToken(issuance_data) => { let result = make_token_id(tx.inputs()) .ok_or(ConnectTransactionError::TokensError( @@ -668,7 +671,8 @@ where | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => Ok(()), + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => Ok(()), } }) } diff --git a/chainstate/tx-verifier/src/transaction_verifier/token_issuance_cache.rs b/chainstate/tx-verifier/src/transaction_verifier/token_issuance_cache.rs index 27b0172f1f..53fd6a1eb6 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/token_issuance_cache.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/token_issuance_cache.rs @@ -239,7 +239,8 @@ fn has_tokens_issuance_to_cache(outputs: &[TxOutput]) -> Option { | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, TxOutput::IssueNft(id, _, _) => Some(*id), }) } diff --git a/common/src/chain/config/builder.rs b/common/src/chain/config/builder.rs index b6725a016a..e6cf6d5009 100644 --- a/common/src/chain/config/builder.rs +++ b/common/src/chain/config/builder.rs @@ -28,9 +28,9 @@ use crate::{ }, pos_initial_difficulty, pow::PoWChainConfigBuilder, - ChainstateUpgrade, CoinUnit, ConsensusUpgrade, Destination, GenBlock, Genesis, NetUpgrades, - PoSChainConfig, PoSConsensusVersion, PoWChainConfig, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, + ChainstateUpgrade, CoinUnit, ConsensusUpgrade, Destination, GenBlock, Genesis, + HtlcActivated, NetUpgrades, PoSChainConfig, PoSConsensusVersion, PoWChainConfig, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, }, primitives::{ id::WithId, per_thousand::PerThousand, semver::SemVer, Amount, BlockCount, BlockDistance, @@ -50,6 +50,9 @@ const TESTNET_TOKEN_FORK_HEIGHT: BlockHeight = BlockHeight::new(78440); // The fork, at which we upgrade chainstate to distribute reward to staker proportionally to their balance // and change various tokens fees const TESTNET_STAKER_REWARD_AND_TOKENS_FEE_FORK_HEIGHT: BlockHeight = BlockHeight::new(138244); +// The fork, at which txs with htlc outputs become valid +const TESTNET_HTLC_FORK_HEIGHT: BlockHeight = BlockHeight::new(99999999); +const MAINNET_HTLC_FORK_HEIGHT: BlockHeight = BlockHeight::new(99999999); impl ChainType { fn default_genesis_init(&self) -> GenesisBlockInit { @@ -156,14 +159,26 @@ impl ChainType { fn default_chainstate_upgrades(&self) -> NetUpgrades { match self { ChainType::Mainnet => { - let upgrades = vec![( - BlockHeight::new(0), - ChainstateUpgrade::new( - TokenIssuanceVersion::V1, - RewardDistributionVersion::V1, - TokensFeeVersion::V1, + let upgrades = vec![ + ( + BlockHeight::new(0), + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + HtlcActivated::No, + ), ), - )]; + ( + MAINNET_HTLC_FORK_HEIGHT, + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + HtlcActivated::Yes, + ), + ), + ]; NetUpgrades::initialize(upgrades).expect("net upgrades") } ChainType::Regtest | ChainType::Signet => { @@ -173,6 +188,7 @@ impl ChainType { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]; NetUpgrades::initialize(upgrades).expect("net upgrades") @@ -185,6 +201,7 @@ impl ChainType { TokenIssuanceVersion::V0, RewardDistributionVersion::V0, TokensFeeVersion::V0, + HtlcActivated::No, ), ), ( @@ -193,6 +210,7 @@ impl ChainType { TokenIssuanceVersion::V1, RewardDistributionVersion::V0, TokensFeeVersion::V0, + HtlcActivated::No, ), ), ( @@ -201,6 +219,16 @@ impl ChainType { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::No, + ), + ), + ( + TESTNET_HTLC_FORK_HEIGHT, + ChainstateUpgrade::new( + TokenIssuanceVersion::V1, + RewardDistributionVersion::V1, + TokensFeeVersion::V1, + HtlcActivated::Yes, ), ), ]; diff --git a/common/src/chain/config/mod.rs b/common/src/chain/config/mod.rs index 524138420a..e6bf7ff89e 100644 --- a/common/src/chain/config/mod.rs +++ b/common/src/chain/config/mod.rs @@ -51,7 +51,7 @@ use self::checkpoints::Checkpoints; use self::emission_schedule::DEFAULT_INITIAL_MINT; use super::output_value::OutputValue; use super::{stakelock::StakePoolData, RequiredConsensus}; -use super::{ChainstateUpgrade, ConsensusUpgrade}; +use super::{ChainstateUpgrade, ConsensusUpgrade, HtlcActivated}; use super::{RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion}; const DEFAULT_MAX_FUTURE_BLOCK_TIME_OFFSET: Duration = Duration::from_secs(120); @@ -864,6 +864,7 @@ pub fn create_unit_test_config_builder() -> Builder { TokenIssuanceVersion::V1, RewardDistributionVersion::V1, TokensFeeVersion::V1, + HtlcActivated::Yes, ), )]) .expect("cannot fail"), diff --git a/common/src/chain/tokens/tokens_utils.rs b/common/src/chain/tokens/tokens_utils.rs index a17620ad2c..4b266714fb 100644 --- a/common/src/chain/tokens/tokens_utils.rs +++ b/common/src/chain/tokens/tokens_utils.rs @@ -39,7 +39,8 @@ pub fn get_issuance_count_via_tokens_op(outputs: &[TxOutput]) -> usize { | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::DataDeposit(_) => false, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => false, TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) => true, }) .count() @@ -77,7 +78,8 @@ pub fn is_token_or_nft_issuance(output: &TxOutput) -> bool { | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::DataDeposit(_) => false, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => false, TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) => true, } } diff --git a/common/src/chain/transaction/output/htlc.rs b/common/src/chain/transaction/output/htlc.rs new file mode 100644 index 0000000000..ed62dab5e2 --- /dev/null +++ b/common/src/chain/transaction/output/htlc.rs @@ -0,0 +1,138 @@ +// Copyright (c) 2024 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. + +// TODO: consider removing this in the future when fixed-hash fixes this problem +#![allow(clippy::non_canonical_clone_impl)] + +use crypto::hash::{self, hash}; +use randomness::Rng; +use serialization::{Decode, Encode}; + +use super::{timelock::OutputTimeLock, Destination}; + +pub enum HashType { + RIPEMD160, + SHA1, + SHA256, + HASH160, + HASH256, +} + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] +pub struct HashedTimelockContract { + // can be spent either by a specific address that knows the secret + pub secret_hash: HtlcSecretHash, + pub spend_key: Destination, + + // or by a multisig after timelock expires making it possible to refund + pub refund_timelock: OutputTimeLock, + pub refund_key: Destination, +} + +#[derive(Clone, Encode, Decode, PartialEq, Eq, Debug)] +pub struct HtlcSecret { + secret: [u8; 32], +} + +impl HtlcSecret { + pub fn new_from_rng(rng: &mut impl Rng) -> Self { + let secret: [u8; 32] = std::array::from_fn(|_| rng.gen::()); + Self { secret } + } + + pub fn secret(&self) -> &[u8] { + &self.secret + } + + pub fn hashed(&self, t: HashType) -> HtlcSecretHash { + match t { + HashType::RIPEMD160 | HashType::SHA1 | HashType::SHA256 | HashType::HASH256 => { + unimplemented!() + } + HashType::HASH160 => HtlcSecretHash::from_slice( + hash::(hash::(self.secret)).as_slice(), + ), + } + } +} + +fixed_hash::construct_fixed_hash! { + #[derive(Encode, Decode)] + pub struct HtlcSecretHash(20); +} + +impl rpc_description::HasValueHint for HtlcSecretHash { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; +} + +impl serde::Serialize for HtlcSecretHash { + fn serialize(&self, s: S) -> Result { + s.serialize_str(&format!("{self:x}")) + } +} + +impl<'de> serde::Deserialize<'de> for HtlcSecretHash { + fn deserialize>(d: D) -> Result { + struct HashVisitor; + impl<'de> serde::de::Visitor<'de> for HashVisitor { + type Value = HtlcSecretHash; + fn expecting(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt.write_str("a hex-encoded hash") + } + fn visit_str(self, s: &str) -> Result { + s.parse().map_err(serde::de::Error::custom) + } + } + d.deserialize_str(HashVisitor) + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + use test_utils::random::Seed; + + use super::HtlcSecretHash; + + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn serialize_roundtrip(#[case] seed: Seed) { + let mut rng = test_utils::random::make_seedable_rng(seed); + + let hash = HtlcSecretHash::random_using(&mut rng); + let s_json = serde_json::to_string(&hash).unwrap(); + let decoded = serde_json::from_str::(&s_json).unwrap(); + + assert_eq!(hash, decoded); + } + + #[rstest] + #[case("\"0000000000000000000000000000000000000000\"")] + #[case("\"0000000000000000000000000000000000000001\"")] + #[case("\"ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd\"")] + #[case("\"e4732fe6f1ed1cddc2ed4b328fff5224276e3f6f\"")] + #[case("\"0103b9683e51e5aba83b8a34c9b98ce67d66136c\"")] + fn deserialize_valid(#[case] s: String) { + serde_json::from_str::(&s).unwrap(); + } + + #[rstest] + #[case("\"00000000000000000000000000000000000000000000000000000000000000\"")] + #[case("\"000000000000000000000000000000000invalid\"")] + fn deserialize_invalid(#[case] s: String) { + serde_json::from_str::(&s).unwrap_err(); + } +} diff --git a/common/src/chain/transaction/output/mod.rs b/common/src/chain/transaction/output/mod.rs index 6f53843d10..03a5412d7a 100644 --- a/common/src/chain/transaction/output/mod.rs +++ b/common/src/chain/transaction/output/mod.rs @@ -13,6 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +// TODO: consider removing this in the future when fixed-hash fixes this problem +#![allow(clippy::non_canonical_clone_impl)] + use crate::{ address::{ hexified::HexifiedAddress, pubkeyhash::PublicKeyHash, traits::Addressable, Address, @@ -31,13 +34,23 @@ use script::Script; use serialization::{Decode, DecodeAll, Encode}; use variant_count::VariantCount; -use self::{stakelock::StakePoolData, timelock::OutputTimeLock}; +use self::{htlc::HashedTimelockContract, stakelock::StakePoolData, timelock::OutputTimeLock}; pub mod classic_multisig; +pub mod htlc; pub mod output_value; pub mod stakelock; pub mod timelock; +fixed_hash::construct_fixed_hash! { + #[derive(Encode, Decode, serde::Serialize, serde::Deserialize)] + pub struct SecretHash(20); +} + +impl rpc_description::HasValueHint for SecretHash { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, VariantCount)] pub enum Destination { #[codec(index = 0)] @@ -133,6 +146,8 @@ pub enum TxOutput { /// Deposit data into the blockchain. This output cannot be spent. #[codec(index = 9)] DataDeposit(Vec), + #[codec(index = 10)] + Htlc(OutputValue, HashedTimelockContract), } impl TxOutput { @@ -146,7 +161,8 @@ impl TxOutput { | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, TxOutput::LockThenTransfer(_, _, tl) => Some(tl), } } @@ -289,7 +305,7 @@ impl TextSummary for TxOutput { } TxOutput::DelegateStaking(amount, del_ig) => { format!( - "DelegateStaking(Owner({}), StakingPool({}))", + "DelegateStaking(Amount({}), Delegation({}))", fmt_ml(amount), fmt_delid(del_ig) ) @@ -308,6 +324,16 @@ impl TextSummary for TxOutput { TxOutput::DataDeposit(data) => { format!("DataDeposit(0x{})", hex::encode(data)) } + TxOutput::Htlc(value, htlc) => { + format!( + "Htlc({}, Htlc:(SecretHash:(0x{}), Spend({}), RefundTimelock({}), Refund({}))", + fmt_val(value), + hex::encode(htlc.secret_hash), + fmt_dest(&htlc.spend_key), + fmt_timelock(&htlc.refund_timelock), + fmt_dest(&htlc.refund_key) + ) + } } } } diff --git a/common/src/chain/transaction/output/output_value.rs b/common/src/chain/transaction/output/output_value.rs index 29c0746254..2dd5a5e277 100644 --- a/common/src/chain/transaction/output/output_value.rs +++ b/common/src/chain/transaction/output/output_value.rs @@ -22,8 +22,11 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] pub enum OutputValue { + #[codec(index = 0)] Coin(Amount), + #[codec(index = 1)] TokenV0(Box), + #[codec(index = 2)] TokenV1(TokenId, Amount), } diff --git a/common/src/chain/transaction/signature/inputsig/authorize_hashed_timelock_contract_spend.rs b/common/src/chain/transaction/signature/inputsig/authorize_hashed_timelock_contract_spend.rs new file mode 100644 index 0000000000..3da43fe7cb --- /dev/null +++ b/common/src/chain/transaction/signature/inputsig/authorize_hashed_timelock_contract_spend.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2024 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 serialization::{Decode, DecodeAll, Encode}; + +use crate::chain::{htlc::HtlcSecret, signature::DestinationSigError}; + +#[derive(Debug, Encode, Decode, PartialEq, Eq)] +pub enum AuthorizedHashedTimelockContractSpend { + Secret(HtlcSecret, Vec), + Multisig(Vec), +} + +impl AuthorizedHashedTimelockContractSpend { + pub fn from_data(data: &[u8]) -> Result { + let decoded = AuthorizedHashedTimelockContractSpend::decode_all(&mut &data[..]) + .map_err(|_| DestinationSigError::InvalidSignatureEncoding)?; + Ok(decoded) + } +} diff --git a/common/src/chain/transaction/signature/inputsig/htlc.rs b/common/src/chain/transaction/signature/inputsig/htlc.rs new file mode 100644 index 0000000000..b5ece5ca88 --- /dev/null +++ b/common/src/chain/transaction/signature/inputsig/htlc.rs @@ -0,0 +1,85 @@ +// Copyright (c) 2021-2022 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 randomness::{CryptoRng, Rng}; +use serialization::Encode; + +use standard_signature::StandardInputSignature; + +use crate::chain::{htlc::HtlcSecret, ChainConfig, Destination, Transaction, TxOutput}; + +use super::{ + super::sighash::sighashtype::SigHashType, + authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, + classical_multisig::authorize_classical_multisig::AuthorizedClassicalMultisigSpend, + standard_signature, DestinationSigError, Signable, +}; + +#[allow(clippy::too_many_arguments)] +pub fn produce_uniparty_signature_for_htlc_input( + private_key: &crypto::key::PrivateKey, + sighash_type: SigHashType, + outpoint_destination: Destination, + tx: &T, + inputs_utxos: &[Option<&TxOutput>], + input_num: usize, + htlc_secret: HtlcSecret, + rng: R, +) -> Result { + let sig = StandardInputSignature::produce_uniparty_signature_for_input( + private_key, + sighash_type, + outpoint_destination, + tx, + inputs_utxos, + input_num, + rng, + )?; + + let sig_with_secret = + AuthorizedHashedTimelockContractSpend::Secret(htlc_secret, sig.raw_signature().to_owned()); + let serialized_sig = sig_with_secret.encode(); + + Ok(StandardInputSignature::new( + sig.sighash_type(), + serialized_sig, + )) +} + +pub fn produce_classical_multisig_signature_for_htlc_input( + chain_config: &ChainConfig, + authorization: &AuthorizedClassicalMultisigSpend, + sighash_type: SigHashType, + tx: &Transaction, + inputs_utxos: &[Option<&TxOutput>], + input_num: usize, +) -> Result { + let sig = StandardInputSignature::produce_classical_multisig_signature_for_input( + chain_config, + authorization, + sighash_type, + tx, + inputs_utxos, + input_num, + )?; + + let raw_signature = + AuthorizedHashedTimelockContractSpend::Multisig(sig.raw_signature().to_owned()).encode(); + + Ok(StandardInputSignature::new( + sig.sighash_type(), + raw_signature, + )) +} diff --git a/common/src/chain/transaction/signature/inputsig/mod.rs b/common/src/chain/transaction/signature/inputsig/mod.rs index 29b1f3cf13..35d27bb8c8 100644 --- a/common/src/chain/transaction/signature/inputsig/mod.rs +++ b/common/src/chain/transaction/signature/inputsig/mod.rs @@ -14,15 +14,19 @@ // limitations under the License. pub mod arbitrary_message; +pub mod authorize_hashed_timelock_contract_spend; pub mod authorize_pubkey_spend; pub mod authorize_pubkeyhash_spend; pub mod classical_multisig; +pub mod htlc; pub mod standard_signature; use serialization::{Decode, Encode}; use standard_signature::StandardInputSignature; +use super::{DestinationSigError, Signable}; + #[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum InputWitness { #[codec(index = 0)] diff --git a/common/src/chain/transaction/signature/inputsig/standard_signature.rs b/common/src/chain/transaction/signature/inputsig/standard_signature.rs index c846e352c5..95887abe69 100644 --- a/common/src/chain/transaction/signature/inputsig/standard_signature.rs +++ b/common/src/chain/transaction/signature/inputsig/standard_signature.rs @@ -21,8 +21,8 @@ use serialization::{Decode, DecodeAll, Encode}; use crate::{ chain::{ signature::{ - sighash::sighashtype::SigHashType, sighash::signature_hash, DestinationSigError, - Signable, + sighash::{sighashtype::SigHashType, signature_hash}, + DestinationSigError, Signable, }, ChainConfig, Destination, Transaction, TxOutput, }, @@ -119,7 +119,6 @@ impl StandardInputSignature { sig.encode() } Destination::ScriptHash(_) => return Err(DestinationSigError::Unsupported), - Destination::AnyoneCanSpend => { // AnyoneCanSpend must use InputWitness::NoSignature, so this is unreachable return Err(DestinationSigError::AttemptedToProduceSignatureForAnyoneCanSpend); @@ -129,6 +128,7 @@ impl StandardInputSignature { DestinationSigError::AttemptedToProduceClassicalMultisigSignatureInUnipartySignatureCode, ), }; + Ok(Self { sighash_type, raw_signature: serialized_sig, @@ -230,7 +230,7 @@ mod test { #[rstest] #[trace] #[case(Seed::from_entropy())] - fn produce_signature_address_missmatch(#[case] seed: Seed) { + fn produce_signature_address_mismatch(#[case] seed: Seed) { let mut rng = test_utils::random::make_seedable_rng(seed); let (private_key, _) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); @@ -262,7 +262,7 @@ mod test { #[rstest] #[trace] #[case(Seed::from_entropy())] - fn produce_signature_key_missmatch(#[case] seed: Seed) { + fn produce_signature_key_mismatch(#[case] seed: Seed) { let mut rng = test_utils::random::make_seedable_rng(seed); let (private_key, _) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); diff --git a/common/src/chain/transaction/signature/mod.rs b/common/src/chain/transaction/signature/mod.rs index b2e47f5c7c..c300fa1b96 100644 --- a/common/src/chain/transaction/signature/mod.rs +++ b/common/src/chain/transaction/signature/mod.rs @@ -129,29 +129,30 @@ impl Signable for Transaction { } } -impl Signable for SignedTransaction { - fn inputs(&self) -> Option<&[TxInput]> { - Some(self.inputs()) - } - - fn outputs(&self) -> Option<&[TxOutput]> { - Some(self.outputs()) - } - - fn version_byte(&self) -> Option { - Some(self.version_byte()) - } - - fn flags(&self) -> Option { - Some(self.flags()) - } -} - -impl Transactable for SignedTransaction { - fn signatures(&self) -> Option<&[InputWitness]> { - Some(self.signatures()) - } -} +// FIXME: fix +//impl Signable for SignedTransaction { +// fn inputs(&self) -> Option<&[TxInput]> { +// Some(self.inputs()) +// } +// +// fn outputs(&self) -> Option<&[TxOutput]> { +// Some(self.outputs()) +// } +// +// fn version_byte(&self) -> Option { +// Some(self.version_byte()) +// } +// +// fn flags(&self) -> Option { +// Some(self.flags()) +// } +//} +// +//impl Transactable for SignedTransaction { +// fn signatures(&self) -> Option<&[InputWitness]> { +// Some(self.signatures()) +// } +//} pub fn verify_signature( chain_config: &ChainConfig, diff --git a/common/src/chain/transaction/signature/tests/mod.rs b/common/src/chain/transaction/signature/tests/mod.rs index 299f375eb7..86b9e9566a 100644 --- a/common/src/chain/transaction/signature/tests/mod.rs +++ b/common/src/chain/transaction/signature/tests/mod.rs @@ -728,6 +728,7 @@ fn check_mutate_output( TxOutput::Transfer(v, d) => TxOutput::Transfer(add_value(v), d), TxOutput::LockThenTransfer(v, d, l) => TxOutput::LockThenTransfer(add_value(v), d, l), TxOutput::Burn(v) => TxOutput::Burn(add_value(v)), + TxOutput::Htlc(_, _) => todo!(), TxOutput::CreateStakePool(_, _) => unreachable!(), // TODO: come back to this later TxOutput::ProduceBlockFromStake(_, _) => unreachable!(), // TODO: come back to this later TxOutput::CreateDelegationId(_, _) => unreachable!(), // TODO: come back to this later 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 724b6a3235..28b721fb11 100644 --- a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs +++ b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs @@ -1081,6 +1081,7 @@ fn mutate_output(_rng: &mut impl Rng, tx: &SignedTransactionWithUtxo) -> SignedT TxOutput::Transfer(v, d) => TxOutput::Transfer(add_value(v), d), TxOutput::LockThenTransfer(v, d, l) => TxOutput::LockThenTransfer(add_value(v), d, l), TxOutput::Burn(v) => TxOutput::Burn(add_value(v)), + TxOutput::Htlc(_, _) => todo!(), TxOutput::CreateStakePool(_, _) => unreachable!(), // TODO: come back to this later TxOutput::ProduceBlockFromStake(_, _) => unreachable!(), // TODO: come back to this later TxOutput::CreateDelegationId(_, _) => unreachable!(), // TODO: come back to this later diff --git a/common/src/chain/transaction/signed_transaction.rs b/common/src/chain/transaction/signed_transaction.rs index 7f8f9c4bef..247e2a2474 100644 --- a/common/src/chain/transaction/signed_transaction.rs +++ b/common/src/chain/transaction/signed_transaction.rs @@ -13,7 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{signature::inputsig::InputWitness, Transaction, TransactionSize, TxOutput}; +use super::{ + signature::{inputsig::InputWitness, Signable, Transactable}, + Transaction, TransactionSize, TxOutput, +}; use crate::{ chain::{TransactionCreationError, TxInput}, primitives::id::{self, H256}, @@ -92,6 +95,34 @@ impl SignedTransaction { } } +impl Signable for SignedTransaction { + fn inputs(&self) -> Option<&[TxInput]> { + Some(self.inputs()) + } + + fn outputs(&self) -> Option<&[TxOutput]> { + Some(self.outputs()) + } + + fn version_byte(&self) -> Option { + Some(self.version_byte()) + } + + fn flags(&self) -> Option { + Some(self.flags()) + } +} + +impl Transactable for SignedTransaction { + //fn signatures(&self) -> Vec> { + // self.signatures.iter().map(|s| Some(s.clone())).collect() + //} + + fn signatures(&self) -> Option<&[InputWitness]> { + Some(self.signatures()) + } +} + impl Decode for SignedTransaction { fn decode(input: &mut I) -> Result { let transaction = Transaction::decode(input)?; diff --git a/common/src/chain/upgrades/chainstate_upgrade.rs b/common/src/chain/upgrades/chainstate_upgrade.rs index 57b55b976d..fa44312988 100644 --- a/common/src/chain/upgrades/chainstate_upgrade.rs +++ b/common/src/chain/upgrades/chainstate_upgrade.rs @@ -39,11 +39,18 @@ pub enum TokensFeeVersion { V1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum HtlcActivated { + Yes, + No, +} + #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct ChainstateUpgrade { token_issuance_version: TokenIssuanceVersion, reward_distribution_version: RewardDistributionVersion, tokens_fee_version: TokensFeeVersion, + htlc_activated: HtlcActivated, } impl ChainstateUpgrade { @@ -51,11 +58,13 @@ impl ChainstateUpgrade { token_issuance_version: TokenIssuanceVersion, reward_distribution_version: RewardDistributionVersion, tokens_fee_version: TokensFeeVersion, + htlc_activated: HtlcActivated, ) -> Self { Self { token_issuance_version, reward_distribution_version, tokens_fee_version, + htlc_activated, } } @@ -70,6 +79,10 @@ impl ChainstateUpgrade { pub fn tokens_fee_version(&self) -> TokensFeeVersion { self.tokens_fee_version } + + pub fn htlc_activated(&self) -> HtlcActivated { + self.htlc_activated + } } impl Activate for ChainstateUpgrade {} diff --git a/common/src/chain/upgrades/mod.rs b/common/src/chain/upgrades/mod.rs index 27e2a03168..f1feae3efb 100644 --- a/common/src/chain/upgrades/mod.rs +++ b/common/src/chain/upgrades/mod.rs @@ -18,7 +18,8 @@ mod consensus_upgrade; mod netupgrade; pub use chainstate_upgrade::{ - ChainstateUpgrade, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + ChainstateUpgrade, HtlcActivated, RewardDistributionVersion, TokenIssuanceVersion, + TokensFeeVersion, }; pub use consensus_upgrade::{ConsensusUpgrade, PoSStatus, PoWStatus, RequiredConsensus}; pub use netupgrade::{Activate, NetUpgrades}; diff --git a/common/src/size_estimation/mod.rs b/common/src/size_estimation/mod.rs index d4fe136ddc..189d877161 100644 --- a/common/src/size_estimation/mod.rs +++ b/common/src/size_estimation/mod.rs @@ -105,6 +105,7 @@ fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> { | TxOutput::IssueNft(_, _, d) | TxOutput::ProduceBlockFromStake(d, _) => Some(d), TxOutput::CreateStakePool(_, data) => Some(data.staker()), + TxOutput::Htlc(_, htlc) => Some(&htlc.spend_key), TxOutput::IssueFungibleToken(_) | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) diff --git a/consensus/src/pos/block_sig.rs b/consensus/src/pos/block_sig.rs index 0dc478a045..da359baf39 100644 --- a/consensus/src/pos/block_sig.rs +++ b/consensus/src/pos/block_sig.rs @@ -50,7 +50,8 @@ fn get_staking_kernel_destination( | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => { + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => { return Err(BlockSignatureError::WrongOutputType(header.get_id())) } TxOutput::CreateStakePool(_, stake_pool) => stake_pool.staker(), diff --git a/consensus/src/pos/mod.rs b/consensus/src/pos/mod.rs index 56610e648b..97988adcf3 100644 --- a/consensus/src/pos/mod.rs +++ b/consensus/src/pos/mod.rs @@ -164,7 +164,8 @@ where | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => { + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => { // only pool outputs can be staked return Err(ConsensusPoSError::RandomnessError( PoSRandomnessError::InvalidOutputTypeInStakeKernel(header.get_id()), diff --git a/mempool/src/error/ban_score.rs b/mempool/src/error/ban_score.rs index ddf962b2cb..7e4cdf4e8d 100644 --- a/mempool/src/error/ban_score.rs +++ b/mempool/src/error/ban_score.rs @@ -478,6 +478,7 @@ impl MempoolBanScore for CheckTransactionError { CheckTransactionError::DataDepositMaxSizeExceeded(_, _, _) => 100, CheckTransactionError::TxSizeTooLarge(_, _, _) => 100, CheckTransactionError::DeprecatedTokenOperationVersion(_, _) => 100, + CheckTransactionError::HtlcsAreNotActivated => 100, } } } diff --git a/mempool/src/pool/tx_pool/store/mem_usage.rs b/mempool/src/pool/tx_pool/store/mem_usage.rs index c29728af19..0010c702e7 100644 --- a/mempool/src/pool/tx_pool/store/mem_usage.rs +++ b/mempool/src/pool/tx_pool/store/mem_usage.rs @@ -18,6 +18,7 @@ use std::{cmp, mem}; use common::chain::{ + htlc::HashedTimelockContract, signature::inputsig::InputWitness, stakelock::StakePoolData, tokens::{NftIssuance, TokenIssuance}, @@ -346,6 +347,7 @@ impl MemoryUsage for TxOutput { TxOutput::IssueFungibleToken(issuance) => issuance.indirect_memory_usage(), TxOutput::IssueNft(_, issuance, _) => issuance.indirect_memory_usage(), TxOutput::DataDeposit(v) => v.indirect_memory_usage(), + TxOutput::Htlc(_, htlc) => htlc.indirect_memory_usage(), } } } @@ -364,7 +366,8 @@ impl_no_indirect_memory_usage!( TxDependency, TxInput, TokenIssuance, - NftIssuance + NftIssuance, + HashedTimelockContract ); /// Types where the object created by T::default() takes no indirect memory. diff --git a/mintscript/src/translate.rs b/mintscript/src/translate.rs index cf90378349..34937e1709 100644 --- a/mintscript/src/translate.rs +++ b/mintscript/src/translate.rs @@ -118,6 +118,7 @@ impl TranslateInput for SignedTransaction { .ok_or(TranslationError::PoolNotFound(*pool_id))?; Ok(checksig(pool_data.decommission_destination())) } + TxOutput::Htlc(_, _) => todo!(), TxOutput::IssueNft(_id, _issuance, dest) => Ok(checksig(dest)), TxOutput::DelegateStaking(_amount, _deleg_id) => Err(TranslationError::Unspendable), TxOutput::CreateDelegationId(_dest, _pool_id) => Err(TranslationError::Unspendable), @@ -163,7 +164,8 @@ impl TranslateInput for BlockRewardTransactable<'_> match utxo.output() { TxOutput::Transfer(_, _) | TxOutput::LockThenTransfer(_, _, _) - | TxOutput::IssueNft(_, _, _) => Err(TranslationError::IllegalOutputSpend), + | TxOutput::IssueNft(_, _, _) + | TxOutput::Htlc(_, _) => Err(TranslationError::IllegalOutputSpend), TxOutput::CreateDelegationId(_, _) | TxOutput::Burn(_) | TxOutput::DataDeposit(_) @@ -200,6 +202,7 @@ impl TranslateInput for TimelockOnly { TxOutput::LockThenTransfer(_val, _dest, timelock) => { Ok(WitnessScript::timelock(*timelock)) } + TxOutput::Htlc(_, _) => todo!(), TxOutput::Transfer(_, _) | TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) diff --git a/utxo/src/cache.rs b/utxo/src/cache.rs index 54bf83e0e5..b2d83df015 100644 --- a/utxo/src/cache.rs +++ b/utxo/src/cache.rs @@ -498,7 +498,8 @@ fn should_include_in_utxo_set(output: &TxOutput) -> bool { | TxOutput::LockThenTransfer(..) | TxOutput::CreateStakePool(..) | TxOutput::ProduceBlockFromStake(..) - | TxOutput::IssueNft(..) => true, + | TxOutput::IssueNft(..) + | TxOutput::Htlc(_, _) => true, TxOutput::CreateDelegationId(..) | TxOutput::DelegateStaking(..) | TxOutput::Burn(..) diff --git a/wallet/src/account/currency_grouper/mod.rs b/wallet/src/account/currency_grouper/mod.rs index a5506065fe..236ed2897f 100644 --- a/wallet/src/account/currency_grouper/mod.rs +++ b/wallet/src/account/currency_grouper/mod.rs @@ -43,9 +43,10 @@ pub(crate) fn group_outputs( for output in outputs { // Get the supported output value let output_value = match get_tx_output(&output) { - TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) | TxOutput::Burn(v) => { - v.clone() - } + TxOutput::Transfer(v, _) + | TxOutput::LockThenTransfer(v, _, _) + | TxOutput::Burn(v) + | TxOutput::Htlc(v, _) => v.clone(), TxOutput::CreateStakePool(_, stake) => OutputValue::Coin(stake.pledge()), TxOutput::DelegateStaking(amount, _) => OutputValue::Coin(*amount), TxOutput::CreateDelegationId(_, _) @@ -92,9 +93,10 @@ pub fn group_outputs_with_issuance_fee( for output in outputs { // Get the supported output value let output_value = match get_tx_output(&output) { - TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) | TxOutput::Burn(v) => { - v.clone() - } + TxOutput::Transfer(v, _) + | TxOutput::LockThenTransfer(v, _, _) + | TxOutput::Burn(v) + | TxOutput::Htlc(v, _) => v.clone(), TxOutput::CreateStakePool(_, stake) => OutputValue::Coin(stake.pledge()), TxOutput::DelegateStaking(amount, _) => OutputValue::Coin(*amount), TxOutput::IssueFungibleToken(_) => { @@ -132,17 +134,19 @@ pub fn group_outputs_with_issuance_fee( fn output_spendable_value(output: &TxOutput) -> Result<(Currency, Amount), UtxoSelectorError> { let value = match output { - TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) => match v { - OutputValue::Coin(output_amount) => (Currency::Coin, *output_amount), - OutputValue::TokenV0(_) => { - return Err(UtxoSelectorError::UnsupportedTransactionOutput(Box::new( - output.clone(), - ))) + TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) | TxOutput::Htlc(v, _) => { + match v { + OutputValue::Coin(output_amount) => (Currency::Coin, *output_amount), + OutputValue::TokenV0(_) => { + return Err(UtxoSelectorError::UnsupportedTransactionOutput(Box::new( + output.clone(), + ))) + } + OutputValue::TokenV1(token_id, output_amount) => { + (Currency::Token(*token_id), *output_amount) + } } - OutputValue::TokenV1(token_id, output_amount) => { - (Currency::Token(*token_id), *output_amount) - } - }, + } TxOutput::IssueNft(token_id, _, _) => (Currency::Token(*token_id), Amount::from_atoms(1)), TxOutput::CreateStakePool(_, _) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 5215587d8e..54fcdda938 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -18,6 +18,9 @@ mod output_cache; pub mod transaction_list; mod utxo_selector; +mod partially_signed_transaction; +pub use partially_signed_transaction::PartiallySignedTransaction; + use common::address::pubkeyhash::PublicKeyHash; use common::chain::block::timestamp::BlockTimestamp; use common::chain::classic_multisig::ClassicMultisigChallenge; @@ -32,6 +35,7 @@ use common::Uint256; use crypto::key::hdkd::child_number::ChildNumber; use mempool::FeeRate; use serialization::hex_encoded::HexEncoded; +use serialization::Encode; use utils::ensure; pub use utxo_selector::UtxoSelectorError; use wallet_types::account_id::AccountPrefixedId; @@ -64,7 +68,6 @@ use crypto::key::hdkd::u31::U31; use crypto::key::{PrivateKey, PublicKey}; use crypto::vrf::VRFPublicKey; use itertools::{izip, Itertools}; -use serialization::{Decode, Encode}; use std::cmp::Reverse; use std::collections::btree_map::Entry; use std::collections::{BTreeMap, BTreeSet}; @@ -108,107 +111,6 @@ impl TransactionToSign { } } -#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] -pub struct PartiallySignedTransaction { - tx: Transaction, - witnesses: Vec>, - - input_utxos: Vec>, - destinations: Vec>, -} - -impl PartiallySignedTransaction { - pub fn new( - tx: Transaction, - witnesses: Vec>, - input_utxos: Vec>, - destinations: Vec>, - ) -> WalletResult { - ensure!( - tx.inputs().len() == witnesses.len(), - TransactionCreationError::InvalidWitnessCount - ); - - ensure!( - input_utxos.len() == witnesses.len(), - TransactionCreationError::InvalidWitnessCount - ); - - ensure!( - input_utxos.len() == destinations.len(), - TransactionCreationError::InvalidWitnessCount - ); - - Ok(Self { - tx, - witnesses, - input_utxos, - destinations, - }) - } - - pub fn new_witnesses(mut self, witnesses: Vec>) -> Self { - self.witnesses = witnesses; - self - } - - pub fn tx(&self) -> &Transaction { - &self.tx - } - - pub fn take_tx(self) -> Transaction { - self.tx - } - - pub fn input_utxos(&self) -> &[Option] { - self.input_utxos.as_ref() - } - - pub fn destinations(&self) -> &[Option] { - self.destinations.as_ref() - } - - pub fn witnesses(&self) -> &[Option] { - self.witnesses.as_ref() - } - - pub fn count_inputs(&self) -> usize { - self.tx.inputs().len() - } - - pub fn count_completed_signatures(&self) -> usize { - self.witnesses.iter().filter(|w| w.is_some()).count() - } - - pub fn is_fully_signed(&self, chain_config: &ChainConfig) -> bool { - let inputs_utxos_refs: Vec<_> = self.input_utxos.iter().map(|out| out.as_ref()).collect(); - self.witnesses - .iter() - .enumerate() - .zip(&self.destinations) - .all(|((input_num, w), d)| match (w, d) { - (Some(InputWitness::NoSignature(_)), None) => true, - (Some(InputWitness::NoSignature(_)), Some(_)) => false, - (Some(InputWitness::Standard(_)), None) => false, - (Some(InputWitness::Standard(sig)), Some(dest)) => { - signature_hash(sig.sighash_type(), &self.tx, &inputs_utxos_refs, input_num) - .and_then(|sighash| sig.verify_signature(chain_config, dest, &sighash)) - .is_ok() - } - (None, _) => false, - }) - } - - pub fn into_signed_tx(self, chain_config: &ChainConfig) -> WalletResult { - if self.is_fully_signed(chain_config) { - let witnesses = self.witnesses.into_iter().map(|w| w.expect("cannot fail")).collect(); - Ok(SignedTransaction::new(self.tx, witnesses)?) - } else { - Err(WalletError::FailedToConvertPartiallySignedTx(self)) - } - } -} - pub struct Account { chain_config: Arc, key_chain: AccountKeyChainImpl, @@ -1017,7 +919,8 @@ impl Account { | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, }) .expect("find output with dummy_pool_id"); *old_pool_id = new_pool_id; @@ -1071,7 +974,8 @@ impl Account { | TxOutput::CreateDelegationId(_, _) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::IssueFungibleToken(_) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, TxOutput::IssueNft(token_id, _, _) => { (*token_id == dummy_token_id).then_some(token_id) } @@ -1365,7 +1269,7 @@ impl Account { Ok(( txo.clone(), get_tx_output_destination(txo, &|pool_id| self.output_cache.pool_data(*pool_id).ok()) - .ok_or(WalletError::InputCannotBeSpent(txo.clone()))?, + .ok_or(WalletError::InputCannotBeSpent(outpoint.clone()))?, )) } @@ -1539,6 +1443,7 @@ impl Account { TxOutput::CreateStakePool(_, data) => { vec![data.decommission_key().clone(), data.staker().clone()] } + TxOutput::Htlc(_, htlc) => vec![htlc.spend_key.clone(), htlc.refund_key.clone()], TxOutput::IssueFungibleToken(_) | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) @@ -2155,7 +2060,9 @@ fn group_preselected_inputs( TxInput::Utxo(_) => { let output = utxo.as_ref().expect("must be present"); let (currency, value) = match output { - TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) => match v { + TxOutput::Transfer(v, _) + | TxOutput::LockThenTransfer(v, _, _) + | TxOutput::Htlc(v, _) => match v { OutputValue::Coin(output_amount) => (Currency::Coin, *output_amount), OutputValue::TokenV0(_) => { return Err(WalletError::UnsupportedTransactionOutput(Box::new( diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index db0491f50f..314745b096 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -557,7 +557,8 @@ impl OutputCache { | TxOutput::DelegateStaking(_, _) | TxOutput::LockThenTransfer(_, _, _) | TxOutput::CreateDelegationId(_, _) - | TxOutput::IssueFungibleToken(_) => false, + | TxOutput::IssueFungibleToken(_) + | TxOutput::Htlc(_, _) => false, } } @@ -705,7 +706,8 @@ impl OutputCache { match output { TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) - | TxOutput::Burn(v) => match v { + | TxOutput::Burn(v) + | TxOutput::Htlc(v, _) => match v { OutputValue::TokenV1(token_id, _) => frozen_token_id == token_id, OutputValue::TokenV0(_) | OutputValue::Coin(_) => false, }, @@ -811,7 +813,8 @@ impl OutputCache { | TxOutput::Burn(_) | TxOutput::DataDeposit(_) | TxOutput::Transfer(_, _) - | TxOutput::LockThenTransfer(_, _, _) => {} + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::Htlc(_, _) => {} TxOutput::IssueFungibleToken(issuance) => { if already_present { continue; @@ -1037,7 +1040,8 @@ impl OutputCache { | TxOutput::DelegateStaking(_, _) | TxOutput::LockThenTransfer(_, _, _) | TxOutput::CreateDelegationId(_, _) - | TxOutput::IssueFungibleToken(_) => {} + | TxOutput::IssueFungibleToken(_) + | TxOutput::Htlc(_, _) => {} } } } @@ -1359,7 +1363,8 @@ impl OutputCache { | TxOutput::IssueNft(_, _, _) | TxOutput::Burn(_) | TxOutput::Transfer(_, _) - | TxOutput::LockThenTransfer(_, _, _) => None, + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::Htlc(_, _) => None, TxOutput::ProduceBlockFromStake(_, pool_id) | TxOutput::CreateStakePool(pool_id, _) => { self.pools.get(pool_id).and_then(|pool_data| { @@ -1389,7 +1394,9 @@ fn wallet_tx_order(x: &WalletTx, y: &WalletTx) -> std::cmp::Ordering { /// Check if the TxOutput is a v0 token fn is_v0_token_output(output: &TxOutput) -> bool { match output { - TxOutput::LockThenTransfer(out, _, _) | TxOutput::Transfer(out, _) => match out { + TxOutput::LockThenTransfer(out, _, _) + | TxOutput::Transfer(out, _) + | TxOutput::Htlc(out, _) => match out { OutputValue::TokenV0(_) => true, OutputValue::Coin(_) | OutputValue::TokenV1(_, _) => false, }, diff --git a/wallet/src/account/partially_signed_transaction.rs b/wallet/src/account/partially_signed_transaction.rs new file mode 100644 index 0000000000..ddb3ee62d5 --- /dev/null +++ b/wallet/src/account/partially_signed_transaction.rs @@ -0,0 +1,151 @@ +// Copyright (c) 2024 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::{ + signature::{inputsig::InputWitness, verify_signature, Signable, Transactable}, + ChainConfig, Destination, SignedTransaction, Transaction, TransactionCreationError, TxInput, + TxOutput, +}; +use serialization::{Decode, Encode}; +use utils::ensure; + +use crate::{WalletError, WalletResult}; + +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] +pub struct PartiallySignedTransaction { + tx: Transaction, + witnesses: Vec>, + + input_utxos: Vec>, + destinations: Vec>, +} + +impl PartiallySignedTransaction { + pub fn new( + tx: Transaction, + witnesses: Vec>, + input_utxos: Vec>, + destinations: Vec>, + ) -> WalletResult { + ensure!( + tx.inputs().len() == witnesses.len(), + TransactionCreationError::InvalidWitnessCount + ); + + ensure!( + input_utxos.len() == witnesses.len(), + TransactionCreationError::InvalidWitnessCount + ); + + ensure!( + input_utxos.len() == destinations.len(), + TransactionCreationError::InvalidWitnessCount + ); + + Ok(Self { + tx, + witnesses, + input_utxos, + destinations, + }) + } + + pub fn new_witnesses(mut self, witnesses: Vec>) -> Self { + self.witnesses = witnesses; + self + } + + pub fn tx(&self) -> &Transaction { + &self.tx + } + + pub fn take_tx(self) -> Transaction { + self.tx + } + + pub fn input_utxos(&self) -> &[Option] { + self.input_utxos.as_ref() + } + + pub fn destinations(&self) -> &[Option] { + self.destinations.as_ref() + } + + pub fn witnesses(&self) -> &[Option] { + self.witnesses.as_ref() + } + + pub fn count_inputs(&self) -> usize { + self.tx.inputs().len() + } + + pub fn count_completed_signatures(&self) -> usize { + self.witnesses.iter().filter(|w| w.is_some()).count() + } + + pub fn is_fully_signed(&self, chain_config: &ChainConfig) -> bool { + let inputs_utxos_refs: Vec<_> = self.input_utxos.iter().map(|out| out.as_ref()).collect(); + self.witnesses + .iter() + .enumerate() + .zip(&self.destinations) + .all(|((input_num, w), d)| match (w, d) { + (Some(InputWitness::NoSignature(_)), None) => true, + (Some(InputWitness::NoSignature(_)), Some(_)) => false, + (Some(InputWitness::Standard(_)), None) => false, + (Some(InputWitness::Standard(_)), Some(dest)) => { + verify_signature(chain_config, dest, self, &inputs_utxos_refs, input_num) + .is_ok() + } + (None, _) => false, + }) + } + + pub fn into_signed_tx(self, chain_config: &ChainConfig) -> WalletResult { + if self.is_fully_signed(chain_config) { + let witnesses = self.witnesses.into_iter().map(|w| w.expect("cannot fail")).collect(); + Ok(SignedTransaction::new(self.tx, witnesses)?) + } else { + Err(WalletError::FailedToConvertPartiallySignedTx(self)) + } + } +} + +impl Signable for PartiallySignedTransaction { + fn inputs(&self) -> Option<&[TxInput]> { + Some(self.tx.inputs()) + } + + fn outputs(&self) -> Option<&[TxOutput]> { + Some(self.tx.outputs()) + } + + fn version_byte(&self) -> Option { + Some(self.tx.version_byte()) + } + + fn flags(&self) -> Option { + Some(self.tx.flags()) + } +} + +impl Transactable for PartiallySignedTransaction { + //fn signatures(&self) -> Vec> { + // self.witnesses.clone() + //} + fn signatures(&self) -> Option<&[InputWitness]> { + todo!() + } +} diff --git a/wallet/src/account/transaction_list/mod.rs b/wallet/src/account/transaction_list/mod.rs index f22c533558..e4ab463dc8 100644 --- a/wallet/src/account/transaction_list/mod.rs +++ b/wallet/src/account/transaction_list/mod.rs @@ -113,6 +113,7 @@ fn own_output(key_chain: &AccountKeyChainImpl, output: &TxOutput) -> bool { TxOutput::Transfer(_, dest) | TxOutput::LockThenTransfer(_, dest, _) => KeyPurpose::ALL .iter() .any(|purpose| key_chain.get_leaf_key_chain(*purpose).is_destination_mine(dest)), + TxOutput::Htlc(_, _) => false, // TODO: support htlc TxOutput::Burn(_) | TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) diff --git a/wallet/src/account/utxo_selector/output_group.rs b/wallet/src/account/utxo_selector/output_group.rs index 4c8e0dc1b0..017fd545ff 100644 --- a/wallet/src/account/utxo_selector/output_group.rs +++ b/wallet/src/account/utxo_selector/output_group.rs @@ -55,7 +55,9 @@ impl OutputGroup { weight: u32, ) -> Result { let output_value = match &output.1 { - TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) => v.clone(), + TxOutput::Transfer(v, _) + | TxOutput::LockThenTransfer(v, _, _) + | TxOutput::Htlc(v, _) => v.clone(), TxOutput::IssueNft(token_id, _, _) => { OutputValue::TokenV1(*token_id, Amount::from_atoms(1)) } diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index 539c18a044..15dbdb16e8 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -307,6 +307,7 @@ where | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) => None, + TxOutput::Htlc(_, _) => None, // TODO: support htlc } } diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 29d56d47ff..4c19adf98e 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -220,7 +220,7 @@ pub enum WalletError { #[error("Sign message error: {0}")] SignMessageError(#[from] SignArbitraryMessageError), #[error("Input cannot be spent {0:?}")] - InputCannotBeSpent(TxOutput), + InputCannotBeSpent(UtxoOutPoint), #[error("Failed to convert partially signed tx to signed")] FailedToConvertPartiallySignedTx(PartiallySignedTransaction), #[error("The specified address is not found in this wallet")] diff --git a/wallet/types/src/utxo_types.rs b/wallet/types/src/utxo_types.rs index f9676baa5f..be9bcf161f 100644 --- a/wallet/types/src/utxo_types.rs +++ b/wallet/types/src/utxo_types.rs @@ -53,6 +53,7 @@ pub fn get_utxo_type(output: &TxOutput) -> Option { | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::DataDeposit(_) => None, + TxOutput::Htlc(_, _) => None, // TODO: support htlc } } pub fn get_utxo_state(output: &TxState) -> UtxoState { diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index cb65870b4d..5982d1f1ea 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -48,6 +48,7 @@ use read::ReadOnlyController; use sync::InSync; use synced_controller::SyncedController; +use chainstate::ConnectTransactionError; use common::{ address::AddressError, chain::{ @@ -55,7 +56,7 @@ use common::{ signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, sighash::signature_hash, - DestinationSigError, + DestinationSigError, Transactable, }, tokens::{RPCTokenInfo, TokenId}, Block, ChainConfig, Destination, GenBlock, PoolId, SignedTransaction, Transaction, TxInput, @@ -625,7 +626,8 @@ impl Controll | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) => None, + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) => None, }); let mut balances = BTreeMap::new(); for pool_id in pool_ids { @@ -780,13 +782,9 @@ impl Controll (InputWitness::NoSignature(_), None) => SignatureStatus::FullySigned, (InputWitness::NoSignature(_), Some(_)) => SignatureStatus::NotSigned, (InputWitness::Standard(_), None) => SignatureStatus::InvalidSignature, - (InputWitness::Standard(sig), Some(dest)) => self.verify_tx_signature( - sig, - stx.transaction(), - &inputs_utxos_refs, - input_num, - &dest, - ), + (InputWitness::Standard(_), Some(dest)) => { + self.verify_tx_signature(stx, &inputs_utxos_refs, input_num, &dest) + } }) .collect(); Ok((fees, signature_statuses)) @@ -808,8 +806,8 @@ impl Controll (Some(InputWitness::NoSignature(_)), None) => SignatureStatus::FullySigned, (Some(InputWitness::NoSignature(_)), Some(_)) => SignatureStatus::InvalidSignature, (Some(InputWitness::Standard(_)), None) => SignatureStatus::UnknownSignature, - (Some(InputWitness::Standard(sig)), Some(dest)) => { - self.verify_tx_signature(sig, ptx.tx(), &inputs_utxos_refs, input_num, dest) + (Some(InputWitness::Standard(_)), Some(dest)) => { + self.verify_tx_signature(&ptx, &inputs_utxos_refs, input_num, dest) } (None, _) => SignatureStatus::NotSigned, }) @@ -857,32 +855,32 @@ impl Controll }) } - fn verify_tx_signature( + fn verify_tx_signature( &self, - sig: &StandardInputSignature, - tx: &Transaction, + tx: &S, inputs_utxos_refs: &[Option<&TxOutput>], input_num: usize, dest: &Destination, ) -> SignatureStatus { - signature_hash(sig.sighash_type(), tx, inputs_utxos_refs, input_num).map_or( - SignatureStatus::InvalidSignature, - |sighash| { - let valid = sig.verify_signature(&self.chain_config, dest, &sighash); - - match valid { - Err(DestinationSigError::IncompleteClassicalMultisigSignature( - required_signatures, - num_signatures, - )) => SignatureStatus::PartialMultisig { - required_signatures, - num_signatures, - }, - Err(_) => SignatureStatus::InvalidSignature, - Ok(_) => SignatureStatus::FullySigned, - } + let valid = common::chain::signature::verify_signature( + &self.chain_config, + dest, + tx, + inputs_utxos_refs, + input_num, + ); + + match valid { + Err(DestinationSigError::IncompleteClassicalMultisigSignature( + required_signatures, + num_signatures, + )) => SignatureStatus::PartialMultisig { + required_signatures, + num_signatures, }, - ) + Err(_) => SignatureStatus::InvalidSignature, + Ok(_) => SignatureStatus::FullySigned, + } } pub async fn compose_transaction( From 93d948db77f7ff92de8203bae026a2a7be2f07ec Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Thu, 13 Jun 2024 20:12:38 +0300 Subject: [PATCH 02/12] Fix Transactable::signatures --- .../transaction_verifier/input_check/mod.rs | 36 ++++++++-- common/src/chain/block/block_reward.rs | 4 +- common/src/chain/transaction/mod.rs | 18 +++++ .../inputsig/arbitrary_message/tests.rs | 4 +- common/src/chain/transaction/signature/mod.rs | 65 ++++--------------- .../chain/transaction/signed_transaction.rs | 8 +-- mintscript/src/checker/signature.rs | 4 +- .../account/partially_signed_transaction.rs | 8 +-- 8 files changed, 71 insertions(+), 76 deletions(-) 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 bbb6d30193..4f725f042a 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs @@ -24,12 +24,13 @@ use common::{ }, primitives::{BlockHeight, Id}, }; +use crypto::key::SignatureError; use mintscript::{ checker::HashlockError, translate::InputInfoProvider, InputInfo, SignatureContext, TimelockContext, TranslateInput, WitnessScript, }; -use crate::TransactionVerifierStorageRef; +use crate::{error::SignatureDestinationGetterError, TransactionVerifierStorageRef}; use super::TransactionSourceForConnect; @@ -89,11 +90,11 @@ pub enum TimelockContextError { pub struct PerInputData<'a> { input: InputInfo<'a>, - witness: &'a InputWitness, + witness: InputWitness, } impl<'a> PerInputData<'a> { - fn new(input: InputInfo<'a>, witness: &'a InputWitness) -> Self { + fn new(input: InputInfo<'a>, witness: InputWitness) -> Self { Self { input, witness } } @@ -101,7 +102,7 @@ impl<'a> PerInputData<'a> { utxo_view: &UV, input_num: usize, input: &'a TxInput, - witness: &'a InputWitness, + witness: InputWitness, ) -> Result { let info = match input { TxInput::Utxo(outpoint) => { @@ -127,7 +128,7 @@ impl mintscript::translate::InputInfoProvider for PerInputData<'_> { } fn witness(&self) -> &InputWitness { - self.witness + &self.witness } } @@ -198,7 +199,7 @@ impl<'a> CoreContext<'a> { transaction: &'a T, ) -> Result { let inputs = transaction.inputs().unwrap_or_default(); - let sigs = transaction.signatures().unwrap_or_default(); + let sigs = transaction.signatures(); assert_eq!(inputs.len(), sigs.len()); @@ -206,7 +207,15 @@ impl<'a> CoreContext<'a> { .iter() .zip(sigs.iter()) .enumerate() - .map(|(n, (input, sig))| PerInputData::from_input(utxo_view, n, input, sig)) + .map(|(n, (input, sig))| { + let witness = sig.clone().ok_or_else(|| { + InputCheckError::new( + n, + ScriptError::Signature(DestinationSigError::SignatureNotFound), + ) + })?; + PerInputData::from_input(utxo_view, n, input, witness) + }) .collect::>()?; Ok(Self { inputs_and_sigs }) @@ -525,3 +534,16 @@ where Ok(()) } + +//pub fn verify_signature( +// chain_config: &ChainConfig, +// outpoint_destination: &Destination, +// tx: &T, +// inputs_utxos: &[Option<&TxOutput>], +// input_num: usize, +//) -> Result<(), ConnectTransactionError> { +// let mut checker = mintscript::checker::StandardSignatureChecker; +// checker.check_signature(ctx, destination, signature)?; +// +// Ok(()) +//} diff --git a/common/src/chain/block/block_reward.rs b/common/src/chain/block/block_reward.rs index 0dbc0d7e04..7f1f387cca 100644 --- a/common/src/chain/block/block_reward.rs +++ b/common/src/chain/block/block_reward.rs @@ -88,7 +88,7 @@ impl<'a> Signable for BlockRewardTransactable<'a> { } impl<'a> Transactable for BlockRewardTransactable<'a> { - fn signatures(&self) -> Option<&[InputWitness]> { - self.witness + fn signatures(&self) -> Vec> { + self.witness.map_or(vec![], |w| w.iter().map(|s| Some(s.clone())).collect()) } } diff --git a/common/src/chain/transaction/mod.rs b/common/src/chain/transaction/mod.rs index 97a56205f7..4d979bb51e 100644 --- a/common/src/chain/transaction/mod.rs +++ b/common/src/chain/transaction/mod.rs @@ -61,6 +61,24 @@ pub enum Transaction { V1(TransactionV1), } +impl signature::Signable for Transaction { + fn inputs(&self) -> Option<&[TxInput]> { + Some(self.inputs()) + } + + fn outputs(&self) -> Option<&[TxOutput]> { + Some(self.outputs()) + } + + fn version_byte(&self) -> Option { + Some(self.version_byte()) + } + + fn flags(&self) -> Option { + Some(self.flags()) + } +} + impl Idable for Transaction { type Tag = Transaction; fn get_id(&self) -> Id { 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 a78f9db3cc..e344032e53 100644 --- a/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs +++ b/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs @@ -19,7 +19,7 @@ use chain::signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, sighash::{sighashtype::SigHashType, signature_hash}, tests::utils::{generate_input_utxo, generate_unsigned_tx}, - verify_signature, SignedTransaction, + verify_signature, }; use crypto::{ hash::StreamHasher, @@ -34,7 +34,7 @@ use test_utils::{ use crate::{ address::pubkeyhash::PublicKeyHash, - chain::{self, Destination}, + chain::{self, Destination, SignedTransaction}, primitives::{id::DefaultHashAlgoStream, Id}, }; diff --git a/common/src/chain/transaction/signature/mod.rs b/common/src/chain/transaction/signature/mod.rs index c300fa1b96..c44664d003 100644 --- a/common/src/chain/transaction/signature/mod.rs +++ b/common/src/chain/transaction/signature/mod.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::chain::{ChainConfig, SignedTransaction, TxInput}; +use crate::chain::{ChainConfig, TxInput}; use self::{ inputsig::{ @@ -32,7 +32,7 @@ pub mod sighash; use thiserror::Error; -use super::{Destination, Transaction, TxOutput}; +use super::{Destination, TxOutput}; #[derive(Error, Debug, PartialEq, Eq, Clone)] pub enum DestinationSigError { @@ -108,52 +108,9 @@ pub trait Signable { } pub trait Transactable: Signable { - fn signatures(&self) -> Option<&[InputWitness]>; + fn signatures(&self) -> Vec>; } -impl Signable for Transaction { - fn inputs(&self) -> Option<&[TxInput]> { - Some(self.inputs()) - } - - fn outputs(&self) -> Option<&[TxOutput]> { - Some(self.outputs()) - } - - fn version_byte(&self) -> Option { - Some(self.version_byte()) - } - - fn flags(&self) -> Option { - Some(self.flags()) - } -} - -// FIXME: fix -//impl Signable for SignedTransaction { -// fn inputs(&self) -> Option<&[TxInput]> { -// Some(self.inputs()) -// } -// -// fn outputs(&self) -> Option<&[TxOutput]> { -// Some(self.outputs()) -// } -// -// fn version_byte(&self) -> Option { -// Some(self.version_byte()) -// } -// -// fn flags(&self) -> Option { -// Some(self.flags()) -// } -//} -// -//impl Transactable for SignedTransaction { -// fn signatures(&self) -> Option<&[InputWitness]> { -// Some(self.signatures()) -// } -//} - pub fn verify_signature( chain_config: &ChainConfig, outpoint_destination: &Destination, @@ -162,11 +119,15 @@ pub fn verify_signature( input_num: usize, ) -> Result<(), DestinationSigError> { let inputs = tx.inputs().ok_or(DestinationSigError::SignatureVerificationWithoutInputs)?; - let sigs = tx.signatures().ok_or(DestinationSigError::SignatureVerificationWithoutSigs)?; - let input_witness = sigs.get(input_num).ok_or(DestinationSigError::InvalidSignatureIndex( - input_num, - inputs.len(), - ))?; + let input_witness = tx + .signatures() + .get(input_num) + .cloned() + .ok_or(DestinationSigError::InvalidSignatureIndex( + input_num, + inputs.len(), + ))? + .ok_or(DestinationSigError::SignatureNotFound)?; match input_witness { InputWitness::NoSignature(_) => match outpoint_destination { @@ -181,7 +142,7 @@ pub fn verify_signature( InputWitness::Standard(witness) => verify_standard_input_signature( chain_config, outpoint_destination, - witness, + &witness, tx, inputs_utxos, input_num, diff --git a/common/src/chain/transaction/signed_transaction.rs b/common/src/chain/transaction/signed_transaction.rs index 247e2a2474..b3bd59f7da 100644 --- a/common/src/chain/transaction/signed_transaction.rs +++ b/common/src/chain/transaction/signed_transaction.rs @@ -114,12 +114,8 @@ impl Signable for SignedTransaction { } impl Transactable for SignedTransaction { - //fn signatures(&self) -> Vec> { - // self.signatures.iter().map(|s| Some(s.clone())).collect() - //} - - fn signatures(&self) -> Option<&[InputWitness]> { - Some(self.signatures()) + fn signatures(&self) -> Vec> { + self.signatures.iter().map(|s| Some(s.clone())).collect() } } diff --git a/mintscript/src/checker/signature.rs b/mintscript/src/checker/signature.rs index 95dd2f2efe..21f808c699 100644 --- a/mintscript/src/checker/signature.rs +++ b/mintscript/src/checker/signature.rs @@ -84,8 +84,8 @@ impl SignatureChecker for StandardSignatureChecker { // assertion should then go away. This goes hand in hand with turning Destinations, not // just outputs/input pairs into script. assert_eq!( - tx.signatures().and_then(|ins| ins.get(input_num)), - Some(signature), + tx.signatures().get(input_num), + Some(&Some(signature.clone())) ); verify_signature(chain_config, destination, tx, ctx.input_utxos(), input_num) diff --git a/wallet/src/account/partially_signed_transaction.rs b/wallet/src/account/partially_signed_transaction.rs index ddb3ee62d5..4b04ad8f5e 100644 --- a/wallet/src/account/partially_signed_transaction.rs +++ b/wallet/src/account/partially_signed_transaction.rs @@ -106,6 +106,7 @@ impl PartiallySignedTransaction { (Some(InputWitness::NoSignature(_)), Some(_)) => false, (Some(InputWitness::Standard(_)), None) => false, (Some(InputWitness::Standard(_)), Some(dest)) => { + // FIXME: move to into_signed_tx? verify_signature(chain_config, dest, self, &inputs_utxos_refs, input_num) .is_ok() } @@ -142,10 +143,7 @@ impl Signable for PartiallySignedTransaction { } impl Transactable for PartiallySignedTransaction { - //fn signatures(&self) -> Vec> { - // self.witnesses.clone() - //} - fn signatures(&self) -> Option<&[InputWitness]> { - todo!() + fn signatures(&self) -> Vec> { + self.witnesses.clone() } } From d5045144085b462e748429287eec55d410558ff9 Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Tue, 18 Jun 2024 15:30:50 +0300 Subject: [PATCH 03/12] Translate TxOutput::Htlc to mintscript --- chainstate/src/detail/ban_score.rs | 1 + chainstate/src/detail/error_classification.rs | 1 + chainstate/test-suite/src/tests/htlc.rs | 126 ++++++++++-------- .../transaction_verifier/input_check/mod.rs | 8 +- common/src/chain/transaction/signature/mod.rs | 39 +++++- mempool/src/error/ban_score.rs | 1 + mintscript/src/checker/signature.rs | 21 ++- mintscript/src/script/mod.rs | 11 +- mintscript/src/tests/translate/mod.rs | 2 + mintscript/src/translate.rs | 75 ++++++++++- wallet/src/signer/software_signer/mod.rs | 1 + 11 files changed, 216 insertions(+), 70 deletions(-) diff --git a/chainstate/src/detail/ban_score.rs b/chainstate/src/detail/ban_score.rs index f0699d3253..0987e8b143 100644 --- a/chainstate/src/detail/ban_score.rs +++ b/chainstate/src/detail/ban_score.rs @@ -171,6 +171,7 @@ impl BanScore for mintscript::translate::TranslationError { | Self::DelegationNotFound(_) | Self::TokenNotFound(_) => 100, + Self::SignatureError(_) => 100, Self::PoSAccounting(e) => e.ban_score(), Self::TokensAccounting(e) => e.ban_score(), } diff --git a/chainstate/src/detail/error_classification.rs b/chainstate/src/detail/error_classification.rs index cdcb40b3a2..bbed9511cf 100644 --- a/chainstate/src/detail/error_classification.rs +++ b/chainstate/src/detail/error_classification.rs @@ -349,6 +349,7 @@ impl BlockProcessingErrorClassification for mintscript::translate::TranslationEr Self::PoSAccounting(e) => e.classify(), Self::TokensAccounting(e) => e.classify(), + Self::SignatureError(e) => e.classify(), } } } diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index e6a033fe5b..a473d6122e 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -40,7 +40,6 @@ use common::{ tokens::{make_token_id, TokenData, TokenIssuance, TokenTransfer}, AccountCommand, AccountNonce, ChainConfig, ChainstateUpgrade, Destination, HtlcActivated, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, - UtxoOutPoint, }, primitives::{Amount, Idable}, }; @@ -48,6 +47,10 @@ use crypto::key::{KeyKind, PrivateKey, PublicKey}; use randomness::CryptoRng; use serialization::Encode; use test_utils::nft_utils::{random_token_issuance, random_token_issuance_v1}; +use tx_verifier::{ + error::{InputCheckError, TranslationError}, + input_check::HashlockError, +}; struct TestFixture { alice_sk: PrivateKey, @@ -157,15 +160,17 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), ) .build_and_process(&mut rng); - // FIXME: is it still valid? - //assert_eq!( - // result.unwrap_err(), - // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - // ConnectTransactionError::SignatureVerificationFailed( - // DestinationSigError::PublicKeyToAddressMismatch - // ) - // )) - //); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::InputCheck(InputCheckError::new( + 0, + tx_verifier::error::ScriptError::Signature( + DestinationSigError::PublicKeyToAddressMismatch + ) + )) + )) + ); } // Bob can't spend the output without the secret @@ -201,15 +206,17 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), ) .build_and_process(&mut rng); - // FIXME: is it still valid? - //assert_eq!( - // result.unwrap_err(), - // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - // ConnectTransactionError::SignatureVerificationFailed( - // DestinationSigError::InvalidSignatureEncoding - // ) - // )) - //); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::InputCheck(InputCheckError::new( + 0, + TranslationError::SignatureError( + DestinationSigError::InvalidSignatureEncoding + ) + )) + )) + ); } // Bob can't spend the output with random secret @@ -248,16 +255,15 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), ) .build_and_process(&mut rng); - todo!(); - //assert_eq!( - // result.unwrap_err(), - // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - // ConnectTransactionError::SecretHashMismatch(UtxoOutPoint::new( - // tx_1_id.into(), - // 0 - // )) - // )) - //); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::InputCheck(InputCheckError::new( + 0, + tx_verifier::error::ScriptError::Hashlock(HashlockError::HashMismatch) + )) + )) + ); } // Success case @@ -298,6 +304,8 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { #[trace] #[case(Seed::from_entropy())] fn refund_htlc(#[case] seed: Seed) { + use tx_verifier::error::TimelockError; + utils::concurrency::model(move || { let mut rng = test_utils::random::make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); @@ -374,13 +382,19 @@ fn refund_htlc(#[case] seed: Seed) { SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), ) .build_and_process(&mut rng); - // FIXME: fix - //assert_eq!( - // result.unwrap_err(), - // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - // ConnectTransactionError::InputCheck(()) - // )) - //); + let best_block_timestamp = tf.best_block_index().block_timestamp(); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::InputCheck(InputCheckError::new( + 0, + tx_verifier::error::ScriptError::Timelock(TimelockError::TimestampLocked( + best_block_timestamp, + best_block_timestamp.add_int_seconds(200).unwrap(), + )) + )) + )) + ); } tf.progress_time_seconds_since_epoch(200); @@ -432,15 +446,17 @@ fn refund_htlc(#[case] seed: Seed) { SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), ) .build_and_process(&mut rng); - // FIXME: is it still valid? - //assert_eq!( - // result.unwrap_err(), - // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - // ConnectTransactionError::SignatureVerificationFailed( - // DestinationSigError::IncompleteClassicalMultisigSignature(2, 1) - // ) - // )) - //); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::InputCheck(InputCheckError::new( + 0, + tx_verifier::error::ScriptError::Signature( + DestinationSigError::IncompleteClassicalMultisigSignature(2, 1) + ) + )) + )) + ); } // Bob can't spend output alone @@ -487,15 +503,17 @@ fn refund_htlc(#[case] seed: Seed) { SignedTransaction::new(tx, vec![InputWitness::Standard(input_sign)]).unwrap(), ) .build_and_process(&mut rng); - // FIXME: is it still valid? - //assert_eq!( - // result.unwrap_err(), - // ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( - // ConnectTransactionError::SignatureVerificationFailed( - // DestinationSigError::IncompleteClassicalMultisigSignature(2, 1) - // ) - // )) - //); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::InputCheck(InputCheckError::new( + 0, + tx_verifier::error::ScriptError::Signature( + DestinationSigError::IncompleteClassicalMultisigSignature(2, 1) + ) + )) + )) + ); } // Success case 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 4f725f042a..484e6a7329 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs @@ -24,16 +24,16 @@ use common::{ }, primitives::{BlockHeight, Id}, }; -use crypto::key::SignatureError; use mintscript::{ - checker::HashlockError, translate::InputInfoProvider, InputInfo, SignatureContext, - TimelockContext, TranslateInput, WitnessScript, + translate::InputInfoProvider, InputInfo, SignatureContext, TimelockContext, TranslateInput, + WitnessScript, }; -use crate::{error::SignatureDestinationGetterError, TransactionVerifierStorageRef}; +use crate::TransactionVerifierStorageRef; use super::TransactionSourceForConnect; +pub type HashlockError = mintscript::checker::HashlockError; pub type TimelockError = mintscript::checker::TimelockError; pub type ScriptError = mintscript::script::ScriptError; diff --git a/common/src/chain/transaction/signature/mod.rs b/common/src/chain/transaction/signature/mod.rs index c44664d003..fc0f752b85 100644 --- a/common/src/chain/transaction/signature/mod.rs +++ b/common/src/chain/transaction/signature/mod.rs @@ -31,6 +31,7 @@ pub mod inputsig; pub mod sighash; use thiserror::Error; +use utils::ensure; use super::{Destination, TxOutput}; @@ -151,7 +152,43 @@ pub fn verify_signature( Ok(()) } -fn verify_standard_input_signature( +pub fn verify_signature_2( + chain_config: &ChainConfig, + tx: &T, + outpoint_destination: &Destination, + input_witness: &InputWitness, + inputs_utxos: &[Option<&TxOutput>], + input_num: usize, +) -> Result<(), DestinationSigError> { + let inputs = tx.inputs().ok_or(DestinationSigError::SignatureVerificationWithoutInputs)?; + ensure!( + input_num < inputs.len(), + DestinationSigError::InvalidSignatureIndex(input_num, inputs.len(),) + ); + + match input_witness { + InputWitness::NoSignature(_) => match outpoint_destination { + Destination::PublicKeyHash(_) + | Destination::PublicKey(_) + | Destination::ScriptHash(_) + | Destination::ClassicMultisig(_) => { + return Err(DestinationSigError::SignatureNotFound) + } + Destination::AnyoneCanSpend => {} + }, + InputWitness::Standard(witness) => verify_standard_input_signature( + chain_config, + outpoint_destination, + &witness, + tx, + inputs_utxos, + input_num, + )?, + } + Ok(()) +} + +fn verify_standard_input_signature( chain_config: &ChainConfig, outpoint_destination: &Destination, witness: &StandardInputSignature, diff --git a/mempool/src/error/ban_score.rs b/mempool/src/error/ban_score.rs index 7e4cdf4e8d..e2713e1cba 100644 --- a/mempool/src/error/ban_score.rs +++ b/mempool/src/error/ban_score.rs @@ -196,6 +196,7 @@ impl MempoolBanScore for mintscript::translate::TranslationError { | Self::DelegationNotFound(_) | Self::TokenNotFound(_) => 100, + Self::SignatureError(_) => 100, Self::PoSAccounting(e) => e.ban_score(), Self::TokensAccounting(e) => e.ban_score(), } diff --git a/mintscript/src/checker/signature.rs b/mintscript/src/checker/signature.rs index 21f808c699..03b23ab2cd 100644 --- a/mintscript/src/checker/signature.rs +++ b/mintscript/src/checker/signature.rs @@ -14,7 +14,7 @@ // limitations under the License. use common::chain::{ - signature::{inputsig::InputWitness, verify_signature, DestinationSigError, Transactable}, + signature::{inputsig::InputWitness, DestinationSigError, Transactable}, ChainConfig, Destination, TxOutput, }; @@ -83,11 +83,18 @@ impl SignatureChecker for StandardSignatureChecker { // from the script should be taken and passed to the signature verification code. This // assertion should then go away. This goes hand in hand with turning Destinations, not // just outputs/input pairs into script. - assert_eq!( - tx.signatures().get(input_num), - Some(&Some(signature.clone())) - ); - - verify_signature(chain_config, destination, tx, ctx.input_utxos(), input_num) + //assert_eq!( + // tx.signatures().get(input_num), + // Some(&Some(signature.clone())) + //); + + common::chain::signature::verify_signature_2( + chain_config, + tx, + destination, + signature, + ctx.input_utxos(), + input_num, + ) } } diff --git a/mintscript/src/script/mod.rs b/mintscript/src/script/mod.rs index 60869d5386..54819e5e76 100644 --- a/mintscript/src/script/mod.rs +++ b/mintscript/src/script/mod.rs @@ -16,7 +16,9 @@ mod display; mod verify; -use common::chain::{signature::inputsig::InputWitness, timelock::OutputTimeLock, Destination}; +use common::chain::{ + htlc::HtlcSecretHash, signature::inputsig::InputWitness, timelock::OutputTimeLock, Destination, +}; use utils::ensure; pub use verify::{ScriptError, ScriptErrorOf, ScriptResult, ScriptVisitor}; @@ -131,6 +133,13 @@ pub enum HashChallenge { Hash256([u8; 32]), } +impl From for HashChallenge { + fn from(value: HtlcSecretHash) -> Self { + // FIXME: endian? + Self::Hash160(value.to_fixed_bytes()) + } +} + /// Script together with witness data presumably satisfying the script. #[derive(Clone, PartialEq, Eq, Debug)] pub enum WitnessScript { diff --git a/mintscript/src/tests/translate/mod.rs b/mintscript/src/tests/translate/mod.rs index e74efc531e..3c1a59b407 100644 --- a/mintscript/src/tests/translate/mod.rs +++ b/mintscript/src/tests/translate/mod.rs @@ -304,3 +304,5 @@ fn translate_snap( expect_test::expect_file![format!("snap.translate.{mode_str}.{name}.txt")].assert_eq(&result); } + +//FIXME: htlc translation tests diff --git a/mintscript/src/translate.rs b/mintscript/src/translate.rs index 34937e1709..477f5bb049 100644 --- a/mintscript/src/translate.rs +++ b/mintscript/src/translate.rs @@ -14,7 +14,15 @@ // limitations under the License. use common::chain::{ - block::BlockRewardTransactable, signature::inputsig::InputWitness, tokens::TokenId, + block::BlockRewardTransactable, + signature::{ + inputsig::{ + authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, + standard_signature::StandardInputSignature, InputWitness, + }, + DestinationSigError, + }, + tokens::TokenId, AccountCommand, AccountOutPoint, AccountSpending, DelegationId, Destination, PoolId, SignedTransaction, TxOutput, UtxoOutPoint, }; @@ -42,6 +50,9 @@ pub enum TranslationError { #[error(transparent)] TokensAccounting(#[from] tokens_accounting::Error), + #[error(transparent)] + SignatureError(#[from] DestinationSigError), + #[error("Stake pool {0} does not exist")] PoolNotFound(PoolId), @@ -118,7 +129,51 @@ impl TranslateInput for SignedTransaction { .ok_or(TranslationError::PoolNotFound(*pool_id))?; Ok(checksig(pool_data.decommission_destination())) } - TxOutput::Htlc(_, _) => todo!(), + TxOutput::Htlc(_, htlc) => { + let script = match ctx.witness() { + InputWitness::NoSignature(_) => { + return Err(TranslationError::SignatureError( + DestinationSigError::SignatureNotFound, + )) + } + InputWitness::Standard(sig) => { + let htlc_spend = AuthorizedHashedTimelockContractSpend::from_data( + sig.raw_signature(), + )?; + match htlc_spend { + AuthorizedHashedTimelockContractSpend::Secret( + secret, + raw_signature, + ) => WitnessScript::satisfied_conjunction([ + WitnessScript::hashlock( + htlc.secret_hash.clone().into(), + secret.secret().try_into().unwrap(), + ), + WitnessScript::signature( + htlc.spend_key.clone(), + InputWitness::Standard(StandardInputSignature::new( + sig.sighash_type(), + raw_signature, + )), + ), + ]), + AuthorizedHashedTimelockContractSpend::Multisig(raw_signature) => { + WitnessScript::satisfied_conjunction([ + WitnessScript::timelock(htlc.refund_timelock), + WitnessScript::signature( + htlc.refund_key.clone(), + InputWitness::Standard(StandardInputSignature::new( + sig.sighash_type(), + raw_signature, + )), + ), + ]) + } + } + } + }; + Ok(script) + } TxOutput::IssueNft(_id, _issuance, dest) => Ok(checksig(dest)), TxOutput::DelegateStaking(_amount, _deleg_id) => Err(TranslationError::Unspendable), TxOutput::CreateDelegationId(_dest, _pool_id) => Err(TranslationError::Unspendable), @@ -202,7 +257,21 @@ impl TranslateInput for TimelockOnly { TxOutput::LockThenTransfer(_val, _dest, timelock) => { Ok(WitnessScript::timelock(*timelock)) } - TxOutput::Htlc(_, _) => todo!(), + TxOutput::Htlc(_, htlc) => match ctx.witness() { + InputWitness::NoSignature(_) => Ok(WitnessScript::TRUE), + InputWitness::Standard(sig) => { + let htlc_spend = + AuthorizedHashedTimelockContractSpend::from_data(sig.raw_signature())?; + match htlc_spend { + AuthorizedHashedTimelockContractSpend::Secret(_, _) => { + Ok(WitnessScript::TRUE) + } + AuthorizedHashedTimelockContractSpend::Multisig(_) => { + Ok(WitnessScript::timelock(htlc.refund_timelock)) + } + } + } + }, TxOutput::Transfer(_, _) | TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index 6d81a74d6f..a3259fc568 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -247,6 +247,7 @@ impl<'a, T: WalletStorageReadUnlocked> Signer for SoftwareSigner<'a, T> { )), InputWitness::Standard(sig) => match destination { Some(destination) => { + // FIXME: do it via tx-verifier let sighash = signature_hash(sig.sighash_type(), ptx.tx(), &inputs_utxo_refs, i)?; From 53f07eab0072aa206b6ac8079059c2e4d5547ee9 Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Tue, 18 Jun 2024 18:26:52 +0300 Subject: [PATCH 04/12] Only use verify_signature with InputWitness --- .../inputsig/arbitrary_message/tests.rs | 7 ++- common/src/chain/transaction/signature/mod.rs | 42 +------------ .../chain/transaction/signature/tests/mod.rs | 59 +++++++++++++++++-- .../signature/tests/sign_and_mutate.rs | 59 +++++++++++++++++-- .../transaction/signature/tests/utils.rs | 11 +++- mintscript/src/checker/signature.rs | 4 +- wallet/src/account/mod.rs | 5 +- .../account/partially_signed_transaction.rs | 16 +++-- wallet/src/signer/software_signer/tests.rs | 11 +++- wallet/wallet-controller/src/lib.rs | 36 +++++++---- 10 files changed, 167 insertions(+), 83 deletions(-) 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 e344032e53..245f062820 100644 --- a/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs +++ b/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs @@ -19,7 +19,6 @@ use chain::signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, sighash::{sighashtype::SigHashType, signature_hash}, tests::utils::{generate_input_utxo, generate_unsigned_tx}, - verify_signature, }; use crypto::{ hash::StreamHasher, @@ -364,10 +363,11 @@ fn signing_transactions_shouldnt_work(#[case] seed: Seed) { let signed_tx = SignedTransaction::new(tx.clone(), vec![InputWitness::Standard(sig)]).unwrap(); - verify_signature( + chain::signature::verify_signature( &chain_config, &destination, &signed_tx, + &signed_tx.signatures()[0], &[Some(&input_utxo)], 0, ) @@ -390,10 +390,11 @@ fn signing_transactions_shouldnt_work(#[case] seed: Seed) { let sig = StandardInputSignature::new(sighash_type, msg_sig.raw_signature); let signed_tx = SignedTransaction::new(tx, vec![InputWitness::Standard(sig)]).unwrap(); - let ver_err = verify_signature( + let ver_err = chain::signature::verify_signature( &chain_config, &destination, &signed_tx, + &signed_tx.signatures()[0], &[Some(&input_utxo)], 0, ) diff --git a/common/src/chain/transaction/signature/mod.rs b/common/src/chain/transaction/signature/mod.rs index fc0f752b85..372f25b893 100644 --- a/common/src/chain/transaction/signature/mod.rs +++ b/common/src/chain/transaction/signature/mod.rs @@ -112,50 +112,10 @@ pub trait Transactable: Signable { fn signatures(&self) -> Vec>; } -pub fn verify_signature( +pub fn verify_signature( chain_config: &ChainConfig, outpoint_destination: &Destination, tx: &T, - inputs_utxos: &[Option<&TxOutput>], - input_num: usize, -) -> Result<(), DestinationSigError> { - let inputs = tx.inputs().ok_or(DestinationSigError::SignatureVerificationWithoutInputs)?; - let input_witness = tx - .signatures() - .get(input_num) - .cloned() - .ok_or(DestinationSigError::InvalidSignatureIndex( - input_num, - inputs.len(), - ))? - .ok_or(DestinationSigError::SignatureNotFound)?; - - match input_witness { - InputWitness::NoSignature(_) => match outpoint_destination { - Destination::PublicKeyHash(_) - | Destination::PublicKey(_) - | Destination::ScriptHash(_) - | Destination::ClassicMultisig(_) => { - return Err(DestinationSigError::SignatureNotFound) - } - Destination::AnyoneCanSpend => {} - }, - InputWitness::Standard(witness) => verify_standard_input_signature( - chain_config, - outpoint_destination, - &witness, - tx, - inputs_utxos, - input_num, - )?, - } - Ok(()) -} - -pub fn verify_signature_2( - chain_config: &ChainConfig, - tx: &T, - outpoint_destination: &Destination, input_witness: &InputWitness, inputs_utxos: &[Option<&TxOutput>], input_num: usize, diff --git a/common/src/chain/transaction/signature/tests/mod.rs b/common/src/chain/transaction/signature/tests/mod.rs index 86b9e9566a..698d4860ee 100644 --- a/common/src/chain/transaction/signature/tests/mod.rs +++ b/common/src/chain/transaction/signature/tests/mod.rs @@ -102,6 +102,7 @@ fn verify_no_signature(#[case] seed: Seed) { &chain_config, &destination, &signed_tx, + &signed_tx.signatures()[0], &inputs_utxos_refs, 0 ), @@ -146,6 +147,7 @@ fn verify_invalid_signature(#[case] seed: Seed) { &chain_config, &destination, &signed_tx, + &signed_tx.signatures()[0], &inputs_utxos_refs, 0 ), @@ -186,6 +188,7 @@ fn verify_signature_invalid_signature_index(#[case] seed: Seed) { &chain_config, &destination, &tx, + &tx.signatures()[0], &inputs_utxos_refs, INVALID_SIGNATURE_INDEX ), @@ -230,6 +233,7 @@ fn verify_signature_wrong_destination(#[case] seed: Seed) { &chain_config, &different_outpoint, &tx, + &tx.signatures()[0], &inputs_utxos_refs, 0 ), @@ -619,7 +623,14 @@ fn check_change_flags( let tx = tx_updater.generate_tx().unwrap(); for (input_num, _) in tx.inputs().iter().enumerate() { assert_eq!( - verify_signature(chain_config, destination, &tx, inputs_utxos, input_num), + verify_signature( + chain_config, + destination, + &tx, + &tx.signatures()[input_num], + inputs_utxos, + input_num + ), Err(DestinationSigError::SignatureVerificationFailed) ); } @@ -647,7 +658,14 @@ fn check_insert_input( tx_updater.inputs.push(TxInput::from_utxo(outpoint_source_id, 1)); tx_updater.witness.push(InputWitness::NoSignature(Some(vec![1, 2, 3]))); let tx = tx_updater.generate_tx().unwrap(); - let res = verify_signature(chain_config, destination, &tx, &inputs_utxos, 0); + let res = verify_signature( + chain_config, + destination, + &tx, + &tx.signatures()[0], + &inputs_utxos, + 0, + ); if should_fail { assert_eq!(res, Err(DestinationSigError::SignatureVerificationFailed)); } else { @@ -675,7 +693,14 @@ fn check_mutate_witness( let tx = tx_updater.generate_tx().unwrap(); assert!(matches!( - verify_signature(chain_config, outpoint_dest, &tx, inputs_utxos, input), + verify_signature( + chain_config, + outpoint_dest, + &tx, + &tx.signatures()[input], + inputs_utxos, + input + ), Err(DestinationSigError::SignatureVerificationFailed | DestinationSigError::InvalidSignatureEncoding) )); @@ -697,7 +722,14 @@ fn check_insert_output( Destination::PublicKey(pub_key), )); let tx = tx_updater.generate_tx().unwrap(); - let res = verify_signature(chain_config, destination, &tx, inputs_utxos, 0); + let res = verify_signature( + chain_config, + destination, + &tx, + &tx.signatures()[0], + inputs_utxos, + 0, + ); if should_fail { assert_eq!(res, Err(DestinationSigError::SignatureVerificationFailed)); } else { @@ -739,7 +771,14 @@ fn check_mutate_output( }; let tx = tx_updater.generate_tx().unwrap(); - let res = verify_signature(chain_config, destination, &tx, inputs_utxos, 0); + let res = verify_signature( + chain_config, + destination, + &tx, + &tx.signatures()[0], + inputs_utxos, + 0, + ); if should_fail { assert_eq!(res, Err(DestinationSigError::SignatureVerificationFailed)); } else { @@ -762,7 +801,14 @@ fn check_mutate_input( 9999, ); let tx = tx_updater.generate_tx().unwrap(); - let res = verify_signature(chain_config, destination, &tx, inputs_utxos, 0); + let res = verify_signature( + chain_config, + destination, + &tx, + &tx.signatures()[0], + inputs_utxos, + 0, + ); if should_fail { assert_eq!(res, Err(DestinationSigError::SignatureVerificationFailed)); } else { @@ -790,6 +836,7 @@ fn check_mutate_inputs_utxos( chain_config, outpoint_dest, original_tx, + &original_tx.signatures()[input], &inputs_utxos, input ), 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 28b721fb11..34ff2e8856 100644 --- a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs +++ b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs @@ -456,7 +456,14 @@ fn mutate_all_anyonecanpay(#[case] seed: Seed) { }; let tx = mutate_first_input(&mut rng, &tx); assert_eq!( - verify_signature(&chain_config, &destination, &tx.tx, &inputs_utxos_refs, 0), + verify_signature( + &chain_config, + &destination, + &tx.tx, + &tx.tx.signatures()[0], + &inputs_utxos_refs, + 0 + ), Err(DestinationSigError::SignatureVerificationFailed), ); } @@ -566,7 +573,14 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { let inputs = tx.tx.inputs().len(); assert_eq!( - verify_signature(&chain_config, &destination, &tx.tx, &inputs_utxos_refs, 0), + verify_signature( + &chain_config, + &destination, + &tx.tx, + &tx.tx.signatures()[0], + &inputs_utxos_refs, + 0 + ), Err(DestinationSigError::SignatureVerificationFailed), ); for input in 1..inputs { @@ -575,6 +589,7 @@ fn mutate_none_anyonecanpay(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[input], &inputs_utxos_refs, input ), @@ -666,6 +681,7 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[input], &inputs_utxos_refs, input ), @@ -677,6 +693,7 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[0], &inputs_utxos_refs, total_inputs ), @@ -700,6 +717,7 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[input], &inputs_utxos_refs, input ), @@ -711,6 +729,7 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[0], &inputs_utxos_refs, inputs ), @@ -726,7 +745,14 @@ fn mutate_single(#[case] seed: Seed) { // Mutation of the first output makes signature invalid. assert_eq!( - verify_signature(&chain_config, &destination, &tx.tx, &inputs_utxos_refs, 0), + verify_signature( + &chain_config, + &destination, + &tx.tx, + &tx.tx.signatures()[0], + &inputs_utxos_refs, + 0 + ), Err(DestinationSigError::SignatureVerificationFailed), ); for input in 1..total_inputs - 1 { @@ -735,6 +761,7 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[input], &inputs_utxos_refs, input ), @@ -746,6 +773,7 @@ fn mutate_single(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[0], &inputs_utxos_refs, total_inputs ), @@ -800,6 +828,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[input], &inputs_utxos_refs, input ), @@ -812,6 +841,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[0], &inputs_utxos_refs, total_inputs ), @@ -830,7 +860,14 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { tx.inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); assert_eq!( - verify_signature(&chain_config, &destination, &tx.tx, &inputs_utxos_refs, 0), + verify_signature( + &chain_config, + &destination, + &tx.tx, + &tx.tx.signatures()[0], + &inputs_utxos_refs, + 0 + ), Err(DestinationSigError::SignatureVerificationFailed), ); for input in 1..total_inputs - 1 { @@ -839,6 +876,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[input], &inputs_utxos_refs, input ), @@ -851,6 +889,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[0], &inputs_utxos_refs, total_inputs ), @@ -876,6 +915,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[input], &inputs_utxos_refs, input ), @@ -888,6 +928,7 @@ fn mutate_single_anyonecanpay(#[case] seed: Seed) { &chain_config, &destination, &tx.tx, + &tx.tx.signatures()[0], &inputs_utxos_refs, total_inputs ), @@ -928,6 +969,7 @@ fn check_mutations( chain_config, destination, &tx.tx, + &tx.tx.signatures()[0], &inputs_utxos_refs, INVALID_INPUT ), @@ -938,7 +980,14 @@ fn check_mutations( ); for input in 0..inputs { assert_eq!( - verify_signature(chain_config, destination, &tx.tx, &inputs_utxos_refs, input), + verify_signature( + chain_config, + destination, + &tx.tx, + &tx.tx.signatures()[input], + &inputs_utxos_refs, + input + ), expected ); } diff --git a/common/src/chain/transaction/signature/tests/utils.rs b/common/src/chain/transaction/signature/tests/utils.rs index bd2816e6ad..4718a1a334 100644 --- a/common/src/chain/transaction/signature/tests/utils.rs +++ b/common/src/chain/transaction/signature/tests/utils.rs @@ -26,7 +26,7 @@ use crate::{ signature::{ inputsig::{standard_signature::StandardInputSignature, InputWitness}, sighash::sighashtype::SigHashType, - verify_signature, DestinationSigError, + DestinationSigError, }, signed_transaction::SignedTransaction, AccountNonce, AccountSpending, ChainConfig, DelegationId, Destination, Transaction, @@ -215,7 +215,14 @@ pub fn verify_signed_tx( destination: &Destination, ) -> Result<(), DestinationSigError> { for i in 0..tx.inputs().len() { - verify_signature(chain_config, destination, tx, inputs_utxos, i)? + crate::chain::signature::verify_signature( + chain_config, + destination, + tx, + &tx.signatures()[i], + inputs_utxos, + i, + )? } Ok(()) } diff --git a/mintscript/src/checker/signature.rs b/mintscript/src/checker/signature.rs index 03b23ab2cd..987d070cf1 100644 --- a/mintscript/src/checker/signature.rs +++ b/mintscript/src/checker/signature.rs @@ -88,10 +88,10 @@ impl SignatureChecker for StandardSignatureChecker { // Some(&Some(signature.clone())) //); - common::chain::signature::verify_signature_2( + common::chain::signature::verify_signature( chain_config, - tx, destination, + tx, signature, ctx.input_utxos(), input_num, diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 54fcdda938..2212bdf564 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -24,8 +24,7 @@ pub use partially_signed_transaction::PartiallySignedTransaction; use common::address::pubkeyhash::PublicKeyHash; use common::chain::block::timestamp::BlockTimestamp; use common::chain::classic_multisig::ClassicMultisigChallenge; -use common::chain::signature::sighash::signature_hash; -use common::chain::{AccountCommand, AccountOutPoint, AccountSpending, TransactionCreationError}; +use common::chain::{AccountCommand, AccountOutPoint, AccountSpending}; use common::primitives::id::WithId; use common::primitives::{Idable, H256}; use common::size_estimation::{ @@ -35,7 +34,6 @@ use common::Uint256; use crypto::key::hdkd::child_number::ChildNumber; use mempool::FeeRate; use serialization::hex_encoded::HexEncoded; -use serialization::Encode; use utils::ensure; pub use utxo_selector::UtxoSelectorError; use wallet_types::account_id::AccountPrefixedId; @@ -54,7 +52,6 @@ use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; use crate::{get_tx_output_destination, SendRequest, WalletError, WalletResult}; use common::address::{Address, RpcAddress}; use common::chain::output_value::OutputValue; -use common::chain::signature::inputsig::InputWitness; use common::chain::tokens::{ make_token_id, IsTokenUnfreezable, NftIssuance, NftIssuanceV0, RPCFungibleTokenInfo, TokenId, }; diff --git a/wallet/src/account/partially_signed_transaction.rs b/wallet/src/account/partially_signed_transaction.rs index 4b04ad8f5e..a0c1cff392 100644 --- a/wallet/src/account/partially_signed_transaction.rs +++ b/wallet/src/account/partially_signed_transaction.rs @@ -14,7 +14,7 @@ // limitations under the License. use common::chain::{ - signature::{inputsig::InputWitness, verify_signature, Signable, Transactable}, + signature::{inputsig::InputWitness, Signable, Transactable}, ChainConfig, Destination, SignedTransaction, Transaction, TransactionCreationError, TxInput, TxOutput, }; @@ -105,10 +105,16 @@ impl PartiallySignedTransaction { (Some(InputWitness::NoSignature(_)), None) => true, (Some(InputWitness::NoSignature(_)), Some(_)) => false, (Some(InputWitness::Standard(_)), None) => false, - (Some(InputWitness::Standard(_)), Some(dest)) => { - // FIXME: move to into_signed_tx? - verify_signature(chain_config, dest, self, &inputs_utxos_refs, input_num) - .is_ok() + (Some(InputWitness::Standard(sig)), Some(dest)) => { + common::chain::signature::verify_signature( + chain_config, + dest, + self, + &InputWitness::Standard(sig.clone()), + &inputs_utxos_refs, + input_num, + ) + .is_ok() } (None, _) => false, }) diff --git a/wallet/src/signer/software_signer/tests.rs b/wallet/src/signer/software_signer/tests.rs index ffd000b343..d278690715 100644 --- a/wallet/src/signer/software_signer/tests.rs +++ b/wallet/src/signer/software_signer/tests.rs @@ -20,7 +20,6 @@ use crate::key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}; use crate::{Account, SendRequest}; use common::chain::config::create_regtest; use common::chain::output_value::OutputValue; -use common::chain::signature::verify_signature; use common::chain::timelock::OutputTimeLock; use common::chain::{GenBlock, TxInput}; use common::primitives::amount::UnsignedIntType; @@ -140,6 +139,14 @@ fn sign_transaction(#[case] seed: Seed) { for i in 0..sig_tx.inputs().len() { let destination = crate::get_tx_output_destination(utxos_ref[i].unwrap(), &|_| None).unwrap(); - verify_signature(&config, &destination, &sig_tx, &utxos_ref, i).unwrap(); + common::chain::signature::verify_signature( + &config, + &destination, + &sig_tx, + &sig_tx.signatures()[i], + &utxos_ref, + i, + ) + .unwrap(); } } diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 5982d1f1ea..9ec9a15ae2 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -48,16 +48,11 @@ use read::ReadOnlyController; use sync::InSync; use synced_controller::SyncedController; -use chainstate::ConnectTransactionError; use common::{ address::AddressError, chain::{ block::timestamp::BlockTimestamp, - signature::{ - inputsig::{standard_signature::StandardInputSignature, InputWitness}, - sighash::signature_hash, - DestinationSigError, Transactable, - }, + signature::{inputsig::InputWitness, DestinationSigError, Transactable}, tokens::{RPCTokenInfo, TokenId}, Block, ChainConfig, Destination, GenBlock, PoolId, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, @@ -862,13 +857,28 @@ impl Controll input_num: usize, dest: &Destination, ) -> SignatureStatus { - let valid = common::chain::signature::verify_signature( - &self.chain_config, - dest, - tx, - inputs_utxos_refs, - input_num, - ); + let valid = (|| { + let inputs = + tx.inputs().ok_or(DestinationSigError::SignatureVerificationWithoutInputs)?; + let witness = tx + .signatures() + .get(input_num) + .cloned() + .ok_or(DestinationSigError::InvalidSignatureIndex( + input_num, + inputs.len(), + ))? + .ok_or(DestinationSigError::SignatureNotFound)?; + + common::chain::signature::verify_signature( + &self.chain_config, + dest, + tx, + &witness, + inputs_utxos_refs, + input_num, + ) + })(); match valid { Err(DestinationSigError::IncompleteClassicalMultisigSignature( From 236dfed923d3855dccfbbebbf89776b9c9a98a98 Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Wed, 19 Jun 2024 12:30:44 +0300 Subject: [PATCH 05/12] Minor fixes --- chainstate/test-suite/src/tests/htlc.rs | 9 ++++++++- common/src/chain/transaction/output/htlc.rs | 20 ++------------------ mintscript/src/script/mod.rs | 11 +---------- mintscript/src/translate.rs | 6 +++--- 4 files changed, 14 insertions(+), 32 deletions(-) diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index a473d6122e..279c2c413b 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -43,6 +43,7 @@ use common::{ }, primitives::{Amount, Idable}, }; +use crypto::hash::{self, hash}; use crypto::key::{KeyKind, PrivateKey, PublicKey}; use randomness::CryptoRng; use serialization::Encode; @@ -52,6 +53,12 @@ use tx_verifier::{ input_check::HashlockError, }; +fn hash_secret(secret: &[u8]) -> HtlcSecretHash { + HtlcSecretHash::from_slice( + hash::(hash::(secret)).as_slice(), + ) +} + struct TestFixture { alice_sk: PrivateKey, bob_sk: PrivateKey, @@ -88,7 +95,7 @@ impl TestFixture { let destination_multisig: PublicKeyHash = (&refund_challenge).into(); let htlc = HashedTimelockContract { - secret_hash: self.secret.hashed(htlc::HashType::HASH160), + secret_hash: hash_secret(self.secret.secret()), spend_key: Destination::PublicKeyHash((&bob_pk).into()), refund_timelock: OutputTimeLock::ForSeconds(200), refund_key: Destination::ClassicMultisig(destination_multisig), diff --git a/common/src/chain/transaction/output/htlc.rs b/common/src/chain/transaction/output/htlc.rs index ed62dab5e2..01d1a49aa3 100644 --- a/common/src/chain/transaction/output/htlc.rs +++ b/common/src/chain/transaction/output/htlc.rs @@ -16,20 +16,11 @@ // TODO: consider removing this in the future when fixed-hash fixes this problem #![allow(clippy::non_canonical_clone_impl)] -use crypto::hash::{self, hash}; use randomness::Rng; use serialization::{Decode, Encode}; use super::{timelock::OutputTimeLock, Destination}; -pub enum HashType { - RIPEMD160, - SHA1, - SHA256, - HASH160, - HASH256, -} - #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] pub struct HashedTimelockContract { // can be spent either by a specific address that knows the secret @@ -56,15 +47,8 @@ impl HtlcSecret { &self.secret } - pub fn hashed(&self, t: HashType) -> HtlcSecretHash { - match t { - HashType::RIPEMD160 | HashType::SHA1 | HashType::SHA256 | HashType::HASH256 => { - unimplemented!() - } - HashType::HASH160 => HtlcSecretHash::from_slice( - hash::(hash::(self.secret)).as_slice(), - ), - } + pub fn consume(self) -> [u8; 32] { + self.secret } } diff --git a/mintscript/src/script/mod.rs b/mintscript/src/script/mod.rs index 54819e5e76..60869d5386 100644 --- a/mintscript/src/script/mod.rs +++ b/mintscript/src/script/mod.rs @@ -16,9 +16,7 @@ mod display; mod verify; -use common::chain::{ - htlc::HtlcSecretHash, signature::inputsig::InputWitness, timelock::OutputTimeLock, Destination, -}; +use common::chain::{signature::inputsig::InputWitness, timelock::OutputTimeLock, Destination}; use utils::ensure; pub use verify::{ScriptError, ScriptErrorOf, ScriptResult, ScriptVisitor}; @@ -133,13 +131,6 @@ pub enum HashChallenge { Hash256([u8; 32]), } -impl From for HashChallenge { - fn from(value: HtlcSecretHash) -> Self { - // FIXME: endian? - Self::Hash160(value.to_fixed_bytes()) - } -} - /// Script together with witness data presumably satisfying the script. #[derive(Clone, PartialEq, Eq, Debug)] pub enum WitnessScript { diff --git a/mintscript/src/translate.rs b/mintscript/src/translate.rs index 477f5bb049..d276624ee9 100644 --- a/mintscript/src/translate.rs +++ b/mintscript/src/translate.rs @@ -30,7 +30,7 @@ use pos_accounting::PoSAccountingView; use tokens_accounting::TokensAccountingView; use utxo::Utxo; -use crate::WitnessScript; +use crate::{script::HashChallenge, WitnessScript}; /// An error that can happen during translation of an input to a script #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] @@ -146,8 +146,8 @@ impl TranslateInput for SignedTransaction { raw_signature, ) => WitnessScript::satisfied_conjunction([ WitnessScript::hashlock( - htlc.secret_hash.clone().into(), - secret.secret().try_into().unwrap(), + HashChallenge::Hash160(htlc.secret_hash.to_fixed_bytes()), + secret.consume(), ), WitnessScript::signature( htlc.spend_key.clone(), From 34b34d9f51276b043dee6788cca83c132e4b18ba Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Thu, 20 Jun 2024 13:02:58 +0300 Subject: [PATCH 06/12] Revert wallet changes --- chainstate/test-suite/src/tests/htlc.rs | 2 +- wallet/src/account/mod.rs | 111 ++++++++++++- .../account/partially_signed_transaction.rs | 155 ------------------ wallet/src/signer/software_signer/mod.rs | 1 - wallet/src/wallet/mod.rs | 2 +- wallet/wallet-controller/src/lib.rs | 75 ++++----- 6 files changed, 142 insertions(+), 204 deletions(-) delete mode 100644 wallet/src/account/partially_signed_transaction.rs diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index 279c2c413b..338509ea77 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -20,7 +20,7 @@ use common::{ address::pubkeyhash::PublicKeyHash, chain::{ classic_multisig::ClassicMultisigChallenge, - htlc::{self, HashedTimelockContract, HtlcSecret, HtlcSecretHash}, + htlc::{HashedTimelockContract, HtlcSecret, HtlcSecretHash}, output_value::OutputValue, signature::{ inputsig::{ diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 2212bdf564..28e42b250f 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -18,13 +18,11 @@ mod output_cache; pub mod transaction_list; mod utxo_selector; -mod partially_signed_transaction; -pub use partially_signed_transaction::PartiallySignedTransaction; - use common::address::pubkeyhash::PublicKeyHash; use common::chain::block::timestamp::BlockTimestamp; use common::chain::classic_multisig::ClassicMultisigChallenge; -use common::chain::{AccountCommand, AccountOutPoint, AccountSpending}; +use common::chain::signature::sighash::signature_hash; +use common::chain::{AccountCommand, AccountOutPoint, AccountSpending, TransactionCreationError}; use common::primitives::id::WithId; use common::primitives::{Idable, H256}; use common::size_estimation::{ @@ -52,6 +50,7 @@ use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; use crate::{get_tx_output_destination, SendRequest, WalletError, WalletResult}; use common::address::{Address, RpcAddress}; use common::chain::output_value::OutputValue; +use common::chain::signature::inputsig::InputWitness; use common::chain::tokens::{ make_token_id, IsTokenUnfreezable, NftIssuance, NftIssuanceV0, RPCFungibleTokenInfo, TokenId, }; @@ -65,6 +64,7 @@ use crypto::key::hdkd::u31::U31; use crypto::key::{PrivateKey, PublicKey}; use crypto::vrf::VRFPublicKey; use itertools::{izip, Itertools}; +use serialization::{Decode, Encode}; use std::cmp::Reverse; use std::collections::btree_map::Entry; use std::collections::{BTreeMap, BTreeSet}; @@ -108,6 +108,107 @@ impl TransactionToSign { } } +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] +pub struct PartiallySignedTransaction { + tx: Transaction, + witnesses: Vec>, + + input_utxos: Vec>, + destinations: Vec>, +} + +impl PartiallySignedTransaction { + pub fn new( + tx: Transaction, + witnesses: Vec>, + input_utxos: Vec>, + destinations: Vec>, + ) -> WalletResult { + ensure!( + tx.inputs().len() == witnesses.len(), + TransactionCreationError::InvalidWitnessCount + ); + + ensure!( + input_utxos.len() == witnesses.len(), + TransactionCreationError::InvalidWitnessCount + ); + + ensure!( + input_utxos.len() == destinations.len(), + TransactionCreationError::InvalidWitnessCount + ); + + Ok(Self { + tx, + witnesses, + input_utxos, + destinations, + }) + } + + pub fn new_witnesses(mut self, witnesses: Vec>) -> Self { + self.witnesses = witnesses; + self + } + + pub fn tx(&self) -> &Transaction { + &self.tx + } + + pub fn take_tx(self) -> Transaction { + self.tx + } + + pub fn input_utxos(&self) -> &[Option] { + self.input_utxos.as_ref() + } + + pub fn destinations(&self) -> &[Option] { + self.destinations.as_ref() + } + + pub fn witnesses(&self) -> &[Option] { + self.witnesses.as_ref() + } + + pub fn count_inputs(&self) -> usize { + self.tx.inputs().len() + } + + pub fn count_completed_signatures(&self) -> usize { + self.witnesses.iter().filter(|w| w.is_some()).count() + } + + pub fn is_fully_signed(&self, chain_config: &ChainConfig) -> bool { + let inputs_utxos_refs: Vec<_> = self.input_utxos.iter().map(|out| out.as_ref()).collect(); + self.witnesses + .iter() + .enumerate() + .zip(&self.destinations) + .all(|((input_num, w), d)| match (w, d) { + (Some(InputWitness::NoSignature(_)), None) => true, + (Some(InputWitness::NoSignature(_)), Some(_)) => false, + (Some(InputWitness::Standard(_)), None) => false, + (Some(InputWitness::Standard(sig)), Some(dest)) => { + signature_hash(sig.sighash_type(), &self.tx, &inputs_utxos_refs, input_num) + .and_then(|sighash| sig.verify_signature(chain_config, dest, &sighash)) + .is_ok() + } + (None, _) => false, + }) + } + + pub fn into_signed_tx(self, chain_config: &ChainConfig) -> WalletResult { + if self.is_fully_signed(chain_config) { + let witnesses = self.witnesses.into_iter().map(|w| w.expect("cannot fail")).collect(); + Ok(SignedTransaction::new(self.tx, witnesses)?) + } else { + Err(WalletError::FailedToConvertPartiallySignedTx(self)) + } + } +} + pub struct Account { chain_config: Arc, key_chain: AccountKeyChainImpl, @@ -1266,7 +1367,7 @@ impl Account { Ok(( txo.clone(), get_tx_output_destination(txo, &|pool_id| self.output_cache.pool_data(*pool_id).ok()) - .ok_or(WalletError::InputCannotBeSpent(outpoint.clone()))?, + .ok_or(WalletError::InputCannotBeSpent(txo.clone()))?, )) } diff --git a/wallet/src/account/partially_signed_transaction.rs b/wallet/src/account/partially_signed_transaction.rs deleted file mode 100644 index a0c1cff392..0000000000 --- a/wallet/src/account/partially_signed_transaction.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2024 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::{ - signature::{inputsig::InputWitness, Signable, Transactable}, - ChainConfig, Destination, SignedTransaction, Transaction, TransactionCreationError, TxInput, - TxOutput, -}; -use serialization::{Decode, Encode}; -use utils::ensure; - -use crate::{WalletError, WalletResult}; - -#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] -pub struct PartiallySignedTransaction { - tx: Transaction, - witnesses: Vec>, - - input_utxos: Vec>, - destinations: Vec>, -} - -impl PartiallySignedTransaction { - pub fn new( - tx: Transaction, - witnesses: Vec>, - input_utxos: Vec>, - destinations: Vec>, - ) -> WalletResult { - ensure!( - tx.inputs().len() == witnesses.len(), - TransactionCreationError::InvalidWitnessCount - ); - - ensure!( - input_utxos.len() == witnesses.len(), - TransactionCreationError::InvalidWitnessCount - ); - - ensure!( - input_utxos.len() == destinations.len(), - TransactionCreationError::InvalidWitnessCount - ); - - Ok(Self { - tx, - witnesses, - input_utxos, - destinations, - }) - } - - pub fn new_witnesses(mut self, witnesses: Vec>) -> Self { - self.witnesses = witnesses; - self - } - - pub fn tx(&self) -> &Transaction { - &self.tx - } - - pub fn take_tx(self) -> Transaction { - self.tx - } - - pub fn input_utxos(&self) -> &[Option] { - self.input_utxos.as_ref() - } - - pub fn destinations(&self) -> &[Option] { - self.destinations.as_ref() - } - - pub fn witnesses(&self) -> &[Option] { - self.witnesses.as_ref() - } - - pub fn count_inputs(&self) -> usize { - self.tx.inputs().len() - } - - pub fn count_completed_signatures(&self) -> usize { - self.witnesses.iter().filter(|w| w.is_some()).count() - } - - pub fn is_fully_signed(&self, chain_config: &ChainConfig) -> bool { - let inputs_utxos_refs: Vec<_> = self.input_utxos.iter().map(|out| out.as_ref()).collect(); - self.witnesses - .iter() - .enumerate() - .zip(&self.destinations) - .all(|((input_num, w), d)| match (w, d) { - (Some(InputWitness::NoSignature(_)), None) => true, - (Some(InputWitness::NoSignature(_)), Some(_)) => false, - (Some(InputWitness::Standard(_)), None) => false, - (Some(InputWitness::Standard(sig)), Some(dest)) => { - common::chain::signature::verify_signature( - chain_config, - dest, - self, - &InputWitness::Standard(sig.clone()), - &inputs_utxos_refs, - input_num, - ) - .is_ok() - } - (None, _) => false, - }) - } - - pub fn into_signed_tx(self, chain_config: &ChainConfig) -> WalletResult { - if self.is_fully_signed(chain_config) { - let witnesses = self.witnesses.into_iter().map(|w| w.expect("cannot fail")).collect(); - Ok(SignedTransaction::new(self.tx, witnesses)?) - } else { - Err(WalletError::FailedToConvertPartiallySignedTx(self)) - } - } -} - -impl Signable for PartiallySignedTransaction { - fn inputs(&self) -> Option<&[TxInput]> { - Some(self.tx.inputs()) - } - - fn outputs(&self) -> Option<&[TxOutput]> { - Some(self.tx.outputs()) - } - - fn version_byte(&self) -> Option { - Some(self.tx.version_byte()) - } - - fn flags(&self) -> Option { - Some(self.tx.flags()) - } -} - -impl Transactable for PartiallySignedTransaction { - fn signatures(&self) -> Vec> { - self.witnesses.clone() - } -} diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index a3259fc568..6d81a74d6f 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -247,7 +247,6 @@ impl<'a, T: WalletStorageReadUnlocked> Signer for SoftwareSigner<'a, T> { )), InputWitness::Standard(sig) => match destination { Some(destination) => { - // FIXME: do it via tx-verifier let sighash = signature_hash(sig.sighash_type(), ptx.tx(), &inputs_utxo_refs, i)?; diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 4c19adf98e..29d56d47ff 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -220,7 +220,7 @@ pub enum WalletError { #[error("Sign message error: {0}")] SignMessageError(#[from] SignArbitraryMessageError), #[error("Input cannot be spent {0:?}")] - InputCannotBeSpent(UtxoOutPoint), + InputCannotBeSpent(TxOutput), #[error("Failed to convert partially signed tx to signed")] FailedToConvertPartiallySignedTx(PartiallySignedTransaction), #[error("The specified address is not found in this wallet")] diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 9ec9a15ae2..91e1205399 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -52,7 +52,11 @@ use common::{ address::AddressError, chain::{ block::timestamp::BlockTimestamp, - signature::{inputsig::InputWitness, DestinationSigError, Transactable}, + signature::{ + inputsig::{standard_signature::StandardInputSignature, InputWitness}, + sighash::signature_hash, + DestinationSigError, + }, tokens::{RPCTokenInfo, TokenId}, Block, ChainConfig, Destination, GenBlock, PoolId, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, @@ -777,9 +781,13 @@ impl Controll (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(sig), Some(dest)) => self.verify_tx_signature( + sig, + stx.transaction(), + &inputs_utxos_refs, + input_num, + &dest, + ), }) .collect(); Ok((fees, signature_statuses)) @@ -801,8 +809,8 @@ impl Controll (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(sig)), Some(dest)) => { + self.verify_tx_signature(sig, ptx.tx(), &inputs_utxos_refs, input_num, dest) } (None, _) => SignatureStatus::NotSigned, }) @@ -850,47 +858,32 @@ impl Controll }) } - fn verify_tx_signature( + fn verify_tx_signature( &self, - tx: &S, + sig: &StandardInputSignature, + tx: &Transaction, inputs_utxos_refs: &[Option<&TxOutput>], input_num: usize, dest: &Destination, ) -> SignatureStatus { - let valid = (|| { - let inputs = - tx.inputs().ok_or(DestinationSigError::SignatureVerificationWithoutInputs)?; - let witness = tx - .signatures() - .get(input_num) - .cloned() - .ok_or(DestinationSigError::InvalidSignatureIndex( - input_num, - inputs.len(), - ))? - .ok_or(DestinationSigError::SignatureNotFound)?; - - common::chain::signature::verify_signature( - &self.chain_config, - dest, - tx, - &witness, - inputs_utxos_refs, - input_num, - ) - })(); - - match valid { - Err(DestinationSigError::IncompleteClassicalMultisigSignature( - required_signatures, - num_signatures, - )) => SignatureStatus::PartialMultisig { - required_signatures, - num_signatures, + signature_hash(sig.sighash_type(), tx, inputs_utxos_refs, input_num).map_or( + SignatureStatus::InvalidSignature, + |sighash| { + let valid = sig.verify_signature(&self.chain_config, dest, &sighash); + + match valid { + Err(DestinationSigError::IncompleteClassicalMultisigSignature( + required_signatures, + num_signatures, + )) => SignatureStatus::PartialMultisig { + required_signatures, + num_signatures, + }, + Err(_) => SignatureStatus::InvalidSignature, + Ok(_) => SignatureStatus::FullySigned, + } }, - Err(_) => SignatureStatus::InvalidSignature, - Ok(_) => SignatureStatus::FullySigned, - } + ) } pub async fn compose_transaction( From cc0238d9ce98b47322a1f8248b9598283038651c Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Thu, 20 Jun 2024 14:58:28 +0300 Subject: [PATCH 07/12] Fix clippy --- .../src/signature_destination_getter.rs | 2 +- chainstate/test-suite/src/tests/htlc.rs | 12 ++++++------ .../input_output_policy/tests/outputs_utils.rs | 4 ++-- common/src/chain/transaction/output/mod.rs | 3 ++- common/src/chain/transaction/signature/mod.rs | 2 +- common/src/chain/transaction/signature/tests/mod.rs | 2 +- .../transaction/signature/tests/sign_and_mutate.rs | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/chainstate/test-framework/src/signature_destination_getter.rs b/chainstate/test-framework/src/signature_destination_getter.rs index 4ae6313853..a2270b92fa 100644 --- a/chainstate/test-framework/src/signature_destination_getter.rs +++ b/chainstate/test-framework/src/signature_destination_getter.rs @@ -116,7 +116,7 @@ impl<'a> SignatureDestinationGetter<'a> { // but this is just a double-check. Err(SignatureDestinationGetterError::SigVerifyOfNotSpendableOutput) } - TxOutput::Htlc(_, _) => todo!(), + TxOutput::Htlc(_, _) => unimplemented!(), } } TxInput::Account(outpoint) => match outpoint.account() { diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index 338509ea77..4e3b604260 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -123,7 +123,7 @@ fn spend_htlc_with_secret(#[case] seed: Seed) { ) .add_output(TxOutput::Htlc( OutputValue::Coin(Amount::from_atoms(100)), - htlc, + Box::new(htlc), )) .build(); let tx_1_id = tx_1.transaction().get_id(); @@ -328,7 +328,7 @@ fn refund_htlc(#[case] seed: Seed) { ) .add_output(TxOutput::Htlc( OutputValue::Coin(Amount::from_atoms(100)), - htlc, + Box::new(htlc), )) .build(); let tx_1_id = tx_1.transaction().get_id(); @@ -632,7 +632,7 @@ fn fork_activation(#[case] seed: Seed) { ) .add_output(TxOutput::Htlc( OutputValue::Coin(Amount::from_atoms(100)), - htlc.clone(), + Box::new(htlc.clone()), )) .build(), ) @@ -662,7 +662,7 @@ fn fork_activation(#[case] seed: Seed) { ) .add_output(TxOutput::Htlc( OutputValue::Coin(Amount::from_atoms(100)), - htlc, + Box::new(htlc), )) .build(), ) @@ -751,7 +751,7 @@ fn spend_tokens(#[case] seed: Seed) { token_id: token_v0_id, amount: Amount::from_atoms(1), }))), - htlc.clone(), + Box::new(htlc.clone()), )) .build(); let tx_id = tx.transaction().get_id(); @@ -808,7 +808,7 @@ fn spend_tokens(#[case] seed: Seed) { ) .add_output(TxOutput::Htlc( OutputValue::TokenV1(token_v1_id, amount_to_mint), - htlc, + Box::new(htlc), )) .build(); let mint_token_v1_tx_id = mint_token_v1_tx.transaction().get_id(); diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs index b3513a60fd..593415e41f 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_output_policy/tests/outputs_utils.rs @@ -151,12 +151,12 @@ pub fn transfer() -> TxOutput { pub fn htlc() -> TxOutput { TxOutput::Htlc( OutputValue::Coin(Amount::ZERO), - HashedTimelockContract { + Box::new(HashedTimelockContract { secret_hash: HtlcSecretHash::zero(), spend_key: Destination::AnyoneCanSpend, refund_timelock: OutputTimeLock::ForSeconds(1), refund_key: Destination::AnyoneCanSpend, - }, + }), ) } diff --git a/common/src/chain/transaction/output/mod.rs b/common/src/chain/transaction/output/mod.rs index 03a5412d7a..c0353c9f2f 100644 --- a/common/src/chain/transaction/output/mod.rs +++ b/common/src/chain/transaction/output/mod.rs @@ -146,8 +146,9 @@ pub enum TxOutput { /// Deposit data into the blockchain. This output cannot be spent. #[codec(index = 9)] DataDeposit(Vec), + /// Transfer an output under Hashed TimeLock Contract. #[codec(index = 10)] - Htlc(OutputValue, HashedTimelockContract), + Htlc(OutputValue, Box), } impl TxOutput { diff --git a/common/src/chain/transaction/signature/mod.rs b/common/src/chain/transaction/signature/mod.rs index 372f25b893..a9c3aad64d 100644 --- a/common/src/chain/transaction/signature/mod.rs +++ b/common/src/chain/transaction/signature/mod.rs @@ -139,7 +139,7 @@ pub fn verify_signature( InputWitness::Standard(witness) => verify_standard_input_signature( chain_config, outpoint_destination, - &witness, + witness, tx, inputs_utxos, input_num, diff --git a/common/src/chain/transaction/signature/tests/mod.rs b/common/src/chain/transaction/signature/tests/mod.rs index 698d4860ee..efdc1908eb 100644 --- a/common/src/chain/transaction/signature/tests/mod.rs +++ b/common/src/chain/transaction/signature/tests/mod.rs @@ -760,7 +760,6 @@ fn check_mutate_output( TxOutput::Transfer(v, d) => TxOutput::Transfer(add_value(v), d), TxOutput::LockThenTransfer(v, d, l) => TxOutput::LockThenTransfer(add_value(v), d, l), TxOutput::Burn(v) => TxOutput::Burn(add_value(v)), - TxOutput::Htlc(_, _) => todo!(), TxOutput::CreateStakePool(_, _) => unreachable!(), // TODO: come back to this later TxOutput::ProduceBlockFromStake(_, _) => unreachable!(), // TODO: come back to this later TxOutput::CreateDelegationId(_, _) => unreachable!(), // TODO: come back to this later @@ -768,6 +767,7 @@ fn check_mutate_output( TxOutput::IssueFungibleToken(_) => unreachable!(), TxOutput::IssueNft(_, _, _) => unreachable!(), TxOutput::DataDeposit(_) => unreachable!(), + TxOutput::Htlc(_, _) => unreachable!(), }; let tx = tx_updater.generate_tx().unwrap(); 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 34ff2e8856..86c26282f5 100644 --- a/common/src/chain/transaction/signature/tests/sign_and_mutate.rs +++ b/common/src/chain/transaction/signature/tests/sign_and_mutate.rs @@ -1130,7 +1130,6 @@ fn mutate_output(_rng: &mut impl Rng, tx: &SignedTransactionWithUtxo) -> SignedT TxOutput::Transfer(v, d) => TxOutput::Transfer(add_value(v), d), TxOutput::LockThenTransfer(v, d, l) => TxOutput::LockThenTransfer(add_value(v), d, l), TxOutput::Burn(v) => TxOutput::Burn(add_value(v)), - TxOutput::Htlc(_, _) => todo!(), TxOutput::CreateStakePool(_, _) => unreachable!(), // TODO: come back to this later TxOutput::ProduceBlockFromStake(_, _) => unreachable!(), // TODO: come back to this later TxOutput::CreateDelegationId(_, _) => unreachable!(), // TODO: come back to this later @@ -1138,6 +1137,7 @@ fn mutate_output(_rng: &mut impl Rng, tx: &SignedTransactionWithUtxo) -> SignedT TxOutput::IssueFungibleToken(_) => unreachable!(), // TODO: come back to this later TxOutput::IssueNft(_, _, _) => unreachable!(), // TODO: come back to this later TxOutput::DataDeposit(_) => unreachable!(), + TxOutput::Htlc(_, _) => unreachable!(), }; SignedTransactionWithUtxo { tx: updater.generate_tx().unwrap(), From 83c17ba6a210698ec4c8b685aa51b58eafdf38ce Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Thu, 20 Jun 2024 15:50:30 +0300 Subject: [PATCH 08/12] Add htlc translation tests --- common/src/chain/transaction/output/htlc.rs | 4 ++ mintscript/src/tests/translate/mod.rs | 49 +++++++++++++++++-- .../snap.translate.reward.htlc_00.txt | 1 + .../snap.translate.reward.htlc_01.txt | 1 + .../snap.translate.reward.htlc_02.txt | 1 + .../snap.translate.reward.htlc_03.txt | 1 + .../snap.translate.reward.htlc_04.txt | 1 + .../snap.translate.tlockonly.htlc_00.txt | 1 + .../snap.translate.tlockonly.htlc_01.txt | 1 + .../snap.translate.tlockonly.htlc_02.txt | 1 + .../snap.translate.tlockonly.htlc_03.txt | 1 + .../snap.translate.tlockonly.htlc_04.txt | 1 + .../translate/snap.translate.txn.htlc_00.txt | 4 ++ .../translate/snap.translate.txn.htlc_01.txt | 4 ++ .../translate/snap.translate.txn.htlc_02.txt | 4 ++ .../translate/snap.translate.txn.htlc_03.txt | 4 ++ .../translate/snap.translate.txn.htlc_04.txt | 4 ++ 17 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 mintscript/src/tests/translate/snap.translate.reward.htlc_00.txt create mode 100644 mintscript/src/tests/translate/snap.translate.reward.htlc_01.txt create mode 100644 mintscript/src/tests/translate/snap.translate.reward.htlc_02.txt create mode 100644 mintscript/src/tests/translate/snap.translate.reward.htlc_03.txt create mode 100644 mintscript/src/tests/translate/snap.translate.reward.htlc_04.txt create mode 100644 mintscript/src/tests/translate/snap.translate.tlockonly.htlc_00.txt create mode 100644 mintscript/src/tests/translate/snap.translate.tlockonly.htlc_01.txt create mode 100644 mintscript/src/tests/translate/snap.translate.tlockonly.htlc_02.txt create mode 100644 mintscript/src/tests/translate/snap.translate.tlockonly.htlc_03.txt create mode 100644 mintscript/src/tests/translate/snap.translate.tlockonly.htlc_04.txt create mode 100644 mintscript/src/tests/translate/snap.translate.txn.htlc_00.txt create mode 100644 mintscript/src/tests/translate/snap.translate.txn.htlc_01.txt create mode 100644 mintscript/src/tests/translate/snap.translate.txn.htlc_02.txt create mode 100644 mintscript/src/tests/translate/snap.translate.txn.htlc_03.txt create mode 100644 mintscript/src/tests/translate/snap.translate.txn.htlc_04.txt diff --git a/common/src/chain/transaction/output/htlc.rs b/common/src/chain/transaction/output/htlc.rs index 01d1a49aa3..e627618259 100644 --- a/common/src/chain/transaction/output/htlc.rs +++ b/common/src/chain/transaction/output/htlc.rs @@ -38,6 +38,10 @@ pub struct HtlcSecret { } impl HtlcSecret { + pub fn new(secret: [u8; 32]) -> Self { + Self { secret } + } + pub fn new_from_rng(rng: &mut impl Rng) -> Self { let secret: [u8; 32] = std::array::from_fn(|_| rng.gen::()); Self { secret } diff --git a/mintscript/src/tests/translate/mod.rs b/mintscript/src/tests/translate/mod.rs index 3c1a59b407..e80d5ac5c6 100644 --- a/mintscript/src/tests/translate/mod.rs +++ b/mintscript/src/tests/translate/mod.rs @@ -18,13 +18,17 @@ use self::mocks::MockSigInfoProvider; use super::*; use common::{ chain::{ - block::BlockRewardTransactable, stakelock::StakePoolData, tokens, AccountNonce, - AccountSpending, + block::BlockRewardTransactable, + htlc::{HashedTimelockContract, HtlcSecret, HtlcSecretHash}, + signature::inputsig::authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, + stakelock::StakePoolData, + tokens, AccountNonce, AccountSpending, }, primitives::per_thousand::PerThousand, }; use crypto::vrf::{VRFPrivateKey, VRFPublicKey}; use pos_accounting::{DelegationData, PoolData}; +use serialization::Encode; use tokens_accounting::TokenData; mod mocks; @@ -85,6 +89,10 @@ fn dest_pk(pk_seed: u64) -> Destination { Destination::PublicKey(keypair(pk_seed).1) } +fn dest_ms(pk_seed: u64) -> Destination { + Destination::ClassicMultisig((&keypair(pk_seed).1).into()) +} + fn outpoint_tx(tx_id_byte: u8, index: u32) -> UtxoOutPoint { UtxoOutPoint::new(fake_id::(tx_id_byte).into(), index) } @@ -142,6 +150,17 @@ fn delegate(amount: u128, del_id_byte: u8) -> TestInputInfo { )) } +fn htlc(spend_seed: u64, refund_seed: u64, timelock: OutputTimeLock) -> TestInputInfo { + let amt = coins(1333); + let htlc = HashedTimelockContract { + secret_hash: HtlcSecretHash::from_low_u64_be(13), + spend_key: dest_pk(spend_seed), + refund_timelock: timelock, + refund_key: dest_ms(refund_seed), + }; + tii(TxOutput::Htlc(amt, Box::new(htlc))) +} + fn nosig() -> InputWitness { InputWitness::NoSignature(None) } @@ -151,6 +170,25 @@ fn stdsig(byte: u8) -> InputWitness { InputWitness::Standard(StandardInputSignature::new(sht, vec![byte; 2])) } +fn htlc_stdsig(byte: u8) -> InputWitness { + let sht = SigHashType::default(); + let raw_sig = vec![byte; 2]; + let secret = HtlcSecret::new([6; 32]); + let sig_with_secret = AuthorizedHashedTimelockContractSpend::Secret(secret, raw_sig); + let serialized_sig = sig_with_secret.encode(); + + InputWitness::Standard(StandardInputSignature::new(sht, serialized_sig)) +} + +fn htlc_multisig(byte: u8) -> InputWitness { + let sht = SigHashType::default(); + let raw_sig = vec![byte; 2]; + let sig_with_secret = AuthorizedHashedTimelockContractSpend::Multisig(raw_sig); + let serialized_sig = sig_with_secret.encode(); + + InputWitness::Standard(StandardInputSignature::new(sht, serialized_sig)) +} + fn deleg0() -> (DelegationId, DelegationData) { let data = DelegationData::new(fake_id(0x57), dest_pk(101)); (fake_id(0x75), data) @@ -284,6 +322,11 @@ fn mode_name<'a, T: TranslationMode<'a>>(_: &T) -> &'static str { #[case("mint_00", mint(fake_id(0xa1), 581), stdsig(0x56))] #[case("mint_01", mint(token0().0, 582), stdsig(0x57))] #[case("mint_02", mint(token0().0, 582), nosig())] +#[case("htlc_00", htlc(11, 12, tl_until_height(999_999)), htlc_stdsig(0x54))] +#[case("htlc_01", htlc(13, 14, tl_for_secs(1111)), htlc_stdsig(0x58))] +#[case("htlc_02", htlc(15, 16, tl_until_time(99)), htlc_stdsig(0x53))] +#[case("htlc_03", htlc(17, 18, tl_for_secs(124)), htlc_multisig(0x54))] +#[case("htlc_04", htlc(19, 20, tl_for_blocks(1000)), htlc_multisig(0x55))] fn translate_snap( #[values(TxnMode, RewardMode, TimelockOnly)] mode: impl for<'a> TranslationMode<'a>, #[case] name: &str, @@ -304,5 +347,3 @@ fn translate_snap( expect_test::expect_file![format!("snap.translate.{mode_str}.{name}.txt")].assert_eq(&result); } - -//FIXME: htlc translation tests diff --git a/mintscript/src/tests/translate/snap.translate.reward.htlc_00.txt b/mintscript/src/tests/translate/snap.translate.reward.htlc_00.txt new file mode 100644 index 0000000000..391e4beff7 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.htlc_00.txt @@ -0,0 +1 @@ +ERROR: Illegal output spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.htlc_01.txt b/mintscript/src/tests/translate/snap.translate.reward.htlc_01.txt new file mode 100644 index 0000000000..391e4beff7 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.htlc_01.txt @@ -0,0 +1 @@ +ERROR: Illegal output spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.htlc_02.txt b/mintscript/src/tests/translate/snap.translate.reward.htlc_02.txt new file mode 100644 index 0000000000..391e4beff7 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.htlc_02.txt @@ -0,0 +1 @@ +ERROR: Illegal output spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.htlc_03.txt b/mintscript/src/tests/translate/snap.translate.reward.htlc_03.txt new file mode 100644 index 0000000000..391e4beff7 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.htlc_03.txt @@ -0,0 +1 @@ +ERROR: Illegal output spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.reward.htlc_04.txt b/mintscript/src/tests/translate/snap.translate.reward.htlc_04.txt new file mode 100644 index 0000000000..391e4beff7 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.reward.htlc_04.txt @@ -0,0 +1 @@ +ERROR: Illegal output spend \ No newline at end of file diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_00.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_00.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_00.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_01.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_01.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_01.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_02.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_02.txt new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_02.txt @@ -0,0 +1 @@ +true diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_03.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_03.txt new file mode 100644 index 0000000000..545cbcaa83 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_03.txt @@ -0,0 +1 @@ +after_seconds(124) diff --git a/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_04.txt b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_04.txt new file mode 100644 index 0000000000..e433ae61a8 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.tlockonly.htlc_04.txt @@ -0,0 +1 @@ +after_blocks(1000) diff --git a/mintscript/src/tests/translate/snap.translate.txn.htlc_00.txt b/mintscript/src/tests/translate/snap.translate.txn.htlc_00.txt new file mode 100644 index 0000000000..db9b3f90ab --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.htlc_00.txt @@ -0,0 +1,4 @@ +threshold(2, [ + Hash160(0x50000000000000000000000000000000000000000d, 0x0606060606060606060606060606060606060606060606060606060606060606), + signature(0x020003574c6b846c9a4c555ea75d771d5a40564b9ef37419682da12573e1d8ac27d71e, 0x0101085454), +]) diff --git a/mintscript/src/tests/translate/snap.translate.txn.htlc_01.txt b/mintscript/src/tests/translate/snap.translate.txn.htlc_01.txt new file mode 100644 index 0000000000..3cdde3a8dc --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.htlc_01.txt @@ -0,0 +1,4 @@ +threshold(2, [ + Hash160(0x50000000000000000000000000000000000000000d, 0x0606060606060606060606060606060606060606060606060606060606060606), + signature(0x020002a3fe239606e407ea161143e42c7c3ef0059573466950a910b28289df247df7a3, 0x0101085858), +]) diff --git a/mintscript/src/tests/translate/snap.translate.txn.htlc_02.txt b/mintscript/src/tests/translate/snap.translate.txn.htlc_02.txt new file mode 100644 index 0000000000..a5b9c54abf --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.htlc_02.txt @@ -0,0 +1,4 @@ +threshold(2, [ + Hash160(0x50000000000000000000000000000000000000000d, 0x0606060606060606060606060606060606060606060606060606060606060606), + signature(0x0200039315c9da756f584d5a7fff618d230bf13115a43d63e7c7d464bb513ab6be7bbc, 0x0101085353), +]) diff --git a/mintscript/src/tests/translate/snap.translate.txn.htlc_03.txt b/mintscript/src/tests/translate/snap.translate.txn.htlc_03.txt new file mode 100644 index 0000000000..c2c25f6747 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.htlc_03.txt @@ -0,0 +1,4 @@ +threshold(2, [ + after_seconds(124), + signature(0x041c9bb73a209c49363022813e7197ac80c761d80b, 0x0101085454), +]) diff --git a/mintscript/src/tests/translate/snap.translate.txn.htlc_04.txt b/mintscript/src/tests/translate/snap.translate.txn.htlc_04.txt new file mode 100644 index 0000000000..ee97600eb8 --- /dev/null +++ b/mintscript/src/tests/translate/snap.translate.txn.htlc_04.txt @@ -0,0 +1,4 @@ +threshold(2, [ + after_blocks(1000), + signature(0x04d55789fd7dd4b58f8bdb889a0d31cac70e67df92, 0x0101085555), +]) From de02614ea156aa2590ab0d734e274117a20e18dc Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Fri, 21 Jun 2024 11:16:17 +0300 Subject: [PATCH 09/12] Support in simulation test --- .../scanner-lib/src/sync/tests/simulation.rs | 2 +- chainstate/test-framework/src/key_manager.rs | 61 +++++- .../test-framework/src/random_tx_maker.rs | 187 ++++++++++-------- .../src/signature_destination_getter.rs | 5 +- .../transaction_verifier/input_check/mod.rs | 13 -- mintscript/src/checker/signature.rs | 10 - 6 files changed, 169 insertions(+), 109 deletions(-) diff --git a/api-server/scanner-lib/src/sync/tests/simulation.rs b/api-server/scanner-lib/src/sync/tests/simulation.rs index 6e4b62ae1d..a809dc44ea 100644 --- a/api-server/scanner-lib/src/sync/tests/simulation.rs +++ b/api-server/scanner-lib/src/sync/tests/simulation.rs @@ -335,7 +335,7 @@ async fn simulation( .and_modify(|amount| *amount = (*amount + *to_stake).unwrap()) .or_insert(*to_stake); } - TxOutput::Htlc(_, _) => todo!(), + TxOutput::Htlc(_, _) => unimplemented!(), | TxOutput::CreateDelegationId(_, _) | TxOutput::Transfer(_, _) | TxOutput::LockThenTransfer(_, _, _) diff --git a/chainstate/test-framework/src/key_manager.rs b/chainstate/test-framework/src/key_manager.rs index 3393077f0c..3d449d6ebd 100644 --- a/chainstate/test-framework/src/key_manager.rs +++ b/chainstate/test-framework/src/key_manager.rs @@ -26,6 +26,7 @@ use common::{ sign_classical_multisig_spending, AuthorizedClassicalMultisigSpend, ClassicalMultisigCompletionStatus, }, + htlc::produce_classical_multisig_signature_for_htlc_input, standard_signature::StandardInputSignature, InputWitness, }, @@ -117,6 +118,31 @@ impl KeyManager { } } + pub fn new_2_of_2_multisig_destination( + &mut self, + chain_config: &ChainConfig, + rng: &mut (impl Rng + CryptoRng), + ) -> Destination { + let min_required_signatures = 2; + let num_pub_keys = 2; + let keys: Vec<_> = (0..num_pub_keys) + .map(|_| PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr)) + .collect(); + let pub_keys = keys.iter().map(|k| k.1.clone()).collect(); + let min_required_signatures = NonZeroU8::new(min_required_signatures).unwrap(); + let challenge = + ClassicMultisigChallenge::new(chain_config, min_required_signatures, pub_keys).unwrap(); + let multisig_hash: PublicKeyHash = (&challenge).into(); + self.multisigs.insert( + multisig_hash, + Multisig { + keys, + min_required_signatures, + }, + ); + Destination::ClassicMultisig(multisig_hash) + } + pub fn get_signature( &self, rng: &mut (impl Rng + CryptoRng), @@ -188,10 +214,21 @@ impl KeyManager { match res { ClassicalMultisigCompletionStatus::Complete(sigs) => { - return Some(InputWitness::Standard(StandardInputSignature::new( - sighash_type, - sigs.encode(), - ))); + 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()) + }; + + return Some(InputWitness::Standard(sig)); } ClassicalMultisigCompletionStatus::Incomplete(sigs) => { current_signatures = sigs; @@ -204,3 +241,19 @@ impl KeyManager { } } } + +fn is_htlc_output(output: &TxOutput) -> bool { + match output { + TxOutput::Transfer(_, _) + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::Burn(_) + | TxOutput::CreateStakePool(_, _) + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::DataDeposit(_) => false, + TxOutput::Htlc(_, _) => true, + } +} diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index b299faed04..78f1485e22 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -20,6 +20,7 @@ use crate::{key_manager::KeyManager, TestChainstate}; use chainstate::chainstate_interface::ChainstateInterface; use common::{ chain::{ + htlc::{HashedTimelockContract, HtlcSecretHash}, output_value::OutputValue, stakelock::StakePoolData, timelock::OutputTimeLock, @@ -77,6 +78,40 @@ fn get_random_delegation_data<'a>( }) } +fn get_random_timelock( + rng: &mut impl Rng, + chainstate: &impl ChainstateInterface, +) -> OutputTimeLock { + const MAX_LOCK_FOR_NUM_BLOCKS: u64 = 5; + let target_block_spacing_sec = chainstate.get_chain_config().target_block_spacing().as_secs(); + match rng.gen_range(0..4) { + 0 => OutputTimeLock::ForBlockCount(rng.gen_range(0..=MAX_LOCK_FOR_NUM_BLOCKS)), + 1 => OutputTimeLock::UntilHeight( + chainstate + .get_best_block_height() + .unwrap() + .checked_add(rng.gen_range(0..=MAX_LOCK_FOR_NUM_BLOCKS)) + .unwrap(), + ), + 2 => OutputTimeLock::ForSeconds( + target_block_spacing_sec * rng.gen_range(0..=MAX_LOCK_FOR_NUM_BLOCKS) + + rng.gen_range(0..=target_block_spacing_sec), + ), + 3 => OutputTimeLock::UntilTime( + chainstate + .get_best_block_index() + .unwrap() + .block_timestamp() + .add_int_seconds( + target_block_spacing_sec * rng.gen_range(0..=MAX_LOCK_FOR_NUM_BLOCKS) + + rng.gen_range(0..=target_block_spacing_sec), + ) + .unwrap(), + ), + _ => unreachable!(), + } +} + pub trait StakingPoolsObserver { fn on_pool_created( &mut self, @@ -567,45 +602,7 @@ impl<'a> RandomTxMaker<'a> { key_manager, ), TxOutput::LockThenTransfer(v, _, timelock) => { - let utxo_block_height = self - .chainstate - .utxo(input.utxo_outpoint().unwrap()) - .unwrap() - .unwrap() - .source() - .clone() - .blockchain_height() - .unwrap(); - let utxo_block_id = self - .chainstate - .get_block_id_from_height(&utxo_block_height) - .unwrap() - .unwrap() - .classify(self.chainstate.get_chain_config()); - - let time_of_tx = match utxo_block_id { - GenBlockId::Block(id) => { - self.chainstate.get_block_header(id).unwrap().unwrap().timestamp() - } - GenBlockId::Genesis(_) => { - self.chainstate.get_chain_config().genesis_block().timestamp() - } - }; - let current_time = self - .chainstate - .calculate_median_time_past(&self.chainstate.get_best_block_id().unwrap()) - .unwrap(); - let current_height = self.chainstate.get_best_block_height().unwrap(); - - let timelock_passed = tx_verifier::timelock_check::check_timelock( - &utxo_block_height, - &time_of_tx, - timelock, - ¤t_height, - ¤t_time, - input.utxo_outpoint().unwrap(), - ) - .is_ok(); + let timelock_passed = self.check_timelock(&input, timelock); if timelock_passed { self.spend_output_value( @@ -662,12 +659,28 @@ impl<'a> RandomTxMaker<'a> { new_inputs.push(input); (new_inputs, new_outputs) } + TxOutput::Htlc(v, htlc) => { + // TODO: currently only refund spending is supported + let timelock_passed = self.check_timelock(&input, &htlc.refund_timelock); + + if timelock_passed { + self.spend_output_value( + rng, + tokens_cache, + pos_accounting_cache, + input, + v, + key_manager, + ) + } else { + (Vec::new(), Vec::new()) + } + } TxOutput::Burn(_) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::DataDeposit(_) => unreachable!(), - TxOutput::Htlc(_, _) => unimplemented!(), }; result_inputs.extend(new_inputs); @@ -779,7 +792,6 @@ impl<'a> RandomTxMaker<'a> { ); TxOutput::CreateStakePool(dummy_pool_id, Box::new(pool_data)) } else { - const MAX_LOCK_FOR_NUM_BLOCKS: u64 = 5; if rng.gen_bool(0.3) { // Send coins to random delegation if let Some((delegation_id, _)) = get_random_delegation_data( @@ -796,51 +808,24 @@ impl<'a> RandomTxMaker<'a> { let destination = key_manager.new_destination(self.chainstate.get_chain_config(), rng); - let target_block_spacing_sec = - self.chainstate.get_chain_config().target_block_spacing().as_secs(); + let timelock = get_random_timelock(rng, self.chainstate); match rng.gen_range(0..5) { 0 => TxOutput::LockThenTransfer( OutputValue::Coin(new_value), destination, - OutputTimeLock::ForBlockCount( - rng.gen_range(0..=MAX_LOCK_FOR_NUM_BLOCKS), - ), + timelock, ), - 1 => TxOutput::LockThenTransfer( + 1 => TxOutput::Htlc( OutputValue::Coin(new_value), - destination, - OutputTimeLock::UntilHeight( - self.chainstate - .get_best_block_height() - .unwrap() - .checked_add(rng.gen_range(0..=MAX_LOCK_FOR_NUM_BLOCKS)) - .unwrap(), - ), - ), - 2 => TxOutput::LockThenTransfer( - OutputValue::Coin(new_value), - destination, - OutputTimeLock::ForSeconds( - target_block_spacing_sec - * rng.gen_range(0..=MAX_LOCK_FOR_NUM_BLOCKS) - + rng.gen_range(0..=target_block_spacing_sec), - ), - ), - 3 => TxOutput::LockThenTransfer( - OutputValue::Coin(new_value), - destination, - OutputTimeLock::UntilTime( - self.chainstate - .get_best_block_index() - .unwrap() - .block_timestamp() - .add_int_seconds( - target_block_spacing_sec - * rng.gen_range(0..=MAX_LOCK_FOR_NUM_BLOCKS) - + rng.gen_range(0..=target_block_spacing_sec), - ) - .unwrap(), - ), + Box::new(HashedTimelockContract { + secret_hash: HtlcSecretHash::zero(), + spend_key: destination, + refund_timelock: timelock, + refund_key: key_manager.new_2_of_2_multisig_destination( + self.chainstate.get_chain_config(), + rng, + ), + }), ), _ => TxOutput::Transfer(OutputValue::Coin(new_value), destination), } @@ -1055,4 +1040,46 @@ impl<'a> RandomTxMaker<'a> { (outputs, new_staking_pools) } + + fn check_timelock(&self, input: &TxInput, timelock: &OutputTimeLock) -> bool { + let utxo_block_height = self + .chainstate + .utxo(input.utxo_outpoint().unwrap()) + .unwrap() + .unwrap() + .source() + .clone() + .blockchain_height() + .unwrap(); + let utxo_block_id = self + .chainstate + .get_block_id_from_height(&utxo_block_height) + .unwrap() + .unwrap() + .classify(self.chainstate.get_chain_config()); + + let time_of_tx = match utxo_block_id { + GenBlockId::Block(id) => { + self.chainstate.get_block_header(id).unwrap().unwrap().timestamp() + } + GenBlockId::Genesis(_) => { + self.chainstate.get_chain_config().genesis_block().timestamp() + } + }; + let current_time = self + .chainstate + .calculate_median_time_past(&self.chainstate.get_best_block_id().unwrap()) + .unwrap(); + let current_height = self.chainstate.get_best_block_height().unwrap(); + + tx_verifier::timelock_check::check_timelock( + &utxo_block_height, + &time_of_tx, + timelock, + ¤t_height, + ¤t_time, + input.utxo_outpoint().unwrap(), + ) + .is_ok() + } } diff --git a/chainstate/test-framework/src/signature_destination_getter.rs b/chainstate/test-framework/src/signature_destination_getter.rs index a2270b92fa..66400a6fe5 100644 --- a/chainstate/test-framework/src/signature_destination_getter.rs +++ b/chainstate/test-framework/src/signature_destination_getter.rs @@ -116,7 +116,10 @@ impl<'a> SignatureDestinationGetter<'a> { // but this is just a double-check. Err(SignatureDestinationGetterError::SigVerifyOfNotSpendableOutput) } - TxOutput::Htlc(_, _) => unimplemented!(), + TxOutput::Htlc(_, htlc) => { + // TODO: consider spending with spend key + secret not only multisig + Ok(htlc.refund_key.clone()) + } } } TxInput::Account(outpoint) => match outpoint.account() { 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 484e6a7329..2126a46af3 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/mod.rs @@ -534,16 +534,3 @@ where Ok(()) } - -//pub fn verify_signature( -// chain_config: &ChainConfig, -// outpoint_destination: &Destination, -// tx: &T, -// inputs_utxos: &[Option<&TxOutput>], -// input_num: usize, -//) -> Result<(), ConnectTransactionError> { -// let mut checker = mintscript::checker::StandardSignatureChecker; -// checker.check_signature(ctx, destination, signature)?; -// -// Ok(()) -//} diff --git a/mintscript/src/checker/signature.rs b/mintscript/src/checker/signature.rs index 987d070cf1..a38c4db5b6 100644 --- a/mintscript/src/checker/signature.rs +++ b/mintscript/src/checker/signature.rs @@ -78,16 +78,6 @@ impl SignatureChecker for StandardSignatureChecker { let input_num = ctx.input_num(); let chain_config = ctx.chain_config(); - // Note: The verify_signature function below looks up the signature in the transaction - // itself. In the future, the logic to find signature may not be so easy, so the signature - // from the script should be taken and passed to the signature verification code. This - // assertion should then go away. This goes hand in hand with turning Destinations, not - // just outputs/input pairs into script. - //assert_eq!( - // tx.signatures().get(input_num), - // Some(&Some(signature.clone())) - //); - common::chain::signature::verify_signature( chain_config, destination, From 4e7b63e80c338912f94f24e411d0eff26a57c94e Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Fri, 21 Jun 2024 16:19:12 +0300 Subject: [PATCH 10/12] Fix api simulation test --- .../scanner-lib/src/sync/tests/simulation.rs | 6 ++-- .../test-framework/src/block_builder.rs | 7 +++- .../test-framework/src/pos_block_builder.rs | 7 +++- .../test-framework/src/random_tx_maker.rs | 35 ++++++++++++------- .../src/tests/tx_verification_simulation.rs | 4 +-- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/api-server/scanner-lib/src/sync/tests/simulation.rs b/api-server/scanner-lib/src/sync/tests/simulation.rs index a809dc44ea..ebbf2dd89e 100644 --- a/api-server/scanner-lib/src/sync/tests/simulation.rs +++ b/api-server/scanner-lib/src/sync/tests/simulation.rs @@ -236,7 +236,7 @@ async fn simulation( let mut block_builder = tf.make_pos_block_builder().with_random_staking_pool(&mut rng); for _ in 0..rng.gen_range(10..max_tx_per_block) { - block_builder = block_builder.add_test_transaction(&mut rng); + block_builder = block_builder.add_test_transaction(&mut rng, false); } let block = block_builder.build(&mut rng); @@ -335,11 +335,11 @@ async fn simulation( .and_modify(|amount| *amount = (*amount + *to_stake).unwrap()) .or_insert(*to_stake); } - TxOutput::Htlc(_, _) => unimplemented!(), | TxOutput::CreateDelegationId(_, _) | TxOutput::Transfer(_, _) | TxOutput::LockThenTransfer(_, _, _) - | TxOutput::ProduceBlockFromStake(_, _) => {} + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::Htlc(_, _) => {} }); tx.inputs().iter().for_each(|inp| match inp { diff --git a/chainstate/test-framework/src/block_builder.rs b/chainstate/test-framework/src/block_builder.rs index bd9844ca20..58b2122580 100644 --- a/chainstate/test-framework/src/block_builder.rs +++ b/chainstate/test-framework/src/block_builder.rs @@ -120,7 +120,11 @@ impl<'f> BlockBuilder<'f> { } /// Adds a transaction that uses random utxos and accounts - pub fn add_test_transaction(mut self, rng: &mut (impl Rng + CryptoRng)) -> Self { + pub fn add_test_transaction( + mut self, + rng: &mut (impl Rng + CryptoRng), + supper_htlc: bool, + ) -> Self { let utxo_set = self .framework .storage @@ -148,6 +152,7 @@ impl<'f> BlockBuilder<'f> { &self.pos_accounting_store, None, account_nonce_getter, + supper_htlc, ) .make( rng, diff --git a/chainstate/test-framework/src/pos_block_builder.rs b/chainstate/test-framework/src/pos_block_builder.rs index 7afdd1bc85..80fd56b762 100644 --- a/chainstate/test-framework/src/pos_block_builder.rs +++ b/chainstate/test-framework/src/pos_block_builder.rs @@ -360,7 +360,11 @@ impl<'f> PoSBlockBuilder<'f> { } /// Adds a transaction that uses random utxos and accounts - pub fn add_test_transaction(mut self, rng: &mut (impl Rng + CryptoRng)) -> Self { + pub fn add_test_transaction( + mut self, + rng: &mut (impl Rng + CryptoRng), + support_htlc: bool, + ) -> Self { let utxo_set = self .framework .storage @@ -388,6 +392,7 @@ impl<'f> PoSBlockBuilder<'f> { &self.pos_accounting_store, self.staking_pool, account_nonce_getter, + support_htlc, ) .make( rng, diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index 78f1485e22..e68b617f32 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -144,6 +144,8 @@ pub struct RandomTxMaker<'a> { account_command_used: bool, + support_htlc: bool, + // There can be only one Unmint operation per transaction. // But it's unknown in advance which token burn would be utilized by unmint operation // so we have to collect all burns for all tokens just in case. @@ -161,6 +163,7 @@ impl<'a> RandomTxMaker<'a> { pos_accounting_store: &'a InMemoryPoSAccounting, staking_pool: Option, account_nonce_getter: Box Option + 'a>, + support_htlc: bool, ) -> Self { Self { chainstate, @@ -174,6 +177,7 @@ impl<'a> RandomTxMaker<'a> { stake_pool_can_be_created: true, delegation_can_be_created: true, account_command_used: false, + support_htlc, unmint_for: None, total_tokens_burned: BTreeMap::new(), fee_input: None, @@ -815,18 +819,25 @@ impl<'a> RandomTxMaker<'a> { destination, timelock, ), - 1 => TxOutput::Htlc( - OutputValue::Coin(new_value), - Box::new(HashedTimelockContract { - secret_hash: HtlcSecretHash::zero(), - spend_key: destination, - refund_timelock: timelock, - refund_key: key_manager.new_2_of_2_multisig_destination( - self.chainstate.get_chain_config(), - rng, - ), - }), - ), + 1 => { + if self.support_htlc { + TxOutput::Htlc( + OutputValue::Coin(new_value), + Box::new(HashedTimelockContract { + secret_hash: HtlcSecretHash::zero(), + spend_key: destination, + refund_timelock: timelock, + refund_key: key_manager + .new_2_of_2_multisig_destination( + self.chainstate.get_chain_config(), + rng, + ), + }), + ) + } else { + TxOutput::Transfer(OutputValue::Coin(new_value), destination) + } + } _ => TxOutput::Transfer(OutputValue::Coin(new_value), destination), } } diff --git a/chainstate/test-suite/src/tests/tx_verification_simulation.rs b/chainstate/test-suite/src/tests/tx_verification_simulation.rs index c02aec85a0..e7fe110c3c 100644 --- a/chainstate/test-suite/src/tests/tx_verification_simulation.rs +++ b/chainstate/test-suite/src/tests/tx_verification_simulation.rs @@ -115,7 +115,7 @@ fn simulation(#[case] seed: Seed, #[case] max_blocks: usize, #[case] max_tx_per_ let mut block_builder = tf.make_pos_block_builder().with_random_staking_pool(&mut rng); for _ in 0..rng.gen_range(10..max_tx_per_block) { - block_builder = block_builder.add_test_transaction(&mut rng); + block_builder = block_builder.add_test_transaction(&mut rng, true); } let block = block_builder.build(&mut rng); @@ -151,7 +151,7 @@ fn simulation(#[case] seed: Seed, #[case] max_blocks: usize, #[case] max_tx_per_ let mut block_builder = tf2.make_pos_block_builder().with_random_staking_pool(&mut rng); for _ in 0..rng.gen_range(10..max_tx_per_block) { - block_builder = block_builder.add_test_transaction(&mut rng); + block_builder = block_builder.add_test_transaction(&mut rng, true); } let block = block_builder.build(&mut rng); From 08c1e848ad4417a4c0bd65365849796c0df9c9f4 Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Fri, 21 Jun 2024 16:58:35 +0300 Subject: [PATCH 11/12] Address review comments --- .../scanner-lib/src/blockchain_state/mod.rs | 6 ++-- common/src/chain/config/builder.rs | 10 +++--- common/src/chain/transaction/output/htlc.rs | 36 +++++++++++++------ common/src/chain/transaction/output/mod.rs | 9 ----- mintscript/src/translate.rs | 4 ++- wallet/src/account/transaction_list/mod.rs | 2 +- wallet/src/send_request/mod.rs | 2 +- wallet/types/src/utxo_types.rs | 2 +- 8 files changed, 39 insertions(+), 32 deletions(-) diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index 770ebe6a4a..3ff5d4c971 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -1201,7 +1201,7 @@ async fn update_tables_from_transaction_inputs( ) .await; } - TxOutput::Htlc(_, _) => {} // TODO: support htlc + TxOutput::Htlc(_, _) => {} // TODO(HTLC) TxOutput::LockThenTransfer(output_value, destination, _) | TxOutput::Transfer(output_value, destination) => { let address = Address::::new(&chain_config, destination) @@ -1616,7 +1616,7 @@ async fn update_tables_from_transaction_outputs( .expect("Unable to set locked utxo"); } } - TxOutput::Htlc(_, _) => {} // TODO: support htlc + TxOutput::Htlc(_, _) => {} // TODO(HTLC) } } @@ -1816,6 +1816,6 @@ fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> { | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) => None, - TxOutput::Htlc(_, _) => None, // TODO: support htlc + TxOutput::Htlc(_, _) => None, // TODO(HTLC) } } diff --git a/common/src/chain/config/builder.rs b/common/src/chain/config/builder.rs index e6cf6d5009..02b13c6d59 100644 --- a/common/src/chain/config/builder.rs +++ b/common/src/chain/config/builder.rs @@ -50,9 +50,9 @@ const TESTNET_TOKEN_FORK_HEIGHT: BlockHeight = BlockHeight::new(78440); // The fork, at which we upgrade chainstate to distribute reward to staker proportionally to their balance // and change various tokens fees const TESTNET_STAKER_REWARD_AND_TOKENS_FEE_FORK_HEIGHT: BlockHeight = BlockHeight::new(138244); -// The fork, at which txs with htlc outputs become valid -const TESTNET_HTLC_FORK_HEIGHT: BlockHeight = BlockHeight::new(99999999); -const MAINNET_HTLC_FORK_HEIGHT: BlockHeight = BlockHeight::new(99999999); +// The fork, at which txs with htlc and orders outputs become valid +const TESTNET_HTLC_AND_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(99999999); +const MAINNET_HTLC_AND_ORDERS_FORK_HEIGHT: BlockHeight = BlockHeight::new(99999999); impl ChainType { fn default_genesis_init(&self) -> GenesisBlockInit { @@ -170,7 +170,7 @@ impl ChainType { ), ), ( - MAINNET_HTLC_FORK_HEIGHT, + MAINNET_HTLC_AND_ORDERS_FORK_HEIGHT, ChainstateUpgrade::new( TokenIssuanceVersion::V1, RewardDistributionVersion::V1, @@ -223,7 +223,7 @@ impl ChainType { ), ), ( - TESTNET_HTLC_FORK_HEIGHT, + TESTNET_HTLC_AND_ORDERS_FORK_HEIGHT, ChainstateUpgrade::new( TokenIssuanceVersion::V1, RewardDistributionVersion::V1, diff --git a/common/src/chain/transaction/output/htlc.rs b/common/src/chain/transaction/output/htlc.rs index e627618259..decaba8b00 100644 --- a/common/src/chain/transaction/output/htlc.rs +++ b/common/src/chain/transaction/output/htlc.rs @@ -21,6 +21,15 @@ use serialization::{Decode, Encode}; use super::{timelock::OutputTimeLock, Destination}; +fixed_hash::construct_fixed_hash! { + #[derive(Encode, Decode, serde::Serialize, serde::Deserialize)] + pub struct SecretHash(20); +} + +impl rpc_description::HasValueHint for SecretHash { + const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; +} + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] pub struct HashedTimelockContract { // can be spent either by a specific address that knows the secret @@ -108,19 +117,24 @@ mod tests { } #[rstest] - #[case("\"0000000000000000000000000000000000000000\"")] - #[case("\"0000000000000000000000000000000000000001\"")] - #[case("\"ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd\"")] - #[case("\"e4732fe6f1ed1cddc2ed4b328fff5224276e3f6f\"")] - #[case("\"0103b9683e51e5aba83b8a34c9b98ce67d66136c\"")] - fn deserialize_valid(#[case] s: String) { - serde_json::from_str::(&s).unwrap(); + #[case("\"0000000000000000000000000000000000000000\"", [0;20])] + #[case("\"0000000000000000000000000000000000000001\"", [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1])] + #[case("\"ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd\"", [0xac,0x7b,0x96,0x0a,0x8d,0x03,0x70,0x5d,0x1a,0xce,0x08,0xb1,0xa1,0x9d,0xa3,0xfd,0xcc,0x99,0xdd,0xbd])] + #[case("\"e4732fe6f1ed1cddc2ed4b328fff5224276e3f6f\"", [0xe4,0x73,0x2f,0xe6,0xf1,0xed,0x1c,0xdd,0xc2,0xed,0x4b,0x32,0x8f,0xff,0x52,0x24,0x27,0x6e,0x3f,0x6f])] + #[case("\"0103b9683e51e5aba83b8a34c9b98ce67d66136c\"", [0x01,0x03,0xb9,0x68,0x3e,0x51,0xe5,0xab,0xa8,0x3b,0x8a,0x34,0xc9,0xb9,0x8c,0xe6,0x7d,0x66,0x13,0x6c])] + fn deserialize_valid(#[case] str: String, #[case] expected: [u8; 20]) { + let result = serde_json::from_str::(&str).unwrap(); + assert_eq!(result, HtlcSecretHash::from_slice(&expected)); } #[rstest] - #[case("\"00000000000000000000000000000000000000000000000000000000000000\"")] - #[case("\"000000000000000000000000000000000invalid\"")] - fn deserialize_invalid(#[case] s: String) { - serde_json::from_str::(&s).unwrap_err(); + #[case( + "\"00000000000000000000000000000000000000000000000000000000000000\"", + "Invalid input length" + )] + #[case("\"000000000000000000000000000000000invalid\"", "Invalid character")] + fn deserialize_invalid(#[case] s: String, #[case] expected_msg: String) { + let err = serde_json::from_str::(&s).unwrap_err(); + assert!(err.to_string().contains(&expected_msg)); } } diff --git a/common/src/chain/transaction/output/mod.rs b/common/src/chain/transaction/output/mod.rs index c0353c9f2f..c075e2c6a6 100644 --- a/common/src/chain/transaction/output/mod.rs +++ b/common/src/chain/transaction/output/mod.rs @@ -42,15 +42,6 @@ pub mod output_value; pub mod stakelock; pub mod timelock; -fixed_hash::construct_fixed_hash! { - #[derive(Encode, Decode, serde::Serialize, serde::Deserialize)] - pub struct SecretHash(20); -} - -impl rpc_description::HasValueHint for SecretHash { - const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; -} - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, VariantCount)] pub enum Destination { #[codec(index = 0)] diff --git a/mintscript/src/translate.rs b/mintscript/src/translate.rs index d276624ee9..7bdaa1e0ca 100644 --- a/mintscript/src/translate.rs +++ b/mintscript/src/translate.rs @@ -258,7 +258,9 @@ impl TranslateInput for TimelockOnly { Ok(WitnessScript::timelock(*timelock)) } TxOutput::Htlc(_, htlc) => match ctx.witness() { - InputWitness::NoSignature(_) => Ok(WitnessScript::TRUE), + InputWitness::NoSignature(_) => Err(TranslationError::SignatureError( + DestinationSigError::SignatureNotFound, + )), InputWitness::Standard(sig) => { let htlc_spend = AuthorizedHashedTimelockContractSpend::from_data(sig.raw_signature())?; diff --git a/wallet/src/account/transaction_list/mod.rs b/wallet/src/account/transaction_list/mod.rs index e4ab463dc8..5cf323367d 100644 --- a/wallet/src/account/transaction_list/mod.rs +++ b/wallet/src/account/transaction_list/mod.rs @@ -113,7 +113,7 @@ fn own_output(key_chain: &AccountKeyChainImpl, output: &TxOutput) -> bool { TxOutput::Transfer(_, dest) | TxOutput::LockThenTransfer(_, dest, _) => KeyPurpose::ALL .iter() .any(|purpose| key_chain.get_leaf_key_chain(*purpose).is_destination_mine(dest)), - TxOutput::Htlc(_, _) => false, // TODO: support htlc + TxOutput::Htlc(_, _) => false, // TODO(HTLC) TxOutput::Burn(_) | TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index 15dbdb16e8..ab727178db 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -307,7 +307,7 @@ where | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) | TxOutput::DataDeposit(_) => None, - TxOutput::Htlc(_, _) => None, // TODO: support htlc + TxOutput::Htlc(_, _) => None, // TODO(HTLC) } } diff --git a/wallet/types/src/utxo_types.rs b/wallet/types/src/utxo_types.rs index be9bcf161f..64555fe2ad 100644 --- a/wallet/types/src/utxo_types.rs +++ b/wallet/types/src/utxo_types.rs @@ -53,7 +53,7 @@ pub fn get_utxo_type(output: &TxOutput) -> Option { | TxOutput::DelegateStaking(_, _) | TxOutput::IssueFungibleToken(_) | TxOutput::DataDeposit(_) => None, - TxOutput::Htlc(_, _) => None, // TODO: support htlc + TxOutput::Htlc(_, _) => None, // TODO(HTLC) } } pub fn get_utxo_state(output: &TxState) -> UtxoState { From 80c76ccae4d2dcf9a6a911d61bc1d4dbae02f14d Mon Sep 17 00:00:00 2001 From: Heorhii Azarov Date: Mon, 24 Jun 2024 16:08:03 +0300 Subject: [PATCH 12/12] More small fixes --- chainstate/test-framework/src/block_builder.rs | 4 ++-- chainstate/test-framework/src/random_tx_maker.rs | 3 ++- common/src/chain/transaction/output/htlc.rs | 9 --------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/chainstate/test-framework/src/block_builder.rs b/chainstate/test-framework/src/block_builder.rs index 58b2122580..bca00c0916 100644 --- a/chainstate/test-framework/src/block_builder.rs +++ b/chainstate/test-framework/src/block_builder.rs @@ -123,7 +123,7 @@ impl<'f> BlockBuilder<'f> { pub fn add_test_transaction( mut self, rng: &mut (impl Rng + CryptoRng), - supper_htlc: bool, + support_htlc: bool, ) -> Self { let utxo_set = self .framework @@ -152,7 +152,7 @@ impl<'f> BlockBuilder<'f> { &self.pos_accounting_store, None, account_nonce_getter, - supper_htlc, + support_htlc, ) .make( rng, diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index e68b617f32..c482c4bd94 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -838,7 +838,8 @@ impl<'a> RandomTxMaker<'a> { TxOutput::Transfer(OutputValue::Coin(new_value), destination) } } - _ => TxOutput::Transfer(OutputValue::Coin(new_value), destination), + 2..=4 => TxOutput::Transfer(OutputValue::Coin(new_value), destination), + _ => unreachable!(), } } }) diff --git a/common/src/chain/transaction/output/htlc.rs b/common/src/chain/transaction/output/htlc.rs index decaba8b00..28439570b4 100644 --- a/common/src/chain/transaction/output/htlc.rs +++ b/common/src/chain/transaction/output/htlc.rs @@ -21,15 +21,6 @@ use serialization::{Decode, Encode}; use super::{timelock::OutputTimeLock, Destination}; -fixed_hash::construct_fixed_hash! { - #[derive(Encode, Decode, serde::Serialize, serde::Deserialize)] - pub struct SecretHash(20); -} - -impl rpc_description::HasValueHint for SecretHash { - const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::HEX_STRING; -} - #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, serde::Serialize, serde::Deserialize)] pub struct HashedTimelockContract { // can be spent either by a specific address that knows the secret