From a86c737a93453798af2549f8c398be97a399e818 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Tue, 12 Sep 2023 12:56:39 +0000 Subject: [PATCH 01/12] fix: sweep funds into shared wallet v2 Signed-off-by: Gregory Hill --- bitcoin/src/electrs/mod.rs | 5 + bitcoin/src/electrs/types.rs | 12 +++ bitcoin/src/iter.rs | 3 + bitcoin/src/lib.rs | 99 ++++++++++++++++---- bitcoin/src/light/mod.rs | 12 +++ bitcoin/tests/integration_test.rs | 55 ++++++++++- runtime/src/integration/bitcoin_simulator.rs | 13 +++ vault/src/connection_manager.rs | 5 + vault/src/execution.rs | 3 + vault/src/issue.rs | 6 +- vault/src/lib.rs | 1 + vault/src/metrics.rs | 3 + vault/src/replace.rs | 1 + vault/src/system.rs | 38 ++++++-- 14 files changed, 226 insertions(+), 30 deletions(-) diff --git a/bitcoin/src/electrs/mod.rs b/bitcoin/src/electrs/mod.rs index 536d3defe..7092a5646 100644 --- a/bitcoin/src/electrs/mod.rs +++ b/bitcoin/src/electrs/mod.rs @@ -105,6 +105,11 @@ impl ElectrsClient { Ok(ret) } + pub async fn is_tx_output_spent(&self, txid: &Txid, vout: u32) -> Result { + let spending_value: SpendingValue = self.get_and_decode(&format!("/tx/{txid}/outspend/{vout}")).await?; + Ok(spending_value.spent) + } + pub async fn get_blocks_tip_height(&self) -> Result { Ok(self.get("/blocks/tip/height").await?.parse()?) } diff --git a/bitcoin/src/electrs/types.rs b/bitcoin/src/electrs/types.rs index 83586a20d..bb72552d7 100644 --- a/bitcoin/src/electrs/types.rs +++ b/bitcoin/src/electrs/types.rs @@ -66,3 +66,15 @@ pub struct UtxoValue { pub status: TransactionStatus, pub value: u64, } + +// https://github.com/Blockstream/electrs/blob/adedee15f1fe460398a7045b292604df2161adc0/src/rest.rs#L448-L457 +#[derive(Deserialize)] +pub struct SpendingValue { + pub spent: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub txid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub vin: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} diff --git a/bitcoin/src/iter.rs b/bitcoin/src/iter.rs index 81dba865e..94994c153 100644 --- a/bitcoin/src/iter.rs +++ b/bitcoin/src/iter.rs @@ -214,6 +214,8 @@ mod tests { async fn get_proof(&self, txid: Txid, block_hash: &BlockHash) -> Result, Error>; async fn get_block_hash(&self, height: u32) -> Result; async fn get_new_address(&self) -> Result; + async fn get_new_sweep_address(&self) -> Result; + async fn get_last_sweep_height(&self) -> Result, Error>; async fn get_new_public_key(&self) -> Result; fn dump_private_key(&self, address: &Address) -> Result; fn import_private_key(&self, private_key: &PrivateKey, is_derivation_key: bool) -> Result<(), Error>; @@ -251,6 +253,7 @@ mod tests { fee_rate: SatPerVbyte, num_confirmations: u32, ) -> Result; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), Error>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), Error>; async fn rescan_electrs_for_addresses( diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 0ef0c1b6b..1ba838dcf 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -101,6 +101,8 @@ const RANDOMIZATION_FACTOR: f64 = 0.25; const DERIVATION_KEY_LABEL: &str = "derivation-key"; const DEPOSIT_LABEL: &str = "deposit"; +const SWEEP_ADDRESS: &str = "sweep-address"; + fn get_exponential_backoff() -> ExponentialBackoff { ExponentialBackoff { current_interval: INITIAL_INTERVAL, @@ -160,6 +162,10 @@ pub trait BitcoinCoreApi { async fn get_new_address(&self) -> Result; + async fn get_new_sweep_address(&self) -> Result; + + async fn get_last_sweep_height(&self) -> Result, Error>; + async fn get_new_public_key(&self) -> Result; fn dump_private_key(&self, address: &Address) -> Result; @@ -207,6 +213,8 @@ pub trait BitcoinCoreApi { num_confirmations: u32, ) -> Result; + async fn sweep_funds(&self, address: Address) -> Result; + async fn create_or_load_wallet(&self) -> Result<(), Error>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), Error>; @@ -362,7 +370,7 @@ impl BitcoinCoreBuilder { #[derive(Clone)] pub struct BitcoinCore { - rpc: Arc, + pub rpc: Arc, wallet_name: Option, network: Network, transaction_creation_lock: Arc>, @@ -791,6 +799,22 @@ impl BitcoinCoreApi for BitcoinCore { .require_network(self.network)?) } + async fn get_new_sweep_address(&self) -> Result { + Ok(self + .rpc + .get_new_address(Some(SWEEP_ADDRESS), Some(AddressType::Bech32))? + .require_network(self.network)?) + } + + async fn get_last_sweep_height(&self) -> Result, Error> { + Ok(self + .rpc + .list_transactions(Some(SWEEP_ADDRESS), Some(DEFAULT_MAX_TX_COUNT), None, None)? + .into_iter() + .filter_map(|tx| tx.info.blockheight) + .min()) + } + /// Gets a new public key for an address in the wallet async fn get_new_public_key(&self) -> Result { let address = self @@ -1035,6 +1059,61 @@ impl BitcoinCoreApi for BitcoinCore { .await?) } + async fn sweep_funds(&self, address: Address) -> Result { + let unspent = self.rpc.list_unspent(None, None, None, None, None)?; + + let mut amount = Amount::ZERO; + let mut utxos = Vec::::new(); + + for entry in unspent { + if self.electrs_client.is_tx_output_spent(&entry.txid, entry.vout).await? { + // skip if already spent + continue; + } + amount += entry.amount; + utxos.push(json::CreateRawTransactionInput { + txid: entry.txid, + vout: entry.vout, + sequence: None, + }) + } + + let mut outputs = serde_json::Map::::new(); + outputs.insert(address.to_string(), serde_json::Value::from(amount.to_btc())); + + let args = [ + serde_json::to_value::<&[json::CreateRawTransactionInput]>(&utxos)?, + serde_json::to_value(outputs)?, + serde_json::to_value(0i64)?, /* locktime - default 0: see https://developer.bitcoin.org/reference/rpc/createrawtransaction.html */ + serde_json::to_value(true)?, // BIP125-replaceable, aka Replace By Fee (RBF) + ]; + let raw_tx: String = self.rpc.call("createrawtransaction", &args)?; + + let funding_opts = FundRawTransactionOptions { + fee_rate: None, + add_inputs: Some(false), + subtract_fee_from_outputs: Some(vec![0]), + ..Default::default() + }; + let funded_raw_tx = self.rpc.fund_raw_transaction(raw_tx, Some(&funding_opts), None)?; + + let signed_funded_raw_tx = + self.rpc + .sign_raw_transaction_with_wallet(&funded_raw_tx.transaction()?, None, None)?; + + if signed_funded_raw_tx.errors.is_some() { + log::warn!( + "Received bitcoin funding errors (complete={}): {:?}", + signed_funded_raw_tx.complete, + signed_funded_raw_tx.errors + ); + return Err(Error::TransactionSigningError); + } + + let transaction = signed_funded_raw_tx.transaction()?; + self.rpc.send_raw_transaction(&transaction).map_err(Into::into) + } + /// Create or load a wallet on Bitcoin Core. async fn create_or_load_wallet(&self) -> Result<(), Error> { let wallet_name = if let Some(ref wallet_name) = self.wallet_name { @@ -1118,24 +1197,6 @@ impl BitcoinCoreApi for BitcoinCore { &[serde_json::to_value(raw_tx)?, serde_json::to_value(raw_merkle_proof)?], )?; } - // TODO: remove this migration after the next runtime upgrade - // filter to remove spent funds, the previous wallet migration caused - // signing failures for pruned nodes because they tried to double spend - let confirmed_payments_from = all_transactions.iter().filter(|tx| { - if let Some(status) = &tx.status { - if !status.confirmed { - return false; - } - }; - tx.vin - .iter() - .filter_map(|input| input.prevout.clone()) - .any(|output| matches!(&output.scriptpubkey_address, Some(addr) if addr == &address)) - }); - for transaction in confirmed_payments_from { - self.rpc - .call("removeprunedfunds", &[serde_json::to_value(transaction.txid)?])?; - } } Ok(()) } diff --git a/bitcoin/src/light/mod.rs b/bitcoin/src/light/mod.rs index 66f20231e..bb5324f85 100644 --- a/bitcoin/src/light/mod.rs +++ b/bitcoin/src/light/mod.rs @@ -168,6 +168,14 @@ impl BitcoinCoreApi for BitcoinLight { Ok(self.get_change_address()?) } + async fn get_new_sweep_address(&self) -> Result { + Ok(self.get_change_address()?) + } + + async fn get_last_sweep_height(&self) -> Result, BitcoinError> { + Ok(None) + } + async fn get_new_public_key(&self) -> Result { Ok(self.private_key.public_key(&self.secp_ctx)) } @@ -331,6 +339,10 @@ impl BitcoinCoreApi for BitcoinLight { .await?) } + async fn sweep_funds(&self, _address: Address) -> Result { + Ok(Txid::all_zeros()) + } + async fn create_or_load_wallet(&self) -> Result<(), BitcoinError> { // nothing to do Ok(()) diff --git a/bitcoin/tests/integration_test.rs b/bitcoin/tests/integration_test.rs index 938f6d63b..6a36f2355 100644 --- a/bitcoin/tests/integration_test.rs +++ b/bitcoin/tests/integration_test.rs @@ -1,6 +1,10 @@ #![cfg(feature = "uses-bitcoind")] -use bitcoin::{Auth, BitcoinCore, BitcoinCoreApi, BitcoinCoreBuilder, Error, Network, PrivateKey, PublicKey}; +use bitcoin::{ + Amount, Auth, BitcoinCore, BitcoinCoreApi, BitcoinCoreBuilder, Error, Network, PrivateKey, PublicKey, RpcApi, +}; +use bitcoincore_rpc::json; +use rand::{distributions::Alphanumeric, Rng}; use regex::Regex; use std::env::var; @@ -89,3 +93,52 @@ async fn should_add_new_deposit_key() -> Result<(), Error> { Ok(()) } + +fn rand_wallet_name() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(7) + .map(char::from) + .collect() +} + +#[tokio::test] +async fn should_sweep_funds() -> Result<(), Error> { + let btc_rpc1 = new_bitcoin_core(Some(rand_wallet_name()))?; + btc_rpc1.create_or_load_wallet().await?; + + let btc_rpc2 = new_bitcoin_core(Some(rand_wallet_name()))?; + btc_rpc2.create_or_load_wallet().await?; + + let btc_rpc3 = new_bitcoin_core(Some(rand_wallet_name()))?; + btc_rpc3.create_or_load_wallet().await?; + + // blocks must have 100 confirmations for reward to be spent + let address1 = btc_rpc1.get_new_address().await?; + btc_rpc1.mine_blocks(101, Some(address1)); + + let address2 = btc_rpc2.get_new_address().await?; + let txid = btc_rpc1.rpc.send_to_address( + &address2, + Amount::from_sat(100000), + None, + None, + None, + None, + None, + Some(json::EstimateMode::Economical), + )?; + btc_rpc1.mine_blocks(1, None); + + assert_eq!(btc_rpc2.get_balance(None)?.to_sat(), 100000); + + let address3 = btc_rpc3.get_new_address().await?; + let _txid = btc_rpc2.sweep_funds(address3).await?; + btc_rpc1.mine_blocks(1, None); + + assert_eq!(btc_rpc2.get_balance(None)?.to_sat(), 0); + // balance before minus fees + assert_eq!(btc_rpc3.get_balance(None)?.to_sat(), 97800); + + Ok(()) +} diff --git a/runtime/src/integration/bitcoin_simulator.rs b/runtime/src/integration/bitcoin_simulator.rs index 96838c98f..36891a013 100644 --- a/runtime/src/integration/bitcoin_simulator.rs +++ b/runtime/src/integration/bitcoin_simulator.rs @@ -405,6 +405,15 @@ impl BitcoinCoreApi for MockBitcoinCore { let address = BtcAddress::P2PKH(H160::from(bytes)); Ok(address.to_address(Network::Regtest)?) } + + async fn get_new_sweep_address(&self) -> Result { + self.get_new_address().await + } + + async fn get_last_sweep_height(&self) -> Result, BitcoinError> { + Ok(None) + } + async fn get_new_public_key(&self) -> Result { let secp = Secp256k1::new(); let raw_secret_key: [u8; SECRET_KEY_SIZE] = thread_rng().gen(); @@ -514,6 +523,9 @@ impl BitcoinCoreApi for MockBitcoinCore { .unwrap(); Ok(metadata) } + async fn sweep_funds(&self, _address: Address) -> Result { + Ok(Txid::all_zeros()) + } async fn create_or_load_wallet(&self) -> Result<(), BitcoinError> { Ok(()) } @@ -524,6 +536,7 @@ impl BitcoinCoreApi for MockBitcoinCore { async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError> { Ok(()) } + fn get_utxo_count(&self) -> Result { Ok(0) } diff --git a/vault/src/connection_manager.rs b/vault/src/connection_manager.rs index 070edc9c8..10fb679ce 100644 --- a/vault/src/connection_manager.rs +++ b/vault/src/connection_manager.rs @@ -28,6 +28,7 @@ pub trait Service { btc_parachain: BtcParachain, bitcoin_core_master: DynBitcoinCoreApi, bitcoin_core_shared: DynBitcoinCoreApi, + bitcoin_core_shared_v2: DynBitcoinCoreApi, config: Config, monitoring_config: MonitoringConfig, shutdown: ShutdownSender, @@ -107,6 +108,9 @@ impl ConnectionManager { let bitcoin_core_shared = config_copy.new_client_with_network(Some(format!("{prefix}-shared")), network_copy)?; bitcoin_core_shared.create_or_load_wallet().await?; + let bitcoin_core_shared_v2 = + config_copy.new_client_with_network(Some(format!("{prefix}-shared-v2")), network_copy)?; + bitcoin_core_shared_v2.create_or_load_wallet().await?; let constructor = move |vault_id: VaultId| { let collateral_currency: CurrencyId = vault_id.collateral_currency(); @@ -128,6 +132,7 @@ impl ConnectionManager { btc_parachain, bitcoin_core_master, bitcoin_core_shared, + bitcoin_core_shared_v2, config, self.monitoring_config.clone(), shutdown_tx.clone(), diff --git a/vault/src/execution.rs b/vault/src/execution.rs index f774defb8..26bdb2128 100644 --- a/vault/src/execution.rs +++ b/vault/src/execution.rs @@ -809,6 +809,8 @@ mod tests { async fn get_block_hash(&self, height: u32) -> Result; async fn get_pruned_height(&self) -> Result; async fn get_new_address(&self) -> Result; + async fn get_new_sweep_address(&self) -> Result; + async fn get_last_sweep_height(&self) -> Result, BitcoinError>; async fn get_new_public_key(&self) -> Result; fn dump_private_key(&self, address: &Address) -> Result; fn import_private_key(&self, private_key: &PrivateKey, is_derivation_key: bool) -> Result<(), BitcoinError>; @@ -820,6 +822,7 @@ mod tests { async fn wait_for_transaction_metadata(&self, txid: Txid, num_confirmations: u32, block_hash: Option, is_wallet: bool) -> Result; async fn create_and_send_transaction(&self, address: Address, sat: u64, fee_rate: SatPerVbyte, request_id: Option) -> Result; async fn send_to_address(&self, address: Address, sat: u64, request_id: Option, fee_rate: SatPerVbyte, num_confirmations: u32) -> Result; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/issue.rs b/vault/src/issue.rs index 9bc8dea54..6f135ee10 100644 --- a/vault/src/issue.rs +++ b/vault/src/issue.rs @@ -166,6 +166,7 @@ pub async fn add_keys_from_past_issue_request( // privkey let btc_end_height = bitcoin_core.get_block_count().await? as usize; let btc_pruned_start_height = bitcoin_core.get_pruned_height().await? as usize; + let btc_max_sweep_height = bitcoin_core.get_last_sweep_height().await?; let issues = issue_requests.clone().into_iter().map(|(_key, issue)| issue).collect(); scanning_status.update(issues, btc_end_height); @@ -182,7 +183,10 @@ pub async fn add_keys_from_past_issue_request( issue_requests .into_iter() .filter_map(|(_, request)| { - if (request.btc_height as usize) < btc_pruned_start_height { + // only import if address is AFTER last sweep height and BEFORE current pruning height + if btc_max_sweep_height.is_some_and(|sweep_height| request.btc_height > sweep_height) + && (request.btc_height as usize) < btc_pruned_start_height + { Some(request.btc_address.to_address(bitcoin_core.network()).ok()?) } else { None diff --git a/vault/src/lib.rs b/vault/src/lib.rs index 8dac97932..a93685643 100644 --- a/vault/src/lib.rs +++ b/vault/src/lib.rs @@ -1,5 +1,6 @@ #![recursion_limit = "256"] #![feature(array_zip)] +#![feature(is_some_and)] mod cancellation; mod cli; diff --git a/vault/src/metrics.rs b/vault/src/metrics.rs index d98bd533a..9a37b7003 100644 --- a/vault/src/metrics.rs +++ b/vault/src/metrics.rs @@ -782,6 +782,8 @@ mod tests { async fn get_block_hash(&self, height: u32) -> Result; async fn get_pruned_height(&self) -> Result; async fn get_new_address(&self) -> Result; + async fn get_new_sweep_address(&self) -> Result; + async fn get_last_sweep_height(&self) -> Result, BitcoinError>; async fn get_new_public_key(&self) -> Result; fn dump_private_key(&self, address: &Address) -> Result; fn import_private_key(&self, private_key: &PrivateKey, is_derivation_key: bool) -> Result<(), BitcoinError>; @@ -793,6 +795,7 @@ mod tests { async fn wait_for_transaction_metadata(&self, txid: Txid, num_confirmations: u32, block_hash: Option, is_wallet: bool) -> Result; async fn create_and_send_transaction(&self, address: Address, sat: u64, fee_rate: SatPerVbyte, request_id: Option) -> Result; async fn send_to_address(&self, address: Address, sat: u64, request_id: Option, fee_rate: SatPerVbyte, num_confirmations: u32) -> Result; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/replace.rs b/vault/src/replace.rs index 36b31669f..37d61d2a0 100644 --- a/vault/src/replace.rs +++ b/vault/src/replace.rs @@ -285,6 +285,7 @@ mod tests { fee_rate: SatPerVbyte, num_confirmations: u32, ) -> Result; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/system.rs b/vault/src/system.rs index 45847904d..a3463b4fe 100644 --- a/vault/src/system.rs +++ b/vault/src/system.rs @@ -215,6 +215,7 @@ pub struct VaultIdManager { btc_parachain: InterBtcParachain, btc_rpc_master_wallet: DynBitcoinCoreApi, pub(crate) btc_rpc_shared_wallet: DynBitcoinCoreApi, + pub(crate) btc_rpc_shared_wallet_v2: DynBitcoinCoreApi, // TODO: remove this #[allow(clippy::type_complexity)] constructor: Arc Result + Send + Sync>>, @@ -226,6 +227,7 @@ impl VaultIdManager { btc_parachain: InterBtcParachain, btc_rpc_master_wallet: DynBitcoinCoreApi, btc_rpc_shared_wallet: DynBitcoinCoreApi, + btc_rpc_shared_wallet_v2: DynBitcoinCoreApi, constructor: impl Fn(VaultId) -> Result + Send + Sync + 'static, db_path: String, ) -> Self { @@ -234,6 +236,7 @@ impl VaultIdManager { constructor: Arc::new(Box::new(constructor)), btc_rpc_master_wallet, btc_rpc_shared_wallet, + btc_rpc_shared_wallet_v2, btc_parachain, db: DatabaseConfig { path: db_path }, } @@ -244,6 +247,7 @@ impl VaultIdManager { btc_parachain: InterBtcParachain, btc_rpc_master_wallet: DynBitcoinCoreApi, btc_rpc_shared_wallet: DynBitcoinCoreApi, + btc_rpc_shared_wallet_v2: DynBitcoinCoreApi, map: HashMap, db_path: &str, ) -> Self { @@ -265,6 +269,7 @@ impl VaultIdManager { constructor: Arc::new(Box::new(|_| unimplemented!())), btc_rpc_master_wallet, btc_rpc_shared_wallet, + btc_rpc_shared_wallet_v2, btc_parachain, db: DatabaseConfig { path: db_path.to_string(), @@ -308,10 +313,9 @@ impl VaultIdManager { } tracing::info!("Merging wallet for {:?}", vault_id); - let all_addresses = btc_rpc.list_addresses()?; // issue keys should be imported separately but we need to iterate // through currency specific wallets to get change addresses - for address in &all_addresses { + for address in btc_rpc.list_addresses()? { tracing::info!("Found {:?}", address); // get private key from currency specific wallet let private_key = btc_rpc.dump_private_key(&address)?; @@ -319,17 +323,11 @@ impl VaultIdManager { btc_rpc_shared.import_private_key(&private_key, false)?; } - if btc_rpc_shared.get_pruned_height().await? != 0 { - // rescan via electrs to import or remove change utxos - // this is required because pruned nodes cannot rescan themselves - btc_rpc_shared.rescan_electrs_for_addresses(all_addresses).await?; - } - tracing::info!("Initializing metrics..."); let metrics = PerCurrencyMetrics::new(&vault_id); let data = VaultData { vault_id: vault_id.clone(), - btc_rpc: btc_rpc_shared, + btc_rpc: self.btc_rpc_shared_wallet_v2.clone(), metrics: metrics.clone(), }; PerCurrencyMetrics::initialize_values(self.btc_parachain.clone(), &data).await; @@ -364,6 +362,23 @@ impl VaultIdManager { Ok(()) } + async fn sweep_shared_wallet(&self) -> Result<(), Error> { + if self.btc_rpc_shared_wallet.get_pruned_height().await? == 0 { + // no need to sweep, full node can rescan + return Ok(()); + } else if self.btc_rpc_shared_wallet_v2.get_last_sweep_height().await?.is_some() { + // already has sweep tx + return Ok(()); + } + + // sweep funds from shared wallet to shared-v2 + let shared_v2_wallet_address = self.btc_rpc_shared_wallet_v2.get_new_sweep_address().await?; + let txid = self.btc_rpc_shared_wallet.sweep_funds(shared_v2_wallet_address).await?; + tracing::info!("Sent sweep tx: {txid}"); + + Ok(()) + } + pub async fn listen_for_vault_id_registrations(self) -> Result<(), Error> { Ok(self .btc_parachain @@ -436,6 +451,7 @@ impl Service for VaultService { btc_parachain: InterBtcParachain, btc_rpc_master_wallet: DynBitcoinCoreApi, btc_rpc_shared_wallet: DynBitcoinCoreApi, + btc_rpc_shared_wallet_v2: DynBitcoinCoreApi, config: VaultServiceConfig, monitoring_config: MonitoringConfig, shutdown: ShutdownSender, @@ -446,6 +462,7 @@ impl Service for VaultService { btc_parachain, btc_rpc_master_wallet, btc_rpc_shared_wallet, + btc_rpc_shared_wallet_v2, config, monitoring_config, shutdown, @@ -520,6 +537,7 @@ impl VaultService { btc_parachain: InterBtcParachain, btc_rpc_master_wallet: DynBitcoinCoreApi, btc_rpc_shared_wallet: DynBitcoinCoreApi, + btc_rpc_shared_wallet_v2: DynBitcoinCoreApi, config: VaultServiceConfig, monitoring_config: MonitoringConfig, shutdown: ShutdownSender, @@ -537,6 +555,7 @@ impl VaultService { btc_parachain, btc_rpc_master_wallet, btc_rpc_shared_wallet, + btc_rpc_shared_wallet_v2, constructor, db_path, ), @@ -650,6 +669,7 @@ impl VaultService { // purposefully _after_ maybe_register_vault and _before_ other calls self.vault_id_manager.fetch_vault_ids().await?; + self.vault_id_manager.sweep_shared_wallet().await?; tracing::info!("Adding keys from past issues..."); issue::add_keys_from_past_issue_request( From 156c9641633287a651653dd990b2b2af0f3a609a Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Tue, 12 Sep 2023 13:28:18 +0000 Subject: [PATCH 02/12] chore: add more logging Signed-off-by: Gregory Hill --- bitcoin/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 1ba838dcf..6439ad9e0 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -1067,6 +1067,7 @@ impl BitcoinCoreApi for BitcoinCore { for entry in unspent { if self.electrs_client.is_tx_output_spent(&entry.txid, entry.vout).await? { + log::info!("{}:{} already spent", entry.txid, entry.vout); // skip if already spent continue; } @@ -1078,6 +1079,8 @@ impl BitcoinCoreApi for BitcoinCore { }) } + log::info!("Sweeping {} from {} utxos", amount, utxos.len()); + let mut outputs = serde_json::Map::::new(); outputs.insert(address.to_string(), serde_json::Value::from(amount.to_btc())); From c0c94fae4bd2e25876bb0fdc313b54ba68a349b8 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 05:20:12 +0000 Subject: [PATCH 03/12] fix: also sweep from currency specific wallets Signed-off-by: Gregory Hill --- bitcoin/src/lib.rs | 7 +++++-- vault/src/issue.rs | 4 ++-- vault/src/lib.rs | 1 - vault/src/system.rs | 14 +++++++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 6439ad9e0..c214ff02f 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -811,7 +811,11 @@ impl BitcoinCoreApi for BitcoinCore { .rpc .list_transactions(Some(SWEEP_ADDRESS), Some(DEFAULT_MAX_TX_COUNT), None, None)? .into_iter() - .filter_map(|tx| tx.info.blockheight) + // we want to return None if there is no sweep tx for full nodes or new + // pruned nodes and we should return an error if any tx is still in the mempool + .map(|tx| tx.info.blockheight.ok_or(Error::ConfirmationError)) + .collect::, _>>()? + .into_iter() .min()) } @@ -1080,7 +1084,6 @@ impl BitcoinCoreApi for BitcoinCore { } log::info!("Sweeping {} from {} utxos", amount, utxos.len()); - let mut outputs = serde_json::Map::::new(); outputs.insert(address.to_string(), serde_json::Value::from(amount.to_btc())); diff --git a/vault/src/issue.rs b/vault/src/issue.rs index 6f135ee10..4d9ee5d4b 100644 --- a/vault/src/issue.rs +++ b/vault/src/issue.rs @@ -166,7 +166,7 @@ pub async fn add_keys_from_past_issue_request( // privkey let btc_end_height = bitcoin_core.get_block_count().await? as usize; let btc_pruned_start_height = bitcoin_core.get_pruned_height().await? as usize; - let btc_max_sweep_height = bitcoin_core.get_last_sweep_height().await?; + let btc_last_sweep_height = bitcoin_core.get_last_sweep_height().await?; let issues = issue_requests.clone().into_iter().map(|(_key, issue)| issue).collect(); scanning_status.update(issues, btc_end_height); @@ -184,7 +184,7 @@ pub async fn add_keys_from_past_issue_request( .into_iter() .filter_map(|(_, request)| { // only import if address is AFTER last sweep height and BEFORE current pruning height - if btc_max_sweep_height.is_some_and(|sweep_height| request.btc_height > sweep_height) + if btc_last_sweep_height.map_or(true, |sweep_height| request.btc_height > sweep_height) && (request.btc_height as usize) < btc_pruned_start_height { Some(request.btc_address.to_address(bitcoin_core.network()).ok()?) diff --git a/vault/src/lib.rs b/vault/src/lib.rs index a93685643..8dac97932 100644 --- a/vault/src/lib.rs +++ b/vault/src/lib.rs @@ -1,6 +1,5 @@ #![recursion_limit = "256"] #![feature(array_zip)] -#![feature(is_some_and)] mod cancellation; mod cli; diff --git a/vault/src/system.rs b/vault/src/system.rs index a3463b4fe..99f8bfb8a 100644 --- a/vault/src/system.rs +++ b/vault/src/system.rs @@ -323,6 +323,14 @@ impl VaultIdManager { btc_rpc_shared.import_private_key(&private_key, false)?; } + // only sweep if using pruned node and there is no sweep tx yet to shared-v2 + if btc_rpc_shared.get_pruned_height().await? != 0 && self.btc_rpc_shared_wallet_v2.get_last_sweep_height().await?.is_none() { + // sweep to old shared wallet which will then sweep again to the v2 wallet + let shared_wallet_address = btc_rpc_shared.get_new_address().await?; + let txid = btc_rpc.sweep_funds(shared_wallet_address).await?; + tracing::info!("Sent sweep tx: {txid}"); + } + tracing::info!("Initializing metrics..."); let metrics = PerCurrencyMetrics::new(&vault_id); let data = VaultData { @@ -435,7 +443,7 @@ impl VaultIdManager { pub struct VaultService { btc_parachain: InterBtcParachain, btc_rpc_master_wallet: DynBitcoinCoreApi, - btc_rpc_shared_wallet: DynBitcoinCoreApi, + btc_rpc_shared_wallet_v2: DynBitcoinCoreApi, config: VaultServiceConfig, monitoring_config: MonitoringConfig, shutdown: ShutdownSender, @@ -547,7 +555,7 @@ impl VaultService { Self { btc_parachain: btc_parachain.clone(), btc_rpc_master_wallet: btc_rpc_master_wallet.clone(), - btc_rpc_shared_wallet: btc_rpc_shared_wallet.clone(), + btc_rpc_shared_wallet_v2: btc_rpc_shared_wallet_v2.clone(), config, monitoring_config, shutdown, @@ -673,7 +681,7 @@ impl VaultService { tracing::info!("Adding keys from past issues..."); issue::add_keys_from_past_issue_request( - &self.btc_rpc_shared_wallet, + &self.btc_rpc_shared_wallet_v2, &self.btc_parachain, &self.vault_id_manager.db, ) From 82542593a37252a277893523f8bd65c874099dd3 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 05:24:01 +0000 Subject: [PATCH 04/12] chore: import derivation key to v2 shared wallet Signed-off-by: Gregory Hill --- vault/src/system.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/vault/src/system.rs b/vault/src/system.rs index 99f8bfb8a..ddae0bfba 100644 --- a/vault/src/system.rs +++ b/vault/src/system.rs @@ -305,6 +305,7 @@ impl VaultIdManager { Ok(private_key) => { // TODO: remove this after the migration is complete btc_rpc_shared.import_private_key(&private_key, true)?; + self.btc_rpc_shared_wallet_v2.import_private_key(&private_key, true)?; } Err(err) => { tracing::error!("Could not find the derivation key in the bitcoin wallet"); From 83472c45ef1e07d1cdce99c8a34bf867998f40b3 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 05:55:34 +0000 Subject: [PATCH 05/12] refactor: don't fail if we can't sweep funds Signed-off-by: Gregory Hill --- bitcoin/src/iter.rs | 2 +- bitcoin/src/lib.rs | 10 +++++++--- bitcoin/src/light/mod.rs | 4 ++-- runtime/src/integration/bitcoin_simulator.rs | 4 ++-- vault/src/execution.rs | 2 +- vault/src/metrics.rs | 2 +- vault/src/replace.rs | 2 +- vault/src/system.rs | 14 +++++++++----- 8 files changed, 24 insertions(+), 16 deletions(-) diff --git a/bitcoin/src/iter.rs b/bitcoin/src/iter.rs index 94994c153..93de8ad41 100644 --- a/bitcoin/src/iter.rs +++ b/bitcoin/src/iter.rs @@ -253,7 +253,7 @@ mod tests { fee_rate: SatPerVbyte, num_confirmations: u32, ) -> Result; - async fn sweep_funds(&self, address: Address) -> Result; + async fn sweep_funds(&self, address: Address) -> Result<(), Error>; async fn create_or_load_wallet(&self) -> Result<(), Error>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), Error>; async fn rescan_electrs_for_addresses( diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index c214ff02f..0ed6390c1 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -213,7 +213,7 @@ pub trait BitcoinCoreApi { num_confirmations: u32, ) -> Result; - async fn sweep_funds(&self, address: Address) -> Result; + async fn sweep_funds(&self, address: Address) -> Result<(), Error>; async fn create_or_load_wallet(&self) -> Result<(), Error>; @@ -1063,7 +1063,7 @@ impl BitcoinCoreApi for BitcoinCore { .await?) } - async fn sweep_funds(&self, address: Address) -> Result { + async fn sweep_funds(&self, address: Address) -> Result<(), Error> { let unspent = self.rpc.list_unspent(None, None, None, None, None)?; let mut amount = Amount::ZERO; @@ -1117,7 +1117,11 @@ impl BitcoinCoreApi for BitcoinCore { } let transaction = signed_funded_raw_tx.transaction()?; - self.rpc.send_raw_transaction(&transaction).map_err(Into::into) + let txid = self.rpc.send_raw_transaction(&transaction)?; + + log::info!("Sent sweep tx: {txid}"); + + Ok(()) } /// Create or load a wallet on Bitcoin Core. diff --git a/bitcoin/src/light/mod.rs b/bitcoin/src/light/mod.rs index bb5324f85..1f98434b5 100644 --- a/bitcoin/src/light/mod.rs +++ b/bitcoin/src/light/mod.rs @@ -339,8 +339,8 @@ impl BitcoinCoreApi for BitcoinLight { .await?) } - async fn sweep_funds(&self, _address: Address) -> Result { - Ok(Txid::all_zeros()) + async fn sweep_funds(&self, _address: Address) -> Result<(), BitcoinError> { + Ok(()) } async fn create_or_load_wallet(&self) -> Result<(), BitcoinError> { diff --git a/runtime/src/integration/bitcoin_simulator.rs b/runtime/src/integration/bitcoin_simulator.rs index 36891a013..4950fe80a 100644 --- a/runtime/src/integration/bitcoin_simulator.rs +++ b/runtime/src/integration/bitcoin_simulator.rs @@ -523,8 +523,8 @@ impl BitcoinCoreApi for MockBitcoinCore { .unwrap(); Ok(metadata) } - async fn sweep_funds(&self, _address: Address) -> Result { - Ok(Txid::all_zeros()) + async fn sweep_funds(&self, _address: Address) -> Result<(), BitcoinError> { + Ok(()) } async fn create_or_load_wallet(&self) -> Result<(), BitcoinError> { Ok(()) diff --git a/vault/src/execution.rs b/vault/src/execution.rs index 26bdb2128..71db81d6c 100644 --- a/vault/src/execution.rs +++ b/vault/src/execution.rs @@ -822,7 +822,7 @@ mod tests { async fn wait_for_transaction_metadata(&self, txid: Txid, num_confirmations: u32, block_hash: Option, is_wallet: bool) -> Result; async fn create_and_send_transaction(&self, address: Address, sat: u64, fee_rate: SatPerVbyte, request_id: Option) -> Result; async fn send_to_address(&self, address: Address, sat: u64, request_id: Option, fee_rate: SatPerVbyte, num_confirmations: u32) -> Result; - async fn sweep_funds(&self, address: Address) -> Result; + async fn sweep_funds(&self, address: Address) -> Result<(), BitcoinError>; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/metrics.rs b/vault/src/metrics.rs index 9a37b7003..77929a21b 100644 --- a/vault/src/metrics.rs +++ b/vault/src/metrics.rs @@ -795,7 +795,7 @@ mod tests { async fn wait_for_transaction_metadata(&self, txid: Txid, num_confirmations: u32, block_hash: Option, is_wallet: bool) -> Result; async fn create_and_send_transaction(&self, address: Address, sat: u64, fee_rate: SatPerVbyte, request_id: Option) -> Result; async fn send_to_address(&self, address: Address, sat: u64, request_id: Option, fee_rate: SatPerVbyte, num_confirmations: u32) -> Result; - async fn sweep_funds(&self, address: Address) -> Result; + async fn sweep_funds(&self, address: Address) -> Result<(), BitcoinError>; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/replace.rs b/vault/src/replace.rs index 37d61d2a0..c3ed6a771 100644 --- a/vault/src/replace.rs +++ b/vault/src/replace.rs @@ -285,7 +285,7 @@ mod tests { fee_rate: SatPerVbyte, num_confirmations: u32, ) -> Result; - async fn sweep_funds(&self, address: Address) -> Result; + async fn sweep_funds(&self, address: Address) -> Result<(), BitcoinError>; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/system.rs b/vault/src/system.rs index ddae0bfba..faf6c0f48 100644 --- a/vault/src/system.rs +++ b/vault/src/system.rs @@ -325,11 +325,14 @@ impl VaultIdManager { } // only sweep if using pruned node and there is no sweep tx yet to shared-v2 - if btc_rpc_shared.get_pruned_height().await? != 0 && self.btc_rpc_shared_wallet_v2.get_last_sweep_height().await?.is_none() { + if btc_rpc_shared.get_pruned_height().await? != 0 + && self.btc_rpc_shared_wallet_v2.get_last_sweep_height().await?.is_none() + { // sweep to old shared wallet which will then sweep again to the v2 wallet let shared_wallet_address = btc_rpc_shared.get_new_address().await?; - let txid = btc_rpc.sweep_funds(shared_wallet_address).await?; - tracing::info!("Sent sweep tx: {txid}"); + if let Err(err) = btc_rpc.sweep_funds(shared_wallet_address).await { + tracing::error!("Could not sweep funds: {err}"); + } } tracing::info!("Initializing metrics..."); @@ -382,8 +385,9 @@ impl VaultIdManager { // sweep funds from shared wallet to shared-v2 let shared_v2_wallet_address = self.btc_rpc_shared_wallet_v2.get_new_sweep_address().await?; - let txid = self.btc_rpc_shared_wallet.sweep_funds(shared_v2_wallet_address).await?; - tracing::info!("Sent sweep tx: {txid}"); + if let Err(err) = self.btc_rpc_shared_wallet.sweep_funds(shared_v2_wallet_address).await { + tracing::error!("Could not sweep funds: {err}"); + } Ok(()) } From c5b78e4e35be9e3ec77dab56a27da026eb5b1104 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 06:08:02 +0000 Subject: [PATCH 06/12] chore: wait for last sweep tx inclusion Signed-off-by: Gregory Hill --- bitcoin/src/iter.rs | 2 +- bitcoin/src/lib.rs | 7 +++---- bitcoin/src/light/mod.rs | 4 ++-- runtime/src/integration/bitcoin_simulator.rs | 4 ++-- vault/src/execution.rs | 2 +- vault/src/metrics.rs | 2 +- vault/src/replace.rs | 2 +- vault/src/system.rs | 12 ++++++++++-- 8 files changed, 21 insertions(+), 14 deletions(-) diff --git a/bitcoin/src/iter.rs b/bitcoin/src/iter.rs index 93de8ad41..94994c153 100644 --- a/bitcoin/src/iter.rs +++ b/bitcoin/src/iter.rs @@ -253,7 +253,7 @@ mod tests { fee_rate: SatPerVbyte, num_confirmations: u32, ) -> Result; - async fn sweep_funds(&self, address: Address) -> Result<(), Error>; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), Error>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), Error>; async fn rescan_electrs_for_addresses( diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 0ed6390c1..4651c2f37 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -213,7 +213,7 @@ pub trait BitcoinCoreApi { num_confirmations: u32, ) -> Result; - async fn sweep_funds(&self, address: Address) -> Result<(), Error>; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), Error>; @@ -1063,7 +1063,7 @@ impl BitcoinCoreApi for BitcoinCore { .await?) } - async fn sweep_funds(&self, address: Address) -> Result<(), Error> { + async fn sweep_funds(&self, address: Address) -> Result { let unspent = self.rpc.list_unspent(None, None, None, None, None)?; let mut amount = Amount::ZERO; @@ -1118,10 +1118,9 @@ impl BitcoinCoreApi for BitcoinCore { let transaction = signed_funded_raw_tx.transaction()?; let txid = self.rpc.send_raw_transaction(&transaction)?; - log::info!("Sent sweep tx: {txid}"); - Ok(()) + Ok(txid) } /// Create or load a wallet on Bitcoin Core. diff --git a/bitcoin/src/light/mod.rs b/bitcoin/src/light/mod.rs index 1f98434b5..bb5324f85 100644 --- a/bitcoin/src/light/mod.rs +++ b/bitcoin/src/light/mod.rs @@ -339,8 +339,8 @@ impl BitcoinCoreApi for BitcoinLight { .await?) } - async fn sweep_funds(&self, _address: Address) -> Result<(), BitcoinError> { - Ok(()) + async fn sweep_funds(&self, _address: Address) -> Result { + Ok(Txid::all_zeros()) } async fn create_or_load_wallet(&self) -> Result<(), BitcoinError> { diff --git a/runtime/src/integration/bitcoin_simulator.rs b/runtime/src/integration/bitcoin_simulator.rs index 4950fe80a..36891a013 100644 --- a/runtime/src/integration/bitcoin_simulator.rs +++ b/runtime/src/integration/bitcoin_simulator.rs @@ -523,8 +523,8 @@ impl BitcoinCoreApi for MockBitcoinCore { .unwrap(); Ok(metadata) } - async fn sweep_funds(&self, _address: Address) -> Result<(), BitcoinError> { - Ok(()) + async fn sweep_funds(&self, _address: Address) -> Result { + Ok(Txid::all_zeros()) } async fn create_or_load_wallet(&self) -> Result<(), BitcoinError> { Ok(()) diff --git a/vault/src/execution.rs b/vault/src/execution.rs index 71db81d6c..26bdb2128 100644 --- a/vault/src/execution.rs +++ b/vault/src/execution.rs @@ -822,7 +822,7 @@ mod tests { async fn wait_for_transaction_metadata(&self, txid: Txid, num_confirmations: u32, block_hash: Option, is_wallet: bool) -> Result; async fn create_and_send_transaction(&self, address: Address, sat: u64, fee_rate: SatPerVbyte, request_id: Option) -> Result; async fn send_to_address(&self, address: Address, sat: u64, request_id: Option, fee_rate: SatPerVbyte, num_confirmations: u32) -> Result; - async fn sweep_funds(&self, address: Address) -> Result<(), BitcoinError>; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/metrics.rs b/vault/src/metrics.rs index 77929a21b..9a37b7003 100644 --- a/vault/src/metrics.rs +++ b/vault/src/metrics.rs @@ -795,7 +795,7 @@ mod tests { async fn wait_for_transaction_metadata(&self, txid: Txid, num_confirmations: u32, block_hash: Option, is_wallet: bool) -> Result; async fn create_and_send_transaction(&self, address: Address, sat: u64, fee_rate: SatPerVbyte, request_id: Option) -> Result; async fn send_to_address(&self, address: Address, sat: u64, request_id: Option, fee_rate: SatPerVbyte, num_confirmations: u32) -> Result; - async fn sweep_funds(&self, address: Address) -> Result<(), BitcoinError>; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/replace.rs b/vault/src/replace.rs index c3ed6a771..37d61d2a0 100644 --- a/vault/src/replace.rs +++ b/vault/src/replace.rs @@ -285,7 +285,7 @@ mod tests { fee_rate: SatPerVbyte, num_confirmations: u32, ) -> Result; - async fn sweep_funds(&self, address: Address) -> Result<(), BitcoinError>; + async fn sweep_funds(&self, address: Address) -> Result; async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>; async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>; async fn rescan_electrs_for_addresses(&self, addresses: Vec
) -> Result<(), BitcoinError>; diff --git a/vault/src/system.rs b/vault/src/system.rs index faf6c0f48..c5ecdaeaf 100644 --- a/vault/src/system.rs +++ b/vault/src/system.rs @@ -374,6 +374,7 @@ impl VaultIdManager { Ok(()) } + // only run AFTER the separate currency wallet sweeps async fn sweep_shared_wallet(&self) -> Result<(), Error> { if self.btc_rpc_shared_wallet.get_pruned_height().await? == 0 { // no need to sweep, full node can rescan @@ -385,8 +386,15 @@ impl VaultIdManager { // sweep funds from shared wallet to shared-v2 let shared_v2_wallet_address = self.btc_rpc_shared_wallet_v2.get_new_sweep_address().await?; - if let Err(err) = self.btc_rpc_shared_wallet.sweep_funds(shared_v2_wallet_address).await { - tracing::error!("Could not sweep funds: {err}"); + match self.btc_rpc_shared_wallet.sweep_funds(shared_v2_wallet_address).await { + Ok(txid) => { + self.btc_rpc_shared_wallet + .wait_for_transaction_metadata(txid, 1, None, true) + .await?; + } + Err(err) => { + tracing::error!("Could not sweep funds: {err}"); + } } Ok(()) From 273aa65673dce501609097242f3d6dd42e2153b3 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 06:15:34 +0000 Subject: [PATCH 07/12] chore: clippy Signed-off-by: Gregory Hill --- vault/src/system.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/vault/src/system.rs b/vault/src/system.rs index c5ecdaeaf..0e14d9754 100644 --- a/vault/src/system.rs +++ b/vault/src/system.rs @@ -376,11 +376,10 @@ impl VaultIdManager { // only run AFTER the separate currency wallet sweeps async fn sweep_shared_wallet(&self) -> Result<(), Error> { - if self.btc_rpc_shared_wallet.get_pruned_height().await? == 0 { - // no need to sweep, full node can rescan - return Ok(()); - } else if self.btc_rpc_shared_wallet_v2.get_last_sweep_height().await?.is_some() { - // already has sweep tx + if self.btc_rpc_shared_wallet.get_pruned_height().await? == 0 + || self.btc_rpc_shared_wallet_v2.get_last_sweep_height().await?.is_some() + { + // no need to sweep, full node can rescan or already has sweep tx return Ok(()); } From 475f6dc97791862ba064c43c0949ee66ace8e82d Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 06:26:14 +0000 Subject: [PATCH 08/12] chore: add missing methods to replace mock Signed-off-by: Gregory Hill --- vault/src/replace.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vault/src/replace.rs b/vault/src/replace.rs index 37d61d2a0..d7e413dd8 100644 --- a/vault/src/replace.rs +++ b/vault/src/replace.rs @@ -249,6 +249,8 @@ mod tests { async fn get_block_hash(&self, height: u32) -> Result; async fn get_pruned_height(&self) -> Result; async fn get_new_address(&self) -> Result; + async fn get_new_sweep_address(&self) -> Result; + async fn get_last_sweep_height(&self) -> Result, BitcoinError>; async fn get_new_public_key(&self) -> Result; fn dump_private_key(&self, address: &Address) -> Result; fn import_private_key(&self, private_key: &PrivateKey, is_derivation_key: bool) -> Result<(), BitcoinError>; From 00c7d92f3edf56b0a5f58616c19493a8d0e9b9f2 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 07:58:54 +0000 Subject: [PATCH 09/12] chore: update tests Signed-off-by: Gregory Hill --- vault/tests/vault_integration_tests.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vault/tests/vault_integration_tests.rs b/vault/tests/vault_integration_tests.rs index 1c249c3e8..fd195a48e 100644 --- a/vault/tests/vault_integration_tests.rs +++ b/vault/tests/vault_integration_tests.rs @@ -96,6 +96,7 @@ async fn test_redeem_succeeds() { let vault_id_manager = VaultIdManager::from_map( vault_provider.clone(), btc_rpc_master_wallet.clone(), + btc_rpc_master_wallet.clone(), btc_rpc_master_wallet, btc_rpcs, "test_redeem_succeeds", @@ -166,6 +167,7 @@ async fn test_replace_succeeds() { let _vault_id_manager = VaultIdManager::from_map( new_vault_provider.clone(), new_btc_rpc_master_wallet.clone(), + new_btc_rpc_master_wallet.clone(), new_btc_rpc_master_wallet, btc_rpcs, "test_replace_succeeds1", @@ -180,6 +182,7 @@ async fn test_replace_succeeds() { let vault_id_manager = VaultIdManager::from_map( old_vault_provider.clone(), old_btc_rpc_master_wallet.clone(), + old_btc_rpc_master_wallet.clone(), old_btc_rpc_master_wallet, btc_rpcs, "test_replace_succeeds2", @@ -355,6 +358,7 @@ async fn test_cancellation_succeeds() { let vault_id_manager = VaultIdManager::from_map( new_vault_provider.clone(), new_btc_rpc_master_wallet.clone(), + new_btc_rpc_master_wallet.clone(), new_btc_rpc_master_wallet, btc_rpcs, "test_cancellation_succeeds", @@ -624,6 +628,7 @@ async fn test_automatic_issue_execution_succeeds() { let vault_id_manager = VaultIdManager::from_map( vault2_provider.clone(), btc_rpc_master_wallet.clone(), + btc_rpc_master_wallet.clone(), btc_rpc_master_wallet, btc_rpcs, "test_automatic_issue_execution_succeeds", @@ -724,6 +729,7 @@ async fn test_automatic_issue_execution_succeeds_with_big_transaction() { let vault_id_manager = VaultIdManager::from_map( vault2_provider.clone(), btc_rpc_master_wallet.clone(), + btc_rpc_master_wallet.clone(), btc_rpc_master_wallet, btc_rpcs, "test_automatic_issue_execution_succeeds_with_big_transaction", @@ -813,6 +819,7 @@ async fn test_execute_open_requests_succeeds() { let vault_id_manager = VaultIdManager::from_map( vault_provider.clone(), btc_rpc_master_wallet.clone(), + btc_rpc_master_wallet.clone(), btc_rpc_master_wallet, btc_rpcs, "test_execute_open_requests_succeeds", @@ -1214,6 +1221,7 @@ mod test_with_bitcoind { let vault_id_manager = VaultIdManager::from_map( vault_provider.clone(), btc_rpc_master_wallet.clone(), + btc_rpc_master_wallet.clone(), btc_rpc_master_wallet, btc_rpcs, "test_automatic_rbf_succeeds", From 238f8477682c5d35c5e1c126a2ad64d08bc849c6 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 09:42:05 +0000 Subject: [PATCH 10/12] chore: rescan issues for old shared wallet Signed-off-by: Gregory Hill --- vault/src/issue.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++- vault/src/system.rs | 13 ++++++-- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/vault/src/issue.rs b/vault/src/issue.rs index 4d9ee5d4b..ccb9b7dee 100644 --- a/vault/src/issue.rs +++ b/vault/src/issue.rs @@ -144,7 +144,84 @@ impl RescanStatus { } } -pub async fn add_keys_from_past_issue_request( +pub async fn add_keys_from_past_issue_request_old( + bitcoin_core: &DynBitcoinCoreApi, + btc_parachain: &InterBtcParachain, + db: &DatabaseConfig, +) -> Result<(), Error> { + let account_id = btc_parachain.get_account_id(); + let mut scanning_status = RescanStatus::get(&account_id, db)?; + tracing::info!("Scanning: {scanning_status:?}"); + + let issue_requests = btc_parachain.get_vault_issue_requests(account_id.clone()).await?; + + for (issue_id, request) in issue_requests.clone().into_iter() { + if let Err(e) = add_new_deposit_key(bitcoin_core, issue_id, request.btc_public_key).await { + tracing::error!("Failed to add deposit key #{}: {}", issue_id, e.to_string()); + } + } + + // read height only _after_ the last add_new_deposit_key. If a new block arrives + // while we rescan, bitcoin core will correctly recognize addressed associated with the + // privkey + let btc_end_height = bitcoin_core.get_block_count().await? as usize; + let btc_pruned_start_height = bitcoin_core.get_pruned_height().await? as usize; + + let issues = issue_requests.clone().into_iter().map(|(_key, issue)| issue).collect(); + scanning_status.update(issues, btc_end_height); + + // use electrs to scan the portion that is not scannable by bitcoin core + if let Some((start, end)) = scanning_status.prune(btc_pruned_start_height) { + tracing::info!( + "Also checking electrs for issue requests between {} and {}...", + start, + end + ); + bitcoin_core + .rescan_electrs_for_addresses( + issue_requests + .into_iter() + .filter_map(|(_, request)| { + // only import if BEFORE current pruning height + if (request.btc_height as usize) < btc_pruned_start_height { + Some(request.btc_address.to_address(bitcoin_core.network()).ok()?) + } else { + None + } + }) + .collect(), + ) + .await?; + } + + // save progress s.t. we don't rescan pruned range again if we crash now + scanning_status.store(account_id, db)?; + + let mut chunk_size = 1; + // rescan the blockchain in chunks, so that we can save progress. The code below + // aims to have each chunk take about 10 seconds (arbitrarily chosen value). + while let Some((chunk_start, chunk_end)) = scanning_status.process_blocks(chunk_size) { + tracing::info!("Rescanning bitcoin chain from {} to {}...", chunk_start, chunk_end); + + let start_time = Instant::now(); + + bitcoin_core.rescan_blockchain(chunk_start, chunk_end).await?; + + // with the code below the rescan time should remain between 5 and 20 seconds + // after the first couple of rounds. + if start_time.elapsed() < Duration::from_secs(10) { + chunk_size = chunk_size.saturating_mul(2); + } else { + chunk_size = (chunk_size.checked_div(2).ok_or(Error::ArithmeticUnderflow)?).max(1); + } + + scanning_status.store(account_id, db)?; + } + + Ok(()) +} + +pub async fn add_keys_from_past_issue_request_new( bitcoin_core: &DynBitcoinCoreApi, btc_parachain: &InterBtcParachain, db: &DatabaseConfig, diff --git a/vault/src/system.rs b/vault/src/system.rs index 0e14d9754..96f386a62 100644 --- a/vault/src/system.rs +++ b/vault/src/system.rs @@ -455,6 +455,7 @@ impl VaultIdManager { pub struct VaultService { btc_parachain: InterBtcParachain, btc_rpc_master_wallet: DynBitcoinCoreApi, + btc_rpc_shared_wallet: DynBitcoinCoreApi, btc_rpc_shared_wallet_v2: DynBitcoinCoreApi, config: VaultServiceConfig, monitoring_config: MonitoringConfig, @@ -567,6 +568,7 @@ impl VaultService { Self { btc_parachain: btc_parachain.clone(), btc_rpc_master_wallet: btc_rpc_master_wallet.clone(), + btc_rpc_shared_wallet: btc_rpc_shared_wallet.clone(), btc_rpc_shared_wallet_v2: btc_rpc_shared_wallet_v2.clone(), config, monitoring_config, @@ -689,10 +691,17 @@ impl VaultService { // purposefully _after_ maybe_register_vault and _before_ other calls self.vault_id_manager.fetch_vault_ids().await?; - self.vault_id_manager.sweep_shared_wallet().await?; tracing::info!("Adding keys from past issues..."); - issue::add_keys_from_past_issue_request( + issue::add_keys_from_past_issue_request_old( + &self.btc_rpc_shared_wallet, + &self.btc_parachain, + &self.vault_id_manager.db, + ) + .await?; + + self.vault_id_manager.sweep_shared_wallet().await?; + issue::add_keys_from_past_issue_request_new( &self.btc_rpc_shared_wallet_v2, &self.btc_parachain, &self.vault_id_manager.db, From 9d8d669772579922d05b8d6f19fd6200e9dc7453 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 10:53:40 +0000 Subject: [PATCH 11/12] chore: use 0 minconf for listunspent Signed-off-by: Gregory Hill --- bitcoin/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 4651c2f37..05da3fd28 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -1064,7 +1064,7 @@ impl BitcoinCoreApi for BitcoinCore { } async fn sweep_funds(&self, address: Address) -> Result { - let unspent = self.rpc.list_unspent(None, None, None, None, None)?; + let unspent = self.rpc.list_unspent(Some(0), None, None, None, None)?; let mut amount = Amount::ZERO; let mut utxos = Vec::::new(); From 71348560da52f03dee4b668e3c08cd22dd340cc6 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 13 Sep 2023 11:34:59 +0000 Subject: [PATCH 12/12] chore: reset rescan db Signed-off-by: Gregory Hill --- vault/src/issue.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vault/src/issue.rs b/vault/src/issue.rs index ccb9b7dee..51e6fa764 100644 --- a/vault/src/issue.rs +++ b/vault/src/issue.rs @@ -86,7 +86,7 @@ struct RescanStatus { impl RescanStatus { // there was a bug pre-v2 that set rescanning status to an invalid range. // by changing the keyname we effectively force a reset - const KEY: &str = "rescan-status-v3"; + const KEY: &str = "rescan-status-v4"; fn update(&mut self, mut issues: Vec, current_bitcoin_height: usize) { // Only look at issues that haven't been processed yet issues.retain(|issue| issue.opentime > self.newest_issue_height);