Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document status.rs #1136

Merged
merged 1 commit into from
Dec 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading