Skip to content

Commit 11cca43

Browse files
Allow using spent unconfirmed UTXOs in add_utxo
UTXOs will be deleted from the database only when the transaction spending them is confirmed, this way you can build a tx double-spending one in mempool using `add_utxo`. listunspent won't return spent in mempool utxos, effectively excluding them from the coin selection and balance calculation. Closes #414
1 parent d20b649 commit 11cca43

File tree

10 files changed

+153
-61
lines changed

10 files changed

+153
-61
lines changed

src/blockchain/script_sync.rs

+41-20
Original file line numberDiff line numberDiff line change
@@ -319,41 +319,62 @@ impl<'a, D: BatchDatabase> State<'a, D> {
319319
batch.del_tx(txid, true)?;
320320
}
321321

322-
// Set every tx we observed
322+
let mut spent_utxos = HashSet::new();
323+
let mut spent_unconfirmed_utxos = HashSet::new();
324+
325+
// track all the spent and spent_unconfirmed utxos
323326
for finished_tx in &finished_txs {
324327
let tx = finished_tx
325328
.transaction
326329
.as_ref()
327330
.expect("transaction will always be present here");
328-
for (i, output) in tx.output.iter().enumerate() {
329-
if let Some((keychain, _)) =
330-
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
331-
{
332-
// add utxos we own from the new transactions we've seen.
333-
batch.set_utxo(&LocalUtxo {
334-
outpoint: OutPoint {
335-
txid: finished_tx.txid,
336-
vout: i as u32,
337-
},
338-
txout: output.clone(),
339-
keychain,
340-
})?;
331+
for input in &tx.input {
332+
if finished_tx.confirmation_time.is_some() {
333+
spent_utxos.insert(&input.previous_output);
334+
} else {
335+
spent_unconfirmed_utxos.insert(&input.previous_output);
341336
}
342337
}
343-
batch.set_tx(finished_tx)?;
344338
}
345339

346-
// we don't do this in the loop above since we may want to delete some of the utxos we
347-
// just added in case there are new tranasactions that spend form each other.
340+
// set every utxo we observed, unless it's already spent
341+
// we don't do this in the loop above as we want to know all the spent outputs before
342+
// adding the non-spent to the batch in case there are new tranasactions
343+
// that spend form each other.
348344
for finished_tx in &finished_txs {
349345
let tx = finished_tx
350346
.transaction
351347
.as_ref()
352348
.expect("transaction will always be present here");
353-
for input in &tx.input {
354-
// Delete any spent utxos
355-
batch.del_utxo(&input.previous_output)?;
349+
for (i, output) in tx.output.iter().enumerate() {
350+
if let Some((keychain, _)) =
351+
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
352+
{
353+
// add utxos we own from the new transactions we've seen.
354+
let outpoint = OutPoint {
355+
txid: finished_tx.txid,
356+
vout: i as u32,
357+
};
358+
359+
// If it's not spent, we add it to the batch
360+
if spent_utxos.get(&outpoint).is_none() {
361+
batch.set_utxo(&LocalUtxo {
362+
outpoint,
363+
txout: output.clone(),
364+
keychain,
365+
// Is this UTXO spent in a tx still in mempool?
366+
is_spent_unconfirmed: spent_unconfirmed_utxos.contains(&outpoint),
367+
})?;
368+
}
369+
}
356370
}
371+
372+
batch.set_tx(finished_tx)?;
373+
}
374+
375+
// delete all the spent utxos
376+
for spent_utxo in spent_utxos {
377+
batch.del_utxo(spent_utxo)?;
357378
}
358379

359380
for (keychain, last_active_index) in self.last_active_index {

src/database/keyvalue.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ macro_rules! impl_batch_operations {
4343
let value = json!({
4444
"t": utxo.txout,
4545
"i": utxo.keychain,
46+
"s": utxo.is_spent_unconfirmed,
4647
});
4748
self.insert(key, serde_json::to_vec(&value)?)$($after_insert)*;
4849

@@ -125,8 +126,9 @@ macro_rules! impl_batch_operations {
125126
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
126127
let txout = serde_json::from_value(val["t"].take())?;
127128
let keychain = serde_json::from_value(val["i"].take())?;
129+
let is_spent_unconfirmed = val.get_mut("s").and_then(|s| s.take().as_bool()).unwrap_or(false);
128130

129-
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain }))
131+
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain, is_spent_unconfirmed, }))
130132
}
131133
}
132134
}
@@ -246,11 +248,16 @@ impl Database for Tree {
246248
let mut val: serde_json::Value = serde_json::from_slice(&v)?;
247249
let txout = serde_json::from_value(val["t"].take())?;
248250
let keychain = serde_json::from_value(val["i"].take())?;
251+
let is_spent_unconfirmed = val
252+
.get_mut("s")
253+
.and_then(|s| s.take().as_bool())
254+
.unwrap_or(false);
249255

250256
Ok(LocalUtxo {
251257
outpoint,
252258
txout,
253259
keychain,
260+
is_spent_unconfirmed,
254261
})
255262
})
256263
.collect()
@@ -314,11 +321,16 @@ impl Database for Tree {
314321
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
315322
let txout = serde_json::from_value(val["t"].take())?;
316323
let keychain = serde_json::from_value(val["i"].take())?;
324+
let is_spent_unconfirmed = val
325+
.get_mut("s")
326+
.and_then(|s| s.take().as_bool())
327+
.unwrap_or(false);
317328

318329
Ok(LocalUtxo {
319330
outpoint: *outpoint,
320331
txout,
321332
keychain,
333+
is_spent_unconfirmed,
322334
})
323335
})
324336
.transpose()

src/database/memory.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,10 @@ impl BatchOperations for MemoryDatabase {
150150

151151
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
152152
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
153-
self.map
154-
.insert(key, Box::new((utxo.txout.clone(), utxo.keychain)));
153+
self.map.insert(
154+
key,
155+
Box::new((utxo.txout.clone(), utxo.keychain, utxo.is_spent_unconfirmed)),
156+
);
155157

156158
Ok(())
157159
}
@@ -228,11 +230,12 @@ impl BatchOperations for MemoryDatabase {
228230
match res {
229231
None => Ok(None),
230232
Some(b) => {
231-
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
233+
let (txout, keychain, is_spent_unconfirmed) = b.downcast_ref().cloned().unwrap();
232234
Ok(Some(LocalUtxo {
233235
outpoint: *outpoint,
234236
txout,
235237
keychain,
238+
is_spent_unconfirmed,
236239
}))
237240
}
238241
}
@@ -326,11 +329,12 @@ impl Database for MemoryDatabase {
326329
.range::<Vec<u8>, _>((Included(&key), Excluded(&after(&key))))
327330
.map(|(k, v)| {
328331
let outpoint = deserialize(&k[1..]).unwrap();
329-
let (txout, keychain) = v.downcast_ref().cloned().unwrap();
332+
let (txout, keychain, is_spent_unconfirmed) = v.downcast_ref().cloned().unwrap();
330333
Ok(LocalUtxo {
331334
outpoint,
332335
txout,
333336
keychain,
337+
is_spent_unconfirmed,
334338
})
335339
})
336340
.collect()
@@ -389,11 +393,12 @@ impl Database for MemoryDatabase {
389393
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
390394
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
391395
Ok(self.map.get(&key).map(|b| {
392-
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
396+
let (txout, keychain, is_spent_unconfirmed) = b.downcast_ref().cloned().unwrap();
393397
LocalUtxo {
394398
outpoint: *outpoint,
395399
txout,
396400
keychain,
401+
is_spent_unconfirmed,
397402
}
398403
}))
399404
}
@@ -527,6 +532,7 @@ macro_rules! populate_test_db {
527532
vout: vout as u32,
528533
},
529534
keychain: $crate::KeychainKind::External,
535+
is_spent_unconfirmed: false,
530536
})
531537
.unwrap();
532538
}

src/database/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ pub mod test {
316316
txout,
317317
outpoint,
318318
keychain: KeychainKind::External,
319+
is_spent_unconfirmed: true,
319320
};
320321

321322
tree.set_utxo(&utxo).unwrap();

src/database/sqlite.rs

+28-34
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ static MIGRATIONS: &[&str] = &[
3535
"CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);",
3636
"CREATE TABLE checksums (keychain TEXT, checksum BLOB);",
3737
"CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
38-
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);"
38+
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);",
39+
"ALTER TABLE utxos ADD COLUMN is_spent_unconfirmed;",
3940
];
4041

4142
/// Sqlite database stored on filesystem
@@ -79,14 +80,16 @@ impl SqliteDatabase {
7980
vout: u32,
8081
txid: &[u8],
8182
script: &[u8],
83+
is_spent_unconfirmed: bool,
8284
) -> Result<i64, Error> {
83-
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script) VALUES (:value, :keychain, :vout, :txid, :script)")?;
85+
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script, is_spent_unconfirmed) VALUES (:value, :keychain, :vout, :txid, :script, :is_spent_unconfirmed)")?;
8486
statement.execute(named_params! {
8587
":value": value,
8688
":keychain": keychain,
8789
":vout": vout,
8890
":txid": txid,
89-
":script": script
91+
":script": script,
92+
":is_spent_unconfirmed": is_spent_unconfirmed,
9093
})?;
9194

9295
Ok(self.connection.last_insert_rowid())
@@ -287,9 +290,9 @@ impl SqliteDatabase {
287290
}
288291

289292
fn select_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
290-
let mut statement = self
291-
.connection
292-
.prepare_cached("SELECT value, keychain, vout, txid, script FROM utxos")?;
293+
let mut statement = self.connection.prepare_cached(
294+
"SELECT value, keychain, vout, txid, script, is_spent_unconfirmed FROM utxos",
295+
)?;
293296
let mut utxos: Vec<LocalUtxo> = vec![];
294297
let mut rows = statement.query([])?;
295298
while let Some(row) = rows.next()? {
@@ -298,6 +301,7 @@ impl SqliteDatabase {
298301
let vout = row.get(2)?;
299302
let txid: Vec<u8> = row.get(3)?;
300303
let script: Vec<u8> = row.get(4)?;
304+
let is_spent_unconfirmed: bool = row.get(5)?;
301305

302306
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
303307

@@ -308,19 +312,16 @@ impl SqliteDatabase {
308312
script_pubkey: script.into(),
309313
},
310314
keychain,
315+
is_spent_unconfirmed,
311316
})
312317
}
313318

314319
Ok(utxos)
315320
}
316321

317-
fn select_utxo_by_outpoint(
318-
&self,
319-
txid: &[u8],
320-
vout: u32,
321-
) -> Result<Option<(u64, KeychainKind, Script)>, Error> {
322+
fn select_utxo_by_outpoint(&self, txid: &[u8], vout: u32) -> Result<Option<LocalUtxo>, Error> {
322323
let mut statement = self.connection.prepare_cached(
323-
"SELECT value, keychain, script FROM utxos WHERE txid=:txid AND vout=:vout",
324+
"SELECT value, keychain, script, is_spent_unconfirmed FROM utxos WHERE txid=:txid AND vout=:vout",
324325
)?;
325326
let mut rows = statement.query(named_params! {":txid": txid,":vout": vout})?;
326327
match rows.next()? {
@@ -329,9 +330,18 @@ impl SqliteDatabase {
329330
let keychain: String = row.get(1)?;
330331
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
331332
let script: Vec<u8> = row.get(2)?;
332-
let script: Script = script.into();
333+
let script_pubkey: Script = script.into();
334+
let is_spent_unconfirmed: bool = row.get(3)?;
333335

334-
Ok(Some((value, keychain, script)))
336+
Ok(Some(LocalUtxo {
337+
outpoint: OutPoint::new(deserialize(&txid)?, vout),
338+
txout: TxOut {
339+
value,
340+
script_pubkey,
341+
},
342+
keychain,
343+
is_spent_unconfirmed,
344+
}))
335345
}
336346
None => Ok(None),
337347
}
@@ -624,6 +634,7 @@ impl BatchOperations for SqliteDatabase {
624634
utxo.outpoint.vout,
625635
&utxo.outpoint.txid,
626636
utxo.txout.script_pubkey.as_bytes(),
637+
utxo.is_spent_unconfirmed,
627638
)?;
628639
Ok(())
629640
}
@@ -698,16 +709,9 @@ impl BatchOperations for SqliteDatabase {
698709

699710
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
700711
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
701-
Some((value, keychain, script_pubkey)) => {
712+
Some(local_utxo) => {
702713
self.delete_utxo_by_outpoint(&outpoint.txid, outpoint.vout)?;
703-
Ok(Some(LocalUtxo {
704-
outpoint: *outpoint,
705-
txout: TxOut {
706-
value,
707-
script_pubkey,
708-
},
709-
keychain,
710-
}))
714+
Ok(Some(local_utxo))
711715
}
712716
None => Ok(None),
713717
}
@@ -836,17 +840,7 @@ impl Database for SqliteDatabase {
836840
}
837841

838842
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
839-
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
840-
Some((value, keychain, script_pubkey)) => Ok(Some(LocalUtxo {
841-
outpoint: *outpoint,
842-
txout: TxOut {
843-
value,
844-
script_pubkey,
845-
},
846-
keychain,
847-
})),
848-
None => Ok(None),
849-
}
843+
self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)
850844
}
851845

852846
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {

src/testutils/blockchain_tests.rs

+44
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,50 @@ macro_rules! bdk_blockchain_tests {
10731073
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
10741074
assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance");
10751075
}
1076+
1077+
#[test]
1078+
fn test_double_spend() {
1079+
// We create a tx and then we try to double spend it; bdk will allow
1080+
// us to do so while the initial tx is still in mempool, even if it's not
1081+
// RBF enabled
1082+
let (wallet, descriptors, mut test_client) = init_single_sig();
1083+
let node_addr = test_client.get_node_address(None);
1084+
let _ = test_client.receive(testutils! {
1085+
@tx ( (@external descriptors, 0) => 50_000 )
1086+
});
1087+
1088+
wallet.sync(noop_progress(), None).unwrap();
1089+
let mut builder = wallet.build_tx();
1090+
builder.add_recipient(node_addr.script_pubkey(), 25_000);
1091+
let (mut psbt, _details) = builder.finish().unwrap();
1092+
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
1093+
assert!(finalized, "Cannot finalize transaction");
1094+
let initial_tx = psbt.extract_tx();
1095+
let _sent_txid = wallet.broadcast(&initial_tx).unwrap();
1096+
wallet.sync(noop_progress(), None).unwrap();
1097+
// We can still create a transaction double spending `initial_tx`, as we keep
1098+
// the utxos spent in mempool in our database until confirmation
1099+
let mut builder = wallet.build_tx();
1100+
builder
1101+
.add_utxo(initial_tx.input[0].previous_output)
1102+
.expect("Can't manually add an UTXO spent in an unconfirmed tx");
1103+
test_client.generate(1, Some(node_addr));
1104+
wallet.sync(noop_progress(), None).unwrap();
1105+
// Now that the transaction is confirmed, we can't create one double spending it
1106+
let mut builder = wallet.build_tx();
1107+
assert!(builder
1108+
.add_utxo(initial_tx.input[0].previous_output)
1109+
.is_err(), "The UTXO should be unknown");
1110+
// Now we invalidate the last block, making `initial_tx` unconfirmed
1111+
test_client.invalidate(1);
1112+
// `initial_tx` is in mempool, we have the info for creating a tx
1113+
// double-spending it
1114+
wallet.sync(noop_progress(), None).unwrap();
1115+
let mut builder = wallet.build_tx();
1116+
builder
1117+
.add_utxo(initial_tx.input[0].previous_output)
1118+
.expect("Can't manually add an UTXO spent in an unconfirmed tx");
1119+
}
10761120
}
10771121
};
10781122

0 commit comments

Comments
 (0)