Skip to content

Commit 9b95f45

Browse files
Index UTXOs by address with creation height
Store UTXO creation height in LMDB so lite wallets can request delta updates. Add utxos_by_address to serve those wallet queries directly and speed up existing fn get_utxos_by_addresses (now using LMDB range queries). Route UTXO put/delete/spend/unspend paths through helpers that keep the primary outpoint index, address index, and STXO table in sync.
1 parent e0fef50 commit 9b95f45

7 files changed

Lines changed: 311 additions & 138 deletions

File tree

app/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fn update_wallet(node: &Node, wallet: &Wallet) -> Result<(), Error> {
6161
let addresses = wallet.get_addresses()?;
6262
let unconfirmed_utxos =
6363
node.get_unconfirmed_utxos_by_addresses(&addresses)?;
64-
let utxos = node.get_utxos_by_addresses(&addresses)?;
64+
let utxos = node.get_utxos_by_addresses(&addresses, 0)?;
6565
let confirmed_outpoints: Vec<_> = wallet.get_utxos()?.into_keys().collect();
6666
let confirmed_spent = node
6767
.get_spent_utxos(&confirmed_outpoints)?

lib/node/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ where
519519
.try_get(&rotxn, &outpoint_key)
520520
.map_err(state::Error::from)?
521521
{
522-
spent.push((*outpoint, output));
522+
spent.push((*outpoint, output.output));
523523
}
524524
}
525525
Ok(spent)
@@ -564,9 +564,14 @@ where
564564
pub fn get_utxos_by_addresses(
565565
&self,
566566
addresses: &HashSet<Address>,
567+
height_threshold: u32,
567568
) -> Result<HashMap<OutPoint, FilledOutput>, Error> {
568569
let rotxn = self.env.read_txn()?;
569-
let utxos = self.state.get_utxos_by_addresses(&rotxn, addresses)?;
570+
let utxos = self.state.get_utxos_by_addresses(
571+
&rotxn,
572+
addresses,
573+
height_threshold,
574+
)?;
570575
Ok(utxos)
571576
}
572577

lib/state/block.rs

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use sneed::{RoTxn, RwTxn};
66
use crate::{
77
state::{Error, PrevalidatedBlock, State, amm, dutch_auction, error},
88
types::{
9-
AmountOverflowError, Authorization, BitAssetId, BlockHash, Body,
10-
FilledOutput, FilledOutputContent, GetAddress as _,
9+
AddressOutPointKey, AmountOverflowError, Authorization, BitAssetId,
10+
BlockHash, Body, FilledOutput, FilledOutputContent, GetAddress as _,
1111
GetBitcoinValue as _, Hash, Header, InPoint, OutPoint, OutPointKey,
1212
OutputContent, SpentOutput, TxData, Verify as _,
1313
},
@@ -207,8 +207,6 @@ pub fn connect_prevalidated(
207207
.sum::<usize>()
208208
+ body.coinbase.len();
209209

210-
// Use Vec + sort_unstable instead of individual DB operations for better performance
211-
let mut utxo_deletes: Vec<OutPointKey> = Vec::with_capacity(total_inputs);
212210
let mut stxo_puts: Vec<(OutPointKey, SpentOutput)> =
213211
Vec::with_capacity(total_inputs);
214212
let mut utxo_puts: Vec<(OutPointKey, FilledOutput)> =
@@ -263,7 +261,6 @@ pub fn connect_prevalidated(
263261
vin: vin as u32,
264262
},
265263
};
266-
utxo_deletes.push(key);
267264
stxo_puts.push((key, spent_output));
268265
}
269266

@@ -358,21 +355,20 @@ pub fn connect_prevalidated(
358355
}
359356

360357
// Sort all vectors in parallel for optimal cursor access
361-
utxo_deletes.par_sort_unstable();
362358
stxo_puts.par_sort_unstable_by_key(|(key, _)| *key);
363359
utxo_puts.par_sort_unstable_by_key(|(key, _)| *key);
364360

365-
// Apply all database operations using pre-sorted keys for optimal B-tree access
366-
for key in &utxo_deletes {
367-
state.utxos.delete(rwtxn, key)?;
368-
}
369-
370-
for (key, spent_output) in &stxo_puts {
371-
state.stxos.put(rwtxn, key, spent_output)?;
361+
for (key, output) in stxo_puts {
362+
if !state.spend_utxo(rwtxn, key, output)? {
363+
return Err(error::NoUtxo {
364+
outpoint: key.to_outpoint(),
365+
}
366+
.into());
367+
}
372368
}
373369

374-
for (key, filled_output) in &utxo_puts {
375-
state.utxos.put(rwtxn, key, filled_output)?;
370+
for (key, filled_output) in utxo_puts {
371+
state.put_utxo(rwtxn, key, filled_output, prevalidated.next_height)?;
376372
}
377373

378374
// Update tip and height using precomputed values
@@ -433,7 +429,7 @@ pub fn connect(
433429
content: filled_content,
434430
memo: output.memo.clone(),
435431
};
436-
state.utxos.put(rwtxn, &outpoint_key, &filled_output)?;
432+
state.put_utxo(rwtxn, outpoint_key, filled_output, height)?;
437433
}
438434
for transaction in &body.transactions {
439435
let filled_tx = state.fill_transaction(rwtxn, transaction)?;
@@ -451,20 +447,21 @@ pub fn connect(
451447
vin: vin as u32,
452448
},
453449
};
454-
state.utxos.delete(rwtxn, &input_key)?;
455-
state.stxos.put(rwtxn, &input_key, &spent_output)?;
450+
if !state.spend_utxo(rwtxn, input_key, spent_output)? {
451+
return Err(error::NoUtxo { outpoint: *input }.into());
452+
}
456453
}
457454
let Some(filled_outputs) = filled_tx.filled_outputs() else {
458455
let err = error::FillTxOutputContents(Box::new(filled_tx));
459456
return Err(err.into());
460457
};
461-
for (vout, filled_output) in filled_outputs.iter().enumerate() {
458+
for (vout, filled_output) in filled_outputs.into_iter().enumerate() {
462459
let outpoint = OutPoint::Regular {
463460
txid,
464461
vout: vout as u32,
465462
};
466463
let outpoint_key = OutPointKey::from_outpoint(&outpoint);
467-
state.utxos.put(rwtxn, &outpoint_key, filled_output)?;
464+
state.put_utxo(rwtxn, outpoint_key, filled_output, height)?;
468465
}
469466
match &transaction.data {
470467
None => (),
@@ -646,13 +643,15 @@ pub fn disconnect_tip(
646643
}
647644
// delete UTXOs, last-to-first
648645
tx.outputs.iter().enumerate().rev().try_for_each(
649-
|(vout, _output)| {
646+
|(vout, output)| {
650647
let outpoint = OutPoint::Regular {
651648
txid,
652649
vout: vout as u32,
653650
};
654651
let outpoint_key = OutPointKey::from_outpoint(&outpoint);
655-
if state.utxos.delete(rwtxn, &outpoint_key)? {
652+
let address_key =
653+
AddressOutPointKey::new(output.address, outpoint_key);
654+
if state.delete_utxo(rwtxn, &address_key)? {
656655
Ok::<_, Error>(())
657656
} else {
658657
Err(error::NoUtxo { outpoint }.into())
@@ -662,13 +661,7 @@ pub fn disconnect_tip(
662661
// unspend STXOs, last-to-first
663662
tx.inputs.iter().rev().try_for_each(|outpoint| {
664663
let outpoint_key = OutPointKey::from_outpoint(outpoint);
665-
if let Some(spent_output) =
666-
state.stxos.try_get(rwtxn, &outpoint_key)?
667-
{
668-
state.stxos.delete(rwtxn, &outpoint_key)?;
669-
state
670-
.utxos
671-
.put(rwtxn, &outpoint_key, &spent_output.output)?;
664+
if state.unspend_utxo(rwtxn, &outpoint_key)? {
672665
Ok(())
673666
} else {
674667
Err(Error::NoStxo {
@@ -678,20 +671,24 @@ pub fn disconnect_tip(
678671
})
679672
})?;
680673
// delete coinbase UTXOs, last-to-first
681-
body.coinbase.iter().enumerate().rev().try_for_each(
682-
|(vout, _output)| {
674+
body.coinbase
675+
.iter()
676+
.enumerate()
677+
.rev()
678+
.try_for_each(|(vout, output)| {
683679
let outpoint = OutPoint::Coinbase {
684680
merkle_root: header.merkle_root,
685681
vout: vout as u32,
686682
};
687683
let outpoint_key = OutPointKey::from_outpoint(&outpoint);
688-
if state.utxos.delete(rwtxn, &outpoint_key)? {
684+
let address_key =
685+
AddressOutPointKey::new(output.address, outpoint_key);
686+
if state.delete_utxo(rwtxn, &address_key)? {
689687
Ok::<_, Error>(())
690688
} else {
691689
Err(error::NoUtxo { outpoint }.into())
692690
}
693-
},
694-
)?;
691+
})?;
695692
match (header.prev_side_hash, height) {
696693
(None, 0) => {
697694
state.tip.delete(rwtxn, &())?;

0 commit comments

Comments
 (0)