DEV Community

TangHaosuan
TangHaosuan

Posted on

REVM Source Code - Execution Flow Part 2

Validate

The previous article mentioned that the handler has five major steps. Validate is step one.

In this article, we'll analyze it in detail and understand what it's responsible for.

// crates/handler/src/handler.rs
 #[inline]
    fn validate(&self, evm: &mut Self::Evm) -> Result<InitialAndFloorGas, Self::Error> {
        self.validate_env(evm)?;
        self.validate_initial_tx_gas(evm)
    }
Enter fullscreen mode Exit fullscreen mode

From the function and internal function call names, we can see that Validate is mainly responsible for two parts:

validate_env: Validate the environment.

validate_initial_tx_gas: Validate the initial gas for the transaction.

validate_env

Let's first analyze validate_env — jump in:

// crates/handler/src/handler.rs
#[inline]
    fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
        validation::validate_env(evm.ctx())
    }
Enter fullscreen mode Exit fullscreen mode

Nothing useful — just a function call. Continue jumping:

// crates/handler/src/validation.rs
pub fn validate_env<CTX: ContextTr, ERROR: From<InvalidHeader> + From<InvalidTransaction>>(
    context: CTX,
) -> Result<(), ERROR> {
    let spec = context.cfg().spec().into();
    // `prevrandao` is required for the merge
    if spec.is_enabled_in(SpecId::MERGE) && context.block().prevrandao().is_none() {
        return Err(InvalidHeader::PrevrandaoNotSet.into());
    }
    // `excess_blob_gas` is required for Cancun
    if spec.is_enabled_in(SpecId::CANCUN) && context.block().blob_excess_gas_and_price().is_none() {
        return Err(InvalidHeader::ExcessBlobGasNotSet.into());
    }
    validate_tx_env::<CTX>(context, spec).map_err(Into::into)
}
Enter fullscreen mode Exit fullscreen mode

let spec = context.cfg().spec().into()

Gets the spec from the current context, which can be understood as the current EVM version. Returns an enum type.

is_enabled_in()

Compares the current spec with the passed-in spec. Returns true if the current spec is greater than or equal to the passed-in spec.

context.block().prevrandao().is_none()

Checks whether prevrandao is set in the context.

context.block().blob_excess_gas_and_price().is_none()

Checks whether blob_excess_gas_and_price is set in the context.

We can see this section mainly checks: if a specific EVM version is set in the context, whether the features and upgrades of that version are implemented.

Moving on to this line — from the function name, it checks the tx environment:

validate_tx_env::<CTX>(context, spec).map_err(Into::into)
Enter fullscreen mode Exit fullscreen mode

Let's jump directly to the function implementation:

// crates/handler/src/validation.rs
pub fn validate_tx_env<CTX: ContextTr>(
    context: CTX,
    spec_id: SpecId,
) -> Result<(), InvalidTransaction> {
    // Check if the transaction's chain id is correct
    let tx_type = context.tx().tx_type();
    let tx = context.tx();

    let base_fee = if context.cfg().is_base_fee_check_disabled() {
        None
    } else {
        Some(context.block().basefee() as u128)
    };

    let tx_type = TransactionType::from(tx_type);

    // Check chain_id if config is enabled.
    // EIP-155: Simple replay attack protection
    if context.cfg().tx_chain_id_check() {
        if let Some(chain_id) = tx.chain_id() {
            if chain_id != context.cfg().chain_id() {
                return Err(InvalidTransaction::InvalidChainId);
            }
        } else if !tx_type.is_legacy() && !tx_type.is_custom() {
            // Legacy transaction are the only one that can omit chain_id.
            return Err(InvalidTransaction::MissingChainId);
        }
    }

    // EIP-7825: Transaction Gas Limit Cap
    let cap = context.cfg().tx_gas_limit_cap();
    if tx.gas_limit() > cap {
        return Err(InvalidTransaction::TxGasLimitGreaterThanCap {
            gas_limit: tx.gas_limit(),
            cap,
        });
    }

    let disable_priority_fee_check = context.cfg().is_priority_fee_check_disabled();

    match tx_type {
        TransactionType::Legacy => {
            validate_legacy_gas_price(tx.gas_price(), base_fee)?;
        }
        TransactionType::Eip2930 => {
            // Enabled in BERLIN hardfork
            if !spec_id.is_enabled_in(SpecId::BERLIN) {
                return Err(InvalidTransaction::Eip2930NotSupported);
            }
            validate_legacy_gas_price(tx.gas_price(), base_fee)?;
        }
        TransactionType::Eip1559 => {
            if !spec_id.is_enabled_in(SpecId::LONDON) {
                return Err(InvalidTransaction::Eip1559NotSupported);
            }
            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
        }
        TransactionType::Eip4844 => {
            if !spec_id.is_enabled_in(SpecId::CANCUN) {
                return Err(InvalidTransaction::Eip4844NotSupported);
            }

            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;

            validate_eip4844_tx(
                tx.blob_versioned_hashes(),
                tx.max_fee_per_blob_gas(),
                context.block().blob_gasprice().unwrap_or_default(),
                context.cfg().max_blobs_per_tx(),
            )?;
        }
        TransactionType::Eip7702 => {
            // Check if EIP-7702 transaction is enabled.
            if !spec_id.is_enabled_in(SpecId::PRAGUE) {
                return Err(InvalidTransaction::Eip7702NotSupported);
            }

            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;

            let auth_list_len = tx.authorization_list_len();
            // The transaction is considered invalid if the length of authorization_list is zero.
            if auth_list_len == 0 {
                return Err(InvalidTransaction::EmptyAuthorizationList);
            }
        }
        TransactionType::Custom => {
            // Custom transaction type check is not done here.
        }
    };

    // Check if gas_limit is more than block_gas_limit
    if !context.cfg().is_block_gas_limit_disabled() && tx.gas_limit() > context.block().gas_limit()
    {
        return Err(InvalidTransaction::CallerGasLimitMoreThanBlock);
    }

    // EIP-3860: Limit and meter initcode. Still valid with EIP-7907 and increase of initcode size.
    if spec_id.is_enabled_in(SpecId::SHANGHAI)
        && tx.kind().is_create()
        && context.tx().input().len() > context.cfg().max_initcode_size()
    {
        return Err(InvalidTransaction::CreateInitCodeSizeLimit);
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

It looks long, but it's really just various checks. Let's break it down section by section:

// crates/handler/src/validation.rs
    // Check chain_id if config is enabled.
    // EIP-155: Simple replay attack protection
    if context.cfg().tx_chain_id_check() {
        if let Some(chain_id) = tx.chain_id() {
            if chain_id != context.cfg().chain_id() {
                return Err(InvalidTransaction::InvalidChainId);
            }
        } else if !tx_type.is_legacy() && !tx_type.is_custom() {
            // Legacy transaction are the only one that can omit chain_id.
            return Err(InvalidTransaction::MissingChainId);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Checks whether tx_chain_id_check is enabled in the Context.

If tx_chain_id_check is enabled, it checks whether the current tx's chain_id matches the context's.

The chain_id verification is to prevent replay attacks.

Without a chain_id, a successful transaction you sent on ETH could be replayed by others on another EVM chain, causing you to lose assets on other chains. (For example, if you have 10 tokens on both BSC and ETH, and you only sent 1 ETH to A on ETH. Someone else could take your raw Transaction from ETH and broadcast it on BSC, causing you to also send 1 BNB to A on BSC.)

// EIP-7825: Transaction Gas Limit Cap
    let cap = context.cfg().tx_gas_limit_cap();
    if tx.gas_limit() > cap {
        return Err(InvalidTransaction::TxGasLimitGreaterThanCap {
            gas_limit: tx.gas_limit(),
            cap,
        });
    }
Enter fullscreen mode Exit fullscreen mode

Checks whether the current tx's GasLimit exceeds the value set in Context.

EIP-7825 is primarily to prevent attacks.

Without a limit, a single tx could bloat the block, reducing the number of transactions that can be packaged. This would cause network congestion.

The higher the GasLimit, the more complex the logic, and during processing CPU/memory can explode, slowing down node synchronization too.

match tx_type {
        TransactionType::Legacy => {
            validate_legacy_gas_price(tx.gas_price(), base_fee)?;
        }
        TransactionType::Eip2930 => {
            // Enabled in BERLIN hardfork
            if !spec_id.is_enabled_in(SpecId::BERLIN) {
                return Err(InvalidTransaction::Eip2930NotSupported);
            }
            validate_legacy_gas_price(tx.gas_price(), base_fee)?;
        }
        TransactionType::Eip1559 => {
            if !spec_id.is_enabled_in(SpecId::LONDON) {
                return Err(InvalidTransaction::Eip1559NotSupported);
            }
            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
        }
        TransactionType::Eip4844 => {
            if !spec_id.is_enabled_in(SpecId::CANCUN) {
                return Err(InvalidTransaction::Eip4844NotSupported);
            }

            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;

            validate_eip4844_tx(
                tx.blob_versioned_hashes(),
                tx.max_fee_per_blob_gas(),
                context.block().blob_gasprice().unwrap_or_default(),
                context.cfg().max_blobs_per_tx(),
            )?;
        }
        TransactionType::Eip7702 => {
            // Check if EIP-7702 transaction is enabled.
            if !spec_id.is_enabled_in(SpecId::PRAGUE) {
                return Err(InvalidTransaction::Eip7702NotSupported);
            }

            validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;

            let auth_list_len = tx.authorization_list_len();
            // The transaction is considered invalid if the length of authorization_list is zero.
            if auth_list_len == 0 {
                return Err(InvalidTransaction::EmptyAuthorizationList);
            }
        }
        TransactionType::Custom => {
            // Custom transaction type check is not done here.
        }
Enter fullscreen mode Exit fullscreen mode

Before we start, let's explain EIP-1559. After EIP-1559, GasPrice is influenced by 3 components:

  • Max Fee: The maximum GasPrice the Sender is willing to pay
  • Priority Fee: Miner tip, used to speed up transactions
  • Base Fee: The block's base fee, which gets burned
  • GasPrice = Priority Fee + min(Base Fee, Max Fee - Base Fee)

Back to the code — this section looks long, but if you look closely,

except for Eip4844 and Eip7702, each check mainly does two things:

  • Checks whether the current EVM version supports the transaction type
  • Checks whether the GasPrice settings are correct
    • validate_legacy_gas_price
      • Ensures the tx's GasPrice is greater than the Base_Fee set in Context
    • validate_priority_fee_for_tx
      • Actually calls validate_priority_fee_tx; specifics are in the code
pub fn validate_priority_fee_tx(
    max_fee: u128,
    max_priority_fee: u128,
    base_fee: Option<u128>,
    disable_priority_fee_check: bool,
) -> Result<(), InvalidTransaction> {
    // Ensures max_priority_fee doesn't exceed max_fee
    if !disable_priority_fee_check && max_priority_fee > max_fee {
        return Err(InvalidTransaction::PriorityFeeGreaterThanMaxFee);
    }
    // This is the formula above — ensures Base fee + Max Priority Fee doesn't exceed Max Fee
    if let Some(base_fee) = base_fee {
        let effective_gas_price = cmp::min(max_fee, base_fee.saturating_add(max_priority_fee));
        if effective_gas_price < base_fee {
            return Err(InvalidTransaction::GasPriceLessThanBasefee);
        }
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode
  • EIP-4844 tx handling — EIP-4844 mainly targets L2, skipping
  • EIP-7702 tx handling — validates whether there's an authorization_list

validate_initial_tx_gas

According to the code comments, validate_initial_tx_gas is responsible for:

  • Calculating the initial gas fee based on the transaction type and input data
  • Including additional costs for access lists and authorization lists
  • Verifying that the initial cost doesn't exceed the transaction gas limit

Let's continue with the code:

// crates/handler/src/handler.rs
 #[inline]
    fn validate_initial_tx_gas(
        &self,
        evm: &mut Self::Evm,
    ) -> Result<InitialAndFloorGas, Self::Error> {
        let ctx = evm.ctx_ref();
        validation::validate_initial_tx_gas(
            ctx.tx(),
            ctx.cfg().spec().into(),
            ctx.cfg().is_eip7623_disabled(),
        )
        .map_err(From::from)
    }
Enter fullscreen mode Exit fullscreen mode

The main call is to validation::validate_initial_tx_gas. Let's jump in:

// crates/handler/src/validation.rs
pub fn validate_initial_tx_gas(
    tx: impl Transaction,
    spec: SpecId,
    is_eip7623_disabled: bool,
) -> Result<InitialAndFloorGas, InvalidTransaction> {
    let mut gas = gas::calculate_initial_tx_gas_for_tx(&tx, spec);

    if is_eip7623_disabled {
        gas.floor_gas = 0
    }

    // Additional check to see if limit is big enough to cover initial gas.
    if gas.initial_gas > tx.gas_limit() {
        return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
            gas_limit: tx.gas_limit(),
            initial_gas: gas.initial_gas,
        });
    }

    // EIP-7623: Increase calldata cost
    // floor gas should be less than gas limit.
    if spec.is_enabled_in(SpecId::PRAGUE) && gas.floor_gas > tx.gas_limit() {
        return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
            gas_floor: gas.floor_gas,
            gas_limit: tx.gas_limit(),
        });
    };

    Ok(gas)
}
Enter fullscreen mode Exit fullscreen mode

Let's analyze step by step. First, focus on the first line — from the name it calculates the tx's initial gas:

let mut gas = gas::calculate_initial_tx_gas_for_tx(&tx, spec);
Enter fullscreen mode Exit fullscreen mode

Jump in to see the implementation:

pub fn calculate_initial_tx_gas_for_tx(tx: impl Transaction, spec: SpecId) -> InitialAndFloorGas {
    let mut accounts = 0;
    let mut storages = 0;
    // legacy is only tx type that does not have access list.
    if tx.tx_type() != TransactionType::Legacy {
        (accounts, storages) = tx
            .access_list()
            .map(|al| {
                al.fold((0, 0), |(mut num_accounts, mut num_storage_slots), item| {
                    num_accounts += 1;
                    num_storage_slots += item.storage_slots().count();

                    (num_accounts, num_storage_slots)
                })
            })
            .unwrap_or_default();
    }

    calculate_initial_tx_gas(
        spec,
        tx.input(),
        tx.kind().is_create(),
        accounts as u64,
        storages as u64,
        tx.authorization_list_len() as u64,
    )
}
Enter fullscreen mode Exit fullscreen mode

The first big section handles the access_list.

access_list: When sending a tx, you can pre-declare the list of addresses and storage keys that the tx will read or write during execution.

Addresses and storage keys declared in the tx are pre-loaded and warmed, which can reduce Gas consumption.

The first section looks uncomfortable to read, so let's break it down step by step.

First, let's look at the access_list return type — it returns

Option>>

fn access_list(&self) -> Option<impl Iterator<Item = Self::AccessListItem<'_>>>
Enter fullscreen mode Exit fullscreen mode
// access_list is an Option — nothing happens for None, for Some the closure function inside map is executed
tx.access_list().map()
Enter fullscreen mode Exit fullscreen mode
// fold performs an accumulation over the iterator's elements
// (0, 0) initial value
// (mut num_accounts, mut num_storage_slots) temp variables for storing results during accumulation
// item — the iterator's element
|al| {al.fold((0, 0), |(mut num_accounts, mut num_storage_slots), item| {
    num_accounts += 1;
    num_storage_slots += item.storage_slots().count();
    (num_accounts, num_storage_slots)
}) }
Enter fullscreen mode Exit fullscreen mode

Combined: if the access_list is not empty, it accumulates the accounts and storage_slots in the access_list, calculating the actual number of accounts and storage_slots.

Next, look at the calculate_initial_tx_gas part — jump to the implementation:

// crates/interpreter/src/gas/calc.rs
pub fn calculate_initial_tx_gas(
    spec_id: SpecId,
    input: &[u8],
    is_create: bool,
    access_list_accounts: u64,
    access_list_storages: u64,
    authorization_list_num: u64,
) -> InitialAndFloorGas {
    let mut gas = InitialAndFloorGas::default();

    // Initdate stipend
    let tokens_in_calldata = get_tokens_in_calldata(input, spec_id.is_enabled_in(SpecId::ISTANBUL));

    gas.initial_gas += tokens_in_calldata * STANDARD_TOKEN_COST;

    // Get number of access list account and storages.
    gas.initial_gas += access_list_accounts * ACCESS_LIST_ADDRESS;
    gas.initial_gas += access_list_storages * ACCESS_LIST_STORAGE_KEY;

    // Base stipend
    gas.initial_gas += if is_create {
        if spec_id.is_enabled_in(SpecId::HOMESTEAD) {
            // EIP-2: Homestead Hard-fork Changes
            53000
        } else {
            21000
        }
    } else {
        21000
    };

    // EIP-3860: Limit and meter initcode
    // Init code stipend for bytecode analysis
    if spec_id.is_enabled_in(SpecId::SHANGHAI) && is_create {
        gas.initial_gas += initcode_cost(input.len())
    }

    // EIP-7702
    if spec_id.is_enabled_in(SpecId::PRAGUE) {
        gas.initial_gas += authorization_list_num * eip7702::PER_EMPTY_ACCOUNT_COST;

        // Calculate gas floor for EIP-7623
        gas.floor_gas = calc_tx_floor_cost(tokens_in_calldata);
    }

    gas
}
Enter fullscreen mode Exit fullscreen mode

Continue jumping to the get_tokens_in_calldata implementation:

#[inline]
pub fn get_tokens_in_calldata(input: &[u8], is_istanbul: bool) -> u64 {
    let zero_data_len = input.iter().filter(|v| **v == 0).count() as u64;
    let non_zero_data_len = input.len() as u64 - zero_data_len;
    let non_zero_data_multiplier = if is_istanbul {
        // EIP-2028: Transaction data gas cost reduction
        NON_ZERO_BYTE_MULTIPLIER_ISTANBUL
    } else {
        NON_ZERO_BYTE_MULTIPLIER
    };
    zero_data_len + non_zero_data_len * non_zero_data_multiplier
}
Enter fullscreen mode Exit fullscreen mode

The logic is simple — it finds the count of zero and non-zero bytes in the tx's calldata, multiplying the non-zero count by a fixed value.

This is done because calldata is permanently stored on-chain. Data with many zero bytes can be efficiently compressed for storage with low cost, while non-zero data is harder to compress and costs more to store.

The remaining logic in calculate_initial_tx_gas is fairly obvious, so we won't go into detail.

Jumping back to validate_initial_tx_gas, the remaining logic is also fairly obvious, so we won't continue either.

The validate stage ends here.

Top comments (0)