Skip to content

Commit 9a6db15

Browse files
committed
Merge bitcoindevkit#515: Never delete spent utxos from the database
f2f0efc Never delete spent utxos from the database (Daniela Brozzoni) Pull request description: <!-- You can erase any parts of this template not applicable to your Pull Request. --> ### Description A `is_spent` field is added to LocalUtxo; when a txo is spent we set this field to true instead of deleting the entire utxo from the database. This allows us to create txs double-spending txs already in blockchain. Listunspent won't return spent in mempool utxos, effectively excluding them from the coin selection and balance calculation Fixes bitcoindevkit#414 ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've updated `CHANGELOG.md` * [x] I'm linking the issue being fixed by this PR ACKs for top commit: afilini: Re-ACK f2f0efc Tree-SHA512: 7984b69c56b3b12746c2301072d637dea1346730aa132f1ea67c5bd8bf685bd557fc7cbdc2fc1185e63c71db07d36cd979514921b2c83c55737cd4b96102377e
2 parents 3e4678d + f2f0efc commit 9a6db15

File tree

13 files changed

+155
-66
lines changed

13 files changed

+155
-66
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- `verify` flag removed from `TransactionDetails`.
1111
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
1212
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
13+
- Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db.
1314

1415
### Sync API change
1516

src/blockchain/compact_filters/mod.rs

+12-3
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,19 @@ 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+
debug!("{} input #{} is mine, setting utxo as spent", tx.txid(), i);
173+
updates.set_utxo(&LocalUtxo {
174+
outpoint: input.previous_output,
175+
txout: previous_output.clone(),
176+
keychain,
177+
is_spent: true,
178+
})?;
171179
}
172180
}
173181
}
@@ -185,6 +193,7 @@ impl CompactFiltersBlockchain {
185193
outpoint: OutPoint::new(tx.txid(), i as u32),
186194
txout: output.clone(),
187195
keychain,
196+
is_spent: false,
188197
})?;
189198
incoming += output.value;
190199

src/blockchain/rpc.rs

+10-7
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ impl WalletSync for RpcBlockchain {
249249
let mut list_txs_ids = HashSet::new();
250250

251251
for tx_result in list_txs.iter().filter(|t| {
252-
// list_txs returns all conflicting tx we want to
252+
// list_txs returns all conflicting txs, we want to
253253
// filter out replaced tx => unconfirmed and not in the mempool
254254
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
255255
}) {
@@ -332,20 +332,23 @@ impl WalletSync for RpcBlockchain {
332332
value: u.amount.as_sat(),
333333
script_pubkey: u.script_pub_key,
334334
},
335+
is_spent: false,
335336
})),
336337
},
337338
)
338339
.collect::<Result<HashSet<_>, Error>>()?;
339340

340341
let spent: HashSet<_> = known_utxos.difference(&current_utxos).collect();
341-
for s in spent {
342-
debug!("removing utxo: {:?}", s);
343-
db.del_utxo(&s.outpoint)?;
342+
for utxo in spent {
343+
debug!("setting as spent utxo: {:?}", utxo);
344+
let mut spent_utxo = utxo.clone();
345+
spent_utxo.is_spent = true;
346+
db.set_utxo(&spent_utxo)?;
344347
}
345348
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
346-
for s in received {
347-
debug!("adding utxo: {:?}", s);
348-
db.set_utxo(s)?;
349+
for utxo in received {
350+
debug!("adding utxo: {:?}", utxo);
351+
db.set_utxo(utxo)?;
349352
}
350353

351354
for (keykind, index) in indexes {

src/blockchain/script_sync.rs

+26-18
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,23 @@ 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+
337+
// track all the spent utxos
338+
for finished_tx in &finished_txs {
339+
let tx = finished_tx
340+
.transaction
341+
.as_ref()
342+
.expect("transaction will always be present here");
343+
for input in &tx.input {
344+
spent_utxos.insert(&input.previous_output);
345+
}
346+
}
347+
348+
// set every utxo we observed, unless it's already spent
349+
// we don't do this in the loop above as we want to know all the spent outputs before
350+
// adding the non-spent to the batch in case there are new tranasactions
351+
// that spend form each other.
336352
for finished_tx in &finished_txs {
337353
let tx = finished_tx
338354
.transaction
@@ -343,30 +359,22 @@ impl<'a, D: BatchDatabase> State<'a, D> {
343359
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
344360
{
345361
// add utxos we own from the new transactions we've seen.
362+
let outpoint = OutPoint {
363+
txid: finished_tx.txid,
364+
vout: i as u32,
365+
};
366+
346367
batch.set_utxo(&LocalUtxo {
347-
outpoint: OutPoint {
348-
txid: finished_tx.txid,
349-
vout: i as u32,
350-
},
368+
outpoint,
351369
txout: output.clone(),
352370
keychain,
371+
// Is this UTXO in the spent_utxos set?
372+
is_spent: spent_utxos.get(&outpoint).is_some(),
353373
})?;
354374
}
355375
}
356-
batch.set_tx(finished_tx)?;
357-
}
358376

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.
361-
for finished_tx in &finished_txs {
362-
let tx = finished_tx
363-
.transaction
364-
.as_ref()
365-
.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)?;
369-
}
377+
batch.set_tx(finished_tx)?;
370378
}
371379

372380
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,
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 = 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, }))
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 = 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,
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 = 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,
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)),
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) = b.downcast_ref().cloned().unwrap();
232234
Ok(Some(LocalUtxo {
233235
outpoint: *outpoint,
234236
txout,
235237
keychain,
238+
is_spent,
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) = v.downcast_ref().cloned().unwrap();
330333
Ok(LocalUtxo {
331334
outpoint,
332335
txout,
333336
keychain,
337+
is_spent,
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) = b.downcast_ref().cloned().unwrap();
393397
LocalUtxo {
394398
outpoint: *outpoint,
395399
txout,
396400
keychain,
401+
is_spent,
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: 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: true,
319320
};
320321

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

0 commit comments

Comments
 (0)