mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-01-08 23:08:19 -05:00
feat: Stateless validation function receives public keys corresponding to each transaction (#17841)
Co-authored-by: Wolfgang Welz <welzwo@gmail.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -10400,6 +10400,7 @@ dependencies = [
|
||||
"alloy-rpc-types-debug",
|
||||
"alloy-trie",
|
||||
"itertools 0.14.0",
|
||||
"k256",
|
||||
"reth-chainspec",
|
||||
"reth-consensus",
|
||||
"reth-errors",
|
||||
@@ -10410,6 +10411,7 @@ dependencies = [
|
||||
"reth-revm",
|
||||
"reth-trie-common",
|
||||
"reth-trie-sparse",
|
||||
"secp256k1 0.30.0",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"thiserror 2.0.16",
|
||||
|
||||
@@ -446,7 +446,7 @@ reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
|
||||
reth-stages = { path = "crates/stages/stages" }
|
||||
reth-stages-api = { path = "crates/stages/api" }
|
||||
reth-stages-types = { path = "crates/stages/types", default-features = false }
|
||||
reth-stateless = { path = "crates/stateless" }
|
||||
reth-stateless = { path = "crates/stateless", default-features = false }
|
||||
reth-static-file = { path = "crates/static-file/static-file" }
|
||||
reth-static-file-types = { path = "crates/static-file/types", default-features = false }
|
||||
reth-storage-api = { path = "crates/storage/storage-api", default-features = false }
|
||||
|
||||
@@ -36,3 +36,11 @@ thiserror.workspace = true
|
||||
itertools.workspace = true
|
||||
serde.workspace = true
|
||||
serde_with.workspace = true
|
||||
|
||||
k256 = { workspace = true, optional = true }
|
||||
secp256k1 = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["k256"]
|
||||
k256 = ["dep:k256"]
|
||||
secp256k1 = ["dep:secp256k1"]
|
||||
|
||||
@@ -35,9 +35,12 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod recover_block;
|
||||
/// Sparse trie implementation for stateless validation
|
||||
pub mod trie;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use recover_block::UncompressedPublicKey;
|
||||
#[doc(inline)]
|
||||
pub use trie::StatelessTrie;
|
||||
#[doc(inline)]
|
||||
|
||||
130
crates/stateless/src/recover_block.rs
Normal file
130
crates/stateless/src/recover_block.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use crate::validation::StatelessValidationError;
|
||||
use alloc::vec::Vec;
|
||||
use alloy_consensus::BlockHeader;
|
||||
use alloy_primitives::{Address, Signature, B256};
|
||||
use reth_chainspec::EthereumHardforks;
|
||||
use reth_ethereum_primitives::{Block, TransactionSigned};
|
||||
use reth_primitives_traits::{Block as _, RecoveredBlock};
|
||||
|
||||
#[cfg(all(feature = "k256", feature = "secp256k1"))]
|
||||
use k256 as _;
|
||||
|
||||
/// Serialized uncompressed public key
|
||||
pub type UncompressedPublicKey = [u8; 65];
|
||||
|
||||
/// Verifies all transactions in a block against a list of public keys and signatures.
|
||||
///
|
||||
/// Returns a `RecoveredBlock`
|
||||
pub(crate) fn recover_block_with_public_keys<ChainSpec>(
|
||||
block: Block,
|
||||
public_keys: Vec<UncompressedPublicKey>,
|
||||
chain_spec: &ChainSpec,
|
||||
) -> Result<RecoveredBlock<Block>, StatelessValidationError>
|
||||
where
|
||||
ChainSpec: EthereumHardforks,
|
||||
{
|
||||
if block.body().transactions.len() != public_keys.len() {
|
||||
return Err(StatelessValidationError::Custom(
|
||||
"Number of public keys must match number of transactions",
|
||||
));
|
||||
}
|
||||
|
||||
// Determine if we're in the Homestead fork for signature validation
|
||||
let is_homestead = chain_spec.is_homestead_active_at_block(block.header().number());
|
||||
|
||||
// Verify each transaction signature against its corresponding public key
|
||||
let senders = public_keys
|
||||
.iter()
|
||||
.zip(block.body().transactions())
|
||||
.map(|(vk, tx)| verify_and_compute_sender(vk, tx, is_homestead))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Create RecoveredBlock with verified senders
|
||||
let block_hash = block.hash_slow();
|
||||
Ok(RecoveredBlock::new(block, senders, block_hash))
|
||||
}
|
||||
|
||||
/// Verifies a transaction using its signature and the given public key.
|
||||
///
|
||||
/// Note: If the signature or the public key is incorrect, then this method
|
||||
/// will return an error.
|
||||
///
|
||||
/// Returns the address derived from the public key.
|
||||
fn verify_and_compute_sender(
|
||||
vk: &UncompressedPublicKey,
|
||||
tx: &TransactionSigned,
|
||||
is_homestead: bool,
|
||||
) -> Result<Address, StatelessValidationError> {
|
||||
let sig = tx.signature();
|
||||
|
||||
// non-normalized signatures are only valid pre-homestead
|
||||
let sig_is_normalized = sig.normalize_s().is_none();
|
||||
if is_homestead && !sig_is_normalized {
|
||||
return Err(StatelessValidationError::HomesteadSignatureNotNormalized);
|
||||
}
|
||||
let sig_hash = tx.signature_hash();
|
||||
#[cfg(all(feature = "k256", feature = "secp256k1"))]
|
||||
{
|
||||
let _ = verify_and_compute_sender_unchecked_k256;
|
||||
}
|
||||
#[cfg(feature = "secp256k1")]
|
||||
{
|
||||
verify_and_compute_sender_unchecked_secp256k1(vk, sig, sig_hash)
|
||||
}
|
||||
#[cfg(all(feature = "k256", not(feature = "secp256k1")))]
|
||||
{
|
||||
verify_and_compute_sender_unchecked_k256(vk, sig, sig_hash)
|
||||
}
|
||||
#[cfg(not(any(feature = "secp256k1", feature = "k256")))]
|
||||
{
|
||||
let _ = vk;
|
||||
let _ = tx;
|
||||
let _: B256 = sig_hash;
|
||||
let _: &Signature = sig;
|
||||
|
||||
unimplemented!("Must choose either k256 or secp256k1 feature")
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "k256")]
|
||||
fn verify_and_compute_sender_unchecked_k256(
|
||||
vk: &UncompressedPublicKey,
|
||||
sig: &Signature,
|
||||
sig_hash: B256,
|
||||
) -> Result<Address, StatelessValidationError> {
|
||||
use k256::ecdsa::{signature::hazmat::PrehashVerifier, VerifyingKey};
|
||||
|
||||
let vk =
|
||||
VerifyingKey::from_sec1_bytes(vk).map_err(|_| StatelessValidationError::SignerRecovery)?;
|
||||
|
||||
sig.to_k256()
|
||||
.and_then(|sig| vk.verify_prehash(sig_hash.as_slice(), &sig))
|
||||
.map_err(|_| StatelessValidationError::SignerRecovery)?;
|
||||
|
||||
Ok(Address::from_public_key(&vk))
|
||||
}
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
fn verify_and_compute_sender_unchecked_secp256k1(
|
||||
vk: &UncompressedPublicKey,
|
||||
sig: &Signature,
|
||||
sig_hash: B256,
|
||||
) -> Result<Address, StatelessValidationError> {
|
||||
use secp256k1::{ecdsa::Signature as SecpSignature, Message, PublicKey, SECP256K1};
|
||||
|
||||
let public_key =
|
||||
PublicKey::from_slice(vk).map_err(|_| StatelessValidationError::SignerRecovery)?;
|
||||
|
||||
let mut sig_bytes = [0u8; 64];
|
||||
sig_bytes[0..32].copy_from_slice(&sig.r().to_be_bytes::<32>());
|
||||
sig_bytes[32..64].copy_from_slice(&sig.s().to_be_bytes::<32>());
|
||||
|
||||
let signature = SecpSignature::from_compact(&sig_bytes)
|
||||
.map_err(|_| StatelessValidationError::SignerRecovery)?;
|
||||
|
||||
let message = Message::from_digest(sig_hash.0);
|
||||
SECP256K1
|
||||
.verify_ecdsa(&message, &signature, &public_key)
|
||||
.map_err(|_| StatelessValidationError::SignerRecovery)?;
|
||||
|
||||
Ok(Address::from_raw_public_key(&vk[1..]))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
recover_block::{recover_block_with_public_keys, UncompressedPublicKey},
|
||||
trie::{StatelessSparseTrie, StatelessTrie},
|
||||
witness_db::WitnessDatabase,
|
||||
ExecutionWitness,
|
||||
@@ -89,6 +90,14 @@ pub enum StatelessValidationError {
|
||||
expected: B256,
|
||||
},
|
||||
|
||||
/// Error during signer recovery.
|
||||
#[error("signer recovery failed")]
|
||||
SignerRecovery,
|
||||
|
||||
/// Error when signature has non-normalized s value in homestead block.
|
||||
#[error("signature s value not normalized for homestead block")]
|
||||
HomesteadSignatureNotNormalized,
|
||||
|
||||
/// Custom error.
|
||||
#[error("{0}")]
|
||||
Custom(&'static str),
|
||||
@@ -130,7 +139,8 @@ pub enum StatelessValidationError {
|
||||
/// If all steps succeed the function returns `Some` containing the hash of the validated
|
||||
/// `current_block`.
|
||||
pub fn stateless_validation<ChainSpec, E>(
|
||||
current_block: RecoveredBlock<Block>,
|
||||
current_block: Block,
|
||||
public_keys: Vec<UncompressedPublicKey>,
|
||||
witness: ExecutionWitness,
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
evm_config: E,
|
||||
@@ -141,6 +151,7 @@ where
|
||||
{
|
||||
stateless_validation_with_trie::<StatelessSparseTrie, ChainSpec, E>(
|
||||
current_block,
|
||||
public_keys,
|
||||
witness,
|
||||
chain_spec,
|
||||
evm_config,
|
||||
@@ -154,7 +165,8 @@ where
|
||||
///
|
||||
/// See `stateless_validation` for detailed documentation of the validation process.
|
||||
pub fn stateless_validation_with_trie<T, ChainSpec, E>(
|
||||
current_block: RecoveredBlock<Block>,
|
||||
current_block: Block,
|
||||
public_keys: Vec<UncompressedPublicKey>,
|
||||
witness: ExecutionWitness,
|
||||
chain_spec: Arc<ChainSpec>,
|
||||
evm_config: E,
|
||||
@@ -164,6 +176,8 @@ where
|
||||
ChainSpec: Send + Sync + EthChainSpec<Header = Header> + EthereumHardforks + Debug,
|
||||
E: ConfigureEvm<Primitives = EthPrimitives> + Clone + 'static,
|
||||
{
|
||||
let current_block = recover_block_with_public_keys(current_block, public_keys, &*chain_spec)?;
|
||||
|
||||
let mut ancestor_headers: Vec<_> = witness
|
||||
.headers
|
||||
.iter()
|
||||
|
||||
@@ -28,7 +28,7 @@ reth-evm.workspace = true
|
||||
reth-evm-ethereum.workspace = true
|
||||
reth-ethereum-consensus.workspace = true
|
||||
reth-revm = { workspace = true, features = ["std", "witness"] }
|
||||
reth-stateless = { workspace = true }
|
||||
reth-stateless = { workspace = true, features = ["secp256k1"] }
|
||||
reth-tracing.workspace = true
|
||||
reth-trie.workspace = true
|
||||
reth-trie-db.workspace = true
|
||||
|
||||
@@ -10,17 +10,20 @@ use reth_chainspec::ChainSpec;
|
||||
use reth_consensus::{Consensus, HeaderValidator};
|
||||
use reth_db_common::init::{insert_genesis_hashes, insert_genesis_history, insert_genesis_state};
|
||||
use reth_ethereum_consensus::{validate_block_post_execution, EthBeaconConsensus};
|
||||
use reth_ethereum_primitives::Block;
|
||||
use reth_ethereum_primitives::{Block, TransactionSigned};
|
||||
use reth_evm::{execute::Executor, ConfigureEvm};
|
||||
use reth_evm_ethereum::EthEvmConfig;
|
||||
use reth_primitives_traits::{RecoveredBlock, SealedBlock};
|
||||
use reth_primitives_traits::{Block as BlockTrait, RecoveredBlock, SealedBlock};
|
||||
use reth_provider::{
|
||||
test_utils::create_test_provider_factory_with_chain_spec, BlockWriter, DatabaseProviderFactory,
|
||||
ExecutionOutcome, HeaderProvider, HistoryWriter, OriginalValuesKnown, StateProofProvider,
|
||||
StateWriter, StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
|
||||
};
|
||||
use reth_revm::{database::StateProviderDatabase, witness::ExecutionWitnessRecord, State};
|
||||
use reth_stateless::{validation::stateless_validation, ExecutionWitness};
|
||||
use reth_stateless::{
|
||||
trie::StatelessSparseTrie, validation::stateless_validation_with_trie, ExecutionWitness,
|
||||
UncompressedPublicKey,
|
||||
};
|
||||
use reth_trie::{HashedPostState, KeccakKeyHasher, StateRoot};
|
||||
use reth_trie_db::DatabaseStateRoot;
|
||||
use std::{
|
||||
@@ -356,9 +359,16 @@ fn run_case(
|
||||
}
|
||||
|
||||
// Now validate using the stateless client if everything else passes
|
||||
for (block, execution_witness) in &program_inputs {
|
||||
stateless_validation(
|
||||
block.clone(),
|
||||
for (recovered_block, execution_witness) in &program_inputs {
|
||||
let block = recovered_block.clone().into_block();
|
||||
|
||||
// Recover the actual public keys from the transaction signatures
|
||||
let public_keys = recover_signers(block.body().transactions())
|
||||
.expect("Failed to recover public keys from transaction signatures");
|
||||
|
||||
stateless_validation_with_trie::<StatelessSparseTrie, _, _>(
|
||||
block,
|
||||
public_keys,
|
||||
execution_witness.clone(),
|
||||
chain_spec.clone(),
|
||||
EthEvmConfig::new(chain_spec.clone()),
|
||||
@@ -413,6 +423,22 @@ fn pre_execution_checks(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recover public keys from transaction signatures.
|
||||
fn recover_signers<'a, I>(txs: I) -> Result<Vec<UncompressedPublicKey>, Box<dyn std::error::Error>>
|
||||
where
|
||||
I: IntoIterator<Item = &'a TransactionSigned>,
|
||||
{
|
||||
txs.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, tx)| {
|
||||
tx.signature()
|
||||
.recover_from_prehash(&tx.signature_hash())
|
||||
.map(|keys| keys.to_encoded_point(false).as_bytes().try_into().unwrap())
|
||||
.map_err(|e| format!("failed to recover signature for tx #{i}: {e}").into())
|
||||
})
|
||||
.collect::<Result<Vec<UncompressedPublicKey>, _>>()
|
||||
}
|
||||
|
||||
/// Returns whether the test at the given path should be skipped.
|
||||
///
|
||||
/// Some tests are edge cases that cannot happen on mainnet, while others are skipped for
|
||||
|
||||
Reference in New Issue
Block a user