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..05da3fd28 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,26 @@ 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() + // 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()) + } + /// Gets a new public key for an address in the wallet async fn get_new_public_key(&self) -> Result { let address = self @@ -1035,6 +1063,66 @@ impl BitcoinCoreApi for BitcoinCore { .await?) } + async fn sweep_funds(&self, address: Address) -> Result { + let unspent = self.rpc.list_unspent(Some(0), 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? { + log::info!("{}:{} already spent", entry.txid, entry.vout); + // skip if already spent + continue; + } + amount += entry.amount; + utxos.push(json::CreateRawTransactionInput { + txid: entry.txid, + vout: entry.vout, + sequence: None, + }) + } + + 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())); + + 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()?; + let txid = self.rpc.send_raw_transaction(&transaction)?; + log::info!("Sent sweep tx: {txid}"); + + Ok(txid) + } + /// 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 +1206,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..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); @@ -144,7 +144,7 @@ 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, @@ -182,6 +182,7 @@ pub async fn add_keys_from_past_issue_request( 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 { @@ -220,6 +221,86 @@ pub async fn add_keys_from_past_issue_request( Ok(()) } +pub async fn add_keys_from_past_issue_request_new( + 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 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); + + // 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 address is AFTER last sweep height and BEFORE current pruning 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()?) + } 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(()) +} + /// execute issue requests with a matching Bitcoin payment async fn process_transaction_and_execute_issue( bitcoin_core: DynBitcoinCoreApi, 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..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>; @@ -285,6 +287,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..96f386a62 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(), @@ -300,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"); @@ -308,10 +314,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 +324,22 @@ 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?; + // 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?; + if let Err(err) = btc_rpc.sweep_funds(shared_wallet_address).await { + tracing::error!("Could not sweep funds: {err}"); + } } 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 +374,31 @@ 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 + || 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(()); + } + + // sweep funds from shared wallet to shared-v2 + let shared_v2_wallet_address = self.btc_rpc_shared_wallet_v2.get_new_sweep_address().await?; + 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(()) + } + pub async fn listen_for_vault_id_registrations(self) -> Result<(), Error> { Ok(self .btc_parachain @@ -421,6 +456,7 @@ 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, @@ -436,6 +472,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 +483,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 +558,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, @@ -530,6 +569,7 @@ impl VaultService { 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, @@ -537,6 +577,7 @@ impl VaultService { btc_parachain, btc_rpc_master_wallet, btc_rpc_shared_wallet, + btc_rpc_shared_wallet_v2, constructor, db_path, ), @@ -652,13 +693,21 @@ impl VaultService { self.vault_id_manager.fetch_vault_ids().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, + ) + .await?; + let startup_height = self.await_parachain_block().await?; let open_request_executor = execute_open_requests( 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",