Skip to content

Commit d39f4a4

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 adf7d0c commit d39f4a4

File tree

13 files changed

+220
-73
lines changed

13 files changed

+220
-73
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
- Add new method on the `TxBuilder` to embed data in the transaction via `OP_RETURN`. To allow that a fix to check the dust only on spendable output has been introduced.
3535
- Update the `Database` trait to store the last sync timestamp and block height
3636
- Rename `ConfirmationTime` to `BlockTime`
37+
- 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`.
3738

3839
## [v0.13.0] - [v0.12.0]
3940

src/blockchain/compact_filters/mod.rs

+21-3
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

+40-10
Original file line numberDiff line numberDiff line change
@@ -213,19 +213,44 @@ 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
219220
.list_transactions(None, Some(1_000), None, Some(true))?;
220221
let mut list_txs_ids = HashSet::new();
221222

222223
for tx_result in list_txs.iter().filter(|t| {
223-
// list_txs returns all conflicting tx we want to
224+
// list_txs returns all conflicting txs, we want to
224225
// filter out replaced tx => unconfirmed and not in the mempool
225226
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
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(txout) = db.get_previous_output(&input.previous_output)? {
240+
if let Some(keychain) =
241+
db.get_path_from_script_pubkey(&txout.script_pubkey)?
242+
{
243+
spent_in_mempool.push(LocalUtxo {
244+
outpoint: input.previous_output,
245+
keychain: keychain.0,
246+
txout,
247+
is_spent_unconfirmed: true,
248+
});
249+
}
250+
}
251+
}
252+
}
253+
229254
if let Some(mut known_tx) = known_txs.get_mut(&txid) {
230255
let confirmation_time =
231256
BlockTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
@@ -239,9 +264,6 @@ impl Blockchain for RpcBlockchain {
239264
db.set_tx(known_tx)?;
240265
}
241266
} 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)?;
245267
let mut received = 0u64;
246268
let mut sent = 0u64;
247269
for output in tx.output.iter() {
@@ -302,19 +324,27 @@ impl Blockchain for RpcBlockchain {
302324
value: u.amount.as_sat(),
303325
script_pubkey: u.script_pub_key,
304326
},
327+
// Here we just claim that this utxo is not spent in mempool; if
328+
// instead it is, the `spent_in_mempool` loop below will
329+
// take care of replacing it
330+
is_spent_unconfirmed: false,
305331
})
306332
})
307333
.collect::<Result<_, Error>>()?;
308334

309335
let spent: HashSet<_> = known_utxos.difference(&current_utxos).collect();
310-
for s in spent {
311-
debug!("removing utxo: {:?}", s);
312-
db.del_utxo(&s.outpoint)?;
336+
for utxo in spent {
337+
debug!("removing utxo: {:?}", utxo);
338+
db.del_utxo(&utxo.outpoint)?;
313339
}
314340
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
315-
for s in received {
316-
debug!("adding utxo: {:?}", s);
317-
db.set_utxo(s)?;
341+
for utxo in received {
342+
debug!("adding utxo: {:?}", utxo);
343+
db.set_utxo(utxo)?;
344+
}
345+
for utxo in spent_in_mempool {
346+
debug!("adding utxo: {:?}", utxo);
347+
db.set_utxo(&utxo)?;
318348
}
319349

320350
for (keykind, index) in indexes {

src/blockchain/script_sync.rs

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

335-
// Set every tx we observed
335+
let mut spent_utxos = HashSet::new();
336+
let mut spent_unconfirmed_utxos = HashSet::new();
337+
338+
// track all the spent and spent_unconfirmed utxos
336339
for finished_tx in &finished_txs {
337340
let tx = finished_tx
338341
.transaction
339342
.as_ref()
340343
.expect("transaction will always be present here");
341-
for (i, output) in tx.output.iter().enumerate() {
342-
if let Some((keychain, _)) =
343-
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
344-
{
345-
// add utxos we own from the new transactions we've seen.
346-
batch.set_utxo(&LocalUtxo {
347-
outpoint: OutPoint {
348-
txid: finished_tx.txid,
349-
vout: i as u32,
350-
},
351-
txout: output.clone(),
352-
keychain,
353-
})?;
344+
for input in &tx.input {
345+
if finished_tx.confirmation_time.is_some() {
346+
spent_utxos.insert(&input.previous_output);
347+
} else {
348+
spent_unconfirmed_utxos.insert(&input.previous_output);
354349
}
355350
}
356-
batch.set_tx(finished_tx)?;
357351
}
358352

359-
// we don't do this in the loop above since we may want to delete some of the utxos we
360-
// just added in case there are new tranasactions that spend form each other.
353+
// set every utxo we observed, unless it's already spent
354+
// we don't do this in the loop above as we want to know all the spent outputs before
355+
// adding the non-spent to the batch in case there are new tranasactions
356+
// that spend form each other.
361357
for finished_tx in &finished_txs {
362358
let tx = finished_tx
363359
.transaction
364360
.as_ref()
365361
.expect("transaction will always be present here");
366-
for input in &tx.input {
367-
// Delete any spent utxos
368-
batch.del_utxo(&input.previous_output)?;
362+
for (i, output) in tx.output.iter().enumerate() {
363+
if let Some((keychain, _)) =
364+
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
365+
{
366+
// add utxos we own from the new transactions we've seen.
367+
let outpoint = OutPoint {
368+
txid: finished_tx.txid,
369+
vout: i as u32,
370+
};
371+
372+
// If it's not spent, we add it to the batch
373+
if spent_utxos.get(&outpoint).is_none() {
374+
batch.set_utxo(&LocalUtxo {
375+
outpoint,
376+
txout: output.clone(),
377+
keychain,
378+
// Is this UTXO spent in a tx still in mempool?
379+
is_spent_unconfirmed: spent_unconfirmed_utxos.contains(&outpoint),
380+
})?;
381+
}
382+
}
369383
}
384+
385+
batch.set_tx(finished_tx)?;
386+
}
387+
388+
// delete all the spent utxos
389+
for spent_utxo in spent_utxos {
390+
batch.del_utxo(spent_utxo)?;
370391
}
371392

372393
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
}
@@ -526,6 +531,7 @@ macro_rules! populate_test_db {
526531
vout: vout as u32,
527532
},
528533
keychain: $crate::KeychainKind::External,
534+
is_spent_unconfirmed: false,
529535
})
530536
.unwrap();
531537
}

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();

0 commit comments

Comments
 (0)