From 79b8ffb8288f8d25d4eb31769cabfc57205ac63f Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 17 Jan 2026 02:24:53 +0100 Subject: [PATCH] feat(primitives-traits): add try_recover_signers for parallel batch recovery (#21103) --- .../src/transaction/recover.rs | 183 ++++++++++++------ crates/rpc/rpc/src/testing.rs | 27 ++- 2 files changed, 144 insertions(+), 66 deletions(-) diff --git a/crates/primitives-traits/src/transaction/recover.rs b/crates/primitives-traits/src/transaction/recover.rs index 59e6e8a694..e23b962fd1 100644 --- a/crates/primitives-traits/src/transaction/recover.rs +++ b/crates/primitives-traits/src/transaction/recover.rs @@ -1,68 +1,137 @@ //! Helpers for recovering signers from a set of transactions -#[cfg(feature = "rayon")] -pub use rayon::*; - -#[cfg(not(feature = "rayon"))] -pub use iter::*; +use crate::{transaction::signed::RecoveryError, Recovered, SignedTransaction}; +use alloc::vec::Vec; +use alloy_consensus::transaction::SignerRecoverable; +use alloy_primitives::Address; #[cfg(feature = "rayon")] -mod rayon { - use crate::{transaction::signed::RecoveryError, SignedTransaction}; - use alloc::vec::Vec; - use alloy_primitives::Address; - use rayon::prelude::{IntoParallelIterator, ParallelIterator}; +use rayon::prelude::{IntoParallelIterator, ParallelIterator}; - /// Recovers a list of signers from a transaction list iterator. - /// - /// Returns `Err(RecoveryError)`, if some transaction's signature is invalid - pub fn recover_signers<'a, I, T>(txes: I) -> Result, RecoveryError> - where - T: SignedTransaction, - I: IntoParallelIterator, - { - txes.into_par_iter().map(|tx| tx.recover_signer()).collect() - } - - /// Recovers a list of signers from a transaction list iterator _without ensuring that the - /// signature has a low `s` value_. - /// - /// Returns `Err(RecoveryError)`, if some transaction's signature is invalid. - pub fn recover_signers_unchecked<'a, I, T>(txes: I) -> Result, RecoveryError> - where - T: SignedTransaction, - I: IntoParallelIterator, - { - txes.into_par_iter().map(|tx| tx.recover_signer_unchecked()).collect() - } +/// Recovers a list of signers from a transaction list iterator. +/// +/// Returns `Err(RecoveryError)`, if some transaction's signature is invalid. +/// +/// When the `rayon` feature is enabled, recovery is performed in parallel. +#[cfg(feature = "rayon")] +pub fn recover_signers<'a, I, T>(txes: I) -> Result, RecoveryError> +where + T: SignedTransaction, + I: IntoParallelIterator, +{ + txes.into_par_iter().map(|tx| tx.recover_signer()).collect() } +/// Recovers a list of signers from a transaction list iterator. +/// +/// Returns `Err(RecoveryError)`, if some transaction's signature is invalid. +#[cfg(not(feature = "rayon"))] +pub fn recover_signers<'a, I, T>(txes: I) -> Result, RecoveryError> +where + T: SignedTransaction, + I: IntoIterator, +{ + txes.into_iter().map(|tx| tx.recover_signer()).collect() +} + +/// Recovers a list of signers from a transaction list iterator _without ensuring that the +/// signature has a low `s` value_. +/// +/// Returns `Err(RecoveryError)`, if some transaction's signature is invalid. +/// +/// When the `rayon` feature is enabled, recovery is performed in parallel. +#[cfg(feature = "rayon")] +pub fn recover_signers_unchecked<'a, I, T>(txes: I) -> Result, RecoveryError> +where + T: SignedTransaction, + I: IntoParallelIterator, +{ + txes.into_par_iter().map(|tx| tx.recover_signer_unchecked()).collect() +} + +/// Recovers a list of signers from a transaction list iterator _without ensuring that the +/// signature has a low `s` value_. +/// +/// Returns `Err(RecoveryError)`, if some transaction's signature is invalid. +#[cfg(not(feature = "rayon"))] +pub fn recover_signers_unchecked<'a, I, T>(txes: I) -> Result, RecoveryError> +where + T: SignedTransaction, + I: IntoIterator, +{ + txes.into_iter().map(|tx| tx.recover_signer_unchecked()).collect() +} + +/// Trait for items that can be used with [`try_recover_signers`]. +#[cfg(feature = "rayon")] +pub trait TryRecoverItems: IntoParallelIterator {} + +/// Trait for items that can be used with [`try_recover_signers`]. +#[cfg(not(feature = "rayon"))] +pub trait TryRecoverItems: IntoIterator {} + +#[cfg(feature = "rayon")] +impl TryRecoverItems for I {} + #[cfg(not(feature = "rayon"))] -mod iter { - use crate::{transaction::signed::RecoveryError, SignedTransaction}; - use alloc::vec::Vec; - use alloy_primitives::Address; +impl TryRecoverItems for I {} - /// Recovers a list of signers from a transaction list iterator. - /// - /// Returns `Err(RecoveryError)`, if some transaction's signature is invalid - pub fn recover_signers<'a, I, T>(txes: I) -> Result, RecoveryError> - where - T: SignedTransaction, - I: IntoIterator, - { - txes.into_iter().map(|tx| tx.recover_signer()).collect() - } +/// Trait for decode functions that can be used with [`try_recover_signers`]. +#[cfg(feature = "rayon")] +pub trait TryRecoverFn: Fn(Item) -> Result + Sync {} - /// Recovers a list of signers from a transaction list iterator _without ensuring that the - /// signature has a low `s` value_. - /// - /// Returns `Err(RecoveryError)`, if some transaction's signature is invalid. - pub fn recover_signers_unchecked<'a, I, T>(txes: I) -> Result, RecoveryError> - where - T: SignedTransaction, - I: IntoIterator, - { - txes.into_iter().map(|tx| tx.recover_signer_unchecked()).collect() - } +/// Trait for decode functions that can be used with [`try_recover_signers`]. +#[cfg(not(feature = "rayon"))] +pub trait TryRecoverFn: Fn(Item) -> Result {} + +#[cfg(feature = "rayon")] +impl Result + Sync> TryRecoverFn for F {} + +#[cfg(not(feature = "rayon"))] +impl Result> TryRecoverFn for F {} + +/// Decodes and recovers a list of [`Recovered`] transactions from an iterator. +/// +/// The `decode` closure transforms each item into a [`SignedTransaction`], which is then +/// recovered. +/// +/// Returns an error if decoding or signature recovery fails for any transaction. +/// +/// When the `rayon` feature is enabled, recovery is performed in parallel. +#[cfg(feature = "rayon")] +pub fn try_recover_signers(items: I, decode: F) -> Result>, RecoveryError> +where + I: IntoParallelIterator, + F: Fn(I::Item) -> Result + Sync, + T: SignedTransaction, +{ + items + .into_par_iter() + .map(|item| { + let tx = decode(item)?; + SignerRecoverable::try_into_recovered(tx) + }) + .collect() +} + +/// Decodes and recovers a list of [`Recovered`] transactions from an iterator. +/// +/// The `decode` closure transforms each item into a [`SignedTransaction`], which is then +/// recovered. +/// +/// Returns an error if decoding or signature recovery fails for any transaction. +#[cfg(not(feature = "rayon"))] +pub fn try_recover_signers(items: I, decode: F) -> Result>, RecoveryError> +where + I: IntoIterator, + F: Fn(I::Item) -> Result, + T: SignedTransaction, +{ + items + .into_iter() + .map(|item| { + let tx = decode(item)?; + SignerRecoverable::try_into_recovered(tx) + }) + .collect() } diff --git a/crates/rpc/rpc/src/testing.rs b/crates/rpc/rpc/src/testing.rs index dfaea2bb54..94e95edae0 100644 --- a/crates/rpc/rpc/src/testing.rs +++ b/crates/rpc/rpc/src/testing.rs @@ -15,6 +15,7 @@ //! on public-facing RPC endpoints without proper authentication. use alloy_consensus::{Header, Transaction}; +use alloy_eips::eip2718::Decodable2718; use alloy_evm::Evm; use alloy_primitives::{map::HashSet, Address, U256}; use alloy_rpc_types_engine::ExecutionPayloadEnvelopeV5; @@ -24,11 +25,14 @@ use reth_errors::RethError; use reth_ethereum_engine_primitives::EthBuiltPayload; use reth_ethereum_primitives::EthPrimitives; use reth_evm::{execute::BlockBuilder, ConfigureEvm, NextBlockEnvAttributes}; -use reth_primitives_traits::{AlloyBlockHeader as BlockTrait, Recovered, TxTy}; +use reth_primitives_traits::{ + transaction::{recover::try_recover_signers, signed::RecoveryError}, + AlloyBlockHeader as BlockTrait, TxTy, +}; use reth_revm::{database::StateProviderDatabase, db::State}; use reth_rpc_api::{TestingApiServer, TestingBuildBlockRequestV1}; use reth_rpc_eth_api::{helpers::Call, FromEthApiError}; -use reth_rpc_eth_types::{utils::recover_raw_transaction, EthApiError}; +use reth_rpc_eth_types::EthApiError; use reth_storage_api::{BlockReader, HeaderProvider}; use revm::context::Block; use revm_primitives::map::DefaultHashBuilder; @@ -106,11 +110,16 @@ where let mut invalid_senders: HashSet = HashSet::default(); - for (idx, tx) in request.transactions.iter().enumerate() { - let tx: Recovered> = recover_raw_transaction(tx)?; - let sender = tx.signer(); + // Decode and recover all transactions in parallel + let recovered_txs = try_recover_signers(&request.transactions, |tx| { + TxTy::::decode_2718_exact(tx.as_ref()) + .map_err(RecoveryError::from_source) + }) + .or(Err(EthApiError::InvalidTransactionSignature))?; - if skip_invalid_transactions && invalid_senders.contains(&sender) { + for (idx, tx) in recovered_txs.into_iter().enumerate() { + let signer = tx.signer(); + if skip_invalid_transactions && invalid_senders.contains(&signer) { continue; } @@ -122,17 +131,17 @@ where debug!( target: "rpc::testing", tx_idx = idx, - ?sender, + ?signer, error = ?err, "Skipping invalid transaction" ); - invalid_senders.insert(sender); + invalid_senders.insert(signer); continue; } debug!( target: "rpc::testing", tx_idx = idx, - ?sender, + ?signer, error = ?err, "Transaction execution failed" );