Skip to content

Commit

Permalink
Feat: add EIP-7623 (#75)
Browse files Browse the repository at this point in the history
* Added basic support for EIP-7623

* Update gas floor

* Added floor-gas logic

* Update floor-gas logic

* Remove mut for used_gas func

* Change mut for fee func
  • Loading branch information
mrLSD authored Jan 22, 2025
1 parent 3956ee6 commit ef796b1
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ jobs:
cargo run -r -p evm-jsontests -F enable-slow-tests -- state -f \
ethtests/GeneralStateTests/ \
ethtests/LegacyTests/Cancun/GeneralStateTests/ \
ethereum-spec-tests/fixtures/state_tests/prague/eip7702_set_code_tx/
ethereum-spec-tests/fixtures/state_tests/prague/eip7702_set_code_tx/ \
ethereum-spec-tests/fixtures/state_tests/prague/eip7623_increase_calldata_cost/
# Temporally disable as EOFv1 not implemented
# ethtests/EIPTests/StateTests/
Expand Down
17 changes: 13 additions & 4 deletions evm-tests/jsontests/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -704,10 +704,10 @@ fn assert_empty_create_caller(expect_exception: &Option<String>, name: &str) {
}

/// Check call expected exception
fn assert_call_exit_exception(expect_exception: &Option<String>) {
fn assert_call_exit_exception(expect_exception: &Option<String>, name: &str) {
assert!(
expect_exception.is_none(),
"unexpected call exception: {expect_exception:?}"
"unexpected call exception: {expect_exception:?} for test: {name}"
);
}

Expand Down Expand Up @@ -747,7 +747,9 @@ fn check_create_exit_reason(
return true;
}
_ => {
panic!("unexpected error: {err:?} for exception: {exception}")
panic!(
"unexpected error: {err:?} for exception: {exception} for test: {name}"
)
}
}
} else {
Expand Down Expand Up @@ -1062,6 +1064,13 @@ fn check_validate_exit_reason(
"unexpected exception {exception:?} for CreateTransaction for test: [{spec:?}] {name}"
);
}
InvalidTxReason::GasFloorMoreThanGasLimit => {
let check_result = exception == "TransactionException.INTRINSIC_GAS_TOO_LOW";
assert!(
check_result,
"unexpected exception {exception:?} for GasFloorMoreThanGasLimit for test: [{spec:?}] {name}"
);
}
_ => {
panic!(
"unexpected exception {exception:?} for reason {reason:?} for test: [{spec:?}] {name}"
Expand Down Expand Up @@ -1268,7 +1277,7 @@ fn test_run(
access_list,
authorization_list,
);
assert_call_exit_exception(&state.expect_exception);
assert_call_exit_exception(&state.expect_exception, name);
}
ethjson::maybe::MaybeEmpty::None => {
let code = data;
Expand Down
52 changes: 40 additions & 12 deletions evm-tests/jsontests/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,32 @@ pub fn flush() {
io::stdout().flush().expect("Could not flush stdout");
}

/// EIP-7623 form Prague hard fork
pub mod eip7623 {
/// The standard cost of calldata token.
pub const STANDARD_TOKEN_COST: usize = 4;
/// The cost of a non-zero byte in calldata adjusted by [EIP-2028](https://eips.ethereum.org/EIPS/eip-2028).
pub const NON_ZERO_BYTE_DATA_COST: usize = 16;
/// The multiplier for a non zero byte in calldata adjusted by [EIP-2028](https://eips.ethereum.org/EIPS/eip-2028).
pub const NON_ZERO_BYTE_MULTIPLIER: usize = NON_ZERO_BYTE_DATA_COST / STANDARD_TOKEN_COST;
// The cost floor per token
pub const TOTAL_COST_FLOOR_PER_TOKEN: u64 = 10;

/// Retrieve the total number of tokens in calldata.
#[must_use]
pub fn get_tokens_in_calldata(input: &[u8]) -> u64 {
let zero_data_len = input.iter().filter(|v| **v == 0).count();
let non_zero_data_len = input.len() - zero_data_len;
u64::try_from(zero_data_len + non_zero_data_len * NON_ZERO_BYTE_MULTIPLIER).unwrap()
}

/// Calculate the transaction cost floor as specified in EIP-7623.
#[must_use]
pub const fn calc_tx_floor_cost(tokens_in_calldata: u64) -> u64 {
tokens_in_calldata * TOTAL_COST_FLOOR_PER_TOKEN + 21_000
}
}

/// EIP-7702
pub mod eip7702 {
use super::{Digest, Keccak256, H160, H256, U256};
Expand Down Expand Up @@ -331,7 +357,7 @@ pub mod eip_4844 {

pub mod transaction {
use crate::state::TxType;
use crate::utils::eip7702;
use crate::utils::{eip7623, eip7702};
use ethjson::hash::Address;
use ethjson::maybe::MaybeEmpty;
use ethjson::spec::ForkSpec;
Expand Down Expand Up @@ -439,6 +465,12 @@ pub mod transaction {
}

if *spec >= ForkSpec::Prague {
// EIP-7623 validation
let floor_gas = eip7623::calc_tx_floor_cost(eip7623::get_tokens_in_calldata(&tx.data));
if floor_gas > tx.gas_limit.into() {
return Err(InvalidTxReason::GasFloorMoreThanGasLimit);
}

// EIP-7702 - if transaction type is EOAAccountCode then
// `authorization_list` must be present
if TxType::from_txbytes(&tx_state.txbytes) == TxType::EOAAccountCode
Expand All @@ -451,22 +483,17 @@ pub mod transaction {
// Other validation step inside EVM transact logic.
for auth in test_tx.authorization_list.iter() {
// 1. Verify the chain id is either 0 or the chain’s current ID.
let mut is_valid = if auth.chain_id.0 > U256::from(u64::MAX) {
false
} else {
auth.chain_id.0 == U256::from(0) || auth.chain_id.0 == vicinity.chain_id
};
// 3. `authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]`
let mut is_valid = auth.chain_id.0 <= U256::from(u64::MAX)
&& (auth.chain_id.0 == U256::from(0) || auth.chain_id.0 == vicinity.chain_id);

// 3. `authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]`
// Validate the signature, as in tests it is possible to have invalid signatures values.
let v = auth.v.0 .0;
if !(v[0] < u64::from(u8::MAX) && v[1..4].iter().all(|&elem| elem == 0)) {
is_valid = false;
}
// Value `v` shouldn't be greater then 1
if v[0] > 1 {
let v = auth.v.0;
if v > U256::from(1) {
is_valid = false;
}

// EIP-2 validation
if auth.s.0 > eip7702::SECP256K1N_HALF {
is_valid = false;
Expand Down Expand Up @@ -546,6 +573,7 @@ pub mod transaction {
InvalidAuthorizationChain,
InvalidAuthorizationSignature,
CreateTransaction,
GasFloorMoreThanGasLimit,
}
}

Expand Down
21 changes: 21 additions & 0 deletions gasometer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ impl<'config> Gasometer<'config> {
memory_gas: 0,
used_gas: 0,
refunded_gas: 0,
floor_gas: 0,
config,
}),
}
Expand Down Expand Up @@ -150,6 +151,13 @@ impl<'config> Gasometer<'config> {
self.gas_limit
}

/// Get floor gas
#[inline]
#[must_use]
pub fn floor_gas(&self) -> u64 {
self.inner.as_ref().map_or(0, |inner| inner.floor_gas)
}

/// Remaining gas.
#[inline]
#[must_use]
Expand Down Expand Up @@ -321,6 +329,9 @@ impl<'config> Gasometer<'config> {
}

/// Record transaction cost.
/// Related EIPs:
/// - [EIP-2028](https://eips.ethereum.org/EIPS/eip-2028)
/// - [EIP-7623](https://eips.ethereum.org/EIPS/eip-7623)
///
/// # Errors
/// Return `ExitError`
Expand All @@ -343,6 +354,15 @@ impl<'config> Gasometer<'config> {
+ access_list_storage_len as u64 * self.config.gas_access_list_storage_key
+ authorization_list_len as u64 * self.config.gas_per_empty_account_cost;

if self.config.has_floor_gas {
// According to EIP-2028: non-zero byte = 16, zero-byte = 4
// According to EIP-7623: tokens_in_calldata = zero_bytes_in_calldata + nonzero_bytes_in_calldata * 4
let tokens_in_calldata = (zero_data_len + non_zero_data_len * 4) as u64;
self.inner_mut()?.floor_gas = tokens_in_calldata
* self.config.total_cost_floor_per_token
+ self.config.gas_transaction_call;
}

log_gas!(
self,
"Record Call {} [gas_transaction_call: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}, authorization_list_len: {}]",
Expand Down Expand Up @@ -935,6 +955,7 @@ struct Inner<'config> {
used_gas: u64,
refunded_gas: i64,
config: &'config Config,
floor_gas: u64,
}

impl Inner<'_> {
Expand Down
24 changes: 24 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ pub struct Config {
pub gas_per_empty_account_cost: u64,
/// EIP-7702
pub gas_per_auth_base_cost: u64,
/// EIP-7623
pub has_floor_gas: bool,
/// EIP-7623
pub total_cost_floor_per_token: u64,
}

impl Config {
Expand Down Expand Up @@ -357,6 +361,8 @@ impl Config {
has_authorization_list: false,
gas_per_empty_account_cost: 0,
gas_per_auth_base_cost: 0,
has_floor_gas: false,
total_cost_floor_per_token: 0,
}
}

Expand Down Expand Up @@ -420,6 +426,8 @@ impl Config {
has_authorization_list: false,
gas_per_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_floor_gas: false,
total_cost_floor_per_token: 0,
}
}

Expand Down Expand Up @@ -478,6 +486,8 @@ impl Config {
has_authorization_list,
gas_per_empty_account_cost,
gas_per_auth_base_cost,
has_floor_gas,
total_cost_floor_per_token,
} = inputs;

// See https://eips.ethereum.org/EIPS/eip-2929
Expand Down Expand Up @@ -551,6 +561,8 @@ impl Config {
has_authorization_list,
gas_per_empty_account_cost,
gas_per_auth_base_cost,
has_floor_gas,
total_cost_floor_per_token,
}
}
}
Expand All @@ -577,6 +589,8 @@ struct DerivedConfigInputs {
has_authorization_list: bool,
gas_per_empty_account_cost: u64,
gas_per_auth_base_cost: u64,
has_floor_gas: bool,
total_cost_floor_per_token: u64,
}

impl DerivedConfigInputs {
Expand All @@ -599,6 +613,8 @@ impl DerivedConfigInputs {
has_authorization_list: false,
gas_per_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_floor_gas: false,
total_cost_floor_per_token: 0,
}
}

Expand All @@ -621,6 +637,8 @@ impl DerivedConfigInputs {
has_authorization_list: false,
gas_per_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_floor_gas: false,
total_cost_floor_per_token: 0,
}
}

Expand All @@ -643,6 +661,8 @@ impl DerivedConfigInputs {
has_authorization_list: false,
gas_per_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_floor_gas: false,
total_cost_floor_per_token: 0,
}
}

Expand All @@ -666,6 +686,8 @@ impl DerivedConfigInputs {
has_authorization_list: false,
gas_per_auth_base_cost: 0,
gas_per_empty_account_cost: 0,
has_floor_gas: false,
total_cost_floor_per_token: 0,
}
}

Expand All @@ -684,6 +706,8 @@ impl DerivedConfigInputs {
config.has_authorization_list = true;
config.gas_per_empty_account_cost = 25000;
config.gas_per_auth_base_cost = 12500;
config.has_floor_gas = true;
config.total_cost_floor_per_token = 10;
config
}
}
25 changes: 17 additions & 8 deletions src/executor/stack/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -831,11 +831,20 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet>
// Avoid uncontrolled `u64` casting
let refunded_gas =
u64::try_from(self.state.metadata().gasometer.refunded_gas()).unwrap_or_default();
self.state.metadata().gasometer.total_used_gas()
let total_used_gas = self.state.metadata().gasometer.total_used_gas();
let total_used_gas_refunded = self.state.metadata().gasometer.total_used_gas()
- min(
self.state.metadata().gasometer.total_used_gas() / self.config.max_refund_quotient,
total_used_gas / self.config.max_refund_quotient,
refunded_gas,
)
);
// EIP-7623: max(total_used_gas, floor_gas)
if self.config.has_floor_gas
&& total_used_gas_refunded < self.state.metadata().gasometer.floor_gas()
{
self.state.metadata().gasometer.floor_gas()
} else {
total_used_gas_refunded
}
}

/// Get fee needed for the current executor, given the price.
Expand Down Expand Up @@ -945,7 +954,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet>
/// 9. Increase the `nonce` of `authority` by one.
///
/// It means, that steps 1,3 of spec must be passed before calling this function:
/// 1 Verify the chain id is either 0 or the chain’s current ID.
/// 1. Verify the chain id is either 0 or the chain’s current ID.
/// 3. `authority = ecrecover(...)`
///
/// See: [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702#behavior)
Expand Down Expand Up @@ -994,13 +1003,13 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet>
// 8. Set the code of authority to be `0xef0100 || address`. This is a delegation designation.
// * As a special case, if address is 0x0000000000000000000000000000000000000000 do not write the designation.
// Clear the account’s code.
let mut delegation_clearing = false;
if authority.address.is_zero() {
delegation_clearing = true;
let delegation_clearing = if authority.address.is_zero() {
state.set_code(authority.authority, Vec::new());
true
} else {
state.set_code(authority.authority, authority.delegation_code());
}
false
};
// 9. Increase the nonce of authority by one.
state.inc_nonce(authority.authority)?;

Expand Down

0 comments on commit ef796b1

Please sign in to comment.