-
Notifications
You must be signed in to change notification settings - Fork 115
test(consensus): test BlockTooBig
#867
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Network> 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::*; | ||
|
|
||
|
|
@@ -554,8 +564,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 +593,25 @@ mod tests { | |
| }; | ||
| } | ||
|
|
||
| /// Helper for building a zero-locktime transaction given the input and output list. | ||
| fn build_tx(input: Vec<TxIn>, output: Vec<TxOut>) -> 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,16 +649,16 @@ 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], | ||
| } | ||
| } | ||
|
|
||
| /// 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sagaz! ☕ |
||
|
|
||
| let tx = block.txdata.choose_mut(&mut rng).unwrap(); | ||
| let out = tx.output.choose_mut(&mut rng).unwrap(); | ||
|
|
@@ -631,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]; | ||
JoseSK999 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: since the coinbase input must have one and only one element, what about leaving an
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Actually better to return |
||
| } | ||
| }; | ||
|
|
||
| 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() | ||
|
Comment on lines
+707
to
+710
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated: Bitcoin Core leaves those test assets in another repo, called
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Davidson-Souza you mean the data under
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea that i got was is that davidson wants a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh like a separate test utils crate |
||
| } | ||
|
|
||
| #[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), | ||
|
|
@@ -659,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() { | ||
|
|
@@ -668,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"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider remove hard coded file location to avoid test failures on other machines. |
||
|
|
||
| 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; | ||
|
|
@@ -839,15 +962,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 +977,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 +993,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 +1071,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 +1087,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 +1107,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(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.