Skip to content

Commit 95e1d8b

Browse files
Index UTXOs by address with creation height
Store UTXO creation height in LMDB so lite wallets can request delta updates. Add to serve those wallet queries directly and speed up existing (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 95e1d8b

7 files changed

Lines changed: 286 additions & 121 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: 3 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,10 @@ 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(&rotxn, addresses, height_threshold)?;
570571
Ok(utxos)
571572
}
572573

lib/state/block.rs

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ 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 _,
11-
GetBitcoinValue as _, Hash, Header, InPoint, OutPoint, OutPointKey,
12-
OutputContent, SpentOutput, TxData, Verify as _,
9+
AddressOutPointKey, AmountOverflowError, Authorization, BitAssetId, BlockHash, Body,
10+
FilledOutput, FilledOutputContent, GetAddress as _, GetBitcoinValue as _, Hash, Header,
11+
InPoint, OutPoint, OutPointKey, OutputContent, SpentOutput, TxData, Verify as _
1312
},
1413
};
1514

@@ -207,8 +206,6 @@ pub fn connect_prevalidated(
207206
.sum::<usize>()
208207
+ body.coinbase.len();
209208

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);
212209
let mut stxo_puts: Vec<(OutPointKey, SpentOutput)> =
213210
Vec::with_capacity(total_inputs);
214211
let mut utxo_puts: Vec<(OutPointKey, FilledOutput)> =
@@ -263,7 +260,6 @@ pub fn connect_prevalidated(
263260
vin: vin as u32,
264261
},
265262
};
266-
utxo_deletes.push(key);
267263
stxo_puts.push((key, spent_output));
268264
}
269265

@@ -358,21 +354,20 @@ pub fn connect_prevalidated(
358354
}
359355

360356
// Sort all vectors in parallel for optimal cursor access
361-
utxo_deletes.par_sort_unstable();
362357
stxo_puts.par_sort_unstable_by_key(|(key, _)| *key);
363358
utxo_puts.par_sort_unstable_by_key(|(key, _)| *key);
364359

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)?;
360+
for (key, output) in stxo_puts {
361+
if !state.spend_utxo(rwtxn, key, output)? {
362+
return Err(error::NoUtxo {
363+
outpoint: key.to_outpoint(),
364+
}
365+
.into());
366+
}
372367
}
373368

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

378373
// Update tip and height using precomputed values
@@ -433,7 +428,7 @@ pub fn connect(
433428
content: filled_content,
434429
memo: output.memo.clone(),
435430
};
436-
state.utxos.put(rwtxn, &outpoint_key, &filled_output)?;
431+
state.put_utxo(rwtxn, outpoint_key, filled_output, height)?;
437432
}
438433
for transaction in &body.transactions {
439434
let filled_tx = state.fill_transaction(rwtxn, transaction)?;
@@ -451,20 +446,24 @@ pub fn connect(
451446
vin: vin as u32,
452447
},
453448
};
454-
state.utxos.delete(rwtxn, &input_key)?;
455-
state.stxos.put(rwtxn, &input_key, &spent_output)?;
449+
if !state.spend_utxo(rwtxn, input_key, spent_output)? {
450+
return Err(error::NoUtxo {
451+
outpoint: *input,
452+
}
453+
.into());
454+
}
456455
}
457456
let Some(filled_outputs) = filled_tx.filled_outputs() else {
458457
let err = error::FillTxOutputContents(Box::new(filled_tx));
459458
return Err(err.into());
460459
};
461-
for (vout, filled_output) in filled_outputs.iter().enumerate() {
460+
for (vout, filled_output) in filled_outputs.into_iter().enumerate() {
462461
let outpoint = OutPoint::Regular {
463462
txid,
464463
vout: vout as u32,
465464
};
466465
let outpoint_key = OutPointKey::from_outpoint(&outpoint);
467-
state.utxos.put(rwtxn, &outpoint_key, filled_output)?;
466+
state.put_utxo(rwtxn, outpoint_key, filled_output, height)?;
468467
}
469468
match &transaction.data {
470469
None => (),
@@ -646,13 +645,14 @@ pub fn disconnect_tip(
646645
}
647646
// delete UTXOs, last-to-first
648647
tx.outputs.iter().enumerate().rev().try_for_each(
649-
|(vout, _output)| {
648+
|(vout, output)| {
650649
let outpoint = OutPoint::Regular {
651650
txid,
652651
vout: vout as u32,
653652
};
654653
let outpoint_key = OutPointKey::from_outpoint(&outpoint);
655-
if state.utxos.delete(rwtxn, &outpoint_key)? {
654+
let address_key = AddressOutPointKey::new(output.address, outpoint_key);
655+
if state.delete_utxo(rwtxn, &address_key)? {
656656
Ok::<_, Error>(())
657657
} else {
658658
Err(error::NoUtxo { outpoint }.into())
@@ -662,13 +662,7 @@ pub fn disconnect_tip(
662662
// unspend STXOs, last-to-first
663663
tx.inputs.iter().rev().try_for_each(|outpoint| {
664664
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)?;
665+
if state.unspend_utxo(rwtxn, &outpoint_key)? {
672666
Ok(())
673667
} else {
674668
Err(Error::NoStxo {
@@ -679,13 +673,14 @@ pub fn disconnect_tip(
679673
})?;
680674
// delete coinbase UTXOs, last-to-first
681675
body.coinbase.iter().enumerate().rev().try_for_each(
682-
|(vout, _output)| {
676+
|(vout, output)| {
683677
let outpoint = OutPoint::Coinbase {
684678
merkle_root: header.merkle_root,
685679
vout: vout as u32,
686680
};
687681
let outpoint_key = OutPointKey::from_outpoint(&outpoint);
688-
if state.utxos.delete(rwtxn, &outpoint_key)? {
682+
let address_key = AddressOutPointKey::new(output.address, outpoint_key);
683+
if state.delete_utxo(rwtxn, &address_key)? {
689684
Ok::<_, Error>(())
690685
} else {
691686
Err(error::NoUtxo { outpoint }.into())

lib/state/mod.rs

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use crate::{
1313
Address, AmountOverflowError, Authorized, AuthorizedTransaction,
1414
BitAssetId, BlockHash, Body, FilledOutput, FilledTransaction,
1515
GetAddress as _, GetBitcoinValue as _, Header, InPoint, M6id, OutPoint,
16-
OutPointKey, SpentOutput, Transaction, TxData, VERSION, Verify as _,
17-
Version, WithdrawalBundle, WithdrawalBundleStatus,
16+
OutPointKey, AddressOutPointKey, SpentOutput, Transaction, TxData, VERSION,
17+
Verify as _, Version, WithdrawalBundle, WithdrawalBundleStatus,
1818
proto::mainchain::TwoWayPegData,
1919
},
2020
util::Watchable,
@@ -68,6 +68,18 @@ type WithdrawalBundlesDb = DatabaseUnique<
6868
)>,
6969
>;
7070

71+
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
72+
pub struct UtxoEntry {
73+
pub created_height: u32,
74+
pub output: FilledOutput,
75+
}
76+
77+
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
78+
pub struct SpentUtxoEntry {
79+
pub created_height: u32,
80+
pub output: SpentOutput,
81+
}
82+
7183
#[derive(Clone)]
7284
pub struct State {
7385
/// Current tip
@@ -80,7 +92,9 @@ pub struct State {
8092
/// Associates Dutch auction sequence numbers with auction state
8193
dutch_auctions: dutch_auction::Db,
8294
utxos: DatabaseUnique<OutPointKey, SerdeBincode<FilledOutput>>,
83-
stxos: DatabaseUnique<OutPointKey, SerdeBincode<SpentOutput>>,
95+
utxos_by_address:
96+
DatabaseUnique<AddressOutPointKey, SerdeBincode<UtxoEntry>>,
97+
stxos: DatabaseUnique<OutPointKey, SerdeBincode<SpentUtxoEntry>>,
8498
/// Pending withdrawal bundle. MUST exist in withdrawal_bundles
8599
pending_withdrawal_bundle: DatabaseUnique<UnitKey, SerdeBincode<M6id>>,
86100
/// Latest failed (known) withdrawal bundle
@@ -104,7 +118,7 @@ pub struct State {
104118
}
105119

106120
impl State {
107-
pub const NUM_DBS: u32 = bitassets::Dbs::NUM_DBS + 12;
121+
pub const NUM_DBS: u32 = bitassets::Dbs::NUM_DBS + 12 + 1;
108122

109123
pub fn new(env: &sneed::Env) -> Result<Self, Error> {
110124
let mut rwtxn = env.write_txn()?;
@@ -115,6 +129,8 @@ impl State {
115129
let dutch_auctions =
116130
DatabaseUnique::create(env, &mut rwtxn, "dutch_auctions")?;
117131
let utxos = DatabaseUnique::create(env, &mut rwtxn, "utxos")?;
132+
let utxos_by_address =
133+
DatabaseUnique::create(env, &mut rwtxn, "utxos_by_address")?;
118134
let stxos = DatabaseUnique::create(env, &mut rwtxn, "stxos")?;
119135
let pending_withdrawal_bundle = DatabaseUnique::create(
120136
env,
@@ -147,6 +163,7 @@ impl State {
147163
bitassets,
148164
dutch_auctions,
149165
utxos,
166+
utxos_by_address,
150167
stxos,
151168
pending_withdrawal_bundle,
152169
latest_failed_withdrawal_bundle,
@@ -180,10 +197,74 @@ impl State {
180197

181198
pub fn stxos(
182199
&self,
183-
) -> &RoDatabaseUnique<OutPointKey, SerdeBincode<SpentOutput>> {
200+
) -> &RoDatabaseUnique<OutPointKey, SerdeBincode<SpentUtxoEntry>> {
184201
&self.stxos
185202
}
186203

204+
fn put_utxo(
205+
&self,
206+
rwtxn: &mut RwTxn,
207+
outpoint_key: OutPointKey,
208+
output: FilledOutput,
209+
created_height: u32,
210+
) -> Result<AddressOutPointKey, sneed::db::Error> {
211+
let entry = UtxoEntry { created_height, output };
212+
let address_key = AddressOutPointKey::new(entry.output.address, outpoint_key);
213+
self.utxos_by_address.put(rwtxn, &address_key, &entry)?;
214+
self.utxos.put(rwtxn, &outpoint_key, &entry.output)?;
215+
Ok(address_key)
216+
}
217+
218+
fn delete_utxo(
219+
&self,
220+
rwtxn: &mut RwTxn,
221+
address_key: &AddressOutPointKey
222+
) -> Result<bool, sneed::db::Error> {
223+
self.utxos_by_address.delete(rwtxn, &address_key)?;
224+
Ok(self.utxos.delete(rwtxn, &address_key.outpoint_key())?)
225+
}
226+
227+
fn spend_utxo(
228+
&self,
229+
rwtxn: &mut RwTxn,
230+
outpoint_key: OutPointKey,
231+
spent_output: SpentOutput,
232+
) -> Result<bool, sneed::db::Error> {
233+
let address_key =
234+
AddressOutPointKey::new(spent_output.output.address, outpoint_key);
235+
236+
let Some(entry) = self.utxos_by_address.try_get(rwtxn, &address_key)? else {
237+
return Ok(false);
238+
};
239+
240+
let entry = SpentUtxoEntry {
241+
created_height: entry.created_height,
242+
output: spent_output,
243+
};
244+
245+
self.stxos.put(rwtxn, &outpoint_key, &entry)?;
246+
self.delete_utxo(rwtxn, &address_key)
247+
}
248+
249+
fn unspend_utxo(
250+
&self,
251+
rwtxn: &mut RwTxn,
252+
outpoint_key: &OutPointKey,
253+
) -> Result<bool, sneed::db::Error> {
254+
let Some(entry) = self.stxos.try_get(rwtxn, outpoint_key)? else {
255+
return Ok(false);
256+
};
257+
258+
self.put_utxo(
259+
rwtxn,
260+
*outpoint_key,
261+
entry.output.output,
262+
entry.created_height,
263+
)?;
264+
265+
Ok(self.stxos.delete(rwtxn, outpoint_key)?)
266+
}
267+
187268
pub fn withdrawal_bundle_event_blocks(
188269
&self,
189270
) -> &RoDatabaseUnique<
@@ -222,13 +303,23 @@ impl State {
222303
&self,
223304
rotxn: &RoTxn,
224305
addresses: &HashSet<Address>,
306+
height_threshold: u32,
225307
) -> Result<HashMap<OutPoint, FilledOutput>, Error> {
226-
let utxos: HashMap<OutPoint, FilledOutput> = self
227-
.utxos
228-
.iter(rotxn)?
229-
.filter(|(_, output)| Ok(addresses.contains(&output.address)))
230-
.map(|(key, output)| Ok((key.to_outpoint(), output)))
231-
.collect()?;
308+
let mut utxos = HashMap::new();
309+
for address in addresses {
310+
let start = AddressOutPointKey::start(*address);
311+
let end = AddressOutPointKey::end(*address);
312+
let mut iter = self
313+
.utxos_by_address
314+
.range(rotxn, &(start..=end))
315+
.map_err(sneed::db::Error::from)?;
316+
while let Some((key, entry)) = iter.next()? {
317+
if entry.created_height >= height_threshold {
318+
let outpoint = key.outpoint_key().to_outpoint();
319+
utxos.insert(outpoint, entry.output);
320+
}
321+
}
322+
}
232323
Ok(utxos)
233324
}
234325

@@ -300,13 +391,13 @@ impl State {
300391
.try_get(rotxn, &key)?
301392
.ok_or(Error::NoStxo { outpoint: *input })?;
302393
assert_eq!(
303-
stxo.inpoint,
394+
stxo.output.inpoint,
304395
InPoint::Regular {
305396
txid,
306397
vin: vin as u32
307398
}
308399
);
309-
spent_utxos.push(stxo.output);
400+
spent_utxos.push(stxo.output.output);
310401
}
311402
spent_utxos.reverse();
312403
Ok(FilledTransaction {
@@ -656,7 +747,8 @@ impl State {
656747
let mut total_deposit_stxo_value = bitcoin::Amount::ZERO;
657748
let mut total_withdrawal_stxo_value = bitcoin::Amount::ZERO;
658749
self.stxos.iter(rotxn)?.map_err(Error::from).for_each(
659-
|(outpoint_key, spent_output)| {
750+
|(outpoint_key, spent_entry)| {
751+
let spent_output = spent_entry.output;
660752
let outpoint = outpoint_key.to_outpoint();
661753
if let OutPoint::Deposit(_) = outpoint {
662754
total_deposit_stxo_value = total_deposit_stxo_value

0 commit comments

Comments
 (0)