Skip to content

Commit 07d4f86

Browse files
committed
feat: avoid computing txids twice
Implement a `Consensus::check_merkle_root`, which follows the same logic as the original `rust-bitcoin` method, except we return the computed `txid` list. This is useful for when SwiftSync builds the `OutPoints`, which avoids recomputing the `txid`.
1 parent 1e42b0a commit 07d4f86

1 file changed

Lines changed: 35 additions & 14 deletions

File tree

crates/floresta-chain/src/pruned_utreexo/consensus.rs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,12 @@ impl Consensus {
219219
/// weaker amount check than that of traditional AssumeValid.
220220
pub fn verify_block_transactions_swiftsync(
221221
transactions: &[Transaction],
222+
txids: Vec<Txid>,
222223
unspent_indexes: HashSet<u64>,
223224
salt: &SipHashKeys,
224225
) -> Result<(SwiftSyncAgg, Amount), BlockchainError> {
226+
assert_eq!(transactions.len(), txids.len());
227+
225228
// Blocks must contain at least one transaction (i.e., the coinbase)
226229
if transactions.is_empty() {
227230
return Err(BlockValidationErrors::EmptyBlock)?;
@@ -232,7 +235,7 @@ impl Consensus {
232235
let mut unspent_amount = Amount::ZERO;
233236
let mut agg = SwiftSyncAgg::zero();
234237

235-
for (n, transaction) in transactions.iter().enumerate() {
238+
for (n, (transaction, txid)) in transactions.iter().zip(txids).enumerate() {
236239
if n == 0 {
237240
if !transaction.is_coinbase() {
238241
return Err(BlockValidationErrors::FirstTxIsNotCoinbase)?;
@@ -267,7 +270,7 @@ impl Consensus {
267270
output_index += 1;
268271
}
269272
// Only add spent outputs to the aggregator
270-
Self::add_outputs_to_agg(salt, &mut agg, transaction, spent_vouts);
273+
Self::add_outputs_to_agg(salt, &mut agg, txid, spent_vouts);
271274
}
272275

273276
Ok((agg, unspent_amount))
@@ -277,23 +280,21 @@ impl Consensus {
277280
fn add_outputs_to_agg(
278281
salt: &SipHashKeys,
279282
agg: &mut SwiftSyncAgg,
280-
transaction: &Transaction,
283+
txid: Txid,
281284
spent_vouts: Vec<u32>,
282285
) {
283-
let txid = || transaction.compute_txid();
284-
285286
match spent_vouts.len() {
286287
// Nothing needs to be added to the aggregator
287288
0 => {}
288289

289290
// Only one `OutPoint` to add
290-
1 => agg.add(salt, &OutPoint::new(txid(), spent_vouts[0])),
291+
1 => agg.add(salt, &OutPoint::new(txid, spent_vouts[0])),
291292

292293
// All `OutPoints` to hash here will share the same txid, only differing in the vout.
293294
// Thus, we can compute the midstate once, amortizing this cost for all `OutPoints`,
294295
// which is around 57% less work given enough spent outputs.
295296
_ => {
296-
let midstate = TxidHashMidstate::new(salt, txid().as_byte_array());
297+
let midstate = TxidHashMidstate::new(salt, txid.as_byte_array());
297298
for vout in spent_vouts {
298299
agg.add_with_vout(midstate.clone(), vout);
299300
}
@@ -453,17 +454,18 @@ impl Consensus {
453454
Ok(out_value)
454455
}
455456

456-
/// Runs inexpensive, consensus-critical block checks that don't require script execution.
457+
/// Runs inexpensive, consensus-critical block checks that don't require script execution. If
458+
/// successful, returns the list of [`Txid`]s computed for the merkle root check.
457459
///
458460
/// This verifies:
459461
/// - the header merkle root matches the block's txids
460462
/// - BIP34 coinbase-encoded height once activated (at `bip34_height`)
461463
/// - if there are SegWit transactions, the witness commitment is present and correct
462464
/// - total block weight is within the 4,000,000 WU limit
463-
pub fn check_block(&self, block: &Block, height: u32) -> Result<(), BlockchainError> {
464-
if !block.check_merkle_root() {
465+
pub fn check_block(&self, block: &Block, height: u32) -> Result<Vec<Txid>, BlockchainError> {
466+
let Some(txids) = Self::check_merkle_root(block) else {
465467
return Err(BlockValidationErrors::BadMerkleRoot)?;
466-
}
468+
};
467469

468470
let bip34_height = self.parameters.params.bip34_height;
469471
// If bip34 is active, check that the encoded block height is correct
@@ -479,7 +481,7 @@ impl Consensus {
479481
return Err(BlockValidationErrors::BlockTooBig)?;
480482
}
481483

482-
Ok(())
484+
Ok(txids)
483485
}
484486

485487
/// Performs AssumeValid SwiftSync validation and returns the resulting [`SwiftSyncAgg`]
@@ -493,9 +495,28 @@ impl Consensus {
493495
unspent_indexes: HashSet<u64>,
494496
salt: &SipHashKeys,
495497
) -> Result<(SwiftSyncAgg, Amount), BlockchainError> {
496-
self.check_block(block, height)?;
498+
let txids = self.check_block(block, height)?;
497499

498-
Consensus::verify_block_transactions_swiftsync(&block.txdata, unspent_indexes, salt)
500+
Consensus::verify_block_transactions_swiftsync(&block.txdata, txids, unspent_indexes, salt)
501+
}
502+
503+
/// Checks if the merkle root of the header matches the merkle root of the transaction list.
504+
///
505+
/// Unlike [`Block::check_merkle_root`], this function returns the list of computed [`Txid`]s
506+
/// if the merkle roots matched, or `None` otherwise.
507+
///
508+
/// The merkle root is computed in the same way as [`Block::compute_merkle_root`].
509+
pub fn check_merkle_root(block: &Block) -> Option<Vec<Txid>> {
510+
let txids: Vec<_> = block.txdata.iter().map(|obj| obj.compute_txid()).collect();
511+
512+
// Copy the hashes into an iterator, as `calculate_root` requires ownership
513+
let hashes_iter = txids.iter().copied().map(|txid| txid.to_raw_hash());
514+
515+
let calculated = bitcoin::merkle_tree::calculate_root(hashes_iter).map(|h| h.into());
516+
match calculated {
517+
Some(merkle_root) if block.header.merkle_root == merkle_root => Some(txids),
518+
_ => None,
519+
}
499520
}
500521

501522
/// Returns the TxOut being spent by the given input.

0 commit comments

Comments
 (0)