Skip to content

Commit 6e11db6

Browse files
committed
locked_outpoints: Remove optional expiration height
- changeset: Changed SQLite to delete rows when an outpoint is unlocked. Any outpoints that are present in the locked outpoints table are logically assumed to be locked.
1 parent cfac1aa commit 6e11db6

File tree

4 files changed

+67
-149
lines changed

4 files changed

+67
-149
lines changed

wallet/src/wallet/changeset.rs

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,6 @@ impl ChangeSet {
194194
"CREATE TABLE {} ( \
195195
txid TEXT NOT NULL, \
196196
vout INTEGER NOT NULL, \
197-
is_locked INTEGER, \
198-
expiration_height INTEGER, \
199197
PRIMARY KEY(txid, vout) \
200198
) STRICT;",
201199
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
@@ -247,27 +245,20 @@ impl ChangeSet {
247245

248246
// Select locked outpoints.
249247
let mut stmt = db_tx.prepare(&format!(
250-
"SELECT txid, vout, is_locked, expiration_height FROM {}",
248+
"SELECT txid, vout FROM {}",
251249
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
252250
))?;
253251
let rows = stmt.query_map([], |row| {
254252
Ok((
255253
row.get::<_, Impl<Txid>>("txid")?,
256254
row.get::<_, u32>("vout")?,
257-
row.get::<_, bool>("is_locked")?,
258-
row.get::<_, Option<u32>>("expiration_height")?,
259255
))
260256
})?;
261-
let locked_outpoints = &mut changeset.locked_outpoints;
257+
let locked_outpoints = &mut changeset.locked_outpoints.locked_outpoints;
262258
for row in rows {
263-
let (Impl(txid), vout, is_locked, expiration_height) = row?;
259+
let (Impl(txid), vout) = row?;
264260
let outpoint = OutPoint::new(txid, vout);
265-
locked_outpoints
266-
.locked_outpoints
267-
.insert(outpoint, is_locked);
268-
locked_outpoints
269-
.expiration_heights
270-
.insert(outpoint, expiration_height);
261+
locked_outpoints.insert(outpoint, true);
271262
}
272263

273264
changeset.local_chain = local_chain::ChangeSet::from_sqlite(db_tx)?;
@@ -318,33 +309,29 @@ impl ChangeSet {
318309
})?;
319310
}
320311

321-
// Insert locked outpoints.
322-
let mut stmt = db_tx.prepare_cached(&format!(
323-
"INSERT INTO {}(txid, vout, is_locked) VALUES(:txid, :vout, :is_locked) ON CONFLICT DO UPDATE SET is_locked=:is_locked",
324-
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
312+
// Insert or delete locked outpoints.
313+
let mut insert_stmt = db_tx.prepare_cached(&format!(
314+
"REPLACE INTO {}(txid, vout) VALUES(:txid, :vout)",
315+
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME
325316
))?;
326-
let locked_outpoints = &self.locked_outpoints.locked_outpoints;
327-
for (&outpoint, is_locked) in locked_outpoints.iter() {
328-
let OutPoint { txid, vout } = outpoint;
329-
stmt.execute(named_params! {
330-
":txid": Impl(txid),
331-
":vout": vout,
332-
":is_locked": is_locked,
333-
})?;
334-
}
335-
// Insert locked outpoints expiration heights.
336-
let mut stmt = db_tx.prepare_cached(&format!(
337-
"INSERT INTO {}(txid, vout, expiration_height) VALUES(:txid, :vout, :expiration_height) ON CONFLICT DO UPDATE SET expiration_height=:expiration_height",
317+
let mut delete_stmt = db_tx.prepare_cached(&format!(
318+
"DELETE FROM {} WHERE txid=:txid AND vout=:vout",
338319
Self::WALLET_OUTPOINT_LOCK_TABLE_NAME,
339320
))?;
340-
let expiration_heights = &self.locked_outpoints.expiration_heights;
341-
for (&outpoint, expiration_height) in expiration_heights.iter() {
321+
let locked_outpoints = &self.locked_outpoints.locked_outpoints;
322+
for (&outpoint, &is_locked) in locked_outpoints.iter() {
342323
let OutPoint { txid, vout } = outpoint;
343-
stmt.execute(named_params! {
344-
":txid": Impl(txid),
345-
":vout": vout,
346-
":expiration_height": expiration_height,
347-
})?;
324+
if is_locked {
325+
insert_stmt.execute(named_params! {
326+
":txid": Impl(txid),
327+
":vout": vout,
328+
})?;
329+
} else {
330+
delete_stmt.execute(named_params! {
331+
":txid": Impl(txid),
332+
":vout": vout,
333+
})?;
334+
}
348335
}
349336

350337
self.local_chain.persist_to_sqlite(db_tx)?;
@@ -390,3 +377,12 @@ impl From<keychain_txout::ChangeSet> for ChangeSet {
390377
}
391378
}
392379
}
380+
381+
impl From<locked_outpoints::ChangeSet> for ChangeSet {
382+
fn from(locked_outpoints: locked_outpoints::ChangeSet) -> Self {
383+
Self {
384+
locked_outpoints,
385+
..Default::default()
386+
}
387+
}
388+
}

wallet/src/wallet/locked_outpoints.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,16 @@ use crate::collections::BTreeMap;
1111
pub struct ChangeSet {
1212
/// The lock status of an outpoint, `true == is_locked`.
1313
pub locked_outpoints: BTreeMap<OutPoint, bool>,
14-
/// The expiration height of the lock.
15-
pub expiration_heights: BTreeMap<OutPoint, Option<u32>>,
1614
}
1715

1816
impl Merge for ChangeSet {
1917
fn merge(&mut self, other: Self) {
18+
// Extend self with other. Any entries in `self` that share the same
19+
// outpoint are overwritten.
2020
self.locked_outpoints.extend(other.locked_outpoints);
21-
self.expiration_heights.extend(other.expiration_heights);
2221
}
2322

2423
fn is_empty(&self) -> bool {
25-
self.locked_outpoints.is_empty() && self.expiration_heights.is_empty()
24+
self.locked_outpoints.is_empty()
2625
}
2726
}

wallet/src/wallet/mod.rs

Lines changed: 31 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -660,21 +660,19 @@ impl Wallet {
660660
indexed_graph.apply_changeset(changeset.tx_graph.into());
661661

662662
// Apply locked outpoints
663-
let mut locked_outpoints = BTreeMap::new();
664-
let locked_outpoints::ChangeSet {
665-
locked_outpoints: locked_utxos,
666-
expiration_heights,
667-
} = changeset.locked_outpoints;
668-
for (outpoint, is_locked) in locked_utxos {
669-
locked_outpoints.insert(
670-
outpoint,
671-
UtxoLock {
663+
let locked_outpoints = changeset.locked_outpoints.locked_outpoints;
664+
let locked_outpoints = locked_outpoints
665+
.into_iter()
666+
.map(|(outpoint, is_locked)| {
667+
(
672668
outpoint,
673-
is_locked,
674-
expiration_height: expiration_heights.get(&outpoint).cloned().flatten(),
675-
},
676-
);
677-
}
669+
UtxoLock {
670+
outpoint,
671+
is_locked,
672+
},
673+
)
674+
})
675+
.collect();
678676

679677
let stage = ChangeSet::default();
680678

@@ -2414,29 +2412,22 @@ impl Wallet {
24142412
.map(|output| output.outpoint)
24152413
}
24162414

2417-
/// Whether the `outpoint` is currently locked. See [`Wallet::lock_outpoint`] for more.
2415+
/// Whether the `outpoint` is locked. See [`Wallet::lock_outpoint`] for more.
24182416
pub fn is_outpoint_locked(&self, outpoint: OutPoint) -> bool {
2419-
if let Some(utxo_lock) = self.locked_outpoints.get(&outpoint) {
2420-
if utxo_lock.is_locked {
2421-
return utxo_lock
2422-
.expiration_height
2423-
.map_or(true, |height| self.chain.tip().height() < height);
2424-
}
2425-
}
2426-
false
2417+
self.locked_outpoints
2418+
.get(&outpoint)
2419+
.map_or(false, |u| u.is_locked)
24272420
}
24282421

24292422
/// Lock a wallet output identified by the given `outpoint`.
24302423
///
24312424
/// A locked UTXO will not be selected as an input to fund a transaction. This is useful
2432-
/// for excluding or reserving candidate inputs during transaction creation. You can optionally
2433-
/// specify the `expiration_height` of the lock that defines the height of the local chain at
2434-
/// which the outpoint becomes spendable.
2425+
/// for excluding or reserving candidate inputs during transaction creation.
24352426
///
2436-
/// You must persist the staged change for the lock status to be persistent. To unlock a
2427+
/// **You must persist the staged change for the lock status to be persistent**. To unlock a
24372428
/// previously locked outpoint, see [`Wallet::unlock_outpoint`].
2438-
pub fn lock_outpoint(&mut self, outpoint: OutPoint, expiration_height: Option<u32>) {
2439-
use alloc::collections::btree_map;
2429+
pub fn lock_outpoint(&mut self, outpoint: OutPoint) {
2430+
use crate::collections::btree_map;
24402431
let lock_value = true;
24412432
let mut changeset = locked_outpoints::ChangeSet::default();
24422433

@@ -2449,36 +2440,26 @@ impl Wallet {
24492440
utxo.is_locked = lock_value;
24502441
changeset.locked_outpoints.insert(outpoint, lock_value);
24512442
}
2452-
if utxo.expiration_height != expiration_height {
2453-
utxo.expiration_height = expiration_height;
2454-
changeset
2455-
.expiration_heights
2456-
.insert(outpoint, expiration_height);
2457-
}
24582443
}
24592444
btree_map::Entry::Vacant(e) => {
24602445
e.insert(UtxoLock {
24612446
outpoint,
24622447
is_locked: lock_value,
2463-
expiration_height,
24642448
});
24652449
changeset.locked_outpoints.insert(outpoint, lock_value);
2466-
changeset
2467-
.expiration_heights
2468-
.insert(outpoint, expiration_height);
24692450
}
24702451
};
24712452

2472-
self.stage.merge(ChangeSet {
2473-
locked_outpoints: changeset,
2474-
..Default::default()
2475-
});
2453+
self.stage.merge(changeset.into());
24762454
}
24772455

24782456
/// Unlock the wallet output of the specified `outpoint`.
2457+
///
2458+
/// **You must persist the staged changes.**
24792459
pub fn unlock_outpoint(&mut self, outpoint: OutPoint) {
2480-
use alloc::collections::btree_map;
2460+
use crate::collections::btree_map;
24812461
let lock_value = false;
2462+
let mut changeset = locked_outpoints::ChangeSet::default();
24822463

24832464
match self.locked_outpoints.entry(outpoint) {
24842465
btree_map::Entry::Vacant(..) => {}
@@ -2488,13 +2469,8 @@ impl Wallet {
24882469
let utxo = e.get_mut();
24892470
if utxo.is_locked {
24902471
utxo.is_locked = lock_value;
2491-
self.stage.merge(ChangeSet {
2492-
locked_outpoints: locked_outpoints::ChangeSet {
2493-
locked_outpoints: [(outpoint, lock_value)].into(),
2494-
..Default::default()
2495-
},
2496-
..Default::default()
2497-
});
2472+
changeset.locked_outpoints.insert(outpoint, lock_value);
2473+
self.stage.merge(changeset.into());
24982474
}
24992475
}
25002476
}
@@ -2703,22 +2679,18 @@ impl AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationBlockTime>> for Wallet {
27032679
}
27042680
}
27052681

2706-
/// Records the lock status of a wallet UTXO (outpoint).
2682+
/// Records the lock status of a wallet UTXO.
27072683
///
2708-
/// Only 1 [`UtxoLock`] may be applied to a particular outpoint at a time. Refer to the docs
2684+
/// Only a single [`UtxoLock`] may be assigned to a particular outpoint at a time. Refer to the docs
27092685
/// for [`Wallet::lock_outpoint`]. Note that the lock status is user-defined and does not take
2710-
/// into account any timelocks directly encoded by the descriptor.
2686+
/// into account any timelocks encoded by the descriptor.
27112687
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27122688
#[non_exhaustive]
27132689
pub struct UtxoLock {
27142690
/// Outpoint.
27152691
pub outpoint: OutPoint,
2716-
/// Whether the outpoint is currently locked.
2692+
/// Whether the outpoint is locked.
27172693
pub is_locked: bool,
2718-
/// Height at which the UTXO lock expires. The outpoint may be selected when the
2719-
/// wallet chain tip is at or above this height. If `None`, the lock remains in
2720-
/// effect unless explicitly unlocked.
2721-
pub expiration_height: Option<u32>,
27222694
}
27232695

27242696
/// Deterministically generate a unique name given the descriptors defining the wallet

wallet/tests/wallet.rs

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ fn test_lock_outpoint_persist() -> anyhow::Result<()> {
7474
let unspent = wallet.list_unspent().collect::<Vec<_>>();
7575
assert!(!unspent.is_empty());
7676
for utxo in unspent {
77-
wallet.lock_outpoint(utxo.outpoint, None);
77+
wallet.lock_outpoint(utxo.outpoint);
7878
assert!(
7979
wallet.is_outpoint_locked(utxo.outpoint),
8080
"Expect outpoint is locked"
@@ -133,55 +133,6 @@ fn test_lock_outpoint_persist() -> anyhow::Result<()> {
133133
assert!(!wallet.locked_outpoints().values().any(|u| u.is_locked));
134134
assert!(wallet.list_locked_unspent().next().is_none());
135135
wallet.persist(&mut conn)?;
136-
137-
// Test: Update lock expiry
138-
let outpoint = unspent.first().unwrap().outpoint;
139-
let mut expiry: u32 = 100;
140-
wallet.lock_outpoint(outpoint, Some(expiry));
141-
let changeset = wallet.staged().unwrap();
142-
assert_eq!(
143-
changeset
144-
.locked_outpoints
145-
.expiration_heights
146-
.get(&outpoint)
147-
.cloned()
148-
.unwrap(),
149-
Some(expiry)
150-
);
151-
152-
expiry *= 2;
153-
wallet.lock_outpoint(outpoint, Some(expiry));
154-
let changeset = wallet.staged().unwrap();
155-
assert_eq!(
156-
changeset
157-
.locked_outpoints
158-
.expiration_heights
159-
.get(&outpoint)
160-
.cloned()
161-
.unwrap(),
162-
Some(expiry)
163-
);
164-
wallet.persist(&mut conn)?;
165-
166-
// Now advance the local chain
167-
let block_199 = BlockId {
168-
height: expiry - 1,
169-
hash: BlockHash::all_zeros(),
170-
};
171-
insert_checkpoint(&mut wallet, block_199);
172-
assert!(
173-
wallet.is_outpoint_locked(outpoint),
174-
"outpoint should be locked before expiration height"
175-
);
176-
let block_200 = BlockId {
177-
height: expiry,
178-
hash: BlockHash::all_zeros(),
179-
};
180-
insert_checkpoint(&mut wallet, block_200);
181-
assert!(
182-
!wallet.is_outpoint_locked(outpoint),
183-
"outpoint should unlock at expiration height"
184-
);
185136
}
186137

187138
Ok(())

0 commit comments

Comments
 (0)