Skip to content

Commit 8af5fa5

Browse files
feat: bdk2
1 parent 69f59ca commit 8af5fa5

5 files changed

Lines changed: 180 additions & 47 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ tracing = "0.1.40"
1313
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde_json", "json"] }
1414

1515
[dependencies.bdk_wallet]
16+
version = "2.0.0"
1617
git = "https://github.com/bitcoindevkit/bdk_wallet.git"
1718
tag = "wallet-2.0.0"
1819
features = ["test-utils"]
@@ -23,8 +24,7 @@ anyhow = "1.0.89"
2324
rustls = "0.23.14"
2425

2526
[dev-dependencies.bdk_electrum]
26-
git = "https://github.com/bitcoindevkit/bdk.git"
27-
tag = "chain-0.23.0"
27+
version = "0.23.0"
2828

2929
[[example]]
3030
name = "bdk_sqlx_postgres"

examples/bdk_sqlx_postgres.rs

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ use bdk_electrum::{electrum_client, BdkElectrumClient};
44
use bdk_sqlx::sqlx::Postgres;
55
use bdk_sqlx::{PgStoreBuilder, Store};
66
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
7-
use bdk_wallet::bitcoin::Network;
8-
use bdk_wallet::chain::spk_client::{FullScanRequest, SyncRequest};
9-
use bdk_wallet::{KeychainKind, PersistedWallet, Update, Wallet};
7+
use bdk_wallet::bitcoin::{constants, Network};
8+
use bdk_wallet::chain::keychain_txout::{KeychainTxOutIndex, DEFAULT_LOOKAHEAD};
9+
use bdk_wallet::chain::local_chain::LocalChain;
10+
use bdk_wallet::chain::{keychain_txout, IndexedTxGraph};
11+
use bdk_wallet::{ChangeSet, KeychainKind, PersistedWallet, Update, Wallet};
1012
use rustls::crypto::ring::default_provider;
1113
use std::collections::HashSet;
1214
use std::io::Write;
@@ -120,45 +122,13 @@ async fn main() -> anyhow::Result<()> {
120122

121123
Ok(())
122124
}
125+
123126
fn electrum(wallet: &mut PersistedWallet<Store<Postgres>>) -> anyhow::Result<()> {
124127
let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?);
125-
126-
// Populate the electrum client's transaction cache so it doesn't re-download transaction we
127-
// already have.
128-
client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));
129-
130-
131-
let graph = wallet.tx_graph();
132-
let chain = wallet.local_chain();
133-
let request = {
134-
FullScanRequest::builder()
135-
.chain_tip(chain.tip())
136-
.spks_for_keychain(
137-
KeychainKind::External,
138-
graph
139-
.index
140-
.unbounded_spk_iter(KeychainKind::External)
141-
.into_iter()
142-
.flatten(),
143-
)
144-
.spks_for_keychain(
145-
KeychainKind::Internal,
146-
graph
147-
.index
148-
.unbounded_spk_iter(KeychainKind::Internal)
149-
.into_iter()
150-
.flatten(),
151-
)
152-
};
153-
128+
let request = wallet.start_full_scan().build();
154129
let res = client
155-
.full_scan::<_>(request, STOP_GAP, BATCH_SIZE, false)
130+
.full_scan::<_>(request, STOP_GAP, BATCH_SIZE, true)
156131
.context("scanning the blockchain")?;
157-
(
158-
res.chain_update,
159-
res.tx_update,
160-
Some(res.last_active_indices),
161-
);
162-
132+
wallet.apply_update(res)?;
163133
Ok(())
164134
}

migrations/postgres/01_bdk_wallet.sql

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,19 @@ CREATE TABLE IF NOT EXISTS bdk_wallet.keychain (
2222
descriptor TEXT NOT NULL,
2323
descriptor_id BYTEA NOT NULL,
2424
last_revealed INTEGER DEFAULT 0,
25-
PRIMARY KEY (wallet_name, keychainkind)
25+
PRIMARY KEY (wallet_name, keychainkind),
26+
UNIQUE (wallet_name, descriptor_id)
27+
);
28+
29+
-- Script pubkey cache for keychains
30+
-- Stores precomputed script pubkeys for each keychain index
31+
CREATE TABLE IF NOT EXISTS bdk_wallet.keychain_spk (
32+
wallet_name TEXT NOT NULL,
33+
descriptor_id BYTEA NOT NULL,
34+
idx INTEGER NOT NULL,
35+
script BYTEA NOT NULL,
36+
PRIMARY KEY (wallet_name, descriptor_id, idx),
37+
FOREIGN KEY (wallet_name, descriptor_id) REFERENCES bdk_wallet.keychain(wallet_name, descriptor_id)
2638
);
2739

2840
-- Hash is block hash hex string,

src/postgres.rs

Lines changed: 155 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
// Standard library imports
66
use std::{
7+
collections::BTreeMap,
78
str::FromStr,
89
sync::{Arc, OnceLock},
910
};
@@ -236,7 +237,8 @@ impl Store<Postgres> {
236237
descriptor TEXT NOT NULL,
237238
descriptor_id BYTEA NOT NULL,
238239
last_revealed INTEGER DEFAULT 0,
239-
PRIMARY KEY (wallet_name, keychainkind)
240+
PRIMARY KEY (wallet_name, keychainkind),
241+
UNIQUE (wallet_name, descriptor_id)
240242
)"#,
241243
r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."block" (
242244
wallet_name TEXT NOT NULL,
@@ -272,6 +274,14 @@ impl Store<Postgres> {
272274
FOREIGN KEY (wallet_name, txid) REFERENCES "bdk_wallet"."tx"(wallet_name, txid)
273275
)"#,
274276
r#"CREATE INDEX IF NOT EXISTS idx_anchor_tx_txid ON "bdk_wallet"."anchor_tx" (txid)"#,
277+
r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."keychain_spk" (
278+
wallet_name TEXT NOT NULL,
279+
descriptor_id BYTEA NOT NULL,
280+
idx INTEGER NOT NULL,
281+
script BYTEA NOT NULL,
282+
PRIMARY KEY (wallet_name, descriptor_id, idx),
283+
FOREIGN KEY (wallet_name, descriptor_id) REFERENCES "bdk_wallet"."keychain"(wallet_name, descriptor_id)
284+
)"#,
275285
];
276286

277287
// Execute each query separately
@@ -310,8 +320,69 @@ impl Store<Postgres> {
310320
table: "alter tx table".to_string(),
311321
source: e,
312322
})?;
323+
324+
// Also add unique constraint and keychain_spk table for v2
325+
sqlx::query(
326+
r#"ALTER TABLE "bdk_wallet"."keychain"
327+
ADD CONSTRAINT keychain_wallet_descriptor_unique
328+
UNIQUE (wallet_name, descriptor_id)"#,
329+
)
330+
.execute(&mut *tx)
331+
.await
332+
.map_err(|e| BdkSqlxError::QueryError {
333+
table: "add unique constraint to keychain".to_string(),
334+
source: e,
335+
})?;
336+
337+
sqlx::query(
338+
r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."keychain_spk" (
339+
wallet_name TEXT NOT NULL,
340+
descriptor_id BYTEA NOT NULL,
341+
idx INTEGER NOT NULL,
342+
script BYTEA NOT NULL,
343+
PRIMARY KEY (wallet_name, descriptor_id, idx),
344+
FOREIGN KEY (wallet_name, descriptor_id) REFERENCES "bdk_wallet"."keychain"(wallet_name, descriptor_id)
345+
)"#,
346+
)
347+
.execute(&mut *tx)
348+
.await
349+
.map_err(|e| BdkSqlxError::QueryError {
350+
table: "create keychain_spk table".to_string(),
351+
source: e,
352+
})?;
313353
}
314-
_ => {} // Fresh install or already at v2
354+
Some(2) => {
355+
// Migrate from v2 to v3: Add unique constraint and keychain_spk table
356+
sqlx::query(
357+
r#"ALTER TABLE "bdk_wallet"."keychain"
358+
ADD CONSTRAINT IF NOT EXISTS keychain_wallet_descriptor_unique
359+
UNIQUE (wallet_name, descriptor_id)"#,
360+
)
361+
.execute(&mut *tx)
362+
.await
363+
.map_err(|e| BdkSqlxError::QueryError {
364+
table: "add unique constraint to keychain".to_string(),
365+
source: e,
366+
})?;
367+
368+
sqlx::query(
369+
r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."keychain_spk" (
370+
wallet_name TEXT NOT NULL,
371+
descriptor_id BYTEA NOT NULL,
372+
idx INTEGER NOT NULL,
373+
script BYTEA NOT NULL,
374+
PRIMARY KEY (wallet_name, descriptor_id, idx),
375+
FOREIGN KEY (wallet_name, descriptor_id) REFERENCES "bdk_wallet"."keychain"(wallet_name, descriptor_id)
376+
)"#,
377+
)
378+
.execute(&mut *tx)
379+
.await
380+
.map_err(|e| BdkSqlxError::QueryError {
381+
table: "create keychain_spk table".to_string(),
382+
source: e,
383+
})?;
384+
}
385+
_ => {} // Fresh install or already at v3
315386
}
316387

317388
// Insert or update to current version
@@ -320,7 +391,7 @@ impl Store<Postgres> {
320391
VALUES ($1)
321392
ON CONFLICT (version) DO NOTHING"#,
322393
)
323-
.bind(2) // Current schema version is now 2
394+
.bind(3) // Current schema version is now 3
324395
.execute(&mut *tx)
325396
.await
326397
.map_err(|e| BdkSqlxError::QueryError {
@@ -395,6 +466,8 @@ impl Store<Postgres> {
395466
if let Some(last_rev) = external_last_revealed {
396467
changeset.indexer.last_revealed.insert(did, last_rev as u32);
397468
}
469+
// Load SPK cache for external descriptor
470+
load_keychain_spks(db_tx, wallet_name, did, changeset).await?;
398471
}
399472

400473
if let Some(desc_str) = internal_desc_str {
@@ -404,6 +477,8 @@ impl Store<Postgres> {
404477
if let Some(last_rev) = internal_last_revealed {
405478
changeset.indexer.last_revealed.insert(did, last_rev as u32);
406479
}
480+
// Load SPK cache for internal descriptor
481+
load_keychain_spks(db_tx, wallet_name, did, changeset).await?;
407482
}
408483

409484
changeset.tx_graph = tx_graph_changeset_from_postgres(db_tx, wallet_name).await?;
@@ -440,6 +515,14 @@ impl Store<Postgres> {
440515
}
441516
}
442517

518+
// Persist SPK cache
519+
let spk_cache = &changeset.indexer.spk_cache;
520+
if !spk_cache.is_empty() {
521+
for (desc_id, spks) in spk_cache {
522+
persist_keychain_spks(&mut tx, wallet_name, *desc_id, spks).await?;
523+
}
524+
}
525+
443526
local_chain_changeset_persist_to_postgres(&mut tx, wallet_name, &changeset.local_chain)
444527
.await?;
445528
tx_graph_changeset_persist_to_postgres(&mut tx, wallet_name, &changeset.tx_graph).await?;
@@ -531,6 +614,74 @@ async fn update_last_revealed(
531614
Ok(())
532615
}
533616

617+
/// Load keychain script pubkeys from the database
618+
#[tracing::instrument(skip(db_tx, changeset))]
619+
async fn load_keychain_spks(
620+
db_tx: &mut Transaction<'_, Postgres>,
621+
wallet_name: &str,
622+
descriptor_id: DescriptorId,
623+
changeset: &mut ChangeSet,
624+
) -> Result<()> {
625+
trace!("load keychain spks");
626+
627+
let rows = sqlx::query(
628+
r#"SELECT idx, script FROM "bdk_wallet"."keychain_spk"
629+
WHERE wallet_name = $1 AND descriptor_id = $2
630+
ORDER BY idx"#,
631+
)
632+
.bind(wallet_name)
633+
.bind(descriptor_id.to_byte_array())
634+
.fetch_all(&mut **db_tx)
635+
.await
636+
.map_err(|e| BdkSqlxError::QueryError {
637+
table: "select keychain_spk".to_string(),
638+
source: e,
639+
})?;
640+
641+
if !rows.is_empty() {
642+
let mut spks = BTreeMap::new();
643+
for row in rows {
644+
let idx: i32 = row.get("idx");
645+
let script: Vec<u8> = row.get("script");
646+
spks.insert(idx as u32, ScriptBuf::from_bytes(script));
647+
}
648+
changeset.indexer.spk_cache.insert(descriptor_id, spks);
649+
}
650+
651+
Ok(())
652+
}
653+
654+
/// Persist keychain script pubkeys to the database
655+
#[tracing::instrument(skip(db_tx, spks))]
656+
async fn persist_keychain_spks(
657+
db_tx: &mut Transaction<'_, Postgres>,
658+
wallet_name: &str,
659+
descriptor_id: DescriptorId,
660+
spks: &BTreeMap<u32, ScriptBuf>,
661+
) -> Result<()> {
662+
trace!("persist keychain spks");
663+
664+
for (idx, spk) in spks {
665+
sqlx::query(
666+
r#"INSERT INTO "bdk_wallet"."keychain_spk" (wallet_name, descriptor_id, idx, script)
667+
VALUES ($1, $2, $3, $4)
668+
ON CONFLICT (wallet_name, descriptor_id, idx) DO UPDATE SET script = $4"#,
669+
)
670+
.bind(wallet_name)
671+
.bind(descriptor_id.to_byte_array())
672+
.bind(*idx as i32)
673+
.bind(spk.as_bytes())
674+
.execute(&mut **db_tx)
675+
.await
676+
.map_err(|e| BdkSqlxError::QueryError {
677+
table: "insert keychain_spk".to_string(),
678+
source: e,
679+
})?;
680+
}
681+
682+
Ok(())
683+
}
684+
534685
/// Select transactions, txouts, and anchors.
535686
#[tracing::instrument(skip(db_tx))]
536687
pub async fn tx_graph_changeset_from_postgres(
@@ -783,7 +934,7 @@ pub async fn local_chain_changeset_persist_to_postgres(
783934

784935
/// Collects information on all the wallets in the database and dumps it to stdout.
785936
#[tracing::instrument]
786-
pub async fn easy_backup(db: Pool<Postgres>) -> Result<()> {
937+
pub async fn _easy_backup(db: Pool<Postgres>) -> Result<()> {
787938
trace!("Starting easy backup");
788939

789940
let statement = r#"SELECT * FROM "bdk_wallet"."keychain""#;

src/sqlite.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ pub async fn local_chain_changeset_persist_to_sqlite(
471471

472472
/// Collects information on all the wallets in the database and dumps it to stdout.
473473
#[tracing::instrument]
474-
pub async fn easy_backup(db: Pool<Sqlite>) -> Result<(), BdkSqlxError> {
474+
pub async fn _easy_backup(db: Pool<Sqlite>) -> Result<(), BdkSqlxError> {
475475
info!("Starting easy backup");
476476

477477
let statement = "SELECT * FROM keychain";

0 commit comments

Comments
 (0)