From 363b52b490a184de49df1d66c96edc7c5015ac4c Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 1 Jun 2026 17:03:39 +0300 Subject: [PATCH 1/3] Always use input commitments v1 in cold wallet mode --- wallet/src/signer/ledger_signer/mod.rs | 13 +- wallet/src/signer/ledger_signer/tests/mod.rs | 24 +- wallet/src/signer/mod.rs | 7 +- wallet/src/signer/software_signer/mod.rs | 58 ++++- wallet/src/signer/software_signer/tests.rs | 35 ++- .../tests/generic_fixed_signature_tests.rs | 25 +- wallet/src/signer/tests/generic_tests.rs | 23 +- wallet/src/signer/tests/mod.rs | 12 +- wallet/src/signer/trezor_signer/mod.rs | 13 +- wallet/src/signer/trezor_signer/tests.rs | 34 ++- wallet/src/wallet/mod.rs | 3 +- wallet/src/wallet/test_helpers.rs | 75 ++++-- wallet/src/wallet/tests.rs | 238 +++++++++++++++++- wallet/types/src/wallet_type.rs | 21 ++ 14 files changed, 485 insertions(+), 96 deletions(-) diff --git a/wallet/src/signer/ledger_signer/mod.rs b/wallet/src/signer/ledger_signer/mod.rs index 1ec3d2cebb..dca77fc261 100644 --- a/wallet/src/signer/ledger_signer/mod.rs +++ b/wallet/src/signer/ledger_signer/mod.rs @@ -1229,8 +1229,17 @@ impl SignerProvider for LedgerSignerProvider { type S = LedgerSigner; type K = AccountKeyChainImplHardware; - fn provide(&mut self, chain_config: Arc, _account_index: U31) -> Self::S { - LedgerSigner::new(chain_config, self.client.clone(), self.clone()) + fn provide( + &mut self, + chain_config: Arc, + _account_index: U31, + _db_tx: &impl WalletStorageReadLocked, + ) -> WalletResult { + Ok(LedgerSigner::new( + chain_config, + self.client.clone(), + self.clone(), + )) } async fn make_new_account( diff --git a/wallet/src/signer/ledger_signer/tests/mod.rs b/wallet/src/signer/ledger_signer/tests/mod.rs index c59a7017fb..8f1d5f5606 100644 --- a/wallet/src/signer/ledger_signer/tests/mod.rs +++ b/wallet/src/signer/ledger_signer/tests/mod.rs @@ -316,13 +316,10 @@ async fn test_sign_transaction_intent(#[case] seed: Seed) { #[rstest] #[trace] #[serial_test::serial] -#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V1)] +#[case(Seed::from_entropy())] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_sign_transaction( - #[case] seed: Seed, - #[case] input_commitments_version: SighashInputCommitmentVersion, -) { - log::debug!("test_sign_transaction, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); +async fn test_sign_transaction(#[case] seed: Seed) { + log::debug!("test_sign_transaction, seed = {seed:?}"); let (auto_confirmer_handle, control_msg_tx, make_ledger_signer) = setup(false).await; @@ -330,7 +327,8 @@ async fn test_sign_transaction( test_sign_transaction_generic( &mut rng, - input_commitments_version, + false, + SighashInputCommitmentVersion::V1, make_ledger_signer, no_another_signer(), false, @@ -397,13 +395,10 @@ async fn test_sign_transaction_intent_sig_consistency(#[case] seed: Seed) { #[rstest] #[trace] #[serial_test::serial] -#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V1)] +#[case(Seed::from_entropy())] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_sign_transaction_sig_consistency( - #[case] seed: Seed, - #[case] input_commitments_version: SighashInputCommitmentVersion, -) { - log::debug!("test_sign_transaction_sig_consistency, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); +async fn test_sign_transaction_sig_consistency(#[case] seed: Seed) { + log::debug!("test_sign_transaction_sig_consistency, seed = {seed:?}"); let (auto_confirmer_handle, control_msg_tx, make_ledger_signer) = setup(true).await; @@ -411,7 +406,8 @@ async fn test_sign_transaction_sig_consistency( test_sign_transaction_generic( &mut rng, - input_commitments_version, + false, + SighashInputCommitmentVersion::V1, make_ledger_signer, Some(make_deterministic_software_signer), false, diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index 80a6dbaabd..25b598a9d4 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -182,7 +182,12 @@ pub trait SignerProvider: Send { type S: Signer + Send; type K: AccountKeyChains + Sync + Send; - fn provide(&mut self, chain_config: Arc, account_index: U31) -> Self::S; + fn provide( + &mut self, + chain_config: Arc, + account_index: U31, + db_tx: &impl WalletStorageReadLocked, + ) -> WalletResult; async fn make_new_account( &mut self, diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index b163a34aa5..fb015448f3 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -39,7 +39,8 @@ use common::{ }, DestinationSigError, }, - ChainConfig, Destination, SignedTransactionIntent, Transaction, TxOutput, + ChainConfig, Destination, SighashInputCommitmentVersion, SignedTransactionIntent, + Transaction, TxOutput, }, primitives::BlockHeight, }; @@ -49,7 +50,7 @@ use crypto::key::{ PredefinedSigAuxDataProvider, PrivateKey, SigAuxDataProvider, }; use randomness::make_true_rng; -use utils::ensure; +use utils::{debug_panic_or_log, ensure}; use wallet_storage::{ WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteUnlocked, }; @@ -58,7 +59,7 @@ use wallet_types::{ partially_signed_transaction::{PartiallySignedTransaction, TokensAdditionalInfo}, seed_phrase::StoreSeedPhrase, signature_status::SignatureStatus, - wallet_type::WalletType, + wallet_type::SoftwareWalletType, AccountId, }; @@ -76,11 +77,16 @@ use super::{utils::is_htlc_utxo, Signer, SignerError, SignerProvider, SignerResu pub struct SoftwareSigner { chain_config: Arc, account_index: U31, + wallet_type: SoftwareWalletType, sig_aux_data_provider: Mutex>, } impl SoftwareSigner { - pub fn new(chain_config: Arc, account_index: U31) -> Self { + pub fn new( + chain_config: Arc, + account_index: U31, + wallet_type: SoftwareWalletType, + ) -> Self { let use_deterministic_signer = *chain_config.chain_type() == ChainType::Regtest && cfg!(feature = "use-deterministic-signatures-in-software-signer-for-regtest"); @@ -88,12 +94,14 @@ impl SoftwareSigner { Self::new_with_sig_aux_data_provider( chain_config, account_index, + wallet_type, Box::new(PredefinedSigAuxDataProvider), ) } else { Self::new_with_sig_aux_data_provider( chain_config, account_index, + wallet_type, Box::new(make_true_rng()), ) } @@ -102,11 +110,13 @@ impl SoftwareSigner { pub fn new_with_sig_aux_data_provider( chain_config: Arc, account_index: U31, + wallet_type: SoftwareWalletType, sig_aux_data_provider: Box, ) -> Self { Self { chain_config, account_index, + wallet_type, sig_aux_data_provider: Mutex::new(sig_aux_data_provider), } } @@ -291,8 +301,19 @@ impl Signer for SoftwareSigner { Vec, Vec, )> { - let input_commitments = - ptx.make_sighash_input_commitments_at_height(&self.chain_config, block_height)?; + let input_commitments = match self.wallet_type { + SoftwareWalletType::Hot => { + ptx.make_sighash_input_commitments_at_height(&self.chain_config, block_height)? + } + SoftwareWalletType::Cold => { + // Wallet in the cold mode is not aware of the actual chain height, so block_height + // will always be zero here. Since at the moment of writing this the fork has already + // happened both on testnet and mainnet, we can unconditionally assume input commitments v1. + // TODO: remove the support of input commitments v0 in the wallet, always assume v1. + // Same for orders v0/v1. + ptx.make_sighash_input_commitments(SighashInputCommitmentVersion::V1)? + } + }; let (witnesses, prev_statuses, new_statuses) = ptx .witnesses() @@ -478,9 +499,10 @@ impl SoftwareSignerProvider { ) -> WalletResult { let this_wallet_type = db_tx.get_wallet_type()?; ensure!( - this_wallet_type == WalletType::Hot || this_wallet_type == WalletType::Cold, + this_wallet_type.to_software_wallet_type().is_some(), WalletError::HardwareWalletOpenedAsSoftwareWallet(this_wallet_type) ); + let master_key_chain = MasterKeyChain::new_from_existing_database(chain_config, db_tx)?; Ok(Self { master_key_chain }) } @@ -491,8 +513,26 @@ impl SignerProvider for SoftwareSignerProvider { type S = SoftwareSigner; type K = AccountKeyChainImplSoftware; - fn provide(&mut self, chain_config: Arc, account_index: U31) -> Self::S { - SoftwareSigner::new(chain_config, account_index) + fn provide( + &mut self, + chain_config: Arc, + account_index: U31, + db_tx: &impl WalletStorageReadLocked, + ) -> WalletResult { + let wallet_type = db_tx.get_wallet_type()?; + let software_wallet_type = + wallet_type.to_software_wallet_type().unwrap_or_else(|| { + debug_panic_or_log!( + "Db tx related to a hardware wallet ({wallet_type:?}) was passed to SoftwareSignerProvider::provide" + ); + SoftwareWalletType::Hot + }); + + Ok(SoftwareSigner::new( + chain_config, + account_index, + software_wallet_type, + )) } async fn make_new_account( diff --git a/wallet/src/signer/software_signer/tests.rs b/wallet/src/signer/software_signer/tests.rs index bf4a8acb22..17dbb9f36c 100644 --- a/wallet/src/signer/software_signer/tests.rs +++ b/wallet/src/signer/software_signer/tests.rs @@ -27,7 +27,8 @@ use crate::signer::tests::{ test_sign_message_generic, test_sign_transaction_generic, test_sign_transaction_intent_generic, MessageToSign, }, - make_deterministic_software_signer, make_software_signer, no_another_signer, + make_deterministic_software_signer, make_software_signer, make_software_signer_for_cold_wallet, + no_another_signer, }; #[rstest] @@ -58,19 +59,21 @@ async fn test_sign_transaction_intent(#[case] seed: Seed) { #[rstest] #[trace] -#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V0)] +#[case(Seed::from_entropy(), true, SighashInputCommitmentVersion::V0)] #[trace] -#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V1)] +#[case(Seed::from_entropy(), false, SighashInputCommitmentVersion::V1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_sign_transaction( #[case] seed: Seed, - #[case] input_commitments_version: SighashInputCommitmentVersion, + #[case] before_fork: bool, + #[case] expected_input_commitments_version: SighashInputCommitmentVersion, ) { let mut rng = make_seedable_rng(seed); test_sign_transaction_generic( &mut rng, - input_commitments_version, + before_fork, + expected_input_commitments_version, make_software_signer, no_another_signer(), true, @@ -78,6 +81,28 @@ async fn test_sign_transaction( .await; } +// Cold wallet should assume v1 commitments regardless of what it thinks the current height is. +#[rstest] +#[case(Seed::from_entropy())] +#[trace] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_sign_transaction_cold_wallet( + #[case] seed: Seed, + #[values(false, true)] before_fork: bool, +) { + let mut rng = make_seedable_rng(seed); + + test_sign_transaction_generic( + &mut rng, + before_fork, + SighashInputCommitmentVersion::V1, + make_software_signer_for_cold_wallet, + no_another_signer(), + true, + ) + .await; +} + #[rstest] #[trace] #[case(Seed::from_entropy())] diff --git a/wallet/src/signer/tests/generic_fixed_signature_tests.rs b/wallet/src/signer/tests/generic_fixed_signature_tests.rs index 8a1dda2331..a0d8e200f4 100644 --- a/wallet/src/signer/tests/generic_fixed_signature_tests.rs +++ b/wallet/src/signer/tests/generic_fixed_signature_tests.rs @@ -387,9 +387,8 @@ pub async fn test_fixed_signatures_generic( .unwrap(); assert!(ptx.all_signatures_available()); - let input_commitments = ptx - .make_sighash_input_commitments_at_height(&chain_config, tx_block_height) - .unwrap(); + let expected_input_commitments = + ptx.make_sighash_input_commitments(SighashInputCommitmentVersion::V0).unwrap(); let all_utxos = utxos .iter() .map(Some) @@ -402,7 +401,7 @@ pub async fn test_fixed_signatures_generic( &chain_config, dest, &ptx, - &input_commitments, + &expected_input_commitments, i, all_utxos[i].cloned(), ) @@ -911,8 +910,8 @@ pub async fn test_fixed_signatures_generic2( ); let ptx = req.into_partially_signed_tx(ptx_additional_info).unwrap(); - let input_commitments = ptx - .make_sighash_input_commitments_at_height(&chain_config, tx_block_height) + let expected_input_commitments = ptx + .make_sighash_input_commitments(input_commitments_version) .unwrap() .into_iter() .map(|comm| comm.deep_clone()) @@ -952,7 +951,7 @@ pub async fn test_fixed_signatures_generic2( &chain_config, dest, &ptx, - &input_commitments, + &expected_input_commitments, i, all_utxos[i].cloned(), ) @@ -1536,8 +1535,8 @@ pub async fn test_fixed_signatures_generic_no_legacy( ); let ptx = req.into_partially_signed_tx(ptx_additional_info).unwrap(); - let input_commitments = ptx - .make_sighash_input_commitments_at_height(&chain_config, tx_block_height) + let expected_input_commitments = ptx + .make_sighash_input_commitments(SighashInputCommitmentVersion::V1) .unwrap() .into_iter() .map(|comm| comm.deep_clone()) @@ -1577,7 +1576,7 @@ pub async fn test_fixed_signatures_generic_no_legacy( &chain_config, dest, &ptx, - &input_commitments, + &expected_input_commitments, i, all_utxos[i].cloned(), ) @@ -1901,8 +1900,8 @@ pub async fn test_fixed_signatures_generic_htlc_refunding( ); let ptx = req.into_partially_signed_tx(ptx_additional_info).unwrap(); - let input_commitments = ptx - .make_sighash_input_commitments_at_height(&chain_config, tx_block_height) + let expected_input_commitments = ptx + .make_sighash_input_commitments(input_commitments_version) .unwrap() .into_iter() .map(|comm| comm.deep_clone()) @@ -1944,7 +1943,7 @@ pub async fn test_fixed_signatures_generic_htlc_refunding( &chain_config, dest, &ptx, - &input_commitments, + &expected_input_commitments, i, all_utxos[i].cloned(), ) diff --git a/wallet/src/signer/tests/generic_tests.rs b/wallet/src/signer/tests/generic_tests.rs index 0f21c305f6..9af9f5640d 100644 --- a/wallet/src/signer/tests/generic_tests.rs +++ b/wallet/src/signer/tests/generic_tests.rs @@ -319,9 +319,15 @@ pub async fn test_sign_transaction_intent_generic( assert_eq!(err, SignerError::DestinationNotFromThisWallet); } +// Note: unlike some other tests (in particular, the "fixed signature" ones) that only accept +// input_commitments_version, which determines both the expected commitments and the height +// at which the signatures will be produced, this test accepts both the expected version +// and the flag `before_fork`, which determines the height. This is used in the software signer +// tests to check that in the cold mode v1 commitments are used regardless of the current height. pub async fn test_sign_transaction_generic( rng: &mut (impl Rng + CryptoRng), - input_commitments_version: SighashInputCommitmentVersion, + before_fork: bool, + expected_input_commitments_version: SighashInputCommitmentVersion, make_signer: MkS1, make_another_signer: Option, include_orders_v0: bool, @@ -333,9 +339,10 @@ pub async fn test_sign_transaction_generic( { let (sighash_input_commitment_version_fork_height, tx_block_height) = { let fork_height = rng.gen_range(100..100_000); - let tx_block_height = match input_commitments_version { - SighashInputCommitmentVersion::V0 => rng.gen_range(1..fork_height), - SighashInputCommitmentVersion::V1 => rng.gen_range(fork_height..fork_height * 2), + let tx_block_height = if before_fork { + rng.gen_range(1..fork_height) + } else { + rng.gen_range(fork_height..fork_height * 2) }; ( BlockHeight::new(fork_height), @@ -888,8 +895,8 @@ pub async fn test_sign_transaction_generic( assert_eq!(ptx, another_ptx); } - let input_commitments = ptx - .make_sighash_input_commitments_at_height(&chain_config, tx_block_height) + let expected_input_commitments = ptx + .make_sighash_input_commitments(expected_input_commitments_version) .unwrap() .into_iter() .map(|comm| comm.deep_clone()) @@ -908,7 +915,7 @@ pub async fn test_sign_transaction_generic( &chain_config, dest, &ptx, - &input_commitments, + &expected_input_commitments, i, all_utxos[i].cloned(), ) @@ -983,7 +990,7 @@ pub async fn test_sign_transaction_generic( &chain_config, dest, &ptx, - &input_commitments, + &expected_input_commitments, i, all_utxos[i].cloned(), ) diff --git a/wallet/src/signer/tests/mod.rs b/wallet/src/signer/tests/mod.rs index f7614cb553..caa63620cd 100644 --- a/wallet/src/signer/tests/mod.rs +++ b/wallet/src/signer/tests/mod.rs @@ -21,7 +21,7 @@ use std::sync::Arc; use common::chain::ChainConfig; use crypto::key::{hdkd::u31::U31, PredefinedSigAuxDataProvider}; use wallet_storage::StoreTxRwUnlocked; -use wallet_types::seed_phrase::StoreSeedPhrase; +use wallet_types::{seed_phrase::StoreSeedPhrase, wallet_type::SoftwareWalletType}; use crate::{ key_chain::{AccountKeyChainImplSoftware, MasterKeyChain, LOOKAHEAD_SIZE}, @@ -58,7 +58,14 @@ fn account_from_mnemonic( } pub fn make_software_signer(chain_config: Arc, account_index: U31) -> SoftwareSigner { - SoftwareSigner::new(chain_config, account_index) + SoftwareSigner::new(chain_config, account_index, SoftwareWalletType::Hot) +} + +pub fn make_software_signer_for_cold_wallet( + chain_config: Arc, + account_index: U31, +) -> SoftwareSigner { + SoftwareSigner::new(chain_config, account_index, SoftwareWalletType::Cold) } // Return a SoftwareSigner that will produce Trezor-like signatures. @@ -69,6 +76,7 @@ pub fn make_deterministic_software_signer( SoftwareSigner::new_with_sig_aux_data_provider( chain_config, account_index, + SoftwareWalletType::Hot, Box::new(PredefinedSigAuxDataProvider), ) } diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs index eb6b0644d6..f8c85a5b72 100644 --- a/wallet/src/signer/trezor_signer/mod.rs +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -1781,8 +1781,17 @@ impl SignerProvider for TrezorSignerProvider { type S = TrezorSigner; type K = AccountKeyChainImplHardware; - fn provide(&mut self, chain_config: Arc, _account_index: U31) -> Self::S { - TrezorSigner::new(chain_config, self.client.clone(), self.session_id.clone()) + fn provide( + &mut self, + chain_config: Arc, + _account_index: U31, + _db_tx: &impl WalletStorageReadLocked, + ) -> WalletResult { + Ok(TrezorSigner::new( + chain_config, + self.client.clone(), + self.session_id.clone(), + )) } async fn make_new_account( diff --git a/wallet/src/signer/trezor_signer/tests.rs b/wallet/src/signer/trezor_signer/tests.rs index 6ccdaab64a..13cf526d82 100644 --- a/wallet/src/signer/trezor_signer/tests.rs +++ b/wallet/src/signer/trezor_signer/tests.rs @@ -112,16 +112,22 @@ async fn test_sign_transaction_intent(#[case] seed: Seed) { #[rstest] #[trace] #[serial] -#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V0)] +#[case(Seed::from_entropy(), true, SighashInputCommitmentVersion::V0)] #[trace] #[serial] -#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V1)] +#[case(Seed::from_entropy(), false, SighashInputCommitmentVersion::V1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_sign_transaction( #[case] seed: Seed, - #[case] input_commitments_version: SighashInputCommitmentVersion, + #[case] before_fork: bool, + #[case] expected_input_commitments_version: SighashInputCommitmentVersion, ) { - log::debug!("test_sign_transaction, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); + log::debug!( + "test_sign_transaction, seed = {:?}, before_fork = {}, expected_input_commitments_version = {:?}", + seed, + before_fork, + expected_input_commitments_version + ); let _join_guard = maybe_spawn_auto_confirmer(); @@ -129,7 +135,8 @@ async fn test_sign_transaction( test_sign_transaction_generic( &mut rng, - input_commitments_version, + before_fork, + expected_input_commitments_version, make_trezor_signer, no_another_signer(), true, @@ -258,16 +265,22 @@ async fn test_sign_transaction_intent_sig_consistency(#[case] seed: Seed) { #[rstest] #[trace] #[serial] -#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V0)] +#[case(Seed::from_entropy(), true, SighashInputCommitmentVersion::V0)] #[trace] #[serial] -#[case(Seed::from_entropy(), SighashInputCommitmentVersion::V1)] +#[case(Seed::from_entropy(), false, SighashInputCommitmentVersion::V1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_sign_transaction_sig_consistency( #[case] seed: Seed, - #[case] input_commitments_version: SighashInputCommitmentVersion, + #[case] before_fork: bool, + #[case] expected_input_commitments_version: SighashInputCommitmentVersion, ) { - log::debug!("test_sign_transaction_sig_consistency, seed = {seed:?}, input_commitments_version = {input_commitments_version:?}"); + log::debug!( + "test_sign_transaction_sig_consistency, seed = {:?}, before_fork = {}, expected_input_commitments_version = {:?}", + seed, + before_fork, + expected_input_commitments_version + ); let _join_guard = maybe_spawn_auto_confirmer(); @@ -275,7 +288,8 @@ async fn test_sign_transaction_sig_consistency( test_sign_transaction_generic( &mut rng, - input_commitments_version, + before_fork, + expected_input_commitments_version, make_deterministic_trezor_signer, Some(make_deterministic_software_signer), true, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index b99edde205..6b768ba209 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -1190,7 +1190,8 @@ where let account = Self::get_account_mut(&mut self.accounts, account_index)?; let mut db_tx = self.db.transaction_rw_unlocked(None)?; let result = create_request(account, &mut db_tx); - let signer = self.signer_provider.provide(self.chain_config.clone(), account_index); + let signer = + self.signer_provider.provide(self.chain_config.clone(), account_index, &db_tx)?; let config = self.chain_config.clone(); let result = sign_request(result, account.key_chain(), &mut db_tx, config, signer).await; diff --git a/wallet/src/wallet/test_helpers.rs b/wallet/src/wallet/test_helpers.rs index dc3b90cf59..1aa7776afa 100644 --- a/wallet/src/wallet/test_helpers.rs +++ b/wallet/src/wallet/test_helpers.rs @@ -25,7 +25,10 @@ use common::{ primitives::BlockHeight, }; use wallet_storage::{DefaultBackend, Store}; -use wallet_types::{seed_phrase::StoreSeedPhrase, wallet_type::WalletType}; +use wallet_types::{ + seed_phrase::StoreSeedPhrase, + wallet_type::{WalletControllerMode, WalletType}, +}; use crate::{ signer::{software_signer::SoftwareSignerProvider, SignerProvider}, @@ -38,27 +41,15 @@ pub async fn create_wallet_with_mnemonic( chain_config: Arc, mnemonic: &str, ) -> DefaultWallet { - let db = create_wallet_in_memory().unwrap(); - let genesis_block_id = chain_config.genesis_block_id(); - Wallet::create_new_wallet( - chain_config.clone(), - db, - (BlockHeight::new(0), genesis_block_id), - WalletType::Hot, - async |db_tx| { - Ok(SoftwareSignerProvider::new_from_mnemonic( - chain_config, - db_tx, - mnemonic, - None, - StoreSeedPhrase::DoNotStore, - )?) - }, - ) - .await - .unwrap() - .wallet() - .unwrap() + create_wallet_with_type_and_mnemonic(chain_config, WalletType::Hot, mnemonic).await +} + +pub async fn create_wallet_with_type_and_mnemonic( + chain_config: Arc, + wallet_type: WalletType, + mnemonic: &str, +) -> DefaultWallet { + create_wallet_generic(chain_config, wallet_type, mnemonic, None).await } pub fn create_named_in_memory_backend(db_name: &str) -> DefaultBackend { @@ -74,13 +65,26 @@ pub async fn create_wallet_with_mnemonic_and_named_db( mnemonic: &str, db_name: &str, ) -> DefaultWallet { - let db = create_named_in_memory_store(db_name); + create_wallet_generic(chain_config, WalletType::Hot, mnemonic, Some(db_name)).await +} + +pub async fn create_wallet_generic( + chain_config: Arc, + wallet_type: WalletType, + mnemonic: &str, + db_name: Option<&str>, +) -> DefaultWallet { + let db = if let Some(db_name) = db_name { + create_named_in_memory_store(db_name) + } else { + create_wallet_in_memory().unwrap() + }; let genesis_block_id = chain_config.genesis_block_id(); Wallet::create_new_wallet( chain_config.clone(), db, (BlockHeight::new(0), genesis_block_id), - WalletType::Hot, + wallet_type, async |db_tx| { SoftwareSignerProvider::new_from_mnemonic( chain_config, @@ -98,6 +102,29 @@ pub async fn create_wallet_with_mnemonic_and_named_db( .unwrap() } +pub async fn load_wallet( + chain_config: Arc, + db_name: &str, + controller_mode: WalletControllerMode, + force_change_wallet_type: bool, +) -> DefaultWallet { + let db = create_named_in_memory_store(db_name); + + Wallet::load_wallet( + Arc::clone(&chain_config), + db, + None, + |_| Ok(()), + controller_mode, + force_change_wallet_type, + async |db_tx| SoftwareSignerProvider::load_from_database(chain_config, &db_tx), + ) + .await + .unwrap() + .wallet() + .unwrap() +} + pub async fn scan_wallet(wallet: &mut Wallet, height: BlockHeight, blocks: Vec) where B: storage::BackendWithSendableTransactions + 'static, diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 1ef8e95ab6..35dd9e8b5b 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -24,6 +24,7 @@ use rstest::rstest; use common::{ address::pubkeyhash::PublicKeyHash, chain::{ + self, block::{consensus_data::PoSData, timestamp::BlockTimestamp, BlockReward, ConsensusData}, config::{create_mainnet, create_regtest, create_unit_test_config, Builder, ChainType}, output_value::{OutputValue, RpcOutputValue}, @@ -32,7 +33,7 @@ use common::{ timelock::OutputTimeLock, tokens::{RPCIsTokenFrozen, TokenData, TokenIssuanceV0, TokenIssuanceV1}, AccountNonce, AccountSpending, ChainstateUpgradeBuilder, Currency, Destination, Genesis, - OutPointSourceId, TxInput, + NetUpgrades, OutPointSourceId, SighashInputCommitmentVersion, TxInput, }, primitives::{per_thousand::PerThousand, Idable, H256}, }; @@ -69,8 +70,9 @@ use crate::{ send_request::{make_address_output, make_create_delegation_output}, signer::software_signer::SoftwareSignerProvider, wallet::test_helpers::{ - create_named_in_memory_backend, create_named_in_memory_store, create_wallet_with_mnemonic, - create_wallet_with_mnemonic_and_named_db, scan_wallet, + create_named_in_memory_backend, create_named_in_memory_store, create_wallet_generic, + create_wallet_with_mnemonic, create_wallet_with_mnemonic_and_named_db, + create_wallet_with_type_and_mnemonic, load_wallet, scan_wallet, }, wallet_events::WalletEventsNoOp, DefaultWallet, @@ -78,6 +80,11 @@ use crate::{ use super::*; +#[ctor::ctor] +fn init() { + logging::init_logging(); +} + // TODO: Many of these tests require randomization... const MNEMONIC: &str = @@ -5493,7 +5500,12 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { // create cold wallet that is not synced and only contains decommission key let another_mnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow"; - let mut cold_wallet = create_wallet_with_mnemonic(chain_config.clone(), another_mnemonic).await; + let mut cold_wallet = create_wallet_with_type_and_mnemonic( + chain_config.clone(), + *[WalletType::Hot, WalletType::Cold].choose(&mut rng).unwrap(), + another_mnemonic, + ) + .await; let decommission_key = cold_wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; let coin_balance = get_coin_balance(&hot_wallet); @@ -5596,6 +5608,217 @@ async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { assert_eq!(coin_balance, pool_amount,); } +// Check that signing a pool decommission tx from a cold wallet produces signatures using +// input commitments v1. +#[rstest] +#[case(Seed::from_entropy())] +#[trace] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_decommission_pool_request_in_cold_wallet_expect_input_commitments_v1( + #[case] seed: Seed, + #[values(false, true)] reload_as_cold: bool, +) { + let mut rng = make_seedable_rng(seed); + + let input_commitments_v1_fork_height = BlockHeight::new(4); + let chain_config = chain::config::Builder::new(ChainType::Regtest) + .chainstate_upgrades({ + NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V0) + .build(), + ), + ( + input_commitments_v1_fork_height, + ChainstateUpgradeBuilder::latest() + .sighash_input_commitment_version(SighashInputCommitmentVersion::V1) + .build(), + ), + ]) + .unwrap() + }) + .build(); + + let chain_config = Arc::new(chain_config); + + let mut hot_wallet = create_wallet(Arc::clone(&chain_config)).await; + + // Create a cold wallet. If reload_as_cold is true, first create a hot wallet and force-reload + // it as cold. Otherwise just create a cold wallet. + let another_mnemonic = + "legal winner thank year wave sausage worth useful legal winner thank yellow"; + let mut cold_wallet = if reload_as_cold { + let db_name = random_ascii_alphanumeric_string(&mut rng, 10..20); + let _tmp_wallet = create_wallet_generic( + Arc::clone(&chain_config), + WalletType::Hot, + another_mnemonic, + Some(&db_name), + ) + .await; + + load_wallet( + Arc::clone(&chain_config), + &db_name, + WalletControllerMode::Cold, + true, + ) + .await + } else { + create_wallet_generic( + Arc::clone(&chain_config), + WalletType::Cold, + another_mnemonic, + None, + ) + .await + }; + let decommission_key = + cold_wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1.into_object(); + + let coin_balance = get_coin_balance(&hot_wallet); + assert_eq!(coin_balance, Amount::ZERO); + + // Generate a new block which sends reward to the wallet + let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); + let (_, _block1) = create_block(&chain_config, &mut hot_wallet, vec![], block1_amount, 0).await; + + let pool_ids = hot_wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + assert!(pool_ids.is_empty()); + + let coin_balance = get_coin_balance(&hot_wallet); + assert_eq!(coin_balance, block1_amount); + + let pool_amount = block1_amount; + + let res = hot_wallet.create_next_account(Some("name".into())).await.unwrap(); + assert_eq!(res, (U31::from_u32(1).unwrap(), Some("name".into()))); + + let pool_creation_tx = hot_wallet + .create_stake_pool( + DEFAULT_ACCOUNT_INDEX, + FeeRate::from_amount_per_kb(Amount::ZERO), + FeeRate::from_amount_per_kb(Amount::ZERO), + StakePoolCreationArguments { + amount: pool_amount, + margin_ratio_per_thousand: PerThousand::new_from_rng(&mut rng), + cost_per_block: Amount::ZERO, + decommission_key: decommission_key.clone(), + staker_key: None, + vrf_public_key: None, + }, + ) + .await + .unwrap() + .tx; + let pool_creation_tx_id = pool_creation_tx.transaction().get_id(); + let (_, _block2) = create_block( + &chain_config, + &mut hot_wallet, + vec![pool_creation_tx], + Amount::ZERO, + 1, + ) + .await; + + let pool_ids = hot_wallet.get_pools(DEFAULT_ACCOUNT_INDEX, WalletPoolsFilter::All).unwrap(); + assert_eq!(pool_ids.len(), 1); + let pool_id = pool_ids.first().unwrap().0; + + let pos_data = hot_wallet.get_pos_gen_block_data(DEFAULT_ACCOUNT_INDEX, pool_id).unwrap(); + let staker_key = Destination::PublicKey(pos_data.stake_public_key()); + + // Create a block using the pool that will be decommissioned, so that the utxo consumed + // during decommissioning is ProduceBlockFromStake (because it's one of the utxos for which + // input commitments v0 and v1 are different). + let block3 = Block::new( + vec![], + chain_config.genesis_block_id(), + chain_config.genesis_block().timestamp(), + ConsensusData::PoS(Box::new(PoSData::new( + vec![TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::Transaction(pool_creation_tx_id), + 0, + ))], + vec![], + pool_id, + pos_data.vrf_private_key().produce_vrf_data(VRFTranscript::new(&[])), + common::primitives::Compact(0), + ))), + BlockReward::new(vec![TxOutput::ProduceBlockFromStake(staker_key, pool_id)]), + ) + .unwrap(); + + scan_wallet(&mut hot_wallet, BlockHeight::new(2), vec![block3]).await; + + let pool_decommission_ptx = hot_wallet + .decommission_stake_pool_request( + DEFAULT_ACCOUNT_INDEX, + pool_id, + pool_amount, + None, + FeeRate::from_amount_per_kb(Amount::from_atoms(0)), + ) + .await + .unwrap(); + + // Sanity check: the consumed utxo is ProduceBlockFromStake. + assert_eq!(pool_decommission_ptx.input_utxos().len(), 1); + let pool_decommission_utxo = pool_decommission_ptx.input_utxos()[0].clone().unwrap(); + assert_matches!( + &pool_decommission_utxo, + TxOutput::ProduceBlockFromStake(_, _) + ); + + let expected_input_commitments = pool_decommission_ptx + .make_sighash_input_commitments_at_height(&chain_config, input_commitments_v1_fork_height) + .unwrap(); + let v0_input_commitments = pool_decommission_ptx + .make_sighash_input_commitments_at_height(&chain_config, BlockHeight::zero()) + .unwrap(); + // Sanity check + assert_ne!(expected_input_commitments, v0_input_commitments); + + // Sign the tx with cold wallet + let pool_decommission_ptx_after_signing = cold_wallet + .sign_raw_transaction( + DEFAULT_ACCOUNT_INDEX, + pool_decommission_ptx.clone(), + &TokensAdditionalInfo::new(), + ) + .await + .unwrap() + .0; + assert!(pool_decommission_ptx_after_signing.all_signatures_available()); + + let pool_decommission_signed_tx = pool_decommission_ptx_after_signing.into_signed_tx().unwrap(); + + // Verify the signature using v1 input commitments. + tx_verifier::input_check::signature_only_check::verify_tx_signature( + &chain_config, + &decommission_key, + &pool_decommission_signed_tx, + &expected_input_commitments, + 0, + Some(pool_decommission_utxo), + ) + .unwrap(); + + let (_, _block4) = create_block( + &chain_config, + &mut hot_wallet, + vec![pool_decommission_signed_tx], + Amount::ZERO, + 2, + ) + .await; + + let coin_balance = get_coin_balance(&hot_wallet); + assert_eq!(coin_balance, pool_amount); +} + #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -5702,7 +5925,12 @@ async fn sign_send_request_cold_wallet(#[case] seed: Seed) { // create cold wallet that is not synced let another_mnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow"; - let mut cold_wallet = create_wallet_with_mnemonic(chain_config.clone(), another_mnemonic).await; + let mut cold_wallet = create_wallet_with_type_and_mnemonic( + chain_config.clone(), + *[WalletType::Hot, WalletType::Cold].choose(&mut rng).unwrap(), + another_mnemonic, + ) + .await; let cold_wallet_address = cold_wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; let coin_balance = get_coin_balance(&hot_wallet); diff --git a/wallet/types/src/wallet_type.rs b/wallet/types/src/wallet_type.rs index d46fda23bc..5203b2b1b8 100644 --- a/wallet/types/src/wallet_type.rs +++ b/wallet/types/src/wallet_type.rs @@ -30,6 +30,27 @@ pub enum WalletType { Ledger, } +impl WalletType { + pub fn to_software_wallet_type(self) -> Option { + match self { + WalletType::Cold => Some(SoftwareWalletType::Cold), + WalletType::Hot => Some(SoftwareWalletType::Hot), + #[cfg(feature = "trezor")] + WalletType::Trezor => None, + #[cfg(feature = "ledger")] + WalletType::Ledger => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SoftwareWalletType { + Cold, + Hot, +} + +// Note: this is conceptually different from SoftwareWalletType, because here "Hot" includes +// hardware wallets as well. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum WalletControllerMode { Cold, From e61a4e5463cc2b6f5969d61937cc330187b9e5e5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 1 Jun 2026 18:42:40 +0300 Subject: [PATCH 2/3] Fix wallet in cold mode spamming error log messages about mempool being not available. --- wallet/wallet-controller/src/lib.rs | 98 +++++++++++-------- .../src/tests/compose_transaction_tests.rs | 6 +- wallet/wallet-node-client/src/node_traits.rs | 1 + .../src/rpc_client/cold_wallet_client.rs | 2 +- 4 files changed, 65 insertions(+), 42 deletions(-) diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 1fb599d7d0..96d6dd50b2 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -227,6 +227,7 @@ pub struct Controller { rpc_client: T, wallet: RuntimeWallet, + wallet_mode: WalletControllerMode, staking_started: BTreeSet, @@ -256,23 +257,20 @@ where wallet: RuntimeWallet, wallet_events: W, ) -> Result> { - let mempool_events = rpc_client - .mempool_subscribe_to_events() - .await - .map_err(ControllerError::NodeCallError)?; - let mut controller = Self { - chain_config, - rpc_client, - wallet, - staking_started: BTreeSet::new(), - wallet_events, - mempool_events, - finished_initial_sync: SetFlag::new(), + let mut controller = + Self::new_unsynced(chain_config, rpc_client, wallet, wallet_events).await?; + + // In the cold mode, try_sync_once is a no-op, so it doesn't matter whether we call it. + // We omit the call to avoid printing the "Syncing the wallet" log line, which looks + // confusing in the cold mode. + match controller.wallet_mode { + WalletControllerMode::Cold => {} + WalletControllerMode::Hot => { + log::info!("Syncing the wallet..."); + controller.try_sync_once().await?; + } }; - log::info!("Syncing the wallet..."); - controller.try_sync_once().await?; - Ok(controller) } @@ -282,14 +280,18 @@ where wallet: RuntimeWallet, wallet_events: W, ) -> Result> { + let wallet_mode = rpc_client.is_cold_wallet_node().await; + let mempool_events = rpc_client .mempool_subscribe_to_events() .await .map_err(ControllerError::NodeCallError)?; + Ok(Self { chain_config, rpc_client, wallet, + wallet_mode, staking_started: BTreeSet::new(), wallet_events, mempool_events, @@ -1457,26 +1459,33 @@ where } } - // after the first successful sync to the tip fetch all mempool transactions - if !self.finished_initial_sync.test() { - let txs = self.rpc_client.mempool_get_transactions().await; - - match txs { - Ok(txs) => { - if let Err(err) = - self.wallet.add_mempool_transactions(&txs, &self.wallet_events) - { - log::error!("Error adding mempool transactions: {err}"); - } else { - self.finished_initial_sync.set(); + match self.wallet_mode { + WalletControllerMode::Hot => { + // after the first successful sync to the tip fetch all mempool transactions + if !self.finished_initial_sync.test() { + let txs = self.rpc_client.mempool_get_transactions().await; + + match txs { + Ok(txs) => { + if let Err(err) = + self.wallet.add_mempool_transactions(&txs, &self.wallet_events) + { + log::error!("Error adding mempool transactions: {err}"); + } else { + self.finished_initial_sync.set(); + } + } + Err(err) => { + log::error!( + "Failed to fetch all transactions from the mempool: {err}" + ); + tokio::time::sleep(ERROR_DELAY).await; + continue; + } } } - Err(err) => { - log::error!("Failed to fetch all transactions from the mempool: {err}"); - tokio::time::sleep(ERROR_DELAY).await; - continue; - } } + WalletControllerMode::Cold => {} } let mut delay = Box::pin(tokio::time::sleep(NORMAL_DELAY)); @@ -1491,10 +1500,9 @@ where let event = match maybe_event { Some(e) => e, None => { - log::error!("Mempool notifications channel is closed."); + log::error!("Mempool notifications channel is closed"); tokio::time::sleep(ERROR_DELAY).await; - match self.rpc_client .mempool_subscribe_to_events() .await { @@ -1502,7 +1510,7 @@ where self.mempool_events = events; } Err(err) => { - log::error!("Subscribing to mempool notifications failed with: {err}"); + log::error!("Re-subscribing to mempool notifications failed: {err}"); } } break @@ -1519,14 +1527,14 @@ where Ok(Some(transaction)) => { let txs = [transaction]; if let Err(err) = self.wallet.add_mempool_transactions(&txs, &self.wallet_events) { - log::error!("Tx {} failed to be added in the wallet because of an error: {err}", tx_id); + log::error!("Error adding mempool transaction {tx_id:x} to the wallet: {err}"); } } Ok(None) => { - log::warn!("Tx {} announced by mempool, but not found when fetched", tx_id); + log::warn!("Transaction {tx_id:x} announced by mempool, but not found when fetched"); } Err(err) => { - log::error!("Error while fetching a transaction from mempool {err}"); + log::error!("Error fetching transaction {tx_id:x} from mempool: {err}"); } } } @@ -1534,7 +1542,17 @@ where } } } - self.rebroadcast_txs(&mut rebroadcast_txs_timer).await; + + // Note: normally a wallet in the cold mode will not have any transactions to broadcast. However, if it was + // force-converted from a hot wallet, it may have such transactions, in which case `rebroadcast_txs` will + // repeatedly print the warning "Rebroadcasting ... failed: Method is not available in cold wallet mode". + // So we avoid calling `rebroadcast_txs` in the cold mode. + match self.wallet_mode { + WalletControllerMode::Cold => {} + WalletControllerMode::Hot => { + self.rebroadcast_txs(&mut rebroadcast_txs_timer).await; + } + } } } @@ -1551,7 +1569,7 @@ where let tx_id = tx.transaction().get_id(); let res = self.rpc_client.submit_transaction(tx, Default::default()).await; if let Err(e) = res { - log::warn!("Rebroadcasting for tx {tx_id} failed: {e}"); + log::warn!("Rebroadcasting tx {tx_id:x} failed: {e}"); } } } diff --git a/wallet/wallet-controller/src/tests/compose_transaction_tests.rs b/wallet/wallet-controller/src/tests/compose_transaction_tests.rs index 4a5626b6d3..e8b18d152a 100644 --- a/wallet/wallet-controller/src/tests/compose_transaction_tests.rs +++ b/wallet/wallet-controller/src/tests/compose_transaction_tests.rs @@ -45,7 +45,9 @@ use wallet::{ account::TransactionToSign, wallet::test_helpers::create_wallet_with_mnemonic, wallet_events::WalletEventsNoOp, }; -use wallet_types::partially_signed_transaction::PtxAdditionalInfo; +use wallet_types::{ + partially_signed_transaction::PtxAdditionalInfo, wallet_type::WalletControllerMode, +}; use crate::{ helpers::get_referenced_token_ids_from_partially_signed_transaction, @@ -171,6 +173,8 @@ async fn general_test(#[case] seed: Seed, #[case] use_htlc_secret: bool) { is_initial_block_download: false, }; + node_mock.expect_is_cold_wallet_node().returning(|| WalletControllerMode::Hot); + node_mock .expect_get_utxo() .returning(move |outpoint| Ok(Some(utxos_to_return.get(&outpoint).unwrap().clone()))); diff --git a/wallet/wallet-node-client/src/node_traits.rs b/wallet/wallet-node-client/src/node_traits.rs index 87c1093e1d..9c15668065 100644 --- a/wallet/wallet-node-client/src/node_traits.rs +++ b/wallet/wallet-node-client/src/node_traits.rs @@ -45,6 +45,7 @@ pub trait NodeInterface { // Note: not requiring the `Error` trait here so that `anyhow::Error` can be used. type Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static; + // TODO: rename this, e.g. to wallet_mode. async fn is_cold_wallet_node(&self) -> WalletControllerMode; async fn chainstate_info(&self) -> Result; diff --git a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs index 743bef161c..ac2617e9a4 100644 --- a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs +++ b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs @@ -291,7 +291,7 @@ impl NodeInterface for ColdWalletClient { } async fn mempool_subscribe_to_events(&self) -> Result { - Ok(Box::new(futures::stream::empty())) + Ok(Box::new(futures::stream::pending())) } async fn mempool_get_transaction( From 5b012dfe1f2c5d7d385152860f11a8611b68a0de Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Mon, 1 Jun 2026 19:11:22 +0300 Subject: [PATCH 3/3] Update changelog for 1.3.1 --- CHANGELOG.md | 9 +++++++++ api-server/CHANGELOG.md | 4 ++++ wasm-wrappers/CHANGELOG.md | 4 ++++ 3 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a4b6f0b4e..9daa017aee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ - `wallet-create`/`wallet-recover`/`wallet-open` support the `ledger` subcommand, in addition to the existing `software` and `trezor`, which specifies the type of the wallet to operate on. +## [1.3.1] + +### Fixed + - Wallet: + - Fixed an issue where the wallet in cold mode would always use input commitments v0, thus producing signatures + that may no longer be valid at the current height. + - Fixed an issue where the wallet in cold mode would still try to access the mempool, which would cause `wallet-cli` + to print lots of error messages like "Method is not available in cold wallet mode". + ## [1.3.0] - 2026-04-09 ### Added diff --git a/api-server/CHANGELOG.md b/api-server/CHANGELOG.md index d2ee456c40..1fdf123d65 100644 --- a/api-server/CHANGELOG.md +++ b/api-server/CHANGELOG.md @@ -6,6 +6,10 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ ## [Unreleased] +## [1.3.1] + +No changes + ## [1.3.0] - 2026-04-09 ### Added diff --git a/wasm-wrappers/CHANGELOG.md b/wasm-wrappers/CHANGELOG.md index a6df926110..f23b00bf72 100644 --- a/wasm-wrappers/CHANGELOG.md +++ b/wasm-wrappers/CHANGELOG.md @@ -6,6 +6,10 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ ## [Unreleased] +## [1.3.1] + +No changes + ## [1.3.0] - 2026-04-09 ### Added