Skip to content

Commit 475c502

Browse files
committed
feat(sqlite): add bdk_sqlite crate implementing PersistBackend backed by a SQLite database
1 parent b8aa76c commit 475c502

File tree

13 files changed

+1197
-154
lines changed

13 files changed

+1197
-154
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Cargo.lock
77

88
# Example persisted files.
99
*.db
10+
*.sqlite*

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"crates/wallet",
55
"crates/chain",
66
"crates/file_store",
7+
"crates/sqlite",
78
"crates/electrum",
89
"crates/esplora",
910
"crates/bitcoind_rpc",

crates/sqlite/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "bdk_sqlite"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MIT OR Apache-2.0"
6+
repository = "https://github.com/bitcoindevkit/bdk"
7+
documentation = "https://docs.rs/bdk_sqlite"
8+
description = "A simple SQLite based implementation of Persist for Bitcoin Dev Kit."
9+
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
10+
authors = ["Bitcoin Dev Kit Developers"]
11+
readme = "README.md"
12+
13+
[dependencies]
14+
anyhow = { version = "1", default-features = false }
15+
bdk_chain = { path = "../chain", version = "0.14.0", features = ["serde", "miniscript"] }
16+
bdk_persist = { path = "../persist", version = "0.2.0", features = ["serde"] }
17+
rusqlite = { version = "0.31.0", features = ["bundled"] }
18+
serde = { version = "1", features = ["derive"] }
19+
serde_json = "1"

crates/sqlite/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# BDK SQLite
2+
3+
This is a simple [SQLite] relational database schema backed implementation of [`PersistBackend`](bdk_persist::PersistBackend).
4+
5+
The main structure is `Store` which persists [`bdk_persist`] `CombinedChangeSet` data into a SQLite database file.
6+
7+
[`bdk_persist`]:https://docs.rs/bdk_persist/latest/bdk_persist/
8+
[SQLite]: https://www.sqlite.org/index.html

crates/sqlite/schema/schema_0.sql

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
-- schema version control
2+
CREATE TABLE version
3+
(
4+
version INTEGER
5+
) STRICT;
6+
INSERT INTO version
7+
VALUES (1);
8+
9+
-- network is the valid network for all other table data
10+
CREATE TABLE network
11+
(
12+
name TEXT UNIQUE NOT NULL
13+
) STRICT;
14+
15+
-- keychain is the json serialized keychain structure as JSONB,
16+
-- descriptor is the complete descriptor string,
17+
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
18+
-- last revealed index is a u32
19+
CREATE TABLE keychain
20+
(
21+
keychain BLOB PRIMARY KEY NOT NULL,
22+
descriptor TEXT NOT NULL,
23+
descriptor_id BLOB NOT NULL,
24+
last_revealed INTEGER
25+
) STRICT;
26+
27+
-- hash is block hash hex string,
28+
-- block height is a u32,
29+
CREATE TABLE block
30+
(
31+
hash TEXT PRIMARY KEY NOT NULL,
32+
height INTEGER NOT NULL
33+
) STRICT;
34+
35+
-- txid is transaction hash hex string (reversed)
36+
-- whole_tx is a consensus encoded transaction,
37+
-- last seen is a u64 unix epoch seconds
38+
CREATE TABLE tx
39+
(
40+
txid TEXT PRIMARY KEY NOT NULL,
41+
whole_tx BLOB,
42+
last_seen INTEGER
43+
) STRICT;
44+
45+
-- Outpoint txid hash hex string (reversed)
46+
-- Outpoint vout
47+
-- TxOut value as SATs
48+
-- TxOut script consensus encoded
49+
CREATE TABLE txout
50+
(
51+
txid TEXT NOT NULL,
52+
vout INTEGER NOT NULL,
53+
value INTEGER NOT NULL,
54+
script BLOB NOT NULL,
55+
PRIMARY KEY (txid, vout)
56+
) STRICT;
57+
58+
-- join table between anchor and tx
59+
-- block hash hex string
60+
-- anchor is a json serialized Anchor structure as JSONB,
61+
-- txid is transaction hash hex string (reversed)
62+
CREATE TABLE anchor_tx
63+
(
64+
block_hash TEXT NOT NULL,
65+
anchor BLOB NOT NULL,
66+
txid TEXT NOT NULL REFERENCES tx (txid),
67+
UNIQUE (anchor, txid),
68+
FOREIGN KEY (block_hash) REFERENCES block(hash)
69+
) STRICT;

crates/sqlite/src/lib.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#![doc = include_str!("../README.md")]
2+
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
3+
#![cfg_attr(docsrs, feature(doc_cfg))]
4+
5+
mod schema;
6+
mod store;
7+
8+
use bdk_chain::bitcoin::Network;
9+
pub use rusqlite;
10+
pub use store::Store;
11+
12+
/// Error that occurs while reading or writing change sets with the SQLite database.
13+
#[derive(Debug)]
14+
pub enum Error {
15+
/// Invalid network, cannot change the one already stored in the database.
16+
Network { expected: Network, given: Network },
17+
/// SQLite error.
18+
Sqlite(rusqlite::Error),
19+
}
20+
21+
impl core::fmt::Display for Error {
22+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
23+
match self {
24+
Self::Network { expected, given } => write!(
25+
f,
26+
"network error trying to read or write change set, expected {}, given {}",
27+
expected, given
28+
),
29+
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e),
30+
}
31+
}
32+
}
33+
34+
impl std::error::Error for Error {}

crates/sqlite/src/schema.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use crate::Store;
2+
use rusqlite::{named_params, Connection, Error};
3+
4+
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
5+
const MIGRATIONS: &[&str] = &[SCHEMA_0];
6+
7+
/// Schema migration related functions.
8+
impl<K, A> Store<K, A> {
9+
/// Migrate sqlite db schema to latest version.
10+
pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
11+
let stmts = &MIGRATIONS
12+
.iter()
13+
.flat_map(|stmt| {
14+
// remove comment lines
15+
let s = stmt
16+
.split('\n')
17+
.filter(|l| !l.starts_with("--") && !l.is_empty())
18+
.collect::<Vec<_>>()
19+
.join(" ");
20+
// split into statements
21+
s.split(';')
22+
// remove extra spaces
23+
.map(|s| {
24+
s.trim()
25+
.split(' ')
26+
.filter(|s| !s.is_empty())
27+
.collect::<Vec<_>>()
28+
.join(" ")
29+
})
30+
.collect::<Vec<_>>()
31+
})
32+
// remove empty statements
33+
.filter(|s| !s.is_empty())
34+
.collect::<Vec<String>>();
35+
36+
let version = Self::get_schema_version(conn)?;
37+
let stmts = &stmts[(version as usize)..];
38+
39+
// begin transaction, all migration statements and new schema version commit or rollback
40+
let tx = conn.transaction()?;
41+
42+
// execute every statement and return `Some` new schema version
43+
// if execution fails, return `Error::Rusqlite`
44+
// if no statements executed returns `None`
45+
let new_version = stmts
46+
.iter()
47+
.enumerate()
48+
.map(|version_stmt| {
49+
tx.execute(version_stmt.1.as_str(), [])
50+
// map result value to next migration version
51+
.map(|_| version_stmt.0 as i32 + version + 1)
52+
})
53+
.last()
54+
.transpose()?;
55+
56+
// if `Some` new statement version, set new schema version
57+
if let Some(version) = new_version {
58+
Self::set_schema_version(&tx, version)?;
59+
}
60+
61+
// commit transaction
62+
tx.commit()?;
63+
Ok(())
64+
}
65+
66+
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
67+
let statement = conn.prepare_cached("SELECT version FROM version");
68+
match statement {
69+
Err(Error::SqliteFailure(e, Some(msg))) => {
70+
if msg == "no such table: version" {
71+
Ok(0)
72+
} else {
73+
Err(Error::SqliteFailure(e, Some(msg)))
74+
}
75+
}
76+
Ok(mut stmt) => {
77+
let mut rows = stmt.query([])?;
78+
match rows.next()? {
79+
Some(row) => {
80+
let version: i32 = row.get(0)?;
81+
Ok(version)
82+
}
83+
None => Ok(0),
84+
}
85+
}
86+
_ => Ok(0),
87+
}
88+
}
89+
90+
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
91+
conn.execute(
92+
"UPDATE version SET version=:version",
93+
named_params! {":version": version},
94+
)
95+
}
96+
}

0 commit comments

Comments
 (0)