From: LLFourn Date: Tue, 23 Nov 2021 00:28:18 +0000 (+1100) Subject: Merge branch 'master' into sync_pipeline X-Git-Tag: v0.15.0~10^2 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/struct.CodeLengthError.html?a=commitdiff_plain;h=a630685a0ab95604cadbb423f65d8358ba4bc4e4;p=bdk Merge branch 'master' into sync_pipeline --- a630685a0ab95604cadbb423f65d8358ba4bc4e4 diff --cc CHANGELOG.md index fae09c56,48b0bca3..866dc2e1 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@@ -8,9 -8,8 +8,11 @@@ and this project adheres to [Semantic V - BIP39 implementation dependency, in `keys::bip39` changed from tiny-bip39 to rust-bip39. - Add new method on the `TxBuilder` to embed data in the transaction via `OP_RETURN`. To allow that a fix to check the dust only on spendable output has been introduced. +- Overhauled sync logic for electrum and esplora. +- Unify ureq and reqwest esplora backends to have the same configuration parameters. This means reqwest now has a timeout parameter and ureq has a concurrency parameter. +- Fixed esplora fee estimation. + - Update the `Database` trait to store the last sync timestamp and block height + - Rename `ConfirmationTime` to `BlockTime` ## [v0.13.0] - [v0.12.0] diff --cc src/blockchain/electrum.rs index 53fb6e86,53d4dabb..cb079a87 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@@ -33,11 -33,11 +33,11 @@@ use bitcoin::{Transaction, Txid} use electrum_client::{Client, ConfigBuilder, ElectrumApi, Socks5Config}; -use self::utils::{ElectrumLikeSync, ElsGetHistoryRes}; +use super::script_sync::Request; use super::*; -use crate::database::BatchDatabase; +use crate::database::{BatchDatabase, Database}; use crate::error::Error; - use crate::{ConfirmationTime, FeeRate}; -use crate::FeeRate; ++use crate::{BlockTime, FeeRate}; /// Wrapper over an Electrum Client that implements the required blockchain traits /// @@@ -71,139 -71,10 +71,139 @@@ impl Blockchain for ElectrumBlockchain fn setup( &self, database: &mut D, - progress_update: P, + _progress_update: P, ) -> Result<(), Error> { - self.client - .electrum_like_setup(self.stop_gap, database, progress_update) + let mut request = script_sync::start(database, self.stop_gap)?; + let mut block_times = HashMap::::new(); + let mut txid_to_height = HashMap::::new(); + let mut tx_cache = TxCache::new(database, &self.client); + let chunk_size = self.stop_gap; + // The electrum server has been inconsistent somehow in its responses during sync. For + // example, we do a batch request of transactions and the response contains less + // tranascations than in the request. This should never happen but we don't want to panic. + let electrum_goof = || Error::Generic("electrum server misbehaving".to_string()); + + let batch_update = loop { + request = match request { + Request::Script(script_req) => { + let scripts = script_req.request().take(chunk_size); + let txids_per_script: Vec> = self + .client + .batch_script_get_history(scripts) + .map_err(Error::Electrum)? + .into_iter() + .map(|txs| { + txs.into_iter() + .map(|tx| { + let tx_height = match tx.height { + none if none <= 0 => None, + height => { + txid_to_height.insert(tx.tx_hash, height as u32); + Some(height as u32) + } + }; + (tx.tx_hash, tx_height) + }) + .collect() + }) + .collect(); + + script_req.satisfy(txids_per_script)? + } + + Request::Conftime(conftime_req) => { + // collect up to chunk_size heights to fetch from electrum + let needs_block_height = { + let mut needs_block_height_iter = conftime_req + .request() + .filter_map(|txid| txid_to_height.get(txid).cloned()) + .filter(|height| block_times.get(height).is_none()); + let mut needs_block_height = HashSet::new(); + + while needs_block_height.len() < chunk_size { + match needs_block_height_iter.next() { + Some(height) => needs_block_height.insert(height), + None => break, + }; + } + needs_block_height + }; + + let new_block_headers = self + .client + .batch_block_header(needs_block_height.iter().cloned())?; + + for (height, header) in needs_block_height.into_iter().zip(new_block_headers) { + block_times.insert(height, header.time); + } + + let conftimes = conftime_req + .request() + .take(chunk_size) + .map(|txid| { + let confirmation_time = txid_to_height + .get(txid) + .map(|height| { + let timestamp = + *block_times.get(height).ok_or_else(electrum_goof)?; - Result::<_, Error>::Ok(ConfirmationTime { ++ Result::<_, Error>::Ok(BlockTime { + height: *height, + timestamp: timestamp.into(), + }) + }) + .transpose()?; + Ok(confirmation_time) + }) + .collect::>()?; + + conftime_req.satisfy(conftimes)? + } + Request::Tx(tx_req) => { + let needs_full = tx_req.request().take(chunk_size); + tx_cache.save_txs(needs_full.clone())?; + let full_transactions = needs_full + .map(|txid| tx_cache.get(*txid).ok_or_else(electrum_goof)) + .collect::, _>>()?; + let input_txs = full_transactions.iter().flat_map(|tx| { + tx.input + .iter() + .filter(|input| !input.previous_output.is_null()) + .map(|input| &input.previous_output.txid) + }); + tx_cache.save_txs(input_txs)?; + + let full_details = full_transactions + .into_iter() + .map(|tx| { + let prev_outputs = tx + .input + .iter() + .map(|input| { + if input.previous_output.is_null() { + return Ok(None); + } + let prev_tx = tx_cache + .get(input.previous_output.txid) + .ok_or_else(electrum_goof)?; + let txout = prev_tx + .output + .get(input.previous_output.vout as usize) + .ok_or_else(electrum_goof)?; + Ok(Some(txout.clone())) + }) + .collect::, Error>>()?; + Ok((prev_outputs, tx)) + }) + .collect::, Error>>()?; + + tx_req.satisfy(full_details)? + } + Request::Finish(batch_update) => break batch_update, + } + }; + + database.commit_batch(batch_update)?; + Ok(()) } fn get_tx(&self, txid: &Txid) -> Result, Error> { diff --cc src/blockchain/esplora/api.rs index 74c46c88,00000000..4e0e3f88 mode 100644,000000..100644 --- a/src/blockchain/esplora/api.rs +++ b/src/blockchain/esplora/api.rs @@@ -1,117 -1,0 +1,117 @@@ +//! structs from the esplora API +//! +//! see: - use crate::ConfirmationTime; ++use crate::BlockTime; +use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid}; + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct PrevOut { + pub value: u64, + pub scriptpubkey: Script, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct Vin { + pub txid: Txid, + pub vout: u32, + // None if coinbase + pub prevout: Option, + pub scriptsig: Script, + #[serde(deserialize_with = "deserialize_witness")] + pub witness: Vec>, + pub sequence: u32, + pub is_coinbase: bool, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct Vout { + pub value: u64, + pub scriptpubkey: Script, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct TxStatus { + pub confirmed: bool, + pub block_height: Option, + pub block_time: Option, +} + +#[derive(serde::Deserialize, Clone, Debug)] +pub struct Tx { + pub txid: Txid, + pub version: i32, + pub locktime: u32, + pub vin: Vec, + pub vout: Vec, + pub status: TxStatus, + pub fee: u64, +} + +impl Tx { + pub fn to_tx(&self) -> Transaction { + Transaction { + version: self.version, + lock_time: self.locktime, + input: self + .vin + .iter() + .cloned() + .map(|vin| TxIn { + previous_output: OutPoint { + txid: vin.txid, + vout: vin.vout, + }, + script_sig: vin.scriptsig, + sequence: vin.sequence, + witness: vin.witness, + }) + .collect(), + output: self + .vout + .iter() + .cloned() + .map(|vout| TxOut { + value: vout.value, + script_pubkey: vout.scriptpubkey, + }) + .collect(), + } + } + - pub fn confirmation_time(&self) -> Option { ++ pub fn confirmation_time(&self) -> Option { + match self.status { + TxStatus { + confirmed: true, + block_height: Some(height), + block_time: Some(timestamp), - } => Some(ConfirmationTime { timestamp, height }), ++ } => Some(BlockTime { timestamp, height }), + _ => None, + } + } + + pub fn previous_outputs(&self) -> Vec> { + self.vin + .iter() + .cloned() + .map(|vin| { + vin.prevout.map(|po| TxOut { + script_pubkey: po.scriptpubkey, + value: po.value, + }) + }) + .collect() + } +} + +fn deserialize_witness<'de, D>(d: D) -> Result>, D::Error> +where + D: serde::de::Deserializer<'de>, +{ + use crate::serde::Deserialize; + use bitcoin::hashes::hex::FromHex; + let list = Vec::::deserialize(d)?; + list.into_iter() + .map(|hex_str| Vec::::from_hex(&hex_str)) + .collect::>, _>>() + .map_err(serde::de::Error::custom) +} diff --cc src/blockchain/script_sync.rs index e7ae2763,00000000..4c9b0222 mode 100644,000000..100644 --- a/src/blockchain/script_sync.rs +++ b/src/blockchain/script_sync.rs @@@ -1,394 -1,0 +1,394 @@@ +/*! +This models a how a sync happens where you have a server that you send your script pubkeys to and it +returns associated transactions i.e. electrum. +*/ +#![allow(dead_code)] +use crate::{ + database::{BatchDatabase, BatchOperations, DatabaseUtils}, + wallet::time::Instant, - ConfirmationTime, Error, KeychainKind, LocalUtxo, TransactionDetails, ++ BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails, +}; +use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; +use log::*; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; + +/// A request for on-chain information +pub enum Request<'a, D: BatchDatabase> { + /// A request for transactions related to script pubkeys. + Script(ScriptReq<'a, D>), + /// A request for confirmation times for some transactions. + Conftime(ConftimeReq<'a, D>), + /// A request for full transaction details of some transactions. + Tx(TxReq<'a, D>), + /// Requests are finished here's a batch database update to reflect data gathered. + Finish(D::Batch), +} + +/// starts a sync +pub fn start(db: &D, stop_gap: usize) -> Result, Error> { + use rand::seq::SliceRandom; + let mut keychains = vec![KeychainKind::Internal, KeychainKind::External]; + // shuffling improve privacy, the server doesn't know my first request is from my internal or external addresses + keychains.shuffle(&mut rand::thread_rng()); + let keychain = keychains.pop().unwrap(); + let scripts_needed = db + .iter_script_pubkeys(Some(keychain))? + .into_iter() + .collect(); + let state = State::new(db); + + Ok(Request::Script(ScriptReq { + state, + scripts_needed, + script_index: 0, + stop_gap, + keychain, + next_keychains: keychains, + })) +} + +pub struct ScriptReq<'a, D: BatchDatabase> { + state: State<'a, D>, + script_index: usize, + scripts_needed: VecDeque