From 1c8bf9e4d007c6b8dbc60639243a2393af4168df Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 7 Mar 2025 08:28:55 +0100 Subject: [PATCH] Add support for signing with standalone keys to Trezor --- wallet/src/signer/mod.rs | 3 + wallet/src/signer/signer_test_helpers.rs | 623 +++++++++++++++++++++ wallet/src/signer/software_signer/tests.rs | 432 +------------- wallet/src/signer/trezor_signer/mod.rs | 376 ++++++++++--- wallet/src/signer/trezor_signer/tests.rs | 491 +--------------- 5 files changed, 938 insertions(+), 987 deletions(-) create mode 100644 wallet/src/signer/signer_test_helpers.rs diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index 837f79947..15b76571c 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -13,6 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(test)] +mod signer_test_helpers; + use std::sync::Arc; use common::{ diff --git a/wallet/src/signer/signer_test_helpers.rs b/wallet/src/signer/signer_test_helpers.rs new file mode 100644 index 000000000..3f7f7bf51 --- /dev/null +++ b/wallet/src/signer/signer_test_helpers.rs @@ -0,0 +1,623 @@ +// Copyright (c) 2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ops::{Add, Div, Sub}; +use std::sync::Arc; + +use common::chain::htlc::{HashedTimelockContract, HtlcSecret}; +use common::chain::stakelock::StakePoolData; +use common::chain::timelock::OutputTimeLock; +use common::chain::tokens::{NftIssuance, NftIssuanceV0, TokenId, TokenIssuance}; +use common::chain::{ + AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, OrderData, + OutPointSourceId, PoolId, +}; +use common::primitives::per_thousand::PerThousand; +use common::primitives::BlockHeight; +use common::{ + chain::{ + config::create_regtest, + output_value::OutputValue, + signature::{inputsig::arbitrary_message::produce_message_challenge, DestinationSigError}, + ChainConfig, Destination, GenBlock, SignedTransactionIntent, Transaction, TxInput, + TxOutput, + }, + primitives::{Amount, Id, H256}, +}; +use crypto::key::hdkd::u31::U31; +use crypto::key::{KeyKind, PrivateKey}; +use itertools::izip; +use randomness::{CryptoRng, Rng}; +use tx_verifier::error::{InputCheckErrorPayload, ScriptError}; +use wallet_storage::{DefaultBackend, Store, StoreTxRwUnlocked, Transactional}; +use wallet_types::partially_signed_transaction::{ + OrderAdditionalInfo, TokenAdditionalInfo, TxAdditionalInfo, +}; +use wallet_types::{account_info::DEFAULT_ACCOUNT_INDEX, seed_phrase::StoreSeedPhrase, KeyPurpose}; + +use crate::key_chain::AccountKeyChainImplSoftware; +use crate::signer::SignerError; +use crate::SendRequest; +use crate::{ + key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}, + Account, +}; + +use super::Signer; + +const MNEMONIC: &str = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +pub fn test_sign_message(rng: &mut (impl Rng + CryptoRng), make_signer: F) +where + F: Fn(Arc, U31) -> S, + S: Signer, +{ + let chain_config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let mut account = account_from_mnemonic(&chain_config, &mut db_tx, DEFAULT_ACCOUNT_INDEX); + + let pkh_destination = account + .get_new_address(&mut db_tx, KeyPurpose::ReceiveFunds) + .unwrap() + .1 + .into_object(); + let pkh = match pkh_destination { + Destination::PublicKeyHash(pkh) => pkh, + _ => panic!("not a public key hash destination"), + }; + let pk = account.find_corresponding_pub_key(&pkh).unwrap(); + let pk_destination = Destination::PublicKey(pk); + + let (standalone_private_key, standalone_pk) = + PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + + account + .add_standalone_private_key(&mut db_tx, standalone_private_key, None) + .unwrap(); + let standalone_pk_destination = Destination::PublicKey(standalone_pk); + + for destination in [pkh_destination, pk_destination, standalone_pk_destination] { + let mut signer = make_signer(chain_config.clone(), account.account_index()); + + let message = vec![rng.gen::(), rng.gen::(), rng.gen::()]; + let res = signer + .sign_challenge(&message, &destination, account.key_chain(), &db_tx) + .unwrap(); + + let message_challenge = produce_message_challenge(&message); + res.verify_signature(&chain_config, &destination, &message_challenge).unwrap(); + } + + // Check we cannot signe if we don't know the destination + let (_, random_pk) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + let random_pk_destination = Destination::PublicKey(random_pk); + + let mut signer = make_signer(chain_config.clone(), account.account_index()); + + let message = vec![rng.gen::(), rng.gen::(), rng.gen::()]; + let err = signer + .sign_challenge( + &message, + &random_pk_destination, + account.key_chain(), + &db_tx, + ) + .unwrap_err(); + + assert_eq!(err, SignerError::DestinationNotFromThisWallet); +} + +pub fn test_sign_transaction_intent(rng: &mut (impl Rng + CryptoRng), make_signer: F) +where + F: Fn(Arc, U31) -> S, + S: Signer, +{ + use common::primitives::Idable; + + let chain_config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let mut account = account_from_mnemonic(&chain_config, &mut db_tx, DEFAULT_ACCOUNT_INDEX); + + let (standalone_private_key, standalone_pk) = + PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + + account + .add_standalone_private_key(&mut db_tx, standalone_private_key, None) + .unwrap(); + let standalone_pk_destination = Destination::PublicKey(standalone_pk); + + let mut signer = make_signer(chain_config.clone(), account.account_index()); + + let inputs: Vec = (0..rng.gen_range(3..6)) + .map(|_| { + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(rng)).into() + } else { + Id::::new(H256::random_using(rng)).into() + }; + TxInput::from_utxo(source_id, rng.next_u32()) + }) + .collect(); + let num_inputs = inputs.len(); + let mut input_destinations: Vec<_> = (1..num_inputs) + .map(|_| { + let pkh_destination = account + .get_new_address(&mut db_tx, KeyPurpose::ReceiveFunds) + .unwrap() + .1 + .into_object(); + if rng.gen::() { + pkh_destination + } else { + let pkh = match pkh_destination { + Destination::PublicKeyHash(pkh) => pkh, + _ => panic!("not a public key hash destination"), + }; + let pk = account.find_corresponding_pub_key(&pkh).unwrap(); + Destination::PublicKey(pk) + } + }) + .chain([standalone_pk_destination]) + .collect(); + + let tx = Transaction::new( + 0, + inputs, + vec![TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(rng.gen())), + account.get_new_address(&mut db_tx, KeyPurpose::Change).unwrap().1.into_object(), + )], + ) + .unwrap(); + + let intent: String = [rng.gen::(), rng.gen::(), rng.gen::()].iter().collect(); + let res = signer + .sign_transaction_intent( + &tx, + &input_destinations, + &intent, + account.key_chain(), + &db_tx, + ) + .unwrap(); + + let expected_signed_message = + SignedTransactionIntent::get_message_to_sign(&intent, &tx.get_id()); + res.verify(&chain_config, &input_destinations, &expected_signed_message) + .unwrap(); + + // cannot sign when there is a random destination + let (_, random_pk) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + let random_pk_destination = Destination::PublicKey(random_pk); + input_destinations[rng.gen_range(0..num_inputs)] = random_pk_destination; + + let err = signer + .sign_transaction_intent( + &tx, + &input_destinations, + &intent, + account.key_chain(), + &db_tx, + ) + .unwrap_err(); + + assert_eq!(err, SignerError::DestinationNotFromThisWallet); +} + +pub fn test_sign_transaction(rng: &mut (impl Rng + CryptoRng), make_signer: F) +where + F: Fn(Arc, U31) -> S, + S: Signer, +{ + use std::num::NonZeroU8; + + use common::{ + chain::{ + classic_multisig::ClassicMultisigChallenge, + tokens::{IsTokenUnfreezable, Metadata, TokenIssuanceV1}, + OrderId, + }, + primitives::amount::UnsignedIntType, + }; + use crypto::vrf::VRFPrivateKey; + use serialization::extras::non_empty_vec::DataOrNoVec; + + let chain_config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let mut account = account_from_mnemonic(&chain_config, &mut db_tx, DEFAULT_ACCOUNT_INDEX); + let mut account2 = account_from_mnemonic(&chain_config, &mut db_tx, U31::ONE); + + let (standalone_private_key, standalone_pk) = + PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + + account + .add_standalone_private_key(&mut db_tx, standalone_private_key, None) + .unwrap(); + let standalone_pk_destination = Destination::PublicKey(standalone_pk.clone()); + + let amounts: Vec = (0..(2 + rng.next_u32() % 5)) + .map(|_| Amount::from_atoms(rng.gen_range(10..100) as UnsignedIntType)) + .collect(); + + let total_amount = amounts.iter().fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); + eprintln!("total utxo amounts: {total_amount:?}"); + + let utxos: Vec = amounts + .iter() + .skip(1) + .map(|a| { + let dest = { + let purpose = if rng.gen_bool(0.5) { + KeyPurpose::ReceiveFunds + } else { + KeyPurpose::Change + }; + + account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object() + }; + + TxOutput::Transfer(OutputValue::Coin(*a), dest) + }) + .chain([TxOutput::Transfer( + OutputValue::Coin(amounts[0]), + standalone_pk_destination.clone(), + )]) + .collect(); + + let inputs: Vec = (0..utxos.len()) + .map(|_| { + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(rng)).into() + } else { + Id::::new(H256::random_using(rng)).into() + }; + TxInput::from_utxo(source_id, rng.next_u32()) + }) + .collect(); + + let (_dest_prv, pub_key1) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + let pub_key2 = if let Destination::PublicKeyHash(pkh) = + account.get_new_address(&mut db_tx, KeyPurpose::Change).unwrap().1.into_object() + { + account.find_corresponding_pub_key(&pkh).unwrap() + } else { + panic!("not a public key hash") + }; + let pub_key3 = if let Destination::PublicKeyHash(pkh) = account2 + .get_new_address(&mut db_tx, KeyPurpose::Change) + .unwrap() + .1 + .into_object() + { + account2.find_corresponding_pub_key(&pkh).unwrap() + } else { + panic!("not a public key hash") + }; + + let min_required_signatures = 3; + // The first account can signe the pub_key2 and the standalone key, + // but it will not be fully signed, so we will need to signe it with the account2 which + // can conplete the signing with the pub_key3 + let challenge = ClassicMultisigChallenge::new( + &chain_config, + NonZeroU8::new(min_required_signatures).unwrap(), + vec![pub_key1.clone(), pub_key2.clone(), standalone_pk, pub_key3.clone()], + ) + .unwrap(); + account.add_standalone_multisig(&mut db_tx, challenge.clone(), None).unwrap(); + let multisig_hash = account2.add_standalone_multisig(&mut db_tx, challenge, None).unwrap(); + + let multisig_dest = Destination::ClassicMultisig(multisig_hash); + + let source_id: OutPointSourceId = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(rng)).into() + } else { + Id::::new(H256::random_using(rng)).into() + }; + let multisig_input = TxInput::from_utxo(source_id.clone(), rng.next_u32()); + let multisig_utxo = TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(1)), + multisig_dest.clone(), + ); + + let secret = HtlcSecret::new_from_rng(rng); + let hash_lock = HashedTimelockContract { + secret_hash: secret.hash(), + spend_key: Destination::PublicKey(pub_key2.clone()), + refund_timelock: OutputTimeLock::UntilHeight(BlockHeight::new(0)), + refund_key: Destination::PublicKey(pub_key1), + }; + + let htlc_input = TxInput::from_utxo(source_id, rng.next_u32()); + let htlc_utxo = TxOutput::Htlc( + OutputValue::Coin(Amount::from_atoms(rng.gen::() as u128)), + Box::new(hash_lock.clone()), + ); + + let token_id = TokenId::new(H256::random_using(rng)); + let order_id = OrderId::new(H256::random_using(rng)); + + let dest_amount = total_amount.div(10).unwrap(); + let lock_amount = total_amount.div(10).unwrap(); + let burn_amount = total_amount.div(10).unwrap(); + let change_amount = total_amount.div(10).unwrap(); + let outputs_amounts_sum = [dest_amount, lock_amount, burn_amount, change_amount] + .iter() + .fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); + let _fee_amount = total_amount.sub(outputs_amounts_sum).unwrap(); + + let acc_inputs = vec![ + TxInput::Account(AccountOutPoint::new( + AccountNonce::new(1), + AccountSpending::DelegationBalance( + DelegationId::new(H256::random_using(rng)), + Amount::from_atoms(1_u128), + ), + )), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::MintTokens(token_id, (dest_amount + Amount::from_atoms(100)).unwrap()), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::UnmintTokens(token_id), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::LockTokenSupply(token_id), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::FreezeToken(token_id, IsTokenUnfreezable::Yes), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::UnfreezeToken(token_id), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ChangeTokenAuthority( + TokenId::new(H256::random_using(rng)), + Destination::AnyoneCanSpend, + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ConcludeOrder(order_id), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::FillOrder(order_id, Amount::from_atoms(1), Destination::AnyoneCanSpend), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ChangeTokenMetadataUri( + TokenId::new(H256::random_using(rng)), + "http://uri".as_bytes().to_vec(), + ), + ), + ]; + let acc_dests: Vec = acc_inputs + .iter() + .map(|_| { + let purpose = if rng.gen_bool(0.5) { + KeyPurpose::ReceiveFunds + } else { + KeyPurpose::Change + }; + + account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object() + }) + .collect(); + + let (_dest_prv, dest_pub) = PrivateKey::new_from_rng(rng, KeyKind::Secp256k1Schnorr); + let (_, vrf_public_key) = VRFPrivateKey::new_from_rng(rng, crypto::vrf::VRFKeyKind::Schnorrkel); + + let pool_id = PoolId::new(H256::random_using(rng)); + let delegation_id = DelegationId::new(H256::random_using(rng)); + let pool_data = StakePoolData::new( + Amount::from_atoms(5), + Destination::PublicKey(dest_pub.clone()), + vrf_public_key, + Destination::PublicKey(dest_pub.clone()), + PerThousand::new_from_rng(rng), + Amount::from_atoms(100), + ); + let token_issuance = TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: "XXXX".as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: "http://uri".as_bytes().to_vec(), + total_supply: common::chain::tokens::TokenTotalSupply::Unlimited, + authority: Destination::PublicKey(dest_pub.clone()), + is_freezable: common::chain::tokens::IsTokenFreezable::No, + }); + + let nft_issuance = NftIssuance::V0(NftIssuanceV0 { + metadata: Metadata { + creator: None, + name: "Name".as_bytes().to_vec(), + description: "SomeNFT".as_bytes().to_vec(), + ticker: "NFTX".as_bytes().to_vec(), + icon_uri: DataOrNoVec::from(None), + additional_metadata_uri: DataOrNoVec::from(None), + media_uri: DataOrNoVec::from(None), + media_hash: "123456".as_bytes().to_vec(), + }, + }); + let nft_id = TokenId::new(H256::random_using(rng)); + + let order_data = OrderData::new( + Destination::PublicKey(dest_pub.clone()), + OutputValue::Coin(Amount::from_atoms(100)), + OutputValue::Coin(total_amount), + ); + + let outputs = vec![ + TxOutput::Transfer( + OutputValue::TokenV1(token_id, dest_amount), + Destination::PublicKey(dest_pub.clone()), + ), + TxOutput::Transfer( + OutputValue::Coin(dest_amount), + Destination::PublicKey(dest_pub), + ), + TxOutput::LockThenTransfer( + OutputValue::Coin(lock_amount), + Destination::AnyoneCanSpend, + OutputTimeLock::ForSeconds(rng.next_u64()), + ), + TxOutput::Burn(OutputValue::Coin(burn_amount)), + TxOutput::CreateStakePool(pool_id, Box::new(pool_data)), + TxOutput::CreateDelegationId( + Destination::AnyoneCanSpend, + PoolId::new(H256::random_using(rng)), + ), + TxOutput::DelegateStaking(burn_amount, delegation_id), + TxOutput::IssueFungibleToken(Box::new(token_issuance)), + TxOutput::IssueNft( + nft_id, + Box::new(nft_issuance.clone()), + Destination::AnyoneCanSpend, + ), + TxOutput::DataDeposit(vec![1, 2, 3]), + TxOutput::Htlc(OutputValue::Coin(burn_amount), Box::new(hash_lock)), + TxOutput::CreateOrder(Box::new(order_data)), + ]; + + let req = SendRequest::new() + .with_inputs( + izip!(inputs.clone(), utxos.clone(), vec![None; inputs.len()]), + &|_| None, + ) + .unwrap() + .with_inputs( + [(htlc_input.clone(), htlc_utxo.clone(), Some(secret))], + &|_| None, + ) + .unwrap() + .with_inputs( + [(multisig_input.clone(), multisig_utxo.clone(), None)], + &|_| None, + ) + .unwrap() + .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) + .with_outputs(outputs); + let destinations = req.destinations().to_vec(); + let additional_info = TxAdditionalInfo::with_token_info( + token_id, + TokenAdditionalInfo { + num_decimals: 1, + ticker: "TKN".as_bytes().to_vec(), + }, + ) + .join(TxAdditionalInfo::with_order_info( + order_id, + OrderAdditionalInfo { + ask_balance: Amount::from_atoms(10), + give_balance: Amount::from_atoms(100), + initially_asked: OutputValue::Coin(Amount::from_atoms(20)), + initially_given: OutputValue::TokenV1(token_id, Amount::from_atoms(200)), + }, + )); + let ptx = req.into_partially_signed_tx(additional_info).unwrap(); + + let mut signer = make_signer(chain_config.clone(), account.account_index()); + let (ptx, _, _) = signer.sign_tx(ptx, account.key_chain(), &db_tx).unwrap(); + + assert!(ptx.all_signatures_available()); + + let utxos_ref = utxos + .iter() + .map(Some) + .chain([Some(&htlc_utxo), Some(&multisig_utxo)]) + .chain(acc_dests.iter().map(|_| None)) + .collect::>(); + + for (i, dest) in destinations.iter().enumerate() { + // the multisig will not be fully signed + if dest == &multisig_dest { + let err = tx_verifier::input_check::signature_only_check::verify_tx_signature( + &chain_config, + dest, + &ptx, + &utxos_ref, + i, + ) + .unwrap_err(); + assert_eq!( + err.error(), + &InputCheckErrorPayload::Verification(ScriptError::Signature( + DestinationSigError::IncompleteClassicalMultisigSignature( + min_required_signatures, + min_required_signatures - 1 + ) + )), + ) + } else { + tx_verifier::input_check::signature_only_check::verify_tx_signature( + &chain_config, + dest, + &ptx, + &utxos_ref, + i, + ) + .unwrap(); + } + } + + // fully sign the remaining key in the multisig address + let mut signer = make_signer(chain_config.clone(), account2.account_index()); + let (ptx, _, _) = signer.sign_tx(ptx, account2.key_chain(), &db_tx).unwrap(); + + assert!(ptx.all_signatures_available()); + + for (i, dest) in destinations.iter().enumerate() { + tx_verifier::input_check::signature_only_check::verify_tx_signature( + &chain_config, + dest, + &ptx, + &utxos_ref, + i, + ) + .unwrap(); + } +} + +fn account_from_mnemonic( + chain_config: &Arc, + db_tx: &mut StoreTxRwUnlocked, + account_index: U31, +) -> Account { + let master_key_chain = MasterKeyChain::new_from_mnemonic( + chain_config.clone(), + db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(db_tx, account_index, LOOKAHEAD_SIZE) + .unwrap(); + Account::new(chain_config.clone(), db_tx, key_chain, None).unwrap() +} diff --git a/wallet/src/signer/software_signer/tests.rs b/wallet/src/signer/software_signer/tests.rs index 75f350960..9455fa41e 100644 --- a/wallet/src/signer/software_signer/tests.rs +++ b/wallet/src/signer/software_signer/tests.rs @@ -13,8 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::ops::{Add, Div, Mul, Sub}; - use super::*; use crate::key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}; use crate::{Account, SendRequest}; @@ -22,21 +20,19 @@ use common::chain::block::timestamp::BlockTimestamp; use common::chain::config::create_regtest; use common::chain::htlc::HashedTimelockContract; use common::chain::output_value::OutputValue; -use common::chain::signature::inputsig::arbitrary_message::produce_message_challenge; use common::chain::signature::inputsig::authorize_pubkeyhash_spend::AuthorizedPublicKeyHashSpend; use common::chain::stakelock::StakePoolData; use common::chain::timelock::OutputTimeLock; use common::chain::tokens::{NftIssuance, NftIssuanceV0, TokenId, TokenIssuance}; use common::chain::{ - AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, GenBlock, - OrderData, OutPointSourceId, PoolId, TxInput, + AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, OrderData, + OutPointSourceId, PoolId, TxInput, }; use common::primitives::per_thousand::PerThousand; use common::primitives::{Amount, BlockHeight, Id, H256}; use crypto::key::secp256k1::Secp256k1PublicKey; -use crypto::key::{KeyKind, PublicKey, Signature}; +use crypto::key::{PublicKey, Signature}; use itertools::izip; -use randomness::{Rng, RngCore}; use rstest::rstest; use serialization::Encode; use test_utils::random::{make_seedable_rng, Seed}; @@ -44,444 +40,46 @@ use wallet_storage::{DefaultBackend, Store, Transactional}; use wallet_types::account_info::DEFAULT_ACCOUNT_INDEX; use wallet_types::partially_signed_transaction::TxAdditionalInfo; use wallet_types::seed_phrase::StoreSeedPhrase; -use wallet_types::KeyPurpose::{Change, ReceiveFunds}; +use wallet_types::KeyPurpose::ReceiveFunds; const MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; +fn software_signer(chain_config: Arc, account_index: U31) -> SoftwareSigner { + SoftwareSigner::new(chain_config, account_index) +} + #[rstest] #[trace] #[case(Seed::from_entropy())] fn sign_message(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - - let config = Arc::new(create_regtest()); - let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); - let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); - - let master_key_chain = MasterKeyChain::new_from_mnemonic( - config.clone(), - &mut db_tx, - MNEMONIC, - None, - StoreSeedPhrase::DoNotStore, - ) - .unwrap(); + use crate::signer::signer_test_helpers::test_sign_message; - let key_chain = master_key_chain - .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) - .unwrap(); - let mut account = Account::new(config.clone(), &mut db_tx, key_chain, None).unwrap(); - - let destination = account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object(); - let mut signer = SoftwareSigner::new(config.clone(), DEFAULT_ACCOUNT_INDEX); - let message = vec![rng.gen::(), rng.gen::(), rng.gen::()]; - let res = signer - .sign_challenge(&message, &destination, account.key_chain(), &db_tx) - .unwrap(); + let mut rng = make_seedable_rng(seed); - let message_challenge = produce_message_challenge(&message); - res.verify_signature(&config, &destination, &message_challenge).unwrap(); + test_sign_message(&mut rng, software_signer); } #[rstest] #[trace] #[case(Seed::from_entropy())] fn sign_transaction_intent(#[case] seed: Seed) { - use common::primitives::Idable; + use crate::signer::signer_test_helpers::test_sign_transaction_intent; let mut rng = make_seedable_rng(seed); - let config = Arc::new(create_regtest()); - let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); - let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); - - let master_key_chain = MasterKeyChain::new_from_mnemonic( - config.clone(), - &mut db_tx, - MNEMONIC, - None, - StoreSeedPhrase::DoNotStore, - ) - .unwrap(); - - let key_chain = master_key_chain - .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) - .unwrap(); - let mut account = Account::new(config.clone(), &mut db_tx, key_chain, None).unwrap(); - - let mut signer = SoftwareSigner::new(config.clone(), DEFAULT_ACCOUNT_INDEX); - - let inputs: Vec = (0..rng.gen_range(1..5)) - .map(|_| { - let source_id = if rng.gen_bool(0.5) { - Id::::new(H256::random_using(&mut rng)).into() - } else { - Id::::new(H256::random_using(&mut rng)).into() - }; - TxInput::from_utxo(source_id, rng.next_u32()) - }) - .collect(); - let input_destinations: Vec<_> = (0..inputs.len()) - .map(|_| account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object()) - .collect(); - - let tx = Transaction::new( - 0, - inputs, - vec![TxOutput::Transfer( - OutputValue::Coin(Amount::from_atoms(rng.gen())), - account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), - )], - ) - .unwrap(); - - let intent: String = [rng.gen::(), rng.gen::(), rng.gen::()].iter().collect(); - let res = signer - .sign_transaction_intent( - &tx, - &input_destinations, - &intent, - account.key_chain(), - &db_tx, - ) - .unwrap(); - - let expected_signed_message = - SignedTransactionIntent::get_message_to_sign(&intent, &tx.get_id()); - res.verify(&config, &input_destinations, &expected_signed_message).unwrap(); + test_sign_transaction_intent(&mut rng, software_signer); } #[rstest] #[trace] #[case(Seed::from_entropy())] fn sign_transaction(#[case] seed: Seed) { - use std::num::NonZeroU8; - - use common::{ - chain::{ - classic_multisig::ClassicMultisigChallenge, - htlc::HashedTimelockContract, - stakelock::StakePoolData, - tokens::{ - IsTokenUnfreezable, Metadata, NftIssuance, NftIssuanceV0, TokenId, TokenIssuance, - TokenIssuanceV1, - }, - AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, - OrderData, OrderId, PoolId, - }, - primitives::amount::UnsignedIntType, - }; - use crypto::vrf::VRFPrivateKey; - use itertools::izip; - use serialization::extras::non_empty_vec::DataOrNoVec; + use crate::signer::signer_test_helpers::test_sign_transaction; let mut rng = make_seedable_rng(seed); - let chain_config = Arc::new(create_regtest()); - let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); - let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); - - let master_key_chain = MasterKeyChain::new_from_mnemonic( - chain_config.clone(), - &mut db_tx, - MNEMONIC, - None, - StoreSeedPhrase::DoNotStore, - ) - .unwrap(); - - let key_chain = master_key_chain - .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) - .unwrap(); - let mut account = Account::new(chain_config.clone(), &mut db_tx, key_chain, None).unwrap(); - - let amounts: Vec = (0..(2 + rng.next_u32() % 5)) - .map(|_| Amount::from_atoms(rng.gen_range(1..10) as UnsignedIntType)) - .collect(); - - let total_amount = amounts.iter().fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); - - let utxos: Vec = amounts - .iter() - .map(|a| { - let purpose = if rng.gen_bool(0.5) { - ReceiveFunds - } else { - Change - }; - - TxOutput::Transfer( - OutputValue::Coin(*a), - account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object(), - ) - }) - .collect(); - - let inputs: Vec = (0..utxos.len()) - .map(|_| { - let source_id = if rng.gen_bool(0.5) { - Id::::new(H256::random_using(&mut rng)).into() - } else { - Id::::new(H256::random_using(&mut rng)).into() - }; - TxInput::from_utxo(source_id, rng.next_u32()) - }) - .collect(); - - let (_dest_prv, pub_key1) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); - let pub_key2 = if let Destination::PublicKeyHash(pkh) = - account.get_new_address(&mut db_tx, Change).unwrap().1.into_object() - { - account.find_corresponding_pub_key(&pkh).unwrap() - } else { - panic!("not a public key hash") - }; - let pub_key3 = if let Destination::PublicKeyHash(pkh) = - account.get_new_address(&mut db_tx, Change).unwrap().1.into_object() - { - account.find_corresponding_pub_key(&pkh).unwrap() - } else { - panic!("not a public key hash") - }; - let min_required_signatures = 2; - let challenge = ClassicMultisigChallenge::new( - &chain_config, - NonZeroU8::new(min_required_signatures).unwrap(), - vec![pub_key1.clone(), pub_key2.clone(), pub_key3], - ) - .unwrap(); - let multisig_hash = account.add_standalone_multisig(&mut db_tx, challenge, None).unwrap(); - - let multisig_dest = Destination::ClassicMultisig(multisig_hash); - - let source_id: OutPointSourceId = if rng.gen_bool(0.5) { - Id::::new(H256::random_using(&mut rng)).into() - } else { - Id::::new(H256::random_using(&mut rng)).into() - }; - let multisig_input = TxInput::from_utxo(source_id.clone(), rng.next_u32()); - let multisig_utxo = TxOutput::Transfer(OutputValue::Coin(Amount::from_atoms(1)), multisig_dest); - - let secret = HtlcSecret::new_from_rng(&mut rng); - let hash_lock = HashedTimelockContract { - secret_hash: secret.hash(), - spend_key: Destination::PublicKey(pub_key2.clone()), - refund_timelock: OutputTimeLock::UntilHeight(BlockHeight::new(0)), - refund_key: Destination::PublicKey(pub_key1), - }; - - let htlc_input = TxInput::from_utxo(source_id, rng.next_u32()); - let htlc_utxo = TxOutput::Htlc( - OutputValue::Coin(Amount::from_atoms(rng.gen::() as u128)), - Box::new(hash_lock.clone()), - ); - - let acc_inputs = vec![ - TxInput::Account(AccountOutPoint::new( - AccountNonce::new(0), - AccountSpending::DelegationBalance( - DelegationId::new(H256::random_using(&mut rng)), - Amount::from_atoms(rng.next_u32() as u128), - ), - )), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::MintTokens( - TokenId::new(H256::random_using(&mut rng)), - Amount::from_atoms(100), - ), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::UnmintTokens(TokenId::new(H256::random_using(&mut rng))), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::LockTokenSupply(TokenId::new(H256::random_using(&mut rng))), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::FreezeToken( - TokenId::new(H256::random_using(&mut rng)), - IsTokenUnfreezable::Yes, - ), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::UnfreezeToken(TokenId::new(H256::random_using(&mut rng))), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::ChangeTokenAuthority( - TokenId::new(H256::random_using(&mut rng)), - Destination::AnyoneCanSpend, - ), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::ConcludeOrder(OrderId::new(H256::random_using(&mut rng))), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::FillOrder( - OrderId::new(H256::random_using(&mut rng)), - Amount::from_atoms(123), - Destination::AnyoneCanSpend, - ), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::ChangeTokenMetadataUri( - TokenId::new(H256::random_using(&mut rng)), - "http://uri".as_bytes().to_vec(), - ), - ), - ]; - let acc_dests: Vec = acc_inputs - .iter() - .map(|_| { - let purpose = if rng.gen_bool(0.5) { - ReceiveFunds - } else { - Change - }; - - account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object() - }) - .collect(); - - let dest_amount = total_amount.div(10).unwrap().mul(5).unwrap(); - let lock_amount = total_amount.div(10).unwrap().mul(1).unwrap(); - let burn_amount = total_amount.div(10).unwrap().mul(1).unwrap(); - let change_amount = total_amount.div(10).unwrap().mul(2).unwrap(); - let outputs_amounts_sum = [dest_amount, lock_amount, burn_amount, change_amount] - .iter() - .fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); - let _fee_amount = total_amount.sub(outputs_amounts_sum).unwrap(); - - let (_dest_prv, dest_pub) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); - let (_, vrf_public_key) = VRFPrivateKey::new_from_entropy(crypto::vrf::VRFKeyKind::Schnorrkel); - - let pool_id = PoolId::new(H256::random()); - let delegation_id = DelegationId::new(H256::random()); - let pool_data = StakePoolData::new( - Amount::from_atoms(5000000), - Destination::PublicKey(dest_pub.clone()), - vrf_public_key, - Destination::PublicKey(dest_pub.clone()), - PerThousand::new_from_rng(&mut rng), - Amount::from_atoms(100), - ); - let token_issuance = TokenIssuance::V1(TokenIssuanceV1 { - token_ticker: "XXXX".as_bytes().to_vec(), - number_of_decimals: rng.gen_range(1..18), - metadata_uri: "http://uri".as_bytes().to_vec(), - total_supply: common::chain::tokens::TokenTotalSupply::Unlimited, - authority: Destination::PublicKey(dest_pub.clone()), - is_freezable: common::chain::tokens::IsTokenFreezable::No, - }); - - let nft_issuance = NftIssuance::V0(NftIssuanceV0 { - metadata: Metadata { - creator: None, - name: "Name".as_bytes().to_vec(), - description: "SomeNFT".as_bytes().to_vec(), - ticker: "NFTX".as_bytes().to_vec(), - icon_uri: DataOrNoVec::from(None), - additional_metadata_uri: DataOrNoVec::from(None), - media_uri: DataOrNoVec::from(None), - media_hash: "123456".as_bytes().to_vec(), - }, - }); - let nft_id = TokenId::new(H256::random()); - - let order_data = OrderData::new( - Destination::PublicKey(dest_pub.clone()), - OutputValue::Coin(Amount::from_atoms(100)), - OutputValue::Coin(total_amount), - ); - - let outputs = vec![ - TxOutput::Transfer( - OutputValue::Coin(dest_amount), - Destination::PublicKey(dest_pub), - ), - TxOutput::LockThenTransfer( - OutputValue::Coin(lock_amount), - Destination::AnyoneCanSpend, - OutputTimeLock::ForSeconds(rng.next_u64()), - ), - TxOutput::Burn(OutputValue::Coin(burn_amount)), - TxOutput::CreateStakePool(pool_id, Box::new(pool_data)), - TxOutput::CreateDelegationId( - Destination::AnyoneCanSpend, - PoolId::new(H256::random_using(&mut rng)), - ), - TxOutput::DelegateStaking(burn_amount, delegation_id), - TxOutput::IssueFungibleToken(Box::new(token_issuance)), - TxOutput::IssueNft( - nft_id, - Box::new(nft_issuance.clone()), - Destination::AnyoneCanSpend, - ), - TxOutput::DataDeposit(vec![1, 2, 3]), - TxOutput::Htlc(OutputValue::Coin(burn_amount), Box::new(hash_lock)), - TxOutput::CreateOrder(Box::new(order_data)), - TxOutput::Transfer( - OutputValue::Coin(Amount::from_atoms(100_000_000_000)), - account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), - ), - ]; - - let req = SendRequest::new() - .with_inputs( - izip!(inputs.clone(), utxos.clone(), vec![None; inputs.len()]), - &|_| None, - ) - .unwrap() - .with_inputs( - [(htlc_input.clone(), htlc_utxo.clone(), Some(secret))], - &|_| None, - ) - .unwrap() - .with_inputs( - [(multisig_input.clone(), multisig_utxo.clone(), None)], - &|_| None, - ) - .unwrap() - .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) - .with_outputs(outputs); - let destinations = req.destinations().to_vec(); - let additional_info = TxAdditionalInfo::new(); - let ptx = req.into_partially_signed_tx(additional_info).unwrap(); - - let mut signer = SoftwareSigner::new(chain_config.clone(), DEFAULT_ACCOUNT_INDEX); - let (ptx, _, _) = signer.sign_tx(ptx, account.key_chain(), &db_tx).unwrap(); - - eprintln!("num inputs in tx: {} {:?}", inputs.len(), ptx.witnesses()); - for (i, w) in ptx.witnesses().iter().enumerate() { - eprintln!("W: {i} {w:?}"); - } - assert!(ptx.all_signatures_available()); - - let utxos_ref = utxos - .iter() - .map(Some) - .chain([Some(&htlc_utxo), Some(&multisig_utxo)]) - .chain(acc_dests.iter().map(|_| None)) - .collect::>(); - - for (i, dest) in destinations.iter().enumerate() { - tx_verifier::input_check::signature_only_check::verify_tx_signature( - &chain_config, - dest, - &ptx, - &utxos_ref, - i, - ) - .unwrap(); - } + test_sign_transaction(&mut rng, software_signer); } #[test] diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index aeee6a0f7..c7d889291 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -30,9 +30,16 @@ use common::{ authorize_pubkey_spend::AuthorizedPublicKeySpend, authorize_pubkeyhash_spend::AuthorizedPublicKeyHashSpend, classical_multisig::{ - authorize_classical_multisig::AuthorizedClassicalMultisigSpend, - multisig_partial_signature::{self, PartiallySignedMultisigChallenge}, + authorize_classical_multisig::{ + sign_classical_multisig_spending, AuthorizedClassicalMultisigSpend, + ClassicalMultisigCompletionStatus, + }, + multisig_partial_signature::{ + self, PartiallySignedMultisigChallenge, + PartiallySignedMultisigStructureError, + }, }, + htlc::produce_uniparty_signature_for_htlc_input, standard_signature::StandardInputSignature, InputWitness, }, @@ -51,9 +58,10 @@ use crypto::key::{ hdkd::{chain_code::ChainCode, derivable::Derivable, u31::U31}, secp256k1::{extended_keys::Secp256k1ExtendedPublicKey, Secp256k1PublicKey}, signature::SignatureKind, - Signature, SignatureError, + PrivateKey, Signature, SignatureError, }; use itertools::Itertools; +use randomness::make_true_rng; use serialization::Encode; use trezor_client::{ client::{mintlayer::MintlayerSignature, TransactionId}, @@ -211,17 +219,21 @@ impl TrezorSigner { operation(&mut client).map_err(|e| TrezorError::DeviceError(e.to_string()).into()) } - fn make_signature( + #[allow(clippy::too_many_arguments)] + fn make_signature<'a, 'b, F, F2>( &self, signatures: &[MintlayerSignature], - destination: &Destination, + standalone_inputs: Option<&'a [StandaloneInput]>, + destination: &'b Destination, sighash_type: SigHashType, sighash: H256, key_chain: &impl AccountKeyChains, add_secret_if_needed: F, + sign_with_standalone_private_key: F2, ) -> SignerResult<(Option, SignatureStatus)> where F: Fn(StandardInputSignature) -> InputWitness, + F2: Fn(&'a StandaloneInput, &'b Destination) -> SignerResult, { match destination { Destination::AnyoneCanSpend => Ok(( @@ -247,7 +259,14 @@ impl TrezorSigner { Ok((Some(sig), SignatureStatus::FullySigned)) } else { - Ok((None, SignatureStatus::NotSigned)) + let standalone = match standalone_inputs { + Some([standalone]) => standalone, + Some(_) => return Err(TrezorError::MultisigSignatureReturned.into()), + None => return Ok((None, SignatureStatus::NotSigned)), + }; + + let sig = sign_with_standalone_private_key(standalone, destination)?; + Ok((Some(sig), SignatureStatus::FullySigned)) } } Destination::PublicKey(_) => { @@ -265,7 +284,14 @@ impl TrezorSigner { Ok((Some(sig), SignatureStatus::FullySigned)) } else { - Ok((None, SignatureStatus::NotSigned)) + let standalone = match standalone_inputs { + Some([standalone]) => standalone, + Some(_) => return Err(TrezorError::MultisigSignatureReturned.into()), + None => return Ok((None, SignatureStatus::NotSigned)), + }; + + let sig = sign_with_standalone_private_key(standalone, destination)?; + Ok((Some(sig), SignatureStatus::FullySigned)) } } Destination::ClassicMultisig(_) => { @@ -276,6 +302,13 @@ impl TrezorSigner { sighash, )?; + let (current_signatures, status) = self.sign_with_standalone_private_keys( + current_signatures, + standalone_inputs.unwrap_or(&[]), + status, + sighash, + )?; + let sig = add_secret_if_needed(StandardInputSignature::new( sighash_type, current_signatures.encode(), @@ -333,11 +366,11 @@ impl TrezorSigner { fn update_and_check_multisig( &self, - signature: &[MintlayerSignature], + signatures: &[MintlayerSignature], mut current_signatures: AuthorizedClassicalMultisigSpend, sighash: H256, ) -> SignerResult<(AuthorizedClassicalMultisigSpend, SignatureStatus)> { - for sig in signature { + for sig in signatures { let idx = sig.multisig_idx.ok_or(TrezorError::MissingMultisigIndexForSignature)?; let sig = Signature::from_raw_data(&sig.signature, SignatureKind::Secp256k1Schnorr) .map_err(TrezorError::SignatureError)?; @@ -348,6 +381,55 @@ impl TrezorSigner { Ok((current_signatures, status)) } + + fn sign_with_standalone_private_keys( + &self, + current_signatures: AuthorizedClassicalMultisigSpend, + standalone_inputs: &[StandaloneInput], + new_status: SignatureStatus, + sighash: H256, + ) -> SignerResult<(AuthorizedClassicalMultisigSpend, SignatureStatus)> { + let challenge = current_signatures.challenge().clone(); + + standalone_inputs.iter().try_fold( + (current_signatures, new_status), + |(mut current_signatures, mut status), inp| -> SignerResult<_> { + if status == SignatureStatus::FullySigned { + return Ok((current_signatures, status)); + } + + let key_index = inp + .multisig_idx + .ok_or(PartiallySignedMultisigStructureError::InvalidSignatureIndex)?; + let res = sign_classical_multisig_spending( + &self.chain_config, + key_index as u8, + &inp.private_key, + &challenge, + &sighash, + current_signatures, + &mut make_true_rng(), + ) + .map_err(DestinationSigError::ClassicalMultisigSigningFailed)?; + + match res { + ClassicalMultisigCompletionStatus::Complete(signatures) => { + current_signatures = signatures; + status = SignatureStatus::FullySigned; + } + ClassicalMultisigCompletionStatus::Incomplete(signatures) => { + current_signatures = signatures; + status = SignatureStatus::PartialMultisig { + required_signatures: challenge.min_required_signatures(), + num_signatures: current_signatures.signatures().len() as u8, + }; + } + }; + + Ok((current_signatures, status)) + }, + ) + } } impl Signer for TrezorSigner { @@ -361,7 +443,8 @@ impl Signer for TrezorSigner { Vec, Vec, )> { - let inputs = to_trezor_input_msgs(&ptx, key_chain, &self.chain_config)?; + let (inputs, standalone_inputs) = + to_trezor_input_msgs(&ptx, key_chain, &self.chain_config, db_tx)?; let outputs = self.to_trezor_output_msgs(&ptx)?; let utxos = to_trezor_utxo_msgs(&ptx, &self.chain_config)?; let chain_type = to_trezor_chain_type(&self.chain_config); @@ -382,7 +465,7 @@ impl Signer for TrezorSigner { .enumerate() .zip(ptx.destinations()) .zip(ptx.htlc_secrets()) - .map(|(((i, witness), destination), secret)| { + .map(|(((input_index, witness), destination), secret)| { let add_secret_if_needed = |sig: StandardInputSignature| { let sig = if let Some(htlc_secret) = secret { let sighash_type = sig.sighash_type(); @@ -400,6 +483,17 @@ impl Signer for TrezorSigner { InputWitness::Standard(sig) }; + let sign_with_standalone_private_key = |standalone, destination| { + sign_input_with_standalone_key( + secret, + standalone, + destination, + &ptx, + &inputs_utxo_refs, + input_index, + ) + }; + match witness { Some(w) => match w { InputWitness::NoSignature(_) => Ok(( @@ -414,7 +508,7 @@ impl Signer for TrezorSigner { destination, &ptx, &inputs_utxo_refs, - i, + input_index, ) .is_ok() { @@ -428,7 +522,7 @@ impl Signer for TrezorSigner { sig.sighash_type(), ptx.tx(), &inputs_utxo_refs, - i, + input_index, )?; let current_signatures = @@ -443,24 +537,28 @@ impl Signer for TrezorSigner { num_signatures: current_signatures.signatures().len() as u8, }; - if let Some(signature) = new_signatures.get(i) { - let (current_signatures, status) = self - .update_and_check_multisig( - signature, - current_signatures, - sighash, - )?; - - let sighash_type = SigHashType::all(); - let sig = add_secret_if_needed(StandardInputSignature::new( - sighash_type, - current_signatures.encode(), - )); - - return Ok((Some(sig), previous_status, status)); + let (current_signatures, new_status) = if let Some(signature) = new_signatures.get(input_index) + { + self.update_and_check_multisig(signature, current_signatures, sighash)? } else { - Ok((None, previous_status, previous_status)) - } + (current_signatures, previous_status) + }; + + let (current_signatures, new_status) = + self.sign_with_standalone_private_keys( + current_signatures, + standalone_inputs.get(&(input_index as u32)).map_or(&[], |x| x.as_slice()), + new_status, + sighash + )?; + + let sighash_type = SigHashType::all(); + let sig = add_secret_if_needed(StandardInputSignature::new( + sighash_type, + current_signatures.encode(), + )); + + Ok((Some(sig), previous_status, new_status)) } else { Ok(( None, @@ -476,21 +574,41 @@ impl Signer for TrezorSigner { )), }, }, - None => match (destination, new_signatures.get(i)) { + None => match (destination, new_signatures.get(input_index)) { (Some(destination), Some(sig)) => { let sighash_type = SigHashType::all(); - let sighash = signature_hash(sighash_type, ptx.tx(), &inputs_utxo_refs, i)?; + let sighash = signature_hash(sighash_type, ptx.tx(), &inputs_utxo_refs, input_index)?; let (sig, status) = self.make_signature( sig, + standalone_inputs.get(&(input_index as u32)).map(|x| x.as_slice()), destination, sighash_type, sighash, key_chain, add_secret_if_needed, + sign_with_standalone_private_key, )?; Ok((sig, SignatureStatus::NotSigned, status)) } - (Some(_) | None, None) | (None, Some(_)) => { + (Some(destination), None) => { + let standalone = match standalone_inputs.get(&(input_index as u32)).map(|x| x.as_slice()) { + Some([standalone]) => standalone, + Some(_) => return Err(TrezorError::MultisigSignatureReturned.into()), + None => return Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)) + }; + + let sig = sign_input_with_standalone_key( + secret, + standalone, + destination, + &ptx, + &inputs_utxo_refs, + input_index + )?; + + Ok((Some(sig), SignatureStatus::NotSigned, SignatureStatus::FullySigned)) + } + (None, _) => { Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)) } }, @@ -580,8 +698,18 @@ impl Signer for TrezorSigner { } } } - Some(FoundPubKey::Standalone(_)) => { - unimplemented!("standalone keys with trezor") + Some(FoundPubKey::Standalone(acc_public_key)) => { + let standalone_pk = db_tx + .get_account_standalone_private_key(&acc_public_key)? + .ok_or(SignerError::DestinationNotFromThisWallet)?; + + let sig = ArbitraryMessageSignature::produce_uniparty_signature( + &standalone_pk, + destination, + message, + make_true_rng(), + )?; + return Ok(sig); } None => return Err(SignerError::DestinationNotFromThisWallet), }; @@ -617,49 +745,90 @@ impl Signer for TrezorSigner { } } +fn sign_input_with_standalone_key( + secret: &Option, + standalone: &StandaloneInput, + destination: &Destination, + ptx: &PartiallySignedTransaction, + inputs_utxo_refs: &[Option<&TxOutput>], + input_index: usize, +) -> SignerResult { + let sighash_type = SigHashType::all(); + match secret { + Some(htlc_secret) => produce_uniparty_signature_for_htlc_input( + &standalone.private_key, + sighash_type, + destination.clone(), + ptx.tx(), + inputs_utxo_refs, + input_index, + htlc_secret.clone(), + make_true_rng(), + ), + None => StandardInputSignature::produce_uniparty_signature_for_input( + &standalone.private_key, + sighash_type, + destination.clone(), + ptx.tx(), + inputs_utxo_refs, + input_index, + make_true_rng(), + ), + } + .map(InputWitness::Standard) + .map_err(SignerError::SigningError) +} + fn to_trezor_input_msgs( ptx: &PartiallySignedTransaction, key_chain: &impl AccountKeyChains, chain_config: &ChainConfig, -) -> SignerResult> { - ptx.tx() + db_tx: &impl WalletStorageReadUnlocked, +) -> SignerResult<(Vec, StandaloneInputs)> { + let res: (Vec<_>, BTreeMap<_, _>) = ptx + .tx() .inputs() .iter() - .zip(ptx.input_utxos()) .zip(ptx.destinations()) - .map(|((inp, utxo), dest)| match (inp, utxo, dest) { - (TxInput::Utxo(outpoint), Some(_), Some(dest)) => { - to_trezor_utxo_input(outpoint, dest, key_chain) - } - (TxInput::Account(outpoint), _, Some(dest)) => { - to_trezor_account_input(chain_config, dest, key_chain, outpoint) - } - (TxInput::AccountCommand(nonce, command), _, Some(dest)) => { - to_trezor_account_command_input( + .enumerate() + .map(|(idx, (inp, dest))| { + let (address_paths, standalone_inputs) = + dest.as_ref().map_or(Ok((vec![], vec![])), |dest| { + destination_to_address_paths(key_chain, dest, db_tx) + })?; + + let inputs = match inp { + TxInput::Utxo(outpoint) => to_trezor_utxo_input(outpoint, address_paths), + TxInput::Account(outpoint) => { + to_trezor_account_input(chain_config, address_paths, outpoint) + } + TxInput::AccountCommand(nonce, command) => to_trezor_account_command_input( chain_config, - dest, - key_chain, + address_paths, nonce, command, ptx.additional_info(), - ) - } - (_, _, None) => Err(SignerError::MissingDestinationInTransaction), - (TxInput::Utxo(_), _, _) => Err(SignerError::MissingUtxo), + ), + }?; + + Ok((inputs, (idx as u32, standalone_inputs))) }) - .collect() + .collect::>>()? + .into_iter() + .unzip(); + + Ok(res) } fn to_trezor_account_command_input( chain_config: &ChainConfig, - dest: &Destination, - key_chain: &impl AccountKeyChains, + address_paths: Vec, nonce: &common::chain::AccountNonce, command: &AccountCommand, additional_info: &TxAdditionalInfo, ) -> SignerResult { let mut inp_req = MintlayerAccountCommandTxInput::new(); - inp_req.addresses = destination_to_address_paths(key_chain, dest); + inp_req.addresses = address_paths; inp_req.set_nonce(nonce.value()); match command { AccountCommand::MintTokens(token_id, amount) => { @@ -803,12 +972,11 @@ fn value_with_new_amount( fn to_trezor_account_input( chain_config: &ChainConfig, - dest: &Destination, - key_chain: &impl AccountKeyChains, + address_paths: Vec, outpoint: &common::chain::AccountOutPoint, ) -> SignerResult { let mut inp_req = MintlayerAccountTxInput::new(); - inp_req.addresses = destination_to_address_paths(key_chain, dest); + inp_req.addresses = address_paths; inp_req.set_nonce(outpoint.nonce().value()); match outpoint.account() { AccountSpending::DelegationBalance(delegation_id, amount) => { @@ -826,8 +994,7 @@ fn to_trezor_account_input( fn to_trezor_utxo_input( outpoint: &common::chain::UtxoOutPoint, - dest: &Destination, - key_chain: &impl AccountKeyChains, + address_paths: Vec, ) -> SignerResult { let mut inp_req = MintlayerUtxoTxInput::new(); let id = match outpoint.source_id() { @@ -843,18 +1010,35 @@ fn to_trezor_utxo_input( inp_req.set_prev_hash(id.to_vec()); inp_req.set_prev_index(outpoint.output_index()); - inp_req.addresses = destination_to_address_paths(key_chain, dest); + inp_req.addresses = address_paths; let mut inp = MintlayerTxInput::new(); inp.utxo = Some(inp_req).into(); Ok(inp) } +struct StandaloneInput { + multisig_idx: Option, + private_key: PrivateKey, +} + +type StandaloneInputs = BTreeMap>; + /// Find the derivation paths to the key in the destination, or multiple in the case of a multisig fn destination_to_address_paths( key_chain: &impl AccountKeyChains, dest: &Destination, -) -> Vec { + db_tx: &impl WalletStorageReadUnlocked, +) -> SignerResult<(Vec, Vec)> { + destination_to_address_paths_impl(key_chain, dest, None, db_tx) +} + +fn destination_to_address_paths_impl( + key_chain: &impl AccountKeyChains, + dest: &Destination, + multisig_idx: Option, + db_tx: &impl WalletStorageReadUnlocked, +) -> SignerResult<(Vec, Vec)> { match key_chain.find_public_key(dest) { Some(FoundPubKey::Hierarchy(xpub)) => { let address_n = xpub @@ -863,44 +1047,52 @@ fn destination_to_address_paths( .iter() .map(|c| c.into_encoded_index()) .collect(); - vec![MintlayerAddressPath { - address_n, - ..Default::default() - }] + Ok(( + vec![MintlayerAddressPath { + address_n, + multisig_idx, + ..Default::default() + }], + vec![], + )) } - Some(FoundPubKey::Standalone(_)) => { - unimplemented!("standalone keys with trezor") + Some(FoundPubKey::Standalone(acc_public_key)) => { + let standalone_input = + db_tx.get_account_standalone_private_key(&acc_public_key)?.map(|private_key| { + StandaloneInput { + multisig_idx, + private_key, + } + }); + Ok((vec![], standalone_input.into_iter().collect())) } - None => { + None if multisig_idx.is_none() => { if let Some(challenge) = key_chain.find_multisig_challenge(dest) { - challenge + let (x, y): (Vec<_>, Vec<_>) = challenge .public_keys() .iter() .enumerate() - .filter_map(|(idx, pk)| { - match key_chain.find_public_key(&Destination::PublicKey(pk.clone())) { - Some(FoundPubKey::Hierarchy(xpub)) => { - let address_n = xpub - .get_derivation_path() - .as_slice() - .iter() - .map(|c| c.into_encoded_index()) - .collect(); - Some(MintlayerAddressPath { - address_n, - multisig_idx: Some(idx as u32), - special_fields: Default::default(), - }) - } - Some(FoundPubKey::Standalone(_)) => unimplemented!("standalone keys"), - None => None, - } + .map(|(idx, pk)| { + destination_to_address_paths_impl( + key_chain, + &Destination::PublicKey(pk.clone()), + Some(idx as u32), + db_tx, + ) }) - .collect() + .collect::, SignerError>>()? + .into_iter() + .unzip(); + + Ok(( + x.into_iter().flatten().collect(), + y.into_iter().flatten().collect(), + )) } else { - vec![] + Ok((vec![], vec![])) } } + None => Ok((vec![], vec![])), } } diff --git a/wallet/src/signer/trezor_signer/tests.rs b/wallet/src/signer/trezor_signer/tests.rs index 7d2f0a40b..0c02a0816 100644 --- a/wallet/src/signer/trezor_signer/tests.rs +++ b/wallet/src/signer/trezor_signer/tests.rs @@ -13,94 +13,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -use core::panic; -use std::ops::{Add, Div, Sub}; - use super::*; -use crate::{ - key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}, - Account, SendRequest, -}; -use common::chain::{ - config::create_regtest, - htlc::{HashedTimelockContract, HtlcSecret}, - output_value::OutputValue, - signature::inputsig::arbitrary_message::produce_message_challenge, - stakelock::StakePoolData, - timelock::OutputTimeLock, - tokens::{NftIssuanceV0, TokenId}, - AccountNonce, AccountOutPoint, DelegationId, Destination, GenBlock, OrderData, PoolId, - Transaction, TxInput, -}; -use common::primitives::{per_thousand::PerThousand, Amount, BlockHeight, Id, H256}; -use crypto::key::{KeyKind, PrivateKey}; -use itertools::izip; -use randomness::{Rng, RngCore}; use rstest::rstest; use serial_test::serial; use test_utils::random::{make_seedable_rng, Seed}; use trezor_client::find_devices; -use wallet_storage::{DefaultBackend, Store, Transactional}; -use wallet_types::{ - account_info::DEFAULT_ACCOUNT_INDEX, - seed_phrase::StoreSeedPhrase, - KeyPurpose::{Change, ReceiveFunds}, -}; -const MNEMONIC: &str = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; +fn trezor_signer(chain_config: Arc, _account_index: U31) -> TrezorSigner { + let mut client = find_test_device(); + let session_id = client.initialize(None).unwrap().ok().unwrap().session_id().to_vec(); + + TrezorSigner::new(chain_config, Arc::new(Mutex::new(client)), session_id) +} #[rstest] #[trace] #[serial] #[case(Seed::from_entropy())] fn sign_message(#[case] seed: Seed) { - let mut rng = make_seedable_rng(seed); - - let chain_config = Arc::new(create_regtest()); - let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); - let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); - - let master_key_chain = MasterKeyChain::new_from_mnemonic( - chain_config.clone(), - &mut db_tx, - MNEMONIC, - None, - StoreSeedPhrase::DoNotStore, - ) - .unwrap(); + use crate::signer::signer_test_helpers::test_sign_message; - let key_chain = master_key_chain - .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) - .unwrap(); - let mut account = Account::new(chain_config.clone(), &mut db_tx, key_chain, None).unwrap(); - - let pkh_destination = - account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object(); - let pkh = match pkh_destination { - Destination::PublicKeyHash(pkh) => pkh, - _ => panic!("not a public key hash destination"), - }; - let pk = account.find_corresponding_pub_key(&pkh).unwrap(); - let pk_destination = Destination::PublicKey(pk); - - for destination in [pkh_destination, pk_destination] { - let mut client = find_test_device(); - let session_id = client.initialize(None).unwrap().ok().unwrap().session_id().to_vec(); - - let mut signer = TrezorSigner::new( - chain_config.clone(), - Arc::new(Mutex::new(client)), - session_id, - ); - let message = vec![rng.gen::(), rng.gen::(), rng.gen::()]; - let res = signer - .sign_challenge(&message, &destination, account.key_chain(), &db_tx) - .unwrap(); + let mut rng = make_seedable_rng(seed); - let message_challenge = produce_message_challenge(&message); - res.verify_signature(&chain_config, &destination, &message_challenge).unwrap(); - } + test_sign_message(&mut rng, trezor_signer); } #[rstest] @@ -108,84 +43,11 @@ fn sign_message(#[case] seed: Seed) { #[case(Seed::from_entropy())] #[serial] fn sign_transaction_intent(#[case] seed: Seed) { - use common::primitives::Idable; + use crate::signer::signer_test_helpers::test_sign_transaction_intent; let mut rng = make_seedable_rng(seed); - let config = Arc::new(create_regtest()); - let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); - let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); - - let master_key_chain = MasterKeyChain::new_from_mnemonic( - config.clone(), - &mut db_tx, - MNEMONIC, - None, - StoreSeedPhrase::DoNotStore, - ) - .unwrap(); - - let key_chain = master_key_chain - .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) - .unwrap(); - let mut account = Account::new(config.clone(), &mut db_tx, key_chain, None).unwrap(); - - let mut client = find_test_device(); - let session_id = client.initialize(None).unwrap().ok().unwrap().session_id().to_vec(); - - let mut signer = TrezorSigner::new(config.clone(), Arc::new(Mutex::new(client)), session_id); - - let inputs: Vec = (0..rng.gen_range(3..6)) - .map(|_| { - let source_id = if rng.gen_bool(0.5) { - Id::::new(H256::random_using(&mut rng)).into() - } else { - Id::::new(H256::random_using(&mut rng)).into() - }; - TxInput::from_utxo(source_id, rng.next_u32()) - }) - .collect(); - let input_destinations: Vec<_> = (0..inputs.len()) - .map(|_| { - let pkh_destination = - account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object(); - if rng.gen::() { - pkh_destination - } else { - let pkh = match pkh_destination { - Destination::PublicKeyHash(pkh) => pkh, - _ => panic!("not a public key hash destination"), - }; - let pk = account.find_corresponding_pub_key(&pkh).unwrap(); - Destination::PublicKey(pk) - } - }) - .collect(); - - let tx = Transaction::new( - 0, - inputs, - vec![TxOutput::Transfer( - OutputValue::Coin(Amount::from_atoms(rng.gen())), - account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), - )], - ) - .unwrap(); - - let intent: String = [rng.gen::(), rng.gen::(), rng.gen::()].iter().collect(); - let res = signer - .sign_transaction_intent( - &tx, - &input_destinations, - &intent, - account.key_chain(), - &db_tx, - ) - .unwrap(); - - let expected_signed_message = - SignedTransactionIntent::get_message_to_sign(&intent, &tx.get_id()); - res.verify(&config, &input_destinations, &expected_signed_message).unwrap(); + test_sign_transaction_intent(&mut rng, trezor_signer); } #[rstest] @@ -193,338 +55,11 @@ fn sign_transaction_intent(#[case] seed: Seed) { #[case(Seed::from_entropy())] #[serial] fn sign_transaction(#[case] seed: Seed) { - use std::num::NonZeroU8; - - use common::{ - chain::{ - classic_multisig::ClassicMultisigChallenge, - tokens::{IsTokenUnfreezable, Metadata, TokenIssuanceV1}, - OrderId, - }, - primitives::amount::UnsignedIntType, - }; - use crypto::vrf::VRFPrivateKey; - use serialization::extras::non_empty_vec::DataOrNoVec; + use crate::signer::signer_test_helpers::test_sign_transaction; let mut rng = make_seedable_rng(seed); - let chain_config = Arc::new(create_regtest()); - let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); - let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); - - let master_key_chain = MasterKeyChain::new_from_mnemonic( - chain_config.clone(), - &mut db_tx, - MNEMONIC, - None, - StoreSeedPhrase::DoNotStore, - ) - .unwrap(); - - let key_chain = master_key_chain - .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) - .unwrap(); - let mut account = Account::new(chain_config.clone(), &mut db_tx, key_chain, None).unwrap(); - - let amounts: Vec = (0..(2 + rng.next_u32() % 5)) - .map(|_| Amount::from_atoms(rng.gen_range(10..100) as UnsignedIntType)) - .collect(); - - let total_amount = amounts.iter().fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); - eprintln!("total utxo amounts: {total_amount:?}"); - - let utxos: Vec = amounts - .iter() - .map(|a| { - let purpose = if rng.gen_bool(0.5) { - ReceiveFunds - } else { - Change - }; - - TxOutput::Transfer( - OutputValue::Coin(*a), - account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object(), - ) - }) - .collect(); - - let inputs: Vec = (0..utxos.len()) - .map(|_| { - let source_id = if rng.gen_bool(0.5) { - Id::::new(H256::random_using(&mut rng)).into() - } else { - Id::::new(H256::random_using(&mut rng)).into() - }; - TxInput::from_utxo(source_id, rng.next_u32()) - }) - .collect(); - - let (_dest_prv, pub_key1) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); - let pub_key2 = if let Destination::PublicKeyHash(pkh) = - account.get_new_address(&mut db_tx, Change).unwrap().1.into_object() - { - account.find_corresponding_pub_key(&pkh).unwrap() - } else { - panic!("not a public key hash") - }; - let pub_key3 = if let Destination::PublicKeyHash(pkh) = - account.get_new_address(&mut db_tx, Change).unwrap().1.into_object() - { - account.find_corresponding_pub_key(&pkh).unwrap() - } else { - panic!("not a public key hash") - }; - let min_required_signatures = 2; - let challenge = ClassicMultisigChallenge::new( - &chain_config, - NonZeroU8::new(min_required_signatures).unwrap(), - vec![pub_key1.clone(), pub_key2.clone(), pub_key3], - ) - .unwrap(); - let multisig_hash = account.add_standalone_multisig(&mut db_tx, challenge, None).unwrap(); - - let multisig_dest = Destination::ClassicMultisig(multisig_hash); - - let source_id: OutPointSourceId = if rng.gen_bool(0.5) { - Id::::new(H256::random_using(&mut rng)).into() - } else { - Id::::new(H256::random_using(&mut rng)).into() - }; - let multisig_input = TxInput::from_utxo(source_id.clone(), rng.next_u32()); - let multisig_utxo = TxOutput::Transfer(OutputValue::Coin(Amount::from_atoms(1)), multisig_dest); - - let secret = HtlcSecret::new_from_rng(&mut rng); - let hash_lock = HashedTimelockContract { - secret_hash: secret.hash(), - spend_key: Destination::PublicKey(pub_key2.clone()), - refund_timelock: OutputTimeLock::UntilHeight(BlockHeight::new(0)), - refund_key: Destination::PublicKey(pub_key1), - }; - - let htlc_input = TxInput::from_utxo(source_id, rng.next_u32()); - let htlc_utxo = TxOutput::Htlc( - OutputValue::Coin(Amount::from_atoms(rng.gen::() as u128)), - Box::new(hash_lock.clone()), - ); - - let token_id = TokenId::new(H256::random_using(&mut rng)); - let order_id = OrderId::new(H256::random_using(&mut rng)); - - let dest_amount = total_amount.div(10).unwrap(); - let lock_amount = total_amount.div(10).unwrap(); - let burn_amount = total_amount.div(10).unwrap(); - let change_amount = total_amount.div(10).unwrap(); - let outputs_amounts_sum = [dest_amount, lock_amount, burn_amount, change_amount] - .iter() - .fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); - let _fee_amount = total_amount.sub(outputs_amounts_sum).unwrap(); - - let acc_inputs = vec![ - TxInput::Account(AccountOutPoint::new( - AccountNonce::new(1), - AccountSpending::DelegationBalance( - DelegationId::new(H256::random_using(&mut rng)), - Amount::from_atoms(1_u128), - ), - )), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::MintTokens(token_id, (dest_amount + Amount::from_atoms(100)).unwrap()), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::UnmintTokens(token_id), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::LockTokenSupply(token_id), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::FreezeToken(token_id, IsTokenUnfreezable::Yes), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::UnfreezeToken(token_id), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::ChangeTokenAuthority( - TokenId::new(H256::random_using(&mut rng)), - Destination::AnyoneCanSpend, - ), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::ConcludeOrder(order_id), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::FillOrder(order_id, Amount::from_atoms(1), Destination::AnyoneCanSpend), - ), - TxInput::AccountCommand( - AccountNonce::new(rng.next_u64()), - AccountCommand::ChangeTokenMetadataUri( - TokenId::new(H256::random_using(&mut rng)), - "http://uri".as_bytes().to_vec(), - ), - ), - ]; - let acc_dests: Vec = acc_inputs - .iter() - .map(|_| { - let purpose = if rng.gen_bool(0.5) { - ReceiveFunds - } else { - Change - }; - - account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object() - }) - .collect(); - - let (_dest_prv, dest_pub) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); - let (_, vrf_public_key) = VRFPrivateKey::new_from_entropy(crypto::vrf::VRFKeyKind::Schnorrkel); - - let pool_id = PoolId::new(H256::random()); - let delegation_id = DelegationId::new(H256::random()); - let pool_data = StakePoolData::new( - Amount::from_atoms(5), - Destination::PublicKey(dest_pub.clone()), - vrf_public_key, - Destination::PublicKey(dest_pub.clone()), - PerThousand::new_from_rng(&mut rng), - Amount::from_atoms(100), - ); - let token_issuance = TokenIssuance::V1(TokenIssuanceV1 { - token_ticker: "XXXX".as_bytes().to_vec(), - number_of_decimals: rng.gen_range(1..18), - metadata_uri: "http://uri".as_bytes().to_vec(), - total_supply: common::chain::tokens::TokenTotalSupply::Unlimited, - authority: Destination::PublicKey(dest_pub.clone()), - is_freezable: common::chain::tokens::IsTokenFreezable::No, - }); - - let nft_issuance = NftIssuance::V0(NftIssuanceV0 { - metadata: Metadata { - creator: None, - name: "Name".as_bytes().to_vec(), - description: "SomeNFT".as_bytes().to_vec(), - ticker: "NFTX".as_bytes().to_vec(), - icon_uri: DataOrNoVec::from(None), - additional_metadata_uri: DataOrNoVec::from(None), - media_uri: DataOrNoVec::from(None), - media_hash: "123456".as_bytes().to_vec(), - }, - }); - let nft_id = TokenId::new(H256::random()); - - let order_data = OrderData::new( - Destination::PublicKey(dest_pub.clone()), - OutputValue::Coin(Amount::from_atoms(100)), - OutputValue::Coin(total_amount), - ); - - let outputs = vec![ - TxOutput::Transfer( - OutputValue::TokenV1(token_id, dest_amount), - Destination::PublicKey(dest_pub.clone()), - ), - TxOutput::Transfer( - OutputValue::Coin(dest_amount), - Destination::PublicKey(dest_pub), - ), - TxOutput::LockThenTransfer( - OutputValue::Coin(lock_amount), - Destination::AnyoneCanSpend, - OutputTimeLock::ForSeconds(rng.next_u64()), - ), - TxOutput::Burn(OutputValue::Coin(burn_amount)), - TxOutput::CreateStakePool(pool_id, Box::new(pool_data)), - TxOutput::CreateDelegationId( - Destination::AnyoneCanSpend, - PoolId::new(H256::random_using(&mut rng)), - ), - TxOutput::DelegateStaking(burn_amount, delegation_id), - TxOutput::IssueFungibleToken(Box::new(token_issuance)), - TxOutput::IssueNft( - nft_id, - Box::new(nft_issuance.clone()), - Destination::AnyoneCanSpend, - ), - TxOutput::DataDeposit(vec![1, 2, 3]), - TxOutput::Htlc(OutputValue::Coin(burn_amount), Box::new(hash_lock)), - TxOutput::CreateOrder(Box::new(order_data)), - ]; - - let req = SendRequest::new() - .with_inputs( - izip!(inputs.clone(), utxos.clone(), vec![None; inputs.len()]), - &|_| None, - ) - .unwrap() - .with_inputs( - [(htlc_input.clone(), htlc_utxo.clone(), Some(secret))], - &|_| None, - ) - .unwrap() - .with_inputs( - [(multisig_input.clone(), multisig_utxo.clone(), None)], - &|_| None, - ) - .unwrap() - .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) - .with_outputs(outputs); - let destinations = req.destinations().to_vec(); - let additional_info = TxAdditionalInfo::with_token_info( - token_id, - TokenAdditionalInfo { - num_decimals: 1, - ticker: "TKN".as_bytes().to_vec(), - }, - ) - .join(TxAdditionalInfo::with_order_info( - order_id, - OrderAdditionalInfo { - ask_balance: Amount::from_atoms(10), - give_balance: Amount::from_atoms(100), - initially_asked: OutputValue::Coin(Amount::from_atoms(20)), - initially_given: OutputValue::TokenV1(token_id, Amount::from_atoms(200)), - }, - )); - let ptx = req.into_partially_signed_tx(additional_info).unwrap(); - - let mut client = find_test_device(); - let session_id = client.initialize(None).unwrap().ok().unwrap().session_id().to_vec(); - - let mut signer = TrezorSigner::new( - chain_config.clone(), - Arc::new(Mutex::new(client)), - session_id, - ); - let (ptx, _, _) = signer.sign_tx(ptx, account.key_chain(), &db_tx).unwrap(); - - eprintln!("num inputs in tx: {} {:?}", inputs.len(), ptx.witnesses()); - assert!(ptx.all_signatures_available()); - - let utxos_ref = utxos - .iter() - .map(Some) - .chain([Some(&htlc_utxo), Some(&multisig_utxo)]) - .chain(acc_dests.iter().map(|_| None)) - .collect::>(); - - for (i, dest) in destinations.iter().enumerate() { - tx_verifier::input_check::signature_only_check::verify_tx_signature( - &chain_config, - dest, - &ptx, - &utxos_ref, - i, - ) - .unwrap(); - } + test_sign_transaction(&mut rng, trezor_signer); } fn find_test_device() -> Trezor {