From 084494521a61ef84239cfae3659ee31b85247041 Mon Sep 17 00:00:00 2001 From: JoseSK999 <83597627+JoseSK999@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:58:07 +0100 Subject: [PATCH 1/2] test(consensus): transaction-building helpers --- .../src/pruned_utreexo/consensus.rs | 80 ++++++++++--------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/crates/floresta-chain/src/pruned_utreexo/consensus.rs b/crates/floresta-chain/src/pruned_utreexo/consensus.rs index ddfb748fe..04f062ba8 100644 --- a/crates/floresta-chain/src/pruned_utreexo/consensus.rs +++ b/crates/floresta-chain/src/pruned_utreexo/consensus.rs @@ -554,8 +554,17 @@ mod tests { }; } - /// Macro for generating a legacy TxIn with an optional sequence number + /// Macro for constructing a legacy [`TxIn`] with optional scriptSig and sequence number. + /// Needs the outpoint and, if not provided, defaults to empty scriptSig and `Sequence::MAX`. macro_rules! txin { + ($outpoint:expr) => { + TxIn { + previous_output: $outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + } + }; ($outpoint:expr, $script:expr) => { TxIn { previous_output: $outpoint, @@ -574,6 +583,25 @@ mod tests { }; } + /// Helper for building a zero-locktime transaction given the input and output list. + fn build_tx(input: Vec, output: Vec) -> Transaction { + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input, + output, + } + } + + /// Helper to avoid boilerplate in test cases. Note this is not a null outpoint, restricted to + /// coinbase transactions only, as that requires the vout to be `u32::MAX`. + fn dummy_outpoint() -> OutPoint { + OutPoint { + txid: Txid::all_zeros(), + vout: 0, + } + } + #[cfg(feature = "bitcoinkernel")] /// Some made up transactions that test our script limits checks. /// Here's what is wrong with each transaction: @@ -611,7 +639,7 @@ mod tests { let output = txout!(5_000_350_000, output_script); Transaction { - version: Version(1), + version: Version::ONE, lock_time: LockTime::from_height(150_007).unwrap(), input: vec![input], output: vec![output], @@ -839,15 +867,12 @@ mod tests { #[test] fn test_output_value_overflow() { - let tx = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![txin!(OutPoint::new(Txid::all_zeros(), 0), ScriptBuf::new())], - output: vec![ - txout!(u64::MAX, ScriptBuf::new()), - txout!(1, ScriptBuf::new()), - ], - }; + let ins = vec![txin!(dummy_outpoint())]; + let outs = vec![ + txout!(u64::MAX, ScriptBuf::new()), + txout!(1, ScriptBuf::new()), + ]; + let tx = build_tx(ins, outs); match Consensus::check_transaction_context_free(&tx) { Err(BlockchainError::BlockValidation(BlockValidationErrors::TooManyCoins)) => (), @@ -857,7 +882,7 @@ mod tests { #[test] fn test_input_value_above_max_money() { - let outpoint = OutPoint::new(Txid::all_zeros(), 0); + let outpoint = dummy_outpoint(); let mut utxos = HashMap::new(); utxos.insert( @@ -873,12 +898,7 @@ mod tests { }, ); - let tx = Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![txin!(outpoint, ScriptBuf::new())], - output: vec![txout!(1, ScriptBuf::new())], - }; + let tx = build_tx(vec![txin!(outpoint)], vec![txout!(1, ScriptBuf::new())]); match Consensus::verify_transaction(&tx, &mut utxos, 0, false, 0) { Err(BlockchainError::BlockValidation(BlockValidationErrors::TooManyCoins)) => (), @@ -956,25 +976,13 @@ mod tests { // This test creates an over-sized script, make sure that transaction containing it is valid. // Then we try to spend this output, and verify if this causes an error. fn test_spending_script_too_big() { - fn build_tx(input: TxIn, output: TxOut) -> Transaction { - Transaction { - version: Version(1), - lock_time: LockTime::ZERO, - input: vec![input], - output: vec![output], - } - } - - let dummy_outpoint = OutPoint { - txid: Txid::all_zeros(), - vout: 0, - }; + let outpoint = dummy_outpoint(); let flags = 0; let dummy_height = 0; let mut utxos = HashMap::new(); utxos.insert( - dummy_outpoint, + outpoint, UtxoData { txout: txout!(0, true_script()), is_coinbase: false, @@ -984,9 +992,9 @@ mod tests { ); // 1. Build a valid transaction that produces an oversized, unspendable output. - let dummy_in = txin!(dummy_outpoint, ScriptBuf::new()); + let dummy_in = txin!(outpoint); let oversized_out = txout!(0, oversized_script()); - let tx_with_oversized = build_tx(dummy_in, oversized_out.clone()); + let tx_with_oversized = build_tx(vec![dummy_in], vec![oversized_out.clone()]); Consensus::verify_transaction(&tx_with_oversized, &mut utxos, dummy_height, false, flags) .unwrap(); @@ -1004,8 +1012,8 @@ mod tests { ); // 3. Attempt to spend the oversized output. - let spending_in = txin!(prevout, ScriptBuf::new()); - let spending_tx = build_tx(spending_in, txout!(0, true_script())); + let spending_in = txin!(prevout); + let spending_tx = build_tx(vec![spending_in], vec![txout!(0, true_script())]); let err = Consensus::verify_transaction(&spending_tx, &mut utxos, dummy_height, false, flags) .unwrap_err(); From 877ddf0011c3260fceb20d0a59f3825ff7a92860 Mon Sep 17 00:00:00 2001 From: JoseSK999 <83597627+JoseSK999@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:15:46 +0100 Subject: [PATCH 2/2] test(consensus): test `BlockTooBig` I've constructed a transaction that, when inserted in block 866,342 makes it have 4,000,001 WUs. Then we update the witness commitment and the merkle root, and then check we get `BlockTooBig`. For this I had to implement the `update_witness_commitment` helper. Note that the block we are testing has witnesses, so the witness commitment is enforced by consensus, and inserting a legacy transaction adds a zero `wtxid` that the commitment must now account for. --- .../src/pruned_utreexo/consensus.rs | 117 ++++++++++++++++-- 1 file changed, 106 insertions(+), 11 deletions(-) diff --git a/crates/floresta-chain/src/pruned_utreexo/consensus.rs b/crates/floresta-chain/src/pruned_utreexo/consensus.rs index 04f062ba8..e04745556 100644 --- a/crates/floresta-chain/src/pruned_utreexo/consensus.rs +++ b/crates/floresta-chain/src/pruned_utreexo/consensus.rs @@ -16,6 +16,7 @@ use bitcoin::script; use bitcoin::Amount; use bitcoin::Block; use bitcoin::CompactTarget; +use bitcoin::Network; use bitcoin::OutPoint; use bitcoin::ScriptBuf; use bitcoin::Target; @@ -76,6 +77,14 @@ pub struct Consensus { pub parameters: ChainParams, } +impl From for Consensus { + fn from(network: Network) -> Self { + Consensus { + parameters: network.into(), + } + } +} + impl Consensus { /// Returns the amount of block subsidy to be paid in a block, given it's height. /// @@ -539,8 +548,9 @@ mod tests { use bitcoin::Witness; use floresta_common::assert_err; use floresta_common::assert_ok; - use rand::rngs::OsRng; + use rand::rngs::StdRng; use rand::seq::SliceRandom; + use rand::SeedableRng; use super::*; @@ -646,9 +656,9 @@ mod tests { } } - /// Modifies a block to have an invalid output script (txdata is tampered with) - fn make_block_invalid(block: &mut Block) { - let mut rng = OsRng; + /// Modifies a block to have a different output script (txdata is tampered with). + fn mutate_block(block: &mut Block) { + let mut rng = StdRng::seed_from_u64(0x_bebe_cafe); let tx = block.txdata.choose_mut(&mut rng).unwrap(); let out = tx.output.choose_mut(&mut rng).unwrap(); @@ -659,14 +669,49 @@ mod tests { *byte += 1; } + /// Test helper to update the witness commitment in a block, assuming txdata was modified. + /// This ensures `block` is not considered mutated, so we can exercise other error cases. + fn update_witness_commitment(block: &mut Block) -> Option<()> { + // BIP141 witness-commitment prefix, where the full commitment data is: + // 1-byte - OP_RETURN (0x6a) + // 1-byte - Push the following 36 bytes (0x24) + // 4-byte - Commitment header (0xaa21a9ed) + // 32-byte - Commitment hash: Double-SHA256(witness root hash|witness reserved value) + const MAGIC: [u8; 6] = [0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed]; + + let coinbase = &block.txdata[0]; + + // Commitment is in the last coinbase output that starts with magic bytes. + let pos = coinbase.output.iter().rposition(|out| { + let spk = out.script_pubkey.as_bytes(); + spk.len() >= 38 && spk[0..6] == MAGIC + })?; + + // Witness reserved value is in coinbase input witness. + let witness_rv: &[u8; 32] = { + let mut it = coinbase.input[0].witness.iter(); + match (it.next(), it.next()) { + (Some(rv), None) => rv.try_into().ok()?, + _ => return None, + } + }; + + let root = block.witness_root()?; + let c = *Block::compute_witness_commitment(&root, witness_rv).as_byte_array(); + + block.txdata[0].output[pos].script_pubkey.as_mut_bytes()[6..38].copy_from_slice(&c); + Some(()) + } + + /// Decode and deserialize a zstd-compressed block in the given file path. + fn decode_block(file_path: &str) -> Block { + let block_file = File::open(file_path).unwrap(); + let block_bytes = zstd::decode_all(block_file).unwrap(); + deserialize(&block_bytes).unwrap() + } + #[test] fn test_check_merkle_root() { - fn decode_block(file_path: &str) -> Block { - let block_file = File::open(file_path).unwrap(); - let block_bytes = zstd::decode_all(block_file).unwrap(); - deserialize(&block_bytes).unwrap() - } - let blocks = [ genesis_block(Network::Bitcoin), genesis_block(Network::Testnet), @@ -687,7 +732,7 @@ mod tests { } // Modifying the txdata should invalidate the block - make_block_invalid(&mut block); + mutate_block(&mut block); assert!(!block.check_merkle_root()); if Consensus::check_merkle_root(&block).is_some() { @@ -696,6 +741,56 @@ mod tests { } } + /// Modifies historical block at height 866,342 by adding one extra transaction so that the + /// updated block weight is 4,000,001 WUs. The block merkle roots are updated accordingly. + fn build_oversized_866_342() -> Block { + let mut block = decode_block("./testdata/block_866342/raw.zst"); + + let consensus = Consensus::from(Network::Bitcoin); + consensus.check_block(&block, 866_342).expect("valid block"); + + // This block is close but below to the max weight + assert_eq!(block.weight().to_wu(), 3_993_209); + + // Modify the block by adding one transaction that makes it exceed the weight limit + let mut script_out = ScriptBuf::default(); + for _ in 0..1_636 { + script_out.push_opcode(OP_NOP); + } + let out = txout!(1, script_out); + let tx = build_tx(vec![txin!(dummy_outpoint())], vec![out]); + + block.txdata.insert(1, tx); + + // Update the witness commitment, and then the merkle root, which depends on the former + update_witness_commitment(&mut block).expect("should be able to update"); + block.header.merkle_root = block.compute_merkle_root().unwrap(); + + block + } + + #[test] + fn test_block_too_big() { + let height = 866_342; + let consensus = Consensus::from(Network::Bitcoin); + let block = build_oversized_866_342(); + + // This block is now just over the weight limit, by one unit! + assert_eq!(block.weight().to_wu(), 4_000_001); + assert!(block.weight() > Weight::MAX_BLOCK); + assert_eq!(Weight::MAX_BLOCK.to_wu(), 4_000_000); + + // The txdata commitments match + assert!(block.check_merkle_root()); + assert!(block.check_witness_commitment()); + Consensus::check_merkle_root(&block).expect("merkle root matches"); + + match consensus.check_block(&block, height) { + Err(BlockchainError::BlockValidation(BlockValidationErrors::BlockTooBig)) => (), + other => panic!("We should have `BlockValidationErrors::BlockTooBig`, got {other:?}"), + } + } + #[test] fn test_validate_script_size() { use bitcoin::hashes::Hash;