From 12f1e9c944f75b70a360ed48f17b7a724028038d Mon Sep 17 00:00:00 2001 From: Dan Cline <6798349+Rjected@users.noreply.github.com> Date: Mon, 13 May 2024 17:07:37 +0300 Subject: [PATCH 1/5] chore: remove validate_block_regarding_chain (#8224) --- crates/consensus/common/Cargo.toml | 2 -- crates/consensus/common/src/validation.rs | 39 +---------------------- 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/crates/consensus/common/Cargo.toml b/crates/consensus/common/Cargo.toml index af93788ee6..accf2d08e0 100644 --- a/crates/consensus/common/Cargo.toml +++ b/crates/consensus/common/Cargo.toml @@ -13,8 +13,6 @@ workspace = true [dependencies] # reth reth-primitives.workspace = true -reth-interfaces.workspace = true -reth-provider.workspace = true reth-consensus.workspace=true [dev-dependencies] diff --git a/crates/consensus/common/src/validation.rs b/crates/consensus/common/src/validation.rs index b67d40e985..bf94937209 100644 --- a/crates/consensus/common/src/validation.rs +++ b/crates/consensus/common/src/validation.rs @@ -1,7 +1,6 @@ //! Collection of methods for block validation. use reth_consensus::ConsensusError; -use reth_interfaces::RethResult; use reth_primitives::{ constants::{ eip4844::{DATA_GAS_PER_BLOB, MAX_DATA_GAS_PER_BLOCK}, @@ -9,7 +8,6 @@ use reth_primitives::{ }, ChainSpec, GotExpected, Hardfork, Header, SealedBlock, SealedHeader, }; -use reth_provider::{HeaderProvider, WithdrawalsProvider}; /// Validate header standalone pub fn validate_header_standalone( @@ -110,33 +108,6 @@ pub fn validate_block_standalone( Ok(()) } -/// Validate block with regard to chain (parent) -/// -/// Checks: -/// If we already know the block. -/// If parent is known -/// -/// Returns parent block header -pub fn validate_block_regarding_chain( - block: &SealedBlock, - provider: &PROV, -) -> RethResult { - let hash = block.header.hash(); - - // Check if block is known. - if provider.is_known(&hash)? { - return Err(ConsensusError::BlockKnown { hash, number: block.header.number }.into()) - } - - // Check if parent is known. - let parent = provider - .header(&block.parent_hash)? - .ok_or(ConsensusError::ParentUnknown { hash: block.parent_hash })?; - - // Return parent header. - Ok(parent.seal(block.parent_hash)) -} - /// Validates that the EIP-4844 header fields exist and conform to the spec. This ensures that: /// /// * `blob_gas_used` exists as a header field @@ -204,7 +175,7 @@ mod tests { BlockNumber, Bytes, ChainSpecBuilder, Signature, Transaction, TransactionSigned, TxEip4844, Withdrawal, Withdrawals, U256, }; - use reth_provider::AccountReader; + use reth_provider::{AccountReader, HeaderProvider, WithdrawalsProvider}; use std::ops::RangeBounds; mock! { @@ -398,22 +369,14 @@ mod tests { assert_eq!(validate_block_standalone(&block, &chain_spec), Ok(())); let block = create_block_with_withdrawals(&[5, 6, 7, 8, 9]); assert_eq!(validate_block_standalone(&block, &chain_spec), Ok(())); - let (_, parent) = mock_block(); - let provider = Provider::new(Some(parent.clone())); - 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)); - 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] From 081796b138fd00a6d0af2ad463123fdbe04765ab Mon Sep 17 00:00:00 2001 From: joshieDo <93316087+joshieDo@users.noreply.github.com> Date: Mon, 13 May 2024 17:39:06 +0300 Subject: [PATCH 2/5] feat: impl `Compact` for `FixedBytes` (#8222) --- .../codecs/derive/src/compact/generator.rs | 3 ++- crates/storage/codecs/src/lib.rs | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/storage/codecs/derive/src/compact/generator.rs b/crates/storage/codecs/derive/src/compact/generator.rs index 03dab1a144..c28bf8d1a4 100644 --- a/crates/storage/codecs/derive/src/compact/generator.rs +++ b/crates/storage/codecs/derive/src/compact/generator.rs @@ -52,7 +52,8 @@ pub fn generate_from_to(ident: &Ident, fields: &FieldList, is_zstd: bool) -> Tok /// Generates code to implement the `Compact` trait method `to_compact`. fn generate_from_compact(fields: &FieldList, ident: &Ident, is_zstd: bool) -> TokenStream2 { let mut lines = vec![]; - let mut known_types = vec!["B256", "Address", "Bloom", "Vec", "TxHash", "BlockHash"]; + let mut known_types = + vec!["B256", "Address", "Bloom", "Vec", "TxHash", "BlockHash", "FixedBytes"]; // Only types without `Bytes` should be added here. It's currently manually added, since // it's hard to figure out with derive_macro which types have Bytes fields. diff --git a/crates/storage/codecs/src/lib.rs b/crates/storage/codecs/src/lib.rs index 9c5d757b9f..907fee440d 100644 --- a/crates/storage/codecs/src/lib.rs +++ b/crates/storage/codecs/src/lib.rs @@ -17,7 +17,7 @@ pub use reth_codecs_derive::*; -use alloy_primitives::{Address, Bloom, Bytes, B256, B512, U256}; +use alloy_primitives::{Address, Bloom, Bytes, FixedBytes, U256}; use bytes::Buf; #[cfg(any(test, feature = "alloy"))] @@ -301,9 +301,9 @@ impl Compact for [u8; N] { } } -/// Implements the [`Compact`] trait for fixed size byte array types like [`B256`]. +/// Implements the [`Compact`] trait for wrappers over fixed size byte array types. #[macro_export] -macro_rules! impl_compact_for_bytes { +macro_rules! impl_compact_for_wrapped_bytes { ($($name:tt),+) => { $( impl Compact for $name { @@ -324,8 +324,23 @@ macro_rules! impl_compact_for_bytes { )+ }; } +impl_compact_for_wrapped_bytes!(Address, Bloom); -impl_compact_for_bytes!(Address, B256, B512, Bloom); +impl Compact for FixedBytes { + #[inline] + fn to_compact(self, buf: &mut B) -> usize + where + B: bytes::BufMut + AsMut<[u8]>, + { + self.0.to_compact(buf) + } + + #[inline] + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + let (v, buf) = <[u8; N]>::from_compact(buf, len); + (Self::from(v), buf) + } +} impl Compact for bool { /// `bool` vars go directly to the `StructFlags` and are not written to the buffer. @@ -378,6 +393,7 @@ const fn decode_varuint_panic() -> ! { #[cfg(test)] mod tests { use super::*; + use alloy_primitives::B256; #[test] fn compact_bytes() { From 19e5fcb003aaa1a1caa7f502d791c91f2f83bd54 Mon Sep 17 00:00:00 2001 From: Delweng Date: Tue, 14 May 2024 20:41:32 +0800 Subject: [PATCH 3/5] docs(network): update command instruction for the --trusted-only (#8246) Signed-off-by: jsvisa Co-authored-by: Matthias Seitz --- crates/net/network/src/peers/manager.rs | 2 +- crates/node-core/src/args/network.rs | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/net/network/src/peers/manager.rs b/crates/net/network/src/peers/manager.rs index d6ae9c4da8..e13b080afa 100644 --- a/crates/net/network/src/peers/manager.rs +++ b/crates/net/network/src/peers/manager.rs @@ -1276,7 +1276,7 @@ pub struct PeersConfig { /// How often to recheck free slots for outbound connections. #[cfg_attr(feature = "serde", serde(with = "humantime_serde"))] pub refill_slots_interval: Duration, - /// Trusted nodes to connect to. + /// Trusted nodes to connect to or accept from pub trusted_nodes: HashSet, /// Connect to or accept from trusted nodes only? #[cfg_attr(feature = "serde", serde(alias = "connect_trusted_nodes_only"))] diff --git a/crates/node-core/src/args/network.rs b/crates/node-core/src/args/network.rs index 9ff93c5a9d..8202739bc9 100644 --- a/crates/node-core/src/args/network.rs +++ b/crates/node-core/src/args/network.rs @@ -21,6 +21,7 @@ use reth_primitives::{mainnet_nodes, ChainSpec, NodeRecord}; use secp256k1::SecretKey; use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, + ops::Not, path::PathBuf, sync::Arc, }; @@ -39,7 +40,7 @@ pub struct NetworkArgs { #[arg(long, value_delimiter = ',')] pub trusted_peers: Vec, - /// Connect only to trusted peers + /// Connect to or accept from trusted peers only #[arg(long)] pub trusted_only: bool, @@ -156,13 +157,9 @@ impl NetworkArgs { self.discovery.apply_to_builder(network_config_builder) } - /// If `no_persist_peers` is true then this returns the path to the persistent peers file path. + /// If `no_persist_peers` is false then this returns the path to the persistent peers file path. pub fn persistent_peers_file(&self, peers_file: PathBuf) -> Option { - if self.no_persist_peers { - return None - } - - Some(peers_file) + self.no_persist_peers.not().then_some(peers_file) } /// Sets the p2p port to zero, to allow the OS to assign a random unused port when @@ -258,12 +255,12 @@ pub struct DiscoveryArgs { /// The interval in seconds at which to carry out boost lookup queries, for a fixed number of /// times, at bootstrap. - #[arg(id = "discovery.v5.bootstrap.lookup-interval", long = "discovery.v5.bootstrap.lookup-interval", value_name = "DISCOVERY_V5_bootstrap_lookup_interval", + #[arg(id = "discovery.v5.bootstrap.lookup-interval", long = "discovery.v5.bootstrap.lookup-interval", value_name = "DISCOVERY_V5_bootstrap_lookup_interval", default_value_t = DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL)] pub discv5_bootstrap_lookup_interval: u64, /// The number of times to carry out boost lookup queries at bootstrap. - #[arg(id = "discovery.v5.bootstrap.lookup-countdown", long = "discovery.v5.bootstrap.lookup-countdown", value_name = "DISCOVERY_V5_bootstrap_lookup_countdown", + #[arg(id = "discovery.v5.bootstrap.lookup-countdown", long = "discovery.v5.bootstrap.lookup-countdown", value_name = "DISCOVERY_V5_bootstrap_lookup_countdown", default_value_t = DEFAULT_COUNT_BOOTSTRAP_LOOKUPS)] pub discv5_bootstrap_lookup_countdown: u64, } From d1f38f16613417ef1d964b9dd383fdcef25950c5 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 14 May 2024 17:27:33 +0200 Subject: [PATCH 4/5] feat: proof verification (#8220) --- Cargo.lock | 5 +-- Cargo.toml | 2 +- crates/primitives/src/trie/mod.rs | 2 +- crates/primitives/src/trie/proofs.rs | 44 +++++++++++++++++-- .../storage/provider/src/test_utils/mock.rs | 4 +- .../storage/provider/src/test_utils/noop.rs | 4 +- crates/trie/src/proof.rs | 34 ++++++++------ 7 files changed, 70 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d46121423..1d4f74de6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -619,9 +619,9 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb28aa4ecd32fdfa1b1bdd111ff7357dd562c6b2372694cf9e613434fcba659" +checksum = "d55bd16fdb7ff4bd74cc4c878eeac7e8a27c0d7ba9df4ab58d9310aaafb62d43" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -633,7 +633,6 @@ dependencies = [ "proptest", "proptest-derive", "serde", - "smallvec", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 3492f7d313..102fffa88a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -290,7 +290,7 @@ alloy-primitives = "0.7.2" alloy-dyn-abi = "0.7.2" alloy-sol-types = "0.7.2" alloy-rlp = "0.3.4" -alloy-trie = "0.3.1" +alloy-trie = "0.4.0" alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "dd7a999" } alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", rev = "dd7a999" } alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", rev = "dd7a999" } diff --git a/crates/primitives/src/trie/mod.rs b/crates/primitives/src/trie/mod.rs index 848aaed646..ed61aca39b 100644 --- a/crates/primitives/src/trie/mod.rs +++ b/crates/primitives/src/trie/mod.rs @@ -24,4 +24,4 @@ pub use storage::StorageTrieEntry; mod subnode; pub use subnode::StoredSubNode; -pub use alloy_trie::{BranchNodeCompact, HashBuilder, TrieMask, EMPTY_ROOT_HASH}; +pub use alloy_trie::{proof, BranchNodeCompact, HashBuilder, TrieMask, EMPTY_ROOT_HASH}; diff --git a/crates/primitives/src/trie/proofs.rs b/crates/primitives/src/trie/proofs.rs index 094f4d29df..1949867be5 100644 --- a/crates/primitives/src/trie/proofs.rs +++ b/crates/primitives/src/trie/proofs.rs @@ -1,10 +1,15 @@ //! Merkle trie proofs. -use super::Nibbles; +use super::{ + proof::{verify_proof, ProofVerificationError}, + Nibbles, TrieAccount, +}; use crate::{keccak256, Account, Address, Bytes, B256, U256}; +use alloy_rlp::encode_fixed_size; +use alloy_trie::EMPTY_ROOT_HASH; /// The merkle proof with the relevant account info. -#[derive(PartialEq, Eq, Default, Debug)] +#[derive(PartialEq, Eq, Debug)] pub struct AccountProof { /// The address associated with the account. pub address: Address, @@ -22,7 +27,13 @@ pub struct AccountProof { impl AccountProof { /// Create new account proof entity. pub fn new(address: Address) -> Self { - Self { address, ..Default::default() } + Self { + address, + info: None, + proof: Vec::new(), + storage_root: EMPTY_ROOT_HASH, + storage_proofs: Vec::new(), + } } /// Set account info, storage root and requested storage proofs. @@ -41,6 +52,26 @@ impl AccountProof { pub fn set_proof(&mut self, proof: Vec) { self.proof = proof; } + + /// Verify the storage proofs and account proof against the provided state root. + pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> { + // Verify storage proofs. + for storage_proof in &self.storage_proofs { + storage_proof.verify(self.storage_root)?; + } + + // Verify the account proof. + let expected = if self.info.is_none() && self.storage_root == EMPTY_ROOT_HASH { + None + } else { + Some(alloy_rlp::encode(TrieAccount::from(( + self.info.unwrap_or_default(), + self.storage_root, + )))) + }; + let nibbles = Nibbles::unpack(keccak256(self.address)); + verify_proof(root, nibbles, expected, &self.proof) + } } /// The merkle proof of the storage entry. @@ -83,4 +114,11 @@ impl StorageProof { pub fn set_proof(&mut self, proof: Vec) { self.proof = proof; } + + /// Verify the proof against the provided storage root. + pub fn verify(&self, root: B256) -> Result<(), ProofVerificationError> { + let expected = + if self.value.is_zero() { None } else { Some(encode_fixed_size(&self.value).to_vec()) }; + verify_proof(root, self.nibbles.clone(), expected, &self.proof) + } } diff --git a/crates/storage/provider/src/test_utils/mock.rs b/crates/storage/provider/src/test_utils/mock.rs index db490bd37c..96e137ac6f 100644 --- a/crates/storage/provider/src/test_utils/mock.rs +++ b/crates/storage/provider/src/test_utils/mock.rs @@ -560,8 +560,8 @@ impl StateProvider for MockEthProvider { })) } - fn proof(&self, _address: Address, _keys: &[B256]) -> ProviderResult { - Ok(AccountProof::default()) + fn proof(&self, address: Address, _keys: &[B256]) -> ProviderResult { + Ok(AccountProof::new(address)) } } diff --git a/crates/storage/provider/src/test_utils/noop.rs b/crates/storage/provider/src/test_utils/noop.rs index 626bd53511..373dc4d7e5 100644 --- a/crates/storage/provider/src/test_utils/noop.rs +++ b/crates/storage/provider/src/test_utils/noop.rs @@ -319,8 +319,8 @@ impl StateProvider for NoopProvider { Ok(None) } - fn proof(&self, _address: Address, _keys: &[B256]) -> ProviderResult { - Ok(AccountProof::default()) + fn proof(&self, address: Address, _keys: &[B256]) -> ProviderResult { + Ok(AccountProof::new(address)) } } diff --git a/crates/trie/src/proof.rs b/crates/trie/src/proof.rs index 55eb47710f..80f0c552e3 100644 --- a/crates/trie/src/proof.rs +++ b/crates/trie/src/proof.rs @@ -11,7 +11,7 @@ use reth_interfaces::trie::{StateRootError, StorageRootError}; use reth_primitives::{ constants::EMPTY_ROOT_HASH, keccak256, - trie::{AccountProof, HashBuilder, Nibbles, StorageProof, TrieAccount}, + trie::{proof::ProofRetainer, AccountProof, HashBuilder, Nibbles, StorageProof, TrieAccount}, Address, B256, }; @@ -60,8 +60,8 @@ where let walker = TrieWalker::new(trie_cursor, prefix_set.freeze()); // Create a hash builder to rebuild the root node since it is not available in the database. - let mut hash_builder = - HashBuilder::default().with_proof_retainer(Vec::from([target_nibbles])); + let retainer = ProofRetainer::from_iter([target_nibbles]); + let mut hash_builder = HashBuilder::default().with_proof_retainer(retainer); let mut account_rlp = Vec::with_capacity(128); let mut account_node_iter = AccountNodeIter::new(walker, hashed_account_cursor); @@ -126,7 +126,8 @@ where ); let walker = TrieWalker::new(trie_cursor, prefix_set); - let mut hash_builder = HashBuilder::default().with_proof_retainer(target_nibbles); + let retainer = ProofRetainer::from_iter(target_nibbles); + let mut hash_builder = HashBuilder::default().with_proof_retainer(retainer); let mut storage_node_iter = StorageNodeIter::new(walker, hashed_storage_cursor, hashed_address); while let Some(node) = storage_node_iter.try_next()? { @@ -200,7 +201,7 @@ mod tests { fn insert_genesis( provider_factory: &ProviderFactory, chain_spec: Arc, - ) -> RethResult<()> { + ) -> RethResult { let mut provider = provider_factory.provider_rw()?; // Hash accounts and insert them into hashing table. @@ -224,21 +225,21 @@ mod tests { }); provider.insert_storage_for_hashing(alloc_storage)?; - let (_, updates) = StateRoot::from_tx(provider.tx_ref()) + let (root, updates) = StateRoot::from_tx(provider.tx_ref()) .root_with_updates() .map_err(Into::::into)?; updates.flush(provider.tx_mut())?; provider.commit()?; - Ok(()) + Ok(root) } #[test] fn testspec_proofs() { // Create test database and insert genesis accounts. let factory = create_test_provider_factory(); - insert_genesis(&factory, TEST_SPEC.clone()).unwrap(); + let root = insert_genesis(&factory, TEST_SPEC.clone()).unwrap(); let data = Vec::from([ ( @@ -288,6 +289,7 @@ mod tests { expected_proof, "proof for {target:?} does not match" ); + assert_eq!(account_proof.verify(root), Ok(())); } } @@ -295,7 +297,7 @@ mod tests { fn testspec_empty_storage_proof() { // Create test database and insert genesis accounts. let factory = create_test_provider_factory(); - insert_genesis(&factory, TEST_SPEC.clone()).unwrap(); + let root = insert_genesis(&factory, TEST_SPEC.clone()).unwrap(); let target = Address::from_str("0x1ed9b1dd266b607ee278726d324b855a093394a6").unwrap(); let slots = Vec::from([B256::with_last_byte(1), B256::with_last_byte(3)]); @@ -306,15 +308,18 @@ mod tests { assert_eq!(slots.len(), account_proof.storage_proofs.len()); for (idx, slot) in slots.into_iter().enumerate() { - assert_eq!(account_proof.storage_proofs.get(idx), Some(&StorageProof::new(slot))); + let proof = account_proof.storage_proofs.get(idx).unwrap(); + assert_eq!(proof, &StorageProof::new(slot)); + assert_eq!(proof.verify(account_proof.storage_root), Ok(())); } + assert_eq!(account_proof.verify(root), Ok(())); } #[test] fn mainnet_genesis_account_proof() { // Create test database and insert genesis accounts. let factory = create_test_provider_factory(); - insert_genesis(&factory, MAINNET.clone()).unwrap(); + let root = insert_genesis(&factory, MAINNET.clone()).unwrap(); // Address from mainnet genesis allocation. // keccak256 - `0xcf67b71c90b0d523dd5004cf206f325748da347685071b34812e21801f5270c4` @@ -332,13 +337,14 @@ mod tests { let provider = factory.provider().unwrap(); let account_proof = Proof::new(provider.tx_ref()).account_proof(target, &[]).unwrap(); similar_asserts::assert_eq!(account_proof.proof, expected_account_proof); + assert_eq!(account_proof.verify(root), Ok(())); } #[test] fn mainnet_genesis_account_proof_nonexistent() { // Create test database and insert genesis accounts. let factory = create_test_provider_factory(); - insert_genesis(&factory, MAINNET.clone()).unwrap(); + let root = insert_genesis(&factory, MAINNET.clone()).unwrap(); // Address that does not exist in mainnet genesis allocation. // keccak256 - `0x18f415ffd7f66bb1924d90f0e82fb79ca8c6d8a3473cd9a95446a443b9db1761` @@ -354,13 +360,14 @@ mod tests { let provider = factory.provider().unwrap(); let account_proof = Proof::new(provider.tx_ref()).account_proof(target, &[]).unwrap(); similar_asserts::assert_eq!(account_proof.proof, expected_account_proof); + assert_eq!(account_proof.verify(root), Ok(())); } #[test] fn holesky_deposit_contract_proof() { // Create test database and insert genesis accounts. let factory = create_test_provider_factory(); - insert_genesis(&factory, HOLESKY.clone()).unwrap(); + let root = insert_genesis(&factory, HOLESKY.clone()).unwrap(); let target = Address::from_str("0x4242424242424242424242424242424242424242").unwrap(); // existent @@ -439,5 +446,6 @@ mod tests { let provider = factory.provider().unwrap(); let account_proof = Proof::new(provider.tx_ref()).account_proof(target, &slots).unwrap(); similar_asserts::assert_eq!(account_proof, expected); + assert_eq!(account_proof.verify(root), Ok(())); } } From 2e2c8e1d63c00e92a972c6a414fddfa7c4241d41 Mon Sep 17 00:00:00 2001 From: Federico Gimenez Date: Tue, 14 May 2024 20:33:50 +0200 Subject: [PATCH 5/5] fix: allow to call V1 methods post-Shanghai (#8250) --- crates/engine-primitives/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/engine-primitives/src/lib.rs b/crates/engine-primitives/src/lib.rs index 99edf521c0..aa2c446815 100644 --- a/crates/engine-primitives/src/lib.rs +++ b/crates/engine-primitives/src/lib.rs @@ -159,10 +159,6 @@ pub fn validate_withdrawals_presence( return Err(message_validation_kind .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1)) } - if is_shanghai_active { - return Err(message_validation_kind - .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai)) - } } EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 => { if is_shanghai_active && !has_withdrawals {