diff --git a/src/persist_test_utils.rs b/src/persist_test_utils.rs index 729d7ef4..9395a0ab 100644 --- a/src/persist_test_utils.rs +++ b/src/persist_test_utils.rs @@ -168,6 +168,7 @@ where descriptor: Some(descriptor.clone()), change_descriptor: Some(change_descriptor.clone()), network: Some(Network::Testnet), + birthday: None, local_chain: local_chain_changeset, tx_graph: tx_graph_changeset, indexer: keychain_txout_changeset, @@ -227,6 +228,7 @@ where descriptor: None, change_descriptor: None, network: None, + birthday: None, local_chain: local_chain_changeset, tx_graph: tx_graph_changeset, indexer: keychain_txout_changeset, diff --git a/src/wallet/changeset.rs b/src/wallet/changeset.rs index 93e5bc90..6e4ed81c 100644 --- a/src/wallet/changeset.rs +++ b/src/wallet/changeset.rs @@ -1,6 +1,7 @@ use bdk_chain::{ indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime, Merge, }; +use chain::BlockId; use miniscript::{Descriptor, DescriptorPublicKey}; use serde::{Deserialize, Serialize}; @@ -110,6 +111,9 @@ pub struct ChangeSet { pub change_descriptor: Option>, /// Stores the network type of the transaction data. pub network: Option, + /// Stores the [`Wallet`]'s birthday, defined as the first + /// [`Block`] with relevant transactions to this [`Wallet`]. + pub birthday: Option, /// Changes to the [`LocalChain`](local_chain::LocalChain). pub local_chain: local_chain::ChangeSet, /// Changes to [`TxGraph`](tx_graph::TxGraph). @@ -145,6 +149,14 @@ impl Merge for ChangeSet { ); self.network = other.network; } + // TODO(@luisschwab): should merging [`ChangeSet`]s with distinct birthdays be possible? + if other.birthday.is_some() { + debug_assert!( + self.birthday.is_none() || self.birthday == other.birthday, + "birthday must never change" + ); + self.birthday = other.birthday; + } // merge locked outpoints self.locked_outpoints.merge(other.locked_outpoints); @@ -158,6 +170,7 @@ impl Merge for ChangeSet { self.descriptor.is_none() && self.change_descriptor.is_none() && self.network.is_none() + && self.birthday.is_none() && self.local_chain.is_empty() && self.tx_graph.is_empty() && self.indexer.is_empty() @@ -174,7 +187,7 @@ impl ChangeSet { /// Name of table to store wallet locked outpoints. pub const WALLET_OUTPOINT_LOCK_TABLE_NAME: &'static str = "bdk_wallet_locked_outpoints"; - /// Get v0 sqlite [ChangeSet] schema + /// Get v0 sqlite [`ChangeSet`] schema. pub fn schema_v0() -> alloc::string::String { format!( "CREATE TABLE {} ( \ @@ -199,12 +212,19 @@ impl ChangeSet { ) } + pub fn schema_v2() -> alloc::string::String { + format!( + "ALTER TABLE {} ADD COLUMN birthday TEXT", + Self::WALLET_TABLE_NAME, + ) + } + /// Initialize sqlite tables for wallet tables. pub fn init_sqlite_tables(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result<()> { crate::rusqlite_impl::migrate_schema( db_tx, Self::WALLET_SCHEMA_NAME, - &[&Self::schema_v0(), &Self::schema_v1()], + &[&Self::schema_v0(), &Self::schema_v1(), &Self::schema_v2()], )?; bdk_chain::local_chain::ChangeSet::init_sqlite_tables(db_tx)?; @@ -223,7 +243,7 @@ impl ChangeSet { let mut changeset = Self::default(); let mut wallet_statement = db_tx.prepare(&format!( - "SELECT descriptor, change_descriptor, network FROM {}", + "SELECT descriptor, change_descriptor, network, birthday FROM {}", Self::WALLET_TABLE_NAME, ))?; let row = wallet_statement @@ -234,13 +254,16 @@ impl ChangeSet { "change_descriptor", )?, row.get::<_, Option>>("network")?, + // TODO(@luisschwab): merge bdk#2097, publish new bdk_chain version and bump it here for [`BlockId`] impls. + row.get::<_, Option>>("birthday")?, )) }) .optional()?; - if let Some((desc, change_desc, network)) = row { + if let Some((desc, change_desc, network, birthday)) = row { changeset.descriptor = desc.map(Impl::into_inner); changeset.change_descriptor = change_desc.map(Impl::into_inner); changeset.network = network.map(Impl::into_inner); + changeset.birthday = birthday.map(Impl::into_inner); } // Select locked outpoints. @@ -309,6 +332,17 @@ impl ChangeSet { })?; } + let mut birthday_statement = db_tx.prepare_cached(&format!( + "INSERT INTO {}(id, birthday) VALUES(:id, :birthday) ON CONFLICT(id) DO UPDATE SET birthday=:birthday", + Self::WALLET_TABLE_NAME, + ))?; + if let Some(birthday) = self.birthday { + birthday_statement.execute(named_params! { + ":id": 0, + ":birthday": Impl(birthday), + })?; + } + // Insert or delete locked outpoints. let mut insert_stmt = db_tx.prepare_cached(&format!( "INSERT OR IGNORE INTO {}(txid, vout) VALUES(:txid, :vout)", diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index ab0a8ff6..e26589d7 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -449,7 +449,12 @@ impl Wallet { let genesis_hash = params .genesis_hash .unwrap_or(genesis_block(network).block_hash()); - let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); + let (mut chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); + if let Some(birthday) = params.birthday { + chain + .apply_update(CheckPoint::new(birthday)) + .expect("wallet birthday overrided genesis hash"); + } let (descriptor, mut descriptor_keymap) = (params.descriptor)(&secp, network_kind)?; check_wallet_descriptor(&descriptor)?; diff --git a/src/wallet/params.rs b/src/wallet/params.rs index 4868074b..252227e7 100644 --- a/src/wallet/params.rs +++ b/src/wallet/params.rs @@ -2,6 +2,7 @@ use alloc::boxed::Box; use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD; use bitcoin::{BlockHash, Network, NetworkKind}; +use chain::BlockId; use miniscript::descriptor::KeyMap; use crate::{ @@ -65,6 +66,7 @@ pub struct CreateParams { pub(crate) change_descriptor_keymap: KeyMap, pub(crate) network: Network, pub(crate) genesis_hash: Option, + pub(crate) birthday: Option, pub(crate) lookahead: u32, pub(crate) use_spk_cache: bool, } @@ -88,6 +90,7 @@ impl CreateParams { change_descriptor_keymap: KeyMap::default(), network: Network::Bitcoin, genesis_hash: None, + birthday: None, lookahead: DEFAULT_LOOKAHEAD, use_spk_cache: false, } @@ -110,6 +113,7 @@ impl CreateParams { change_descriptor_keymap: KeyMap::default(), network: Network::Bitcoin, genesis_hash: None, + birthday: None, lookahead: DEFAULT_LOOKAHEAD, use_spk_cache: false, } @@ -135,6 +139,7 @@ impl CreateParams { change_descriptor_keymap: KeyMap::default(), network: Network::Bitcoin, genesis_hash: None, + birthday: None, lookahead: DEFAULT_LOOKAHEAD, use_spk_cache: false, } @@ -162,6 +167,15 @@ impl CreateParams { self } + /// Set the [`Wallet`]'s birthday, as a [`BlockId`]. + /// + /// The birthday can be used to limit how far back a chain oracle is queried, + /// saving up time and data bandwidth in full-scans. + pub fn birthday(mut self, birthday: BlockId) -> Self { + self.birthday = Some(birthday); + self + } + /// Use a custom `lookahead` value. /// /// The `lookahead` defines a number of script pubkeys to derive over and above the last @@ -218,6 +232,7 @@ pub struct LoadParams { pub(crate) lookahead: u32, pub(crate) check_network: Option, pub(crate) check_genesis_hash: Option, + pub(crate) check_birthday: Option, pub(crate) check_descriptor: Option>, pub(crate) check_change_descriptor: Option>, pub(crate) extract_keys: bool, @@ -235,6 +250,7 @@ impl LoadParams { lookahead: DEFAULT_LOOKAHEAD, check_network: None, check_genesis_hash: None, + check_birthday: None, check_descriptor: None, check_change_descriptor: None, extract_keys: false, @@ -282,6 +298,12 @@ impl LoadParams { self } + /// Checks that the given `birthday` matches the one loaded from persistence. + pub fn check_birthday(mut self, birthday: BlockId) -> Self { + self.check_birthday = Some(birthday); + self + } + /// Use a custom `lookahead` value. /// /// The `lookahead` defines a number of script pubkeys to derive over and above the last