diff --git a/Cargo.lock b/Cargo.lock index 5b4fb682db..71f1994d0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,6 +1369,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.8.1" @@ -1492,6 +1498,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dunce" version = "1.0.3" @@ -2032,6 +2044,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2047,6 +2068,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "funty" version = "2.0.0" @@ -3359,6 +3386,33 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "mockall" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0" +dependencies = [ + "cfg-if", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + [[package]] name = "modular-bitfield" version = "0.11.2" @@ -3417,6 +3471,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3935,6 +3995,36 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools 0.10.5", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" + +[[package]] +name = "predicates-tree" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" version = "0.1.23" @@ -4414,6 +4504,7 @@ name = "reth-consensus" version = "0.1.0" dependencies = [ "assert_matches", + "mockall", "reth-interfaces", "reth-primitives", "reth-provider", @@ -6145,6 +6236,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" + [[package]] name = "test-fuzz" version = "3.0.5" diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index 1cd67f69d9..fbd98504de 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -19,3 +19,4 @@ tokio = { version = "1", features = ["sync"] } reth-interfaces = { path = "../interfaces", features = ["test-utils"] } reth-provider = { path = "../storage/provider", features = ["test-utils"] } assert_matches = "1.5.0" +mockall = "0.11.3" diff --git a/crates/consensus/src/validation.rs b/crates/consensus/src/validation.rs index ff4310757b..60364f3d8a 100644 --- a/crates/consensus/src/validation.rs +++ b/crates/consensus/src/validation.rs @@ -4,7 +4,7 @@ use reth_primitives::{ BlockNumber, ChainSpec, Hardfork, Header, InvalidTransactionError, SealedBlock, SealedHeader, Transaction, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxLegacy, }; -use reth_provider::{AccountProvider, HeaderProvider}; +use reth_provider::{AccountProvider, HeaderProvider, WithdrawalsProvider}; use std::{ collections::{hash_map::Entry, HashMap}, time::SystemTime, @@ -348,9 +348,10 @@ pub fn validate_header_regarding_parent( /// Checks: /// If we already know the block. /// If parent is known +/// If withdarwals are valid /// /// Returns parent block header -pub fn validate_block_regarding_chain( +pub fn validate_block_regarding_chain( block: &SealedBlock, provider: &PROV, ) -> RethResult { @@ -366,12 +367,39 @@ pub fn validate_block_regarding_chain( .header(&block.parent_hash)? .ok_or(ConsensusError::ParentUnknown { hash: block.parent_hash })?; + // Check if withdrawals are valid. + if let Some(withdrawals) = &block.withdrawals { + if !withdrawals.is_empty() { + let latest_withdrawal = provider.latest_withdrawal()?; + match latest_withdrawal { + Some(withdrawal) => { + if withdrawal.index + 1 != withdrawals.first().unwrap().index { + return Err(ConsensusError::WithdrawalIndexInvalid { + got: withdrawals.first().unwrap().index, + expected: withdrawal.index + 1, + } + .into()) + } + } + None => { + if withdrawals.first().unwrap().index != 0 { + return Err(ConsensusError::WithdrawalIndexInvalid { + got: withdrawals.first().unwrap().index, + expected: 0, + } + .into()) + } + } + } + } + } + // Return parent header. Ok(parent.seal(block.parent_hash)) } /// Full validation of block before execution. -pub fn full_validation( +pub fn full_validation( block: &SealedBlock, provider: Provider, chain_spec: &ChainSpec, @@ -401,10 +429,11 @@ pub fn full_validation( mod tests { use super::*; use assert_matches::assert_matches; - use reth_interfaces::Result; + use mockall::mock; + use reth_interfaces::{Error::Consensus, Result}; use reth_primitives::{ - hex_literal::hex, proofs, Account, Address, BlockHash, Bytes, ChainSpecBuilder, Header, - Signature, TransactionKind, TransactionSigned, Withdrawal, MAINNET, U256, + hex_literal::hex, proofs, Account, Address, BlockHash, BlockId, Bytes, ChainSpecBuilder, + Header, Signature, TransactionKind, TransactionSigned, Withdrawal, MAINNET, U256, }; use std::ops::RangeBounds; @@ -435,20 +464,45 @@ mod tests { } } + mock! { + WithdrawalsProvider {} + + impl WithdrawalsProvider for WithdrawalsProvider { + fn latest_withdrawal(&self) -> Result> ; + + fn withdrawals_by_block( + &self, + _id: BlockId, + _timestamp: u64, + ) -> RethResult>> ; + } + } + struct Provider { is_known: bool, parent: Option
, account: Option, + withdrawals_provider: MockWithdrawalsProvider, } impl Provider { /// New provider with parent fn new(parent: Option
) -> Self { - Self { is_known: false, parent, account: None } + Self { + is_known: false, + parent, + account: None, + withdrawals_provider: MockWithdrawalsProvider::new(), + } } /// New provider where is_known is always true fn new_known() -> Self { - Self { is_known: true, parent: None, account: None } + Self { + is_known: true, + parent: None, + account: None, + withdrawals_provider: MockWithdrawalsProvider::new(), + } } } @@ -484,6 +538,20 @@ mod tests { } } + impl WithdrawalsProvider for Provider { + fn latest_withdrawal(&self) -> Result> { + self.withdrawals_provider.latest_withdrawal() + } + + fn withdrawals_by_block( + &self, + _id: BlockId, + _timestamp: u64, + ) -> RethResult>> { + self.withdrawals_provider.withdrawals_by_block(_id, _timestamp) + } + } + fn mock_tx(nonce: u64) -> TransactionSignedEcRecovered { let request = Transaction::Eip2930(TxEip2930 { chain_id: 1u64, @@ -525,7 +593,7 @@ mod tests { mix_hash: hex!("0000000000000000000000000000000000000000000000000000000000000000").into(), nonce: 0x0000000000000000, base_fee_per_gas: 0x28f0001df.into(), - withdrawals_root: None + withdrawals_root: None, }; // size: 0x9b5 @@ -656,6 +724,39 @@ mod tests { validate_block_standalone(&block, &chain_spec), Err(ConsensusError::WithdrawalIndexInvalid { .. }) ); + + let (_, parent) = mock_block(); + let mut provider = Provider::new(Some(parent.clone())); + // Withdrawal index should be 0 if there are no withdrawals in the chain + let block = create_block_with_withdrawals(&[1, 2, 3]); + provider.withdrawals_provider.expect_latest_withdrawal().return_const(Ok(None)); + assert_matches!( + validate_block_regarding_chain(&block, &provider), + Err(Consensus(ConsensusError::WithdrawalIndexInvalid { got: 1, expected: 0 })) + ); + let block = create_block_with_withdrawals(&[0, 1, 2]); + let res = validate_block_regarding_chain(&block, &provider); + assert!(res.is_ok()); + + // Withdrawal index should be the last withdrawal index + 1 + let mut provider = Provider::new(Some(parent.clone())); + let block = create_block_with_withdrawals(&[4, 5, 6]); + provider + .withdrawals_provider + .expect_latest_withdrawal() + .return_const(Ok(Some(Withdrawal { index: 2, ..Default::default() }))); + assert_matches!( + validate_block_regarding_chain(&block, &provider), + Err(Consensus(ConsensusError::WithdrawalIndexInvalid { got: 4, expected: 3 })) + ); + + let block = create_block_with_withdrawals(&[3, 4, 5]); + provider + .withdrawals_provider + .expect_latest_withdrawal() + .return_const(Ok(Some(Withdrawal { index: 2, ..Default::default() }))); + let res = validate_block_regarding_chain(&block, &provider); + assert!(res.is_ok()); } #[test] diff --git a/crates/storage/provider/src/providers/mod.rs b/crates/storage/provider/src/providers/mod.rs index 6b090dba42..bd5df0818b 100644 --- a/crates/storage/provider/src/providers/mod.rs +++ b/crates/storage/provider/src/providers/mod.rs @@ -267,6 +267,17 @@ impl WithdrawalsProvider for ShareableDatabase { } Ok(None) } + + fn latest_withdrawal(&self) -> Result> { + let latest_block_withdrawal = + self.db.view(|tx| tx.cursor_read::()?.last())?; + latest_block_withdrawal + .map(|block_withdrawal_pair| { + block_withdrawal_pair + .and_then(|(_, block_withdrawal)| block_withdrawal.withdrawals.last().cloned()) + }) + .map_err(Into::into) + } } impl EvmEnvProvider for ShareableDatabase { diff --git a/crates/storage/provider/src/traits/withdrawals.rs b/crates/storage/provider/src/traits/withdrawals.rs index 2f37e3bc27..9a6fe80450 100644 --- a/crates/storage/provider/src/traits/withdrawals.rs +++ b/crates/storage/provider/src/traits/withdrawals.rs @@ -5,4 +5,7 @@ use reth_primitives::{BlockId, Withdrawal}; pub trait WithdrawalsProvider: Send + Sync { /// Get withdrawals by block id. fn withdrawals_by_block(&self, id: BlockId, timestamp: u64) -> Result>>; + + /// Get latest withdrawal from this block or earlier . + fn latest_withdrawal(&self) -> Result>; }