Skip to content

feat!: Persist utxo lock status #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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: 1 addition & 1 deletion .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Clippy Results
args: --all-features --all-targets -- -D warnings
args: --workspace --exclude 'example_*' --all-features --all-targets -- -D warnings

build-examples:
needs: prepare
Expand Down
5 changes: 1 addition & 4 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
# TODO fix, see: https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
enum-variant-size-threshold = 1032
# TODO fix, see: https://rust-lang.github.io/rust-clippy/master/index.html#result_large_err
large-error-threshold = 993
msrv = "1.63.0"
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ build:
check:
cargo +nightly fmt --all -- --check
cargo check --workspace --exclude 'example_*' --all-features
cargo clippy --all-features --all-targets -- -D warnings
cargo clippy --workspace --exclude 'example_*' --all-features --all-targets -- -D warnings
@[ "$(git log --pretty='format:%G?' -1 HEAD)" = "N" ] && \
echo "\n⚠️ Unsigned commit: BDK requires that commits be signed." || \
true
Expand Down
81 changes: 78 additions & 3 deletions wallet/src/wallet/changeset.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use bdk_chain::{
indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime, Merge,
};
use bitcoin::{OutPoint, Txid};
use miniscript::{Descriptor, DescriptorPublicKey};
use serde::{Deserialize, Serialize};

type IndexedTxGraphChangeSet =
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>;

/// A change set for [`Wallet`]
use crate::locked_outpoints;

/// A change set for [`Wallet`].
///
/// ## Definition
///
/// The change set is responsible for transmiting data between the persistent storage layer and the
/// The change set is responsible for transmitting data between the persistent storage layer and the
/// core library components. Specifically, it serves two primary functions:
///
/// 1) Recording incremental changes to the in-memory representation that need to be persisted to
Expand Down Expand Up @@ -114,6 +117,8 @@ pub struct ChangeSet {
pub tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>,
/// Changes to [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex).
pub indexer: keychain_txout::ChangeSet,
/// Changes to locked outpoints.
pub locked_outpoints: locked_outpoints::ChangeSet,
}

impl Merge for ChangeSet {
Expand Down Expand Up @@ -142,6 +147,9 @@ impl Merge for ChangeSet {
self.network = other.network;
}

// merge locked outpoints
self.locked_outpoints.merge(other.locked_outpoints);

Merge::merge(&mut self.local_chain, other.local_chain);
Merge::merge(&mut self.tx_graph, other.tx_graph);
Merge::merge(&mut self.indexer, other.indexer);
Expand All @@ -154,6 +162,7 @@ impl Merge for ChangeSet {
&& self.local_chain.is_empty()
&& self.tx_graph.is_empty()
&& self.indexer.is_empty()
&& self.locked_outpoints.is_empty()
}
}

Expand All @@ -163,6 +172,8 @@ impl ChangeSet {
pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet";
/// Name of table to store wallet descriptors and network.
pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet";
/// 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
pub fn schema_v0() -> alloc::string::String {
Expand All @@ -177,12 +188,24 @@ impl ChangeSet {
)
}

/// Get v1 sqlite [`ChangeSet`] schema. Schema v1 adds a table for locked outpoints.
pub fn schema_v1() -> alloc::string::String {
format!(
"CREATE TABLE {} ( \
txid TEXT NOT NULL, \
vout INTEGER NOT NULL, \
PRIMARY KEY(txid, vout) \
) STRICT;",
Self::WALLET_OUTPOINT_LOCK_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_v0(), &Self::schema_v1()],
)?;

bdk_chain::local_chain::ChangeSet::init_sqlite_tables(db_tx)?;
Expand Down Expand Up @@ -220,6 +243,24 @@ impl ChangeSet {
changeset.network = network.map(Impl::into_inner);
}

// Select locked outpoints.
let mut stmt = db_tx.prepare(&format!(
"SELECT txid, vout FROM {}",
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
))?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, Impl<Txid>>("txid")?,
row.get::<_, u32>("vout")?,
))
})?;
let locked_outpoints = &mut changeset.locked_outpoints.locked_outpoints;
for row in rows {
let (Impl(txid), vout) = row?;
let outpoint = OutPoint::new(txid, vout);
locked_outpoints.insert(outpoint, true);
}

changeset.local_chain = local_chain::ChangeSet::from_sqlite(db_tx)?;
changeset.tx_graph = tx_graph::ChangeSet::<_>::from_sqlite(db_tx)?;
changeset.indexer = keychain_txout::ChangeSet::from_sqlite(db_tx)?;
Expand Down Expand Up @@ -268,6 +309,31 @@ impl ChangeSet {
})?;
}

// Insert or delete locked outpoints.
let mut insert_stmt = db_tx.prepare_cached(&format!(
"REPLACE INTO {}(txid, vout) VALUES(:txid, :vout)",
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME
))?;
let mut delete_stmt = db_tx.prepare_cached(&format!(
"DELETE FROM {} WHERE txid=:txid AND vout=:vout",
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
))?;
let locked_outpoints = &self.locked_outpoints.locked_outpoints;
for (&outpoint, &is_locked) in locked_outpoints.iter() {
let OutPoint { txid, vout } = outpoint;
if is_locked {
insert_stmt.execute(named_params! {
":txid": Impl(txid),
":vout": vout,
})?;
} else {
delete_stmt.execute(named_params! {
":txid": Impl(txid),
":vout": vout,
})?;
}
}

self.local_chain.persist_to_sqlite(db_tx)?;
self.tx_graph.persist_to_sqlite(db_tx)?;
self.indexer.persist_to_sqlite(db_tx)?;
Expand Down Expand Up @@ -311,3 +377,12 @@ impl From<keychain_txout::ChangeSet> for ChangeSet {
}
}
}

impl From<locked_outpoints::ChangeSet> for ChangeSet {
fn from(locked_outpoints: locked_outpoints::ChangeSet) -> Self {
Self {
locked_outpoints,
..Default::default()
}
}
}
26 changes: 26 additions & 0 deletions wallet/src/wallet/locked_outpoints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Module containing the locked outpoints change set.

use bdk_chain::Merge;
use bitcoin::OutPoint;
use serde::{Deserialize, Serialize};

use crate::collections::BTreeMap;

/// Represents changes to locked outpoints.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ChangeSet {
/// The lock status of an outpoint, `true == is_locked`.
pub locked_outpoints: BTreeMap<OutPoint, bool>,
}

impl Merge for ChangeSet {
fn merge(&mut self, other: Self) {
// Extend self with other. Any entries in `self` that share the same
// outpoint are overwritten.
self.locked_outpoints.extend(other.locked_outpoints);
}

fn is_empty(&self) -> bool {
self.locked_outpoints.is_empty()
}
}
Loading
Loading