use std::collections::HashMap; use std::str::FromStr; use std::sync::MutexGuard; use anyhow::{Error, Result}; use bitcoincore_rpc::bitcoin::absolute::Height; use electrum_client::ElectrumApi; use sdk_common::silentpayments::SpWallet; use sdk_common::sp_client::bitcoin::bip158::BlockFilter; use sdk_common::sp_client::bitcoin::hex::DisplayHex; use sdk_common::sp_client::bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; use sdk_common::sp_client::bitcoin::{BlockHash, OutPoint, Transaction, TxOut, XOnlyPublicKey}; use sdk_common::sp_client::silentpayments::receiving::Receiver; use sdk_common::sp_client::silentpayments::utils::receiving::{ calculate_tweak_data, get_pubkey_from_input, }; use sdk_common::sp_client::{OutputSpendStatus, OwnedOutput}; use tokio::time::Instant; use crate::{electrumclient, MutexExt, DAEMON, STORAGE, WALLET}; pub fn compute_partial_tweak_to_transaction(tx: &Transaction) -> Result { let daemon = DAEMON.get().ok_or(Error::msg("DAEMON not initialized"))?; let mut outpoints: Vec<(String, u32)> = Vec::with_capacity(tx.input.len()); let mut pubkeys: Vec = Vec::with_capacity(tx.input.len()); // TODO we should cache transactions to prevent multiple rpc request when transaction spends multiple outputs from the same tx for input in tx.input.iter() { outpoints.push(( input.previous_output.txid.to_string(), input.previous_output.vout, )); let prev_tx = daemon .lock_anyhow()? .get_transaction(&input.previous_output.txid, None) .map_err(|e| Error::msg(format!("Failed to find previous transaction: {}", e)))?; if let Some(output) = prev_tx.output.get(input.previous_output.vout as usize) { match get_pubkey_from_input( &input.script_sig.to_bytes(), &input.witness.to_vec(), &output.script_pubkey.to_bytes(), ) { Ok(Some(pubkey)) => pubkeys.push(pubkey), Ok(None) => continue, Err(e) => { return Err(Error::msg(format!( "Can't extract pubkey from input: {}", e ))) } } } else { return Err(Error::msg("Transaction with a non-existing input")); } } let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect(); let partial_tweak = calculate_tweak_data(&input_pub_keys, &outpoints)?; Ok(partial_tweak) } fn get_script_to_secret_map( sp_receiver: &Receiver, tweak_data_vec: Vec, scan_key_scalar: Scalar, secp: &Secp256k1, ) -> Result> { let mut res = HashMap::new(); let shared_secrets: Result> = tweak_data_vec .into_iter() .map(|s| { let x = PublicKey::from_str(&s).map_err(Error::new)?; x.mul_tweak(secp, &scan_key_scalar).map_err(Error::new) }) .collect(); let shared_secrets = shared_secrets?; for shared_secret in shared_secrets { let spks = sp_receiver.get_spks_from_shared_secret(&shared_secret)?; for spk in spks.into_values() { res.insert(spk, shared_secret); } } Ok(res) } pub fn check_transaction_alone(mut wallet: MutexGuard, tx: &Transaction, tweak_data: &PublicKey) -> Result> { let updates = match wallet.update_with_transaction(tx, tweak_data, 0) { Ok(updates) => updates, Err(e) => { log::debug!("Error while checking transaction: {}", e); HashMap::new() } }; if updates.len() > 0 { let storage = STORAGE.get().ok_or_else(|| Error::msg("Failed to get STORAGE"))?; storage.lock_anyhow()?.wallet_file.save(&serde_json::to_value(wallet.clone())?)?; } Ok(updates) } fn check_block( blkfilter: BlockFilter, blkhash: BlockHash, candidate_spks: Vec<&[u8; 34]>, owned_spks: Vec>, ) -> Result { // check output scripts let mut scripts_to_match: Vec<_> = candidate_spks.into_iter().map(|spk| spk.as_ref()).collect(); // check input scripts scripts_to_match.extend(owned_spks.iter().map(|spk| spk.as_slice())); // note: match will always return true for an empty query! if !scripts_to_match.is_empty() { Ok(blkfilter.match_any(&blkhash, &mut scripts_to_match.into_iter())?) } else { Ok(false) } } fn scan_block_outputs( sp_receiver: &Receiver, txdata: &Vec, blkheight: u64, spk2secret: HashMap<[u8; 34], PublicKey>, ) -> Result> { let mut res: HashMap = HashMap::new(); // loop over outputs for tx in txdata { let txid = tx.txid(); // collect all taproot outputs from transaction let p2tr_outs: Vec<(usize, &TxOut)> = tx .output .iter() .enumerate() .filter(|(_, o)| o.script_pubkey.is_p2tr()) .collect(); if p2tr_outs.is_empty() { continue; }; // no taproot output let mut secret: Option = None; // Does this transaction contains one of the outputs we already found? for spk in p2tr_outs.iter().map(|(_, o)| &o.script_pubkey) { if let Some(s) = spk2secret.get(spk.as_bytes()) { // we might have at least one output in this transaction secret = Some(*s); break; } } if secret.is_none() { continue; }; // we don't have a secret that matches any of the keys // Now we can just run sp_receiver on all the p2tr outputs let xonlykeys: Result> = p2tr_outs .iter() .map(|(_, o)| { XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]).map_err(Error::new) }) .collect(); let ours = sp_receiver.scan_transaction(&secret.unwrap(), xonlykeys?)?; let height = Height::from_consensus(blkheight as u32)?; for (label, map) in ours { res.extend(p2tr_outs.iter().filter_map(|(i, o)| { match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) { Ok(key) => { if let Some(scalar) = map.get(&key) { match SecretKey::from_slice(&scalar.to_be_bytes()) { Ok(tweak) => { let outpoint = OutPoint { txid, vout: *i as u32, }; let label_str: Option; if let Some(l) = &label { label_str = Some(l.as_inner().to_be_bytes().to_lower_hex_string()); } else { label_str = None; } return Some(( outpoint, OwnedOutput { blockheight: height, tweak: tweak.secret_bytes(), amount: o.value, script: o.script_pubkey.clone(), label: label_str, spend_status: OutputSpendStatus::Unspent, }, )); } Err(_) => { return None; } } } None } Err(_) => None, } })); } } Ok(res) } fn scan_block_inputs( our_outputs: &HashMap, txdata: Vec, ) -> Result> { let mut found = vec![]; for tx in txdata { for input in tx.input { let prevout = input.previous_output; if our_outputs.contains_key(&prevout) { found.push(prevout); } } } Ok(found) } pub fn scan_blocks(mut n_blocks_to_scan: u32, electrum_url: &str) -> anyhow::Result<()> { log::info!("Starting a rescan"); let electrum_client = electrumclient::create_electrum_client(electrum_url)?; let mut sp_wallet = WALLET.get().ok_or(Error::msg("Wallet not initialized"))?.lock_anyhow()?; let core = DAEMON .get() .ok_or(Error::msg("DAEMON not initialized"))? .lock_anyhow()?; let secp = Secp256k1::new(); let scan_height = sp_wallet.get_last_scan(); let tip_height: u32 = core.get_current_height()?.try_into()?; // 0 means scan to tip if n_blocks_to_scan == 0 { n_blocks_to_scan = tip_height - scan_height; } let start = scan_height + 1; let end = if scan_height + n_blocks_to_scan <= tip_height { scan_height + n_blocks_to_scan } else { tip_height }; if start > end { return Ok(()); } log::info!("start: {} end: {}", start, end); let mut filters: Vec<(u32, BlockHash, BlockFilter)> = vec![]; for blkheight in start..=end { filters.push(core.get_filters(blkheight)?); } let mut tweak_data_map = electrum_client.sp_tweaks(start as usize)?; let scan_sk = sp_wallet.get_sp_client().get_scan_key(); let start_time = Instant::now(); for (blkheight, blkhash, blkfilter) in filters { let spk2secret = match tweak_data_map.remove(&blkheight) { Some(tweak_data_vec) => { get_script_to_secret_map(&sp_wallet.get_sp_client().sp_receiver, tweak_data_vec, scan_sk.into(), &secp)? } None => HashMap::new(), }; // check if new possible outputs are payments to us let candidate_spks: Vec<&[u8; 34]> = spk2secret.keys().collect(); // check if owned inputs are spent let owned_spks: Vec> = sp_wallet.get_outputs() .iter() .map(|(_, output)| { let script = output.script.to_bytes(); script }) .collect(); let matched = check_block(blkfilter, blkhash, candidate_spks, owned_spks)?; if matched { let blk = core.get_block(blkhash)?; // scan block for new outputs, and add them to our list let utxo_created_in_block = scan_block_outputs(&sp_wallet.get_sp_client().sp_receiver, &blk.txdata, blkheight.into(), spk2secret)?; if !utxo_created_in_block.is_empty() { sp_wallet .get_mut_outputs() .extend(utxo_created_in_block); } // update the list of outputs just in case // utxos may be created and destroyed in the same block // search inputs and mark as mined let utxo_destroyed_in_block = scan_block_inputs(sp_wallet.get_outputs(), blk.txdata)?; if !utxo_destroyed_in_block.is_empty() { let outputs = sp_wallet.get_mut_outputs(); for outpoint in utxo_destroyed_in_block { if let Some(output) = outputs.get_mut(&outpoint) { output.spend_status = OutputSpendStatus::Mined(blkhash.to_string()); } } } } } // time elapsed for the scan log::info!( "Scan complete in {} seconds", start_time.elapsed().as_secs() ); // update last_scan height sp_wallet .set_last_scan(end); STORAGE.get().unwrap().lock_anyhow()?.wallet_file.save(&serde_json::to_value(sp_wallet.clone())?)?; Ok(()) }