use crate::types::{OutPoint, UTXO, UtxoSet};
#[cfg(feature = "production")]
use rustc_hash::{FxHashMap, FxHashSet};
#[cfg(not(feature = "production"))]
use std::collections::{HashMap as FxHashMap, HashSet as FxHashSet};
pub type UtxoDeletionKey = [u8; 36];
type OutPointKey = UtxoDeletionKey;
#[inline]
fn outpoint_to_key(op: &OutPoint) -> OutPointKey {
let mut key = [0u8; 36];
key[..32].copy_from_slice(&op.hash);
key[32..36].copy_from_slice(&op.index.to_be_bytes());
key
}
#[inline]
pub fn utxo_deletion_key_to_outpoint(key: &UtxoDeletionKey) -> OutPoint {
let mut hash = [0u8; 32];
hash.copy_from_slice(&key[..32]);
let index = u32::from_be_bytes(key[32..36].try_into().unwrap());
OutPoint { hash, index }
}
#[inline]
fn key_to_outpoint(key: &OutPointKey) -> OutPoint {
utxo_deletion_key_to_outpoint(key)
}
pub trait UtxoLookup {
fn get(&self, outpoint: &OutPoint) -> Option<&UTXO>;
fn contains_key(&self, outpoint: &OutPoint) -> bool {
self.get(outpoint).is_some()
}
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(feature = "production")]
impl UtxoLookup for UtxoSet {
#[inline]
fn get(&self, outpoint: &OutPoint) -> Option<&UTXO> {
FxHashMap::get(self, outpoint).map(|a| a.as_ref())
}
#[inline]
fn contains_key(&self, outpoint: &OutPoint) -> bool {
FxHashMap::contains_key(self, outpoint)
}
#[inline]
fn len(&self) -> usize {
FxHashMap::len(self)
}
#[inline]
fn is_empty(&self) -> bool {
FxHashMap::is_empty(self)
}
}
#[cfg(not(feature = "production"))]
impl UtxoLookup for UtxoSet {
#[inline]
fn get(&self, outpoint: &OutPoint) -> Option<&UTXO> {
std::collections::HashMap::get(self, outpoint).map(|a| a.as_ref())
}
#[inline]
fn contains_key(&self, outpoint: &OutPoint) -> bool {
std::collections::HashMap::contains_key(self, outpoint)
}
#[inline]
fn len(&self) -> usize {
std::collections::HashMap::len(self)
}
#[inline]
fn is_empty(&self) -> bool {
std::collections::HashMap::is_empty(self)
}
}
#[derive(Debug)]
pub struct UtxoOverlay<'a, B: UtxoLookup + ?Sized = UtxoSet> {
base: &'a B,
additions: FxHashMap<OutPoint, std::sync::Arc<UTXO>>,
deletions: FxHashSet<OutPointKey>,
has_deletions: bool,
}
impl<'a, B: UtxoLookup + ?Sized> UtxoOverlay<'a, B> {
#[inline]
pub fn new(base: &'a B) -> Self {
Self {
base,
additions: FxHashMap::with_capacity_and_hasher(6000, Default::default()),
deletions: FxHashSet::with_capacity_and_hasher(6000, Default::default()),
has_deletions: false, }
}
#[inline]
pub fn with_capacity(base: &'a B, additions_cap: usize, deletions_cap: usize) -> Self {
Self {
base,
additions: FxHashMap::with_capacity_and_hasher(additions_cap, Default::default()),
deletions: FxHashSet::with_capacity_and_hasher(deletions_cap, Default::default()),
has_deletions: false, }
}
#[inline]
pub fn get(&self, outpoint: &OutPoint) -> Option<&UTXO> {
if self.has_deletions && self.deletions.contains(&outpoint_to_key(outpoint)) {
return None;
}
if let Some(arc) = self.additions.get(outpoint) {
return Some(arc.as_ref());
}
self.base.get(outpoint)
}
#[inline]
pub fn contains_key(&self, outpoint: &OutPoint) -> bool {
if self.has_deletions && self.deletions.contains(&outpoint_to_key(outpoint)) {
return false;
}
self.additions.contains_key(outpoint) || self.base.contains_key(outpoint)
}
#[inline]
pub fn insert(&mut self, outpoint: OutPoint, utxo: UTXO) {
self.insert_arc(outpoint, std::sync::Arc::new(utxo));
}
#[inline]
pub fn insert_arc(&mut self, outpoint: OutPoint, utxo: std::sync::Arc<UTXO>) {
if self.deletions.remove(&outpoint_to_key(&outpoint)) {
if self.deletions.is_empty() {
self.has_deletions = false;
}
}
self.additions.insert(outpoint, utxo);
}
#[inline]
pub fn mark_spent(&mut self, outpoint: &OutPoint) {
if self.additions.remove(outpoint).is_some() {
return;
}
if self.base.contains_key(outpoint) {
self.deletions.insert(outpoint_to_key(outpoint));
self.has_deletions = true;
}
}
#[inline]
pub fn remove(&mut self, outpoint: &OutPoint) -> Option<std::sync::Arc<UTXO>> {
if let Some(arc) = self.additions.remove(outpoint) {
return Some(arc);
}
if let Some(utxo) = self.base.get(outpoint) {
self.deletions.insert(outpoint_to_key(outpoint));
self.has_deletions = true;
return Some(std::sync::Arc::new(utxo.clone()));
}
None
}
#[inline]
pub fn additions_len(&self) -> usize {
self.additions.len()
}
#[inline]
pub fn deletions_len(&self) -> usize {
self.deletions.len()
}
#[inline]
pub fn base_len(&self) -> usize {
self.base.len()
}
#[inline]
pub fn additions(&self) -> &FxHashMap<OutPoint, std::sync::Arc<UTXO>> {
&self.additions
}
#[inline]
pub fn deletions(&self) -> &FxHashSet<OutPointKey> {
&self.deletions
}
#[inline]
pub fn into_changes(
self,
) -> (
FxHashMap<OutPoint, std::sync::Arc<UTXO>>,
FxHashSet<UtxoDeletionKey>,
) {
(self.additions, self.deletions)
}
}
impl<'a> UtxoOverlay<'a, UtxoSet> {
pub fn apply_to_base(self) -> UtxoSet {
let mut result = self.base.clone();
for key in self.deletions {
result.remove(&key_to_outpoint(&key));
}
for (outpoint, arc) in self.additions {
result.insert(outpoint, arc);
}
result
}
}
impl<'a, B: UtxoLookup + ?Sized> UtxoLookup for UtxoOverlay<'a, B> {
#[inline]
fn get(&self, outpoint: &OutPoint) -> Option<&UTXO> {
if self.has_deletions && self.deletions.contains(&outpoint_to_key(outpoint)) {
return None;
}
if let Some(arc) = self.additions.get(outpoint) {
return Some(arc.as_ref());
}
self.base.get(outpoint)
}
#[inline]
fn contains_key(&self, outpoint: &OutPoint) -> bool {
if self.has_deletions && self.deletions.contains(&outpoint_to_key(outpoint)) {
return false;
}
self.additions.contains_key(outpoint) || self.base.contains_key(outpoint)
}
#[inline]
fn len(&self) -> usize {
self.base.len() + self.additions.len() - self.deletions.len()
}
}
#[cfg(feature = "production")]
pub type FastUtxoSet = rustc_hash::FxHashMap<OutPoint, UTXO>;
#[cfg(not(feature = "production"))]
pub type FastUtxoSet = std::collections::HashMap<OutPoint, UTXO>;
#[cfg(feature = "production")]
#[inline]
pub fn to_fast_utxo_set(utxo_set: &UtxoSet) -> FastUtxoSet {
utxo_set.iter().map(|(k, v)| (*k, (**v).clone())).collect()
}
#[cfg(not(feature = "production"))]
#[inline]
pub fn to_fast_utxo_set(utxo_set: &UtxoSet) -> FastUtxoSet {
utxo_set.iter().map(|(k, v)| (*k, (**v).clone())).collect()
}
#[inline]
pub fn apply_transaction_to_overlay<B: UtxoLookup + ?Sized>(
overlay: &mut UtxoOverlay<'_, B>,
tx: &crate::types::Transaction,
tx_id: crate::types::Hash,
height: crate::types::Natural,
) -> Vec<crate::reorganization::UndoEntry> {
use crate::reorganization::UndoEntry;
use crate::transaction::is_coinbase;
let mut undo_entries = Vec::with_capacity(tx.inputs.len() + tx.outputs.len());
if !is_coinbase(tx) {
for input in &tx.inputs {
if let Some(previous_utxo) = overlay.remove(&input.prevout) {
undo_entries.push(UndoEntry {
outpoint: input.prevout,
previous_utxo: Some(previous_utxo),
new_utxo: None, });
}
}
}
for (i, output) in tx.outputs.iter().enumerate() {
let outpoint = OutPoint {
hash: tx_id,
index: i as u32,
};
let utxo = UTXO {
value: output.value,
script_pubkey: output.script_pubkey.as_slice().into(),
height,
is_coinbase: is_coinbase(tx),
};
let utxo_arc = std::sync::Arc::new(utxo);
undo_entries.push(UndoEntry {
outpoint,
previous_utxo: None, new_utxo: Some(std::sync::Arc::clone(&utxo_arc)),
});
overlay.insert_arc(outpoint, utxo_arc);
}
undo_entries
}
#[inline]
fn utxo_from_tx_output(
output: &crate::types::TransactionOutput,
height: crate::types::Natural,
is_coinbase: bool,
) -> UTXO {
UTXO {
value: output.value,
script_pubkey: output.script_pubkey.as_slice().into(),
height,
is_coinbase,
}
}
pub fn build_block_output_utxo_cache(
block: &crate::types::Block,
tx_ids: &[crate::types::Hash],
height: crate::types::Natural,
) -> FxHashMap<OutPoint, std::sync::Arc<UTXO>> {
let cap: usize = block.transactions.iter().map(|tx| tx.outputs.len()).sum();
let mut cache = FxHashMap::with_capacity_and_hasher(cap.max(1), Default::default());
for (ti, tx) in block.transactions.iter().enumerate() {
if ti >= tx_ids.len() {
break;
}
let tx_id = tx_ids[ti];
let is_cb = crate::transaction::is_coinbase(tx);
for (oi, output) in tx.outputs.iter().enumerate() {
let op = OutPoint {
hash: tx_id,
index: oi as u32,
};
cache.insert(
op,
std::sync::Arc::new(utxo_from_tx_output(output, height, is_cb)),
);
}
}
cache
}
#[inline]
pub fn apply_transaction_to_overlay_no_undo<B: UtxoLookup + ?Sized>(
overlay: &mut UtxoOverlay<'_, B>,
tx: &crate::types::Transaction,
tx_id: crate::types::Hash,
height: crate::types::Natural,
) {
apply_transaction_to_overlay_no_undo_impl(overlay, tx, tx_id, height, None);
}
#[inline]
pub fn apply_transaction_to_overlay_no_undo_with_output_cache<B: UtxoLookup + ?Sized>(
overlay: &mut UtxoOverlay<'_, B>,
tx: &crate::types::Transaction,
tx_id: crate::types::Hash,
height: crate::types::Natural,
output_cache: &FxHashMap<OutPoint, std::sync::Arc<UTXO>>,
) {
apply_transaction_to_overlay_no_undo_impl(overlay, tx, tx_id, height, Some(output_cache));
}
#[inline]
pub fn apply_transaction_to_overlay_no_undo_cached<B: UtxoLookup + ?Sized>(
overlay: &mut UtxoOverlay<'_, B>,
tx: &crate::types::Transaction,
tx_id: crate::types::Hash,
height: crate::types::Natural,
output_cache: Option<&FxHashMap<OutPoint, std::sync::Arc<UTXO>>>,
) {
if let Some(cache) = output_cache {
apply_transaction_to_overlay_no_undo_with_output_cache(overlay, tx, tx_id, height, cache);
} else {
apply_transaction_to_overlay_no_undo(overlay, tx, tx_id, height);
}
}
fn apply_transaction_to_overlay_no_undo_impl<B: UtxoLookup + ?Sized>(
overlay: &mut UtxoOverlay<'_, B>,
tx: &crate::types::Transaction,
tx_id: crate::types::Hash,
height: crate::types::Natural,
output_cache: Option<&FxHashMap<OutPoint, std::sync::Arc<UTXO>>>,
) {
let is_cb = crate::transaction::is_coinbase(tx);
if !is_cb {
for input in &tx.inputs {
overlay.mark_spent(&input.prevout);
}
}
for (i, output) in tx.outputs.iter().enumerate() {
let outpoint = OutPoint {
hash: tx_id,
index: i as u32,
};
if let Some(cache) = output_cache {
if let Some(arc) = cache.get(&outpoint) {
overlay.insert_arc(outpoint, std::sync::Arc::clone(arc));
continue;
}
}
overlay.insert(outpoint, utxo_from_tx_output(output, height, is_cb));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utxo_set_insert;
fn make_outpoint(idx: u8) -> OutPoint {
OutPoint {
hash: [idx; 32],
index: idx as u32,
}
}
fn make_outpoint_u32(i: u32) -> OutPoint {
let mut hash = [0u8; 32];
hash[..4].copy_from_slice(&i.to_le_bytes());
OutPoint { hash, index: i }
}
fn make_utxo(value: i64) -> UTXO {
UTXO {
value,
script_pubkey: vec![crate::opcodes::OP_DUP, crate::opcodes::OP_HASH160].into(), height: 1,
is_coinbase: false,
}
}
#[test]
fn test_overlay_lookup_from_base() {
let mut base = UtxoSet::default();
utxo_set_insert(&mut base, make_outpoint(1), make_utxo(1000));
utxo_set_insert(&mut base, make_outpoint(2), make_utxo(2000));
let overlay = UtxoOverlay::new(&base);
assert_eq!(overlay.get(&make_outpoint(1)).unwrap().value, 1000);
assert_eq!(overlay.get(&make_outpoint(2)).unwrap().value, 2000);
assert!(overlay.get(&make_outpoint(3)).is_none());
}
#[test]
fn test_overlay_additions() {
let base = UtxoSet::default();
let mut overlay = UtxoOverlay::new(&base);
overlay.insert(make_outpoint(1), make_utxo(1000));
assert_eq!(overlay.get(&make_outpoint(1)).unwrap().value, 1000);
assert_eq!(overlay.additions_len(), 1);
}
#[test]
fn test_overlay_deletions() {
let mut base = UtxoSet::default();
utxo_set_insert(&mut base, make_outpoint(1), make_utxo(1000));
let mut overlay = UtxoOverlay::new(&base);
let removed = overlay.remove(&make_outpoint(1));
assert_eq!(removed.unwrap().value, 1000);
assert!(overlay.get(&make_outpoint(1)).is_none());
assert_eq!(overlay.deletions_len(), 1);
}
#[test]
fn test_overlay_intra_block_spend() {
let base = UtxoSet::default();
let mut overlay = UtxoOverlay::new(&base);
overlay.insert(make_outpoint(1), make_utxo(1000));
assert!(overlay.get(&make_outpoint(1)).is_some());
let removed = overlay.remove(&make_outpoint(1));
assert_eq!(removed.unwrap().value, 1000);
assert!(overlay.get(&make_outpoint(1)).is_none());
assert_eq!(overlay.deletions_len(), 0);
assert_eq!(overlay.additions_len(), 0);
}
#[test]
fn test_overlay_apply() {
let mut base = UtxoSet::default();
utxo_set_insert(&mut base, make_outpoint(1), make_utxo(1000));
utxo_set_insert(&mut base, make_outpoint(2), make_utxo(2000));
let mut overlay = UtxoOverlay::new(&base);
overlay.remove(&make_outpoint(1));
overlay.insert(make_outpoint(3), make_utxo(3000));
let result = overlay.apply_to_base();
assert!(result.get(&make_outpoint(1)).is_none()); assert_eq!(result.get(&make_outpoint(2)).unwrap().value, 2000); assert_eq!(result.get(&make_outpoint(3)).unwrap().value, 3000); }
#[test]
fn test_overlay_no_clone_on_creation() {
let mut base = UtxoSet::default();
for i in 0..10_000 {
utxo_set_insert(&mut base, make_outpoint_u32(i), make_utxo(i as i64));
}
assert_eq!(base.len(), 10_000);
let start = std::time::Instant::now();
let _overlay = UtxoOverlay::new(&base);
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(100),
"Overlay creation took {elapsed:?} (expected no full base clone)"
);
}
#[test]
fn apply_cached_matches_direct_for_block_outputs() {
use crate::block::calculate_tx_id;
use crate::types::{Transaction, TransactionInput, TransactionOutput};
let fund = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: make_outpoint(1),
script_sig: vec![].into(),
sequence: 0xffff_ffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_000,
script_pubkey: vec![1, 2, 3].into(),
}]
.into(),
lock_time: 0,
};
let fund_id = calculate_tx_id(&fund);
let mut base = UtxoSet::default();
crate::utxo_set_insert(&mut base, make_outpoint(1), make_utxo(50_000));
let block = crate::types::Block {
header: crate::types::BlockHeader::default(),
transactions: std::iter::once(fund.clone()).collect::<Vec<_>>().into(),
};
let tx_ids = vec![fund_id];
let cache = build_block_output_utxo_cache(&block, &tx_ids, 100);
let mut overlay_direct = UtxoOverlay::new(&base);
apply_transaction_to_overlay_no_undo(&mut overlay_direct, &fund, fund_id, 100);
let mut overlay_cached = UtxoOverlay::new(&base);
apply_transaction_to_overlay_no_undo_cached(
&mut overlay_cached,
&fund,
fund_id,
100,
Some(&cache),
);
assert_eq!(
overlay_direct.additions_len(),
overlay_cached.additions_len()
);
let op = OutPoint {
hash: fund_id,
index: 0,
};
assert_eq!(
overlay_direct.get(&op).map(|u| u.value),
overlay_cached.get(&op).map(|u| u.value)
);
}
}