Skip to content

Commit

Permalink
Document status.rs (#1136)
Browse files Browse the repository at this point in the history
Also, refactor `Unspent` balance a bit.
  • Loading branch information
romanz authored Dec 28, 2024
1 parent 39ebadd commit 25ad4c0
Showing 1 changed file with 42 additions and 30 deletions.
72 changes: 42 additions & 30 deletions src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct TxEntry {
spent: Vec<OutPoint>, // relevant spent outpoints
}

// Funded outputs of a transaction
struct TxOutput {
index: u32,
value: Amount,
Expand Down Expand Up @@ -103,6 +104,8 @@ pub(crate) struct HistoryEntry {
}

impl HistoryEntry {
// Hash to compute ScriptHash status, as defined here:
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status
fn hash(&self, engine: &mut sha256::HashEngine) {
let s = format!("{}:{}:", self.txid, self.height);
engine.input(s.as_bytes());
Expand Down Expand Up @@ -138,6 +141,7 @@ pub struct ScriptHashStatus {
}

/// Specific scripthash balance
/// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-balance
#[derive(Default, Eq, PartialEq, Serialize)]
pub(crate) struct Balance {
#[serde(with = "bitcoin::amount::serde::as_sat", rename = "confirmed")]
Expand All @@ -146,8 +150,8 @@ pub(crate) struct Balance {
mempool_delta: SignedAmount,
}

// A single unspent transaction output entry:
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-listunspent
/// A single unspent transaction output entry
/// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-listunspent
#[derive(Serialize)]
pub(crate) struct UnspentEntry {
height: usize, // 0 = mempool entry
Expand All @@ -161,28 +165,28 @@ pub(crate) struct UnspentEntry {
struct Unspent {
// mapping an outpoint to its value & confirmation height
outpoints: HashMap<OutPoint, (Amount, usize)>,
confirmed_balance: Amount,
mempool_delta: SignedAmount,
balance: Balance,
}

impl Unspent {
fn build(status: &ScriptHashStatus, chain: &Chain) -> Self {
let mut unspent = Unspent::default();

// First, add all relevant entries' funding outputs to the outpoints' map
status
.confirmed_height_entries(chain)
.for_each(|(height, entries)| entries.iter().for_each(|e| unspent.insert(e, height)));
// Then, remove spent outpoints from the map
status
.confirmed_entries(chain)
.for_each(|e| unspent.remove(e));

unspent.confirmed_balance = unspent.balance();

unspent.balance.confirmed_balance = unspent.balance();
// Now, do the same over the mempool (first add funding outputs, and then remove spent ones)
status.mempool.iter().for_each(|e| unspent.insert(e, 0)); // mempool height = 0
status.mempool.iter().for_each(|e| unspent.remove(e));

unspent.mempool_delta =
unspent.balance().to_signed().unwrap() - unspent.confirmed_balance.to_signed().unwrap();
unspent.balance.mempool_delta = unspent.balance().to_signed().unwrap()
- unspent.balance.confirmed_balance.to_signed().unwrap();

unspent
}
Expand All @@ -199,6 +203,7 @@ impl Unspent {
.collect()
}

/// Total amount of unspent outputs
fn balance(&self) -> Amount {
self.outpoints
.values()
Expand Down Expand Up @@ -264,18 +269,17 @@ impl ScriptHashStatus {
.collect()
}

/// Collect unspent transaction entries
pub(crate) fn get_unspent(&self, chain: &Chain) -> Vec<UnspentEntry> {
Unspent::build(self, chain).into_entries()
}

/// Collect unspent transaction balance
pub(crate) fn get_balance(&self, chain: &Chain) -> Balance {
let unspent = Unspent::build(self, chain);
Balance {
confirmed_balance: unspent.confirmed_balance,
mempool_delta: unspent.mempool_delta,
}
Unspent::build(self, chain).balance
}

/// Collect transaction history entries
pub(crate) fn get_history(&self) -> &[HistoryEntry] {
&self.history
}
Expand Down Expand Up @@ -307,7 +311,7 @@ impl ScriptHashStatus {
.collect()
}

/// Apply func only on the new blocks (fetched from daemon).
/// Apply `func` only on the new blocks (to be fetched via p2p interface).
fn for_new_blocks<B, F>(&self, blockhashes: B, daemon: &Daemon, func: F) -> Result<()>
where
B: IntoIterator<Item = BlockHash>,
Expand All @@ -330,34 +334,39 @@ impl ScriptHashStatus {
cache: &Cache,
outpoints: &mut HashSet<OutPoint>,
) -> Result<HashMap<BlockHash, Vec<TxEntry>>> {
let scripthash = self.scripthash;
// Will be updated during the following block scans
let mut result = HashMap::<BlockHash, HashMap<usize, TxEntry>>::new();

let funding_blockhashes = index.limit_result(index.filter_by_funding(scripthash))?;
let funding_blockhashes = index.limit_result(index.filter_by_funding(self.scripthash))?;
self.for_new_blocks(funding_blockhashes, daemon, |blockhash, block| {
let block_entries = result.entry(blockhash).or_default();
for filtered_outputs in filter_block_txs_outputs(block, scripthash) {
let block_entries = result.entry(blockhash).or_default(); // the block may already exist

// extract relevant funding transactions
for filtered_outputs in filter_block_txs_outputs(block, self.scripthash) {
cache.add_tx(filtered_outputs.txid, move || filtered_outputs.tx_bytes);
// store funded outpoints (to check for spending later)
outpoints.extend(make_outpoints(
filtered_outputs.txid,
&filtered_outputs.result,
));
block_entries
.entry(filtered_outputs.pos)
.entry(filtered_outputs.pos) // the transaction may already exist
.or_insert_with(|| TxEntry::new(filtered_outputs.txid))
.outputs = filtered_outputs.result;
}
})?;
let spending_blockhashes: HashSet<BlockHash> = outpoints
.par_iter()
.par_iter() // use rayon for concurrent index lookups
.flat_map_iter(|outpoint| index.filter_by_spending(*outpoint))
.collect();
self.for_new_blocks(spending_blockhashes, daemon, |blockhash, block| {
let block_entries = result.entry(blockhash).or_default();
let block_entries = result.entry(blockhash).or_default(); // the block may already exist

// extract relevant spending transactions
for filtered_inputs in filter_block_txs_inputs(&block, outpoints) {
cache.add_tx(filtered_inputs.txid, move || filtered_inputs.tx_bytes);
block_entries
.entry(filtered_inputs.pos)
.entry(filtered_inputs.pos) // the transaction may already exist
.or_insert_with(|| TxEntry::new(filtered_inputs.txid))
.spent = filtered_inputs.result;
}
Expand All @@ -366,11 +375,10 @@ impl ScriptHashStatus {
Ok(result
.into_iter()
.map(|(blockhash, entries_map)| {
// sort transactions by their position in a block
let sorted_entries = entries_map
let sorted_entries: Vec<TxEntry> = entries_map
.into_iter()
.collect::<BTreeMap<usize, TxEntry>>()
.into_values()
.collect::<BTreeMap<usize, TxEntry>>() // sort transactions by their position in a block
.into_values() // drop position within block
.collect();
(blockhash, sorted_entries)
})
Expand All @@ -386,12 +394,14 @@ impl ScriptHashStatus {
outpoints: &mut HashSet<OutPoint>,
) -> Vec<TxEntry> {
let mut result = HashMap::<Txid, TxEntry>::new();
// extract relevant funding transactions
for entry in mempool.filter_by_funding(&self.scripthash) {
let funding_outputs = filter_outputs(&entry.tx, self.scripthash);
assert!(!funding_outputs.is_empty());
// store funded outpoints (to check for spending later)
outpoints.extend(make_outpoints(entry.txid, &funding_outputs));
result
.entry(entry.txid)
.entry(entry.txid) // the transaction may already exist
.or_insert_with(|| TxEntry::new(entry.txid))
.outputs = funding_outputs;
cache.add_tx(entry.txid, || serialize(&entry.tx).into_boxed_slice());
Expand All @@ -403,7 +413,7 @@ impl ScriptHashStatus {
let spent_outpoints = filter_inputs(&entry.tx, outpoints);
assert!(!spent_outpoints.is_empty());
result
.entry(entry.txid)
.entry(entry.txid) // the transaction may already exist
.or_insert_with(|| TxEntry::new(entry.txid))
.spent = spent_outpoints;
cache.add_tx(entry.txid, || serialize(&entry.tx).into_boxed_slice());
Expand All @@ -425,7 +435,7 @@ impl ScriptHashStatus {
let new_tip = index.chain().tip();
if self.tip != new_tip {
let update = self.sync_confirmed(index, daemon, cache, &mut outpoints)?;
self.confirmed.extend(update);
self.confirmed.extend(update); // add new blocks to the map
self.tip = new_tip;
}
if !self.confirmed.is_empty() {
Expand All @@ -439,6 +449,7 @@ impl ScriptHashStatus {
if !self.mempool.is_empty() {
debug!("{} mempool transactions", self.mempool.len());
}
// update history entries and status hash
self.history.clear();
self.history
.extend(self.get_confirmed_history(index.chain()));
Expand Down Expand Up @@ -489,6 +500,7 @@ fn filter_inputs(tx: &Transaction, outpoints: &HashSet<OutPoint>) -> Vec<OutPoin
.collect()
}

// See https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status for details
fn compute_status_hash(history: &[HistoryEntry]) -> Option<StatusHash> {
if history.is_empty() {
return None;
Expand Down

0 comments on commit 25ad4c0

Please sign in to comment.