Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/persist_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
42 changes: 38 additions & 4 deletions src/wallet/changeset.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -110,6 +111,9 @@
pub change_descriptor: Option<Descriptor<DescriptorPublicKey>>,
/// Stores the network type of the transaction data.
pub network: Option<bitcoin::Network>,
/// Stores the [`Wallet`]'s birthday, defined as the first
/// [`Block`] with relevant transactions to this [`Wallet`].
pub birthday: Option<BlockId>,
/// Changes to the [`LocalChain`](local_chain::LocalChain).
pub local_chain: local_chain::ChangeSet,
/// Changes to [`TxGraph`](tx_graph::TxGraph).
Expand Down Expand Up @@ -145,6 +149,14 @@
);
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);
Expand All @@ -158,6 +170,7 @@
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()
Expand All @@ -174,7 +187,7 @@
/// 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 {} ( \
Expand All @@ -199,12 +212,19 @@
)
}

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)?;
Expand All @@ -223,7 +243,7 @@
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
Expand All @@ -234,13 +254,16 @@
"change_descriptor",
)?,
row.get::<_, Option<Impl<bitcoin::Network>>>("network")?,
// TODO(@luisschwab): merge bdk#2097, publish new bdk_chain version and bump it here for [`BlockId`] impls.
row.get::<_, Option<Impl<BlockId>>>("birthday")?,

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Code Coverage

the trait bound `Impl<BlockId>: FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Rust clippy

the trait bound `chain::Impl<chain::BlockId>: chain::rusqlite::types::FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test Rust Stable (ubuntu-24.04-arm, --all-features)

the trait bound `Impl<BlockId>: FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test MSRV (ubuntu-24.04-arm, --no-default-features --features miniscript/no-std,bdk_chain...

the trait bound `Impl<BlockId>: FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test Rust Stable (ubuntu-24.04-arm, --no-default-features --features miniscript/no-std,bd...

the trait bound `Impl<BlockId>: FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test MSRV (ubuntu-latest, --no-default-features --features miniscript/no-std,bdk_chain/ha...

the trait bound `Impl<BlockId>: FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test MSRV (ubuntu-latest, --all-features)

the trait bound `Impl<BlockId>: FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test MSRV (ubuntu-24.04-arm, --all-features)

the trait bound `Impl<BlockId>: FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test Rust Stable (ubuntu-latest, --all-features)

the trait bound `Impl<BlockId>: FromSql` is not satisfied

Check failure on line 258 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test Rust Stable (ubuntu-latest, --no-default-features --features miniscript/no-std,bdk_c...

the trait bound `Impl<BlockId>: FromSql` is not satisfied
))
})
.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.
Expand Down Expand Up @@ -309,6 +332,17 @@
})?;
}

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! {

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Code Coverage

the trait bound `Impl<BlockId>: ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Rust clippy

the trait bound `chain::Impl<chain::BlockId>: chain::rusqlite::ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test Rust Stable (ubuntu-24.04-arm, --all-features)

the trait bound `Impl<BlockId>: ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test MSRV (ubuntu-24.04-arm, --no-default-features --features miniscript/no-std,bdk_chain...

the trait bound `Impl<BlockId>: ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test Rust Stable (ubuntu-24.04-arm, --no-default-features --features miniscript/no-std,bd...

the trait bound `Impl<BlockId>: ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test MSRV (ubuntu-latest, --no-default-features --features miniscript/no-std,bdk_chain/ha...

the trait bound `Impl<BlockId>: ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test MSRV (ubuntu-latest, --all-features)

the trait bound `Impl<BlockId>: ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test MSRV (ubuntu-24.04-arm, --all-features)

the trait bound `Impl<BlockId>: ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test Rust Stable (ubuntu-latest, --all-features)

the trait bound `Impl<BlockId>: ToSql` is not satisfied

Check failure on line 340 in src/wallet/changeset.rs

View workflow job for this annotation

GitHub Actions / Build & Test Rust Stable (ubuntu-latest, --no-default-features --features miniscript/no-std,bdk_c...

the trait bound `Impl<BlockId>: ToSql` is not satisfied
":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)",
Expand Down
7 changes: 6 additions & 1 deletion src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
22 changes: 22 additions & 0 deletions src/wallet/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -65,6 +66,7 @@ pub struct CreateParams {
pub(crate) change_descriptor_keymap: KeyMap,
pub(crate) network: Network,
pub(crate) genesis_hash: Option<BlockHash>,
pub(crate) birthday: Option<BlockId>,
pub(crate) lookahead: u32,
pub(crate) use_spk_cache: bool,
}
Expand All @@ -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,
}
Expand All @@ -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,
}
Expand All @@ -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,
}
Expand Down Expand Up @@ -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
}

Comment on lines +170 to +178
Copy link
Member Author

@luisschwab luisschwab Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be public. It gives the user the freedom to set whatever value he wants, even if they're incorrect. @ValuedMammal or @evanlinjin can you shed some light on what would happen if I create a CheckPoint with a height+hash pair that doesn't exist on the chain, and then synched from genesis?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question and probably requires testing before going ahead with this feature. The hope is that the bad block will be "reorged out" of the wallet, but that'll depend on which block IDs the chain source decides to return.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best approach would be to not allow manually setting the birthday, and instead rely on the result of a full-scan to define it.

/// Use a custom `lookahead` value.
///
/// The `lookahead` defines a number of script pubkeys to derive over and above the last
Expand Down Expand Up @@ -218,6 +232,7 @@ pub struct LoadParams {
pub(crate) lookahead: u32,
pub(crate) check_network: Option<Network>,
pub(crate) check_genesis_hash: Option<BlockHash>,
pub(crate) check_birthday: Option<BlockId>,
pub(crate) check_descriptor: Option<Option<DescriptorToExtract>>,
pub(crate) check_change_descriptor: Option<Option<DescriptorToExtract>>,
pub(crate) extract_keys: bool,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading