Skip to content

Commit 754889c

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 754889c

File tree

13 files changed

+210
-67
lines changed

13 files changed

+210
-67
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Fixed esplora fee estimation.
1414
- Update the `Database` trait to store the last sync timestamp and block height
1515
- Rename `ConfirmationTime` to `BlockTime`
16+
- Add `is_spent_unconfirmed` field in `LocalUtxo`, which marks an output that is currently being used in an unconfirmed transaction. This UTXO won't be selected in an automatic coin selection and its value won't be added to the balance, but it can be added manually to a transaction using `add_utxo`.
1617

1718
## [v0.13.0] - [v0.12.0]
1819

src/blockchain/compact_filters/mod.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,28 @@ impl CompactFiltersBlockchain {
163163
if let Some(previous_output) = database.get_previous_output(&input.previous_output)? {
164164
inputs_sum += previous_output.value;
165165

166-
if database.is_mine(&previous_output.script_pubkey)? {
166+
// this output is ours, we have a path to derive it
167+
if let Some((keychain, _)) =
168+
database.get_path_from_script_pubkey(&previous_output.script_pubkey)?
169+
{
167170
outgoing += previous_output.value;
168171

169-
debug!("{} input #{} is mine, removing from utxo", tx.txid(), i);
170-
updates.del_utxo(&input.previous_output)?;
172+
if height.is_none() {
173+
debug!(
174+
"{} input #{} is mine, setting utxo as spent_unconfirmed",
175+
tx.txid(),
176+
i
177+
);
178+
updates.set_utxo(&LocalUtxo {
179+
outpoint: input.previous_output,
180+
txout: previous_output.clone(),
181+
keychain,
182+
is_spent_unconfirmed: true,
183+
})?;
184+
} else {
185+
debug!("{} input #{} is mine, removing from utxo", tx.txid(), i);
186+
updates.del_utxo(&input.previous_output)?;
187+
}
171188
}
172189
}
173190
}
@@ -185,6 +202,7 @@ impl CompactFiltersBlockchain {
185202
outpoint: OutPoint::new(tx.txid(), i as u32),
186203
txout: output.clone(),
187204
keychain,
205+
is_spent_unconfirmed: false,
188206
})?;
189207
incoming += output.value;
190208

src/blockchain/rpc.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ impl Blockchain for RpcBlockchain {
213213
.list_unspent(Some(0), None, None, Some(true), None)?;
214214
debug!("current_utxo len {}", current_utxo.len());
215215

216+
let mut spent_in_mempool = Vec::new();
216217
//TODO supported up to 1_000 txs, should use since_blocks or do paging
217218
let list_txs = self
218219
.client
@@ -226,6 +227,28 @@ impl Blockchain for RpcBlockchain {
226227
}) {
227228
let txid = tx_result.info.txid;
228229
list_txs_ids.insert(txid);
230+
231+
let tx_result = self.client.get_transaction(&txid, Some(true))?;
232+
let tx: Transaction = deserialize(&tx_result.hex)?;
233+
234+
// If it's unconfirmed, we may want to add its inputs to the currently spent in mempool
235+
if tx_result.info.blockhash.is_none() {
236+
for input in tx.input.iter() {
237+
// This input is mine if I have both the previous tx and the script_pubkey in
238+
// the db
239+
if let Some(o) = db.get_previous_output(&input.previous_output)? {
240+
if let Some(keychain) = db.get_path_from_script_pubkey(&o.script_pubkey)? {
241+
spent_in_mempool.push(LocalUtxo {
242+
outpoint: input.previous_output,
243+
keychain: keychain.0,
244+
txout: o,
245+
is_spent_unconfirmed: true,
246+
});
247+
}
248+
}
249+
}
250+
}
251+
229252
if let Some(mut known_tx) = known_txs.get_mut(&txid) {
230253
let confirmation_time =
231254
BlockTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
@@ -239,9 +262,6 @@ impl Blockchain for RpcBlockchain {
239262
db.set_tx(known_tx)?;
240263
}
241264
} else {
242-
//TODO check there is already the raw tx in db?
243-
let tx_result = self.client.get_transaction(&txid, Some(true))?;
244-
let tx: Transaction = deserialize(&tx_result.hex)?;
245265
let mut received = 0u64;
246266
let mut sent = 0u64;
247267
for output in tx.output.iter() {
@@ -301,6 +321,10 @@ impl Blockchain for RpcBlockchain {
301321
value: u.amount.as_sat(),
302322
script_pubkey: u.script_pub_key,
303323
},
324+
// Here we just say this utxo is not spent in mempool, if,
325+
// instead, it is, the `spent_in_mempool` loop below will
326+
// take care of replacing it
327+
is_spent_unconfirmed: false,
304328
})
305329
})
306330
.collect::<Result<_, Error>>()?;
@@ -315,6 +339,10 @@ impl Blockchain for RpcBlockchain {
315339
debug!("adding utxo: {:?}", s);
316340
db.set_utxo(s)?;
317341
}
342+
for s in spent_in_mempool {
343+
debug!("adding utxo: {:?}", s);
344+
db.set_utxo(&s)?;
345+
}
318346

319347
for (keykind, index) in indexes {
320348
debug!("{:?} max {}", keykind, index);

src/blockchain/script_sync.rs

Lines changed: 41 additions & 20 deletions
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

Lines changed: 13 additions & 1 deletion
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

Lines changed: 11 additions & 5 deletions
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

Lines changed: 1 addition & 0 deletions
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();

0 commit comments

Comments
 (0)