Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions crates/ev-revm/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,38 @@ impl DeployAllowlistSettings {
self.allowlist.binary_search(&caller).is_ok()
}
}

/// Error returned by deploy allowlist checks.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeployCheckError {
/// Caller is not allowed to perform top-level contract creation.
NotAllowed,
}

// Intentionally no envelope discriminator here to keep dependencies light.

/// Enforces the deploy allowlist policy.
///
/// If `is_top_level_create` is false or settings are None or not active yet, this is a no-op.
/// Otherwise returns `NotAllowed` if `caller` is not in the allowlist.
pub fn check_deploy_allowed(
settings: Option<&DeployAllowlistSettings>,
caller: Address,
is_top_level_create: bool,
block_number: u64,
) -> Result<(), DeployCheckError> {
if !is_top_level_create {
return Ok(());
}
let Some(settings) = settings else {
return Ok(());
};
if !settings.is_active(block_number) {
return Ok(());
}
if settings.is_allowed(caller) {
Ok(())
} else {
Err(DeployCheckError::NotAllowed)
}
}
11 changes: 7 additions & 4 deletions crates/ev-revm/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,14 @@ impl<EVM, ERROR, FRAME> EvHandler<EVM, ERROR, FRAME> {
.number()
.try_into()
.unwrap_or(u64::MAX);
let Some(settings) = self.deploy_allowlist_for_block(block_number) else {
return Ok(());
};
let tx = evm.ctx_ref().tx();
if matches!(tx.kind(), TxKind::Create) && !settings.is_allowed(tx.caller()) {
let caller = tx.caller();
let is_create = matches!(tx.kind(), TxKind::Create);

let settings = self.deploy_allowlist_for_block(block_number);
if let Err(_e) =
crate::deploy::check_deploy_allowed(settings, caller, is_create, block_number)
{
return Err(
<ERROR as reth_revm::revm::context::result::FromStringError>::from_string(
"contract deployment not allowed".to_string(),
Expand Down
134 changes: 122 additions & 12 deletions crates/node/src/txpool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ use reth_node_builder::{
BuilderContext,
};
use reth_primitives_traits::NodePrimitives;
use reth_storage_api::{AccountInfoReader, StateProviderFactory};
use reth_storage_api::{AccountInfoReader, BlockNumReader, StateProviderFactory};
use reth_transaction_pool::{
blobstore::DiskFileBlobStore,
error::{InvalidPoolTransactionError, PoolTransactionError},
CoinbaseTipOrdering, EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction,
EthTransactionValidator, PoolTransaction, TransactionOrigin, TransactionValidationOutcome,
TransactionValidationTaskExecutor, TransactionValidator,
};
use tracing::{debug, info};
use tracing::{debug, info, warn};

/// Pool transaction wrapper for `EvTxEnvelope`.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -286,13 +286,19 @@ pub enum EvTxPoolError {
/// Error while querying account info from the state provider.
#[error("state provider error: {0}")]
StateProvider(String),
/// Top-level contract deployment not allowed for caller.
#[error("contract deployment not allowed")]
DeployNotAllowed,
}

impl PoolTransactionError for EvTxPoolError {
fn is_bad_transaction(&self) -> bool {
matches!(
self,
Self::EmptyCalls | Self::InvalidCreatePosition | Self::InvalidSponsorSignature
Self::EmptyCalls
| Self::InvalidCreatePosition
| Self::InvalidSponsorSignature
| Self::DeployNotAllowed
)
}

Expand All @@ -305,13 +311,24 @@ impl PoolTransactionError for EvTxPoolError {
#[derive(Debug, Clone)]
pub struct EvTransactionValidator<Client> {
inner: Arc<EthTransactionValidator<Client, EvPooledTransaction>>,
deploy_allowlist: Option<ev_revm::deploy::DeployAllowlistSettings>,
}

impl<Client> EvTransactionValidator<Client> {
impl<Client> EvTransactionValidator<Client>
where
Client: BlockNumReader,
{
/// Wraps the provided Ethereum validator with EV-specific validation logic.
pub fn new(inner: EthTransactionValidator<Client, EvPooledTransaction>) -> Self {
pub fn new(
inner: EthTransactionValidator<Client, EvPooledTransaction>,
deploy_allowlist: Option<ev_revm::deploy::DeployAllowlistSettings>,
) -> Self
where
Client: BlockNumReader,
{
Self {
inner: Arc::new(inner),
deploy_allowlist,
}
}

Expand Down Expand Up @@ -383,6 +400,31 @@ impl<Client> EvTransactionValidator<Client> {
where
Client: StateProviderFactory,
{
// Unified deploy allowlist check (covers both Ethereum and EvNode txs).
if let Some(settings) = &self.deploy_allowlist {
let is_top_level_create = match pooled.transaction().inner() {
EvTxEnvelope::Ethereum(tx) => alloy_consensus::Transaction::is_create(tx),
EvTxEnvelope::EvNode(ref signed) => {
let tx = signed.tx();
tx.calls.first().map(|c| c.to.is_create()).unwrap_or(false)
}
};
let caller = pooled.transaction().signer();
let block_number = self.inner.client().best_block_number().map_err(|err| {
InvalidPoolTransactionError::other(EvTxPoolError::StateProvider(err.to_string()))
})?;
if let Err(_e) = ev_revm::deploy::check_deploy_allowed(
Some(settings),
caller,
is_top_level_create,
block_number,
) {
return Err(InvalidPoolTransactionError::other(
EvTxPoolError::DeployNotAllowed,
));
}
}

let consensus = pooled.transaction().inner();
let EvTxEnvelope::EvNode(tx) = consensus else {
if sender_balance < *pooled.cost() {
Expand Down Expand Up @@ -422,7 +464,7 @@ impl<Client> EvTransactionValidator<Client> {

impl<Client> TransactionValidator for EvTransactionValidator<Client>
where
Client: ChainSpecProvider<ChainSpec: EthereumHardforks> + StateProviderFactory,
Client: ChainSpecProvider<ChainSpec: EthereumHardforks> + StateProviderFactory + BlockNumReader,
{
type Transaction = EvPooledTransaction;

Expand Down Expand Up @@ -470,7 +512,7 @@ pub struct EvolvePoolBuilder;
impl<Types, Node> PoolBuilder<Node> for EvolvePoolBuilder
where
Types: NodeTypes<
ChainSpec: EthereumHardforks,
ChainSpec = reth_chainspec::ChainSpec,
Primitives: NodePrimitives<SignedTx = TransactionSigned>,
>,
Node: FullNodeTypes<Types = Types>,
Expand Down Expand Up @@ -521,7 +563,26 @@ where
ctx.task_executor().clone(),
blob_store.clone(),
)
.map(EvTransactionValidator::new);
.map(|inner| {
// Wire deploy-allowlist from chainspec extras into the pool validator.
let evolve_config = crate::config::EvolvePayloadBuilderConfig::from_chain_spec(
ctx.chain_spec().as_ref(),
)
.unwrap_or_else(|err| {
warn!(
target: "reth::cli",
"Failed to parse evolve config from chainspec: {err}"
);
Default::default()
});
let deploy_allowlist =
evolve_config
.deploy_allowlist_settings()
.map(|(allowlist, activation)| {
ev_revm::deploy::DeployAllowlistSettings::new(allowlist, activation)
});
EvTransactionValidator::new(inner, deploy_allowlist)
});

if validator.validator().inner.eip4844() {
let kzg_settings = validator.validator().inner.kzg_settings().clone();
Expand Down Expand Up @@ -576,14 +637,38 @@ mod tests {
Signed::new_unhashed(tx, sample_signature())
}

/// Creates a non-sponsored `EvNode` transaction with CREATE as the first call.
fn create_non_sponsored_evnode_create_tx(
gas_limit: u64,
max_fee_per_gas: u128,
) -> EvNodeSignedTx {
let tx = EvNodeTransaction {
chain_id: 1,
nonce: 0,
max_priority_fee_per_gas: 1,
max_fee_per_gas,
gas_limit,
calls: vec![Call {
to: TxKind::Create,
value: U256::ZERO,
input: Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xf3]), // minimal initcode
}],
access_list: AccessList::default(),
fee_payer_signature: None,
};
Signed::new_unhashed(tx, sample_signature())
}

fn create_pooled_tx(signed_tx: EvNodeSignedTx, signer: Address) -> EvPooledTransaction {
let envelope = EvTxEnvelope::EvNode(signed_tx);
let recovered = alloy_consensus::transaction::Recovered::new_unchecked(envelope, signer);
let encoded_length = 200; // Approximate length for test
EvPooledTransaction::new(recovered, encoded_length)
}

fn create_test_validator() -> EvTransactionValidator<MockEthProvider> {
fn create_test_validator(
deploy_allowlist: Option<ev_revm::deploy::DeployAllowlistSettings>,
) -> EvTransactionValidator<MockEthProvider> {
use reth_transaction_pool::{
blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder,
};
Expand All @@ -594,7 +679,7 @@ mod tests {
.no_shanghai()
.no_cancun()
.build(blob_store);
EvTransactionValidator::new(inner)
EvTransactionValidator::new(inner, deploy_allowlist)
}

/// Tests that non-sponsored `EvNode` transactions with insufficient sender balance
Expand All @@ -604,7 +689,7 @@ mod tests {
/// sender balance for non-sponsored `EvNode` transactions.
#[test]
fn non_sponsored_evnode_rejects_insufficient_balance() {
let validator = create_test_validator();
let validator = create_test_validator(None);

// Create a non-sponsored EvNode transaction
let gas_limit = 21_000u64;
Expand Down Expand Up @@ -638,7 +723,7 @@ mod tests {
/// Tests that non-sponsored `EvNode` transactions with sufficient balance are accepted.
#[test]
fn non_sponsored_evnode_accepts_sufficient_balance() {
let validator = create_test_validator();
let validator = create_test_validator(None);

let gas_limit = 21_000u64;
let max_fee_per_gas = 1_000_000_000u128;
Expand All @@ -661,4 +746,29 @@ mod tests {
result
);
}

/// Tests pool-level deploy allowlist rejection for `EvNode` CREATE when caller not allowlisted.
#[test]
fn evnode_create_rejected_when_not_allowlisted() {
// Configure deploy allowlist with a different address than the signer
let allowed = Address::from([0x11u8; 20]);
let settings = ev_revm::deploy::DeployAllowlistSettings::new(vec![allowed], 0);
let validator = create_test_validator(Some(settings));

let gas_limit = 200_000u64;
let max_fee_per_gas = 1_000_000_000u128;
let signed_tx = create_non_sponsored_evnode_create_tx(gas_limit, max_fee_per_gas);

let signer = Address::from([0x22u8; 20]); // not allowlisted
let pooled = create_pooled_tx(signed_tx, signer);

let sender_balance = *pooled.cost() + U256::from(1);
let mut state: Option<Box<dyn AccountInfoReader>> = None;

let result = validator.validate_evnode(&pooled, sender_balance, &mut state);
assert!(result.is_err());
if let Err(err) = result {
assert!(matches!(err, InvalidPoolTransactionError::Other(_)));
}
}
}