Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7a67167
Fix intrinsic/minimal gas calculation for L1 transactions in `process…
antoniolocascio Feb 19, 2026
ac600e5
Clear returndata when reverting
antoniolocascio Feb 19, 2026
a1138d7
Move assertion about blob base fee outside of if block
antoniolocascio Feb 19, 2026
9d54029
Fix L1 simulation native per gas
antoniolocascio Feb 19, 2026
fedc8ff
Update comment
antoniolocascio Feb 19, 2026
f36e4f1
Simplify UsizeDeserializable for B160
antoniolocascio Feb 19, 2026
a9e2b6c
Rename `is_l1_tx` to `is_priority_tx` in the transaction processing c…
antoniolocascio Feb 19, 2026
e7f8f26
Do not charge Ergs in hooks
antoniolocascio Feb 19, 2026
381bb8e
Fix u256_to_usize_saturated
antoniolocascio Feb 19, 2026
48972ef
Add log when gas_limit < intrinsic in L1 tx
antoniolocascio Feb 19, 2026
ae77acd
Reuse allocated records in transient storage
antoniolocascio Feb 19, 2026
46001b2
Check that value is 0 in mint base token hook
antoniolocascio Feb 19, 2026
18f984e
Fix references to EIP-170
antoniolocascio Feb 19, 2026
fc18d3f
Fix debug logs
antoniolocascio Feb 19, 2026
56549ce
Implement missing io methods
antoniolocascio Feb 19, 2026
3c8f35a
Remove UB in aligned_vector
antoniolocascio Feb 19, 2026
4ef2583
Update comment
antoniolocascio Feb 24, 2026
e647c66
Remove dead code
antoniolocascio Feb 24, 2026
3daac53
Rename reverted()
antoniolocascio Feb 24, 2026
c9ef994
Add missing log on saturating arith
antoniolocascio Feb 24, 2026
c2c60b1
Rename clear history map fun and add tests
antoniolocascio Feb 24, 2026
2630281
Add more regression tests
antoniolocascio Feb 24, 2026
0db2015
Merge branch 'dev' into alocascio-audittens-fixes
0xVolosnikov Feb 25, 2026
724f6f2
Update tests using new approach
0xVolosnikov Feb 25, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ pub fn read_multichain_root<
&MESSAGE_ROOT_ADDRESS,
&root_slot,
)
.expect("must read MessageRoot shared tree height")
.expect("must read MessageRoot multichain root")
}

///
Expand Down
2 changes: 1 addition & 1 deletion basic_bootloader/src/bootloader/block_flow/zk/tx_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ where
block_data
.transaction_hashes_accumulator
.add_tx_hash(&tx_processing_result.tx_hash);
if tx_processing_result.is_l1_tx {
if tx_processing_result.is_priority_tx {
block_data
.enforced_transaction_hashes_accumulator
.add_tx_hash(&tx_processing_result.tx_hash);
Expand Down
6 changes: 3 additions & 3 deletions basic_bootloader/src/bootloader/transaction_flow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ impl<'a, IOTypes: SystemIOTypesConfig> ExecutionResult<'a, IOTypes> {
pub fn reverted(self) -> Self {
match self {
Self::Success {
output: ExecutionOutput::Call(r),
output: ExecutionOutput::Call(_),
}
| Self::Success {
output: ExecutionOutput::Create(r, _),
} => Self::Revert { output: r },
output: ExecutionOutput::Create(_, _),
} => Self::Revert { output: &[] },
a => a,
}
}
Expand Down
13 changes: 6 additions & 7 deletions basic_bootloader/src/bootloader/transaction_flow/zk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub struct ZkTransactionFlowOnlyEOA<S: EthereumLikeTypes> {
pub struct ZkTxResult<'a> {
pub result: ExecutionResult<'a, EthereumIOTypesConfig>,
pub tx_hash: Bytes32,
pub is_l1_tx: bool,
pub is_priority_tx: bool,
pub is_upgrade_tx: bool,
pub is_service_tx: bool,
pub gas_refunded: u64,
Expand Down Expand Up @@ -136,7 +136,7 @@ impl<S: EthereumLikeTypes> core::fmt::Debug for TxContextForPreAndPostProcessing
.field("native_per_gas", &self.native_per_gas)
.field("tx_gas_limit", &self.tx_gas_limit)
.field("gas_used", &self.gas_used)
.field("gas_refunded", &self.gas_used)
.field("gas_refunded", &self.gas_refunded)
.field("validation_pubdata", &self.validation_pubdata)
.field("total_pubdata", &self.total_pubdata)
.field("native_used", &self.native_used)
Expand Down Expand Up @@ -478,10 +478,9 @@ where
// go to the operator. Base fees are effectively "burned" (not transferred anywhere).
let gas_price_for_operator = if cfg!(feature = "burn_base_fee") {
let base_fee = system.get_eip1559_basefee();
context
.gas_price
.checked_sub(base_fee)
.ok_or(internal_error!("Gas_price - base_fee underflow"))?
// Note, underflow can only happen during simulation or
// service transactions
context.gas_price.saturating_sub(base_fee)
} else {
context.gas_price
};
Expand Down Expand Up @@ -564,7 +563,7 @@ where
ZkTxResult {
result,
tx_hash: context.tx_hash,
is_l1_tx: false,
is_priority_tx: false,
is_upgrade_tx: false,
is_service_tx: transaction.is_service(),
gas_used: context.gas_used,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::bootloader::config::BasicBootloaderExecutionConfig;
use crate::bootloader::constants::{
FREE_L1_TX_NATIVE_PER_GAS, L1_TX_INTRINSIC_NATIVE_COST, L1_TX_INTRINSIC_PUBDATA,
L1_TX_NATIVE_PRICE,
FREE_L1_TX_NATIVE_PER_GAS, L1_TX_INTRINSIC_L2_GAS, L1_TX_INTRINSIC_NATIVE_COST,
L1_TX_INTRINSIC_PUBDATA, L1_TX_NATIVE_PRICE,
};
use crate::bootloader::errors::BootloaderInterfaceError;
use crate::bootloader::errors::TxError;
Expand Down Expand Up @@ -90,7 +90,7 @@ where
native_per_gas,
native_per_pubdata,
minimal_gas_used,
} = prepare_and_check_resources::<S>(
} = prepare_and_check_resources::<S, Config>(
system,
transaction,
is_priority_op,
Expand Down Expand Up @@ -333,7 +333,7 @@ where
Ok(ZkTxResult {
result,
tx_hash,
is_l1_tx: is_priority_op,
is_priority_tx: is_priority_op,
is_upgrade_tx: !is_priority_op,
is_service_tx: false,
gas_used,
Expand Down Expand Up @@ -365,7 +365,11 @@ struct ResourceAndFeeInfo<S: EthereumLikeTypes> {
/// The approach is to use saturating arithmetic and emit a system
/// log if this situation ever happens.
///
fn prepare_and_check_resources<'a, S: EthereumLikeTypes + 'a>(
fn prepare_and_check_resources<
'a,
S: EthereumLikeTypes + 'a,
Config: BasicBootloaderExecutionConfig,
>(
system: &mut System<S>,
transaction: &AbiEncodedTransaction<S::Allocator>,
is_priority_op: bool,
Expand All @@ -382,15 +386,24 @@ where
let native_price = L1_TX_NATIVE_PRICE;
let native_per_gas = if is_priority_op {
if gas_price.is_zero() {
FREE_L1_TX_NATIVE_PER_GAS
} else {
u256_try_to_u64(&gas_price.div_ceil(native_price))
.unwrap_or_else(|| {
system_log!(
system,
"Native per gas calculation for L1 tx overflows, using saturated arithmetic instead");
if Config::SIMULATION {
u256_try_to_u64(&system.get_eip1559_basefee().div_ceil(native_price))
.unwrap_or_else(|| {
system_log!(
system,
"Native per gas calculation for L1 tx overflows, using saturated arithmetic instead");
u64::MAX
})
})
} else {
FREE_L1_TX_NATIVE_PER_GAS
}
} else {
u256_try_to_u64(&gas_price.div_ceil(native_price)).unwrap_or_else(|| {
system_log!(
system,
"Native per gas calculation for L1 tx overflows, using saturated arithmetic instead");
u64::MAX
})
}
} else {
// Upgrade txs are paid by the protocol, so we use a fixed native per gas
Expand All @@ -408,7 +421,7 @@ where

let native_prepaid_from_gas = native_per_gas.saturating_mul(gas_limit);

let (calldata_tokens, intrinsic_cost) =
let (calldata_tokens, minimal_gas_used) =
compute_calldata_tokens(system, transaction.calldata(), true);

// With L1ResourcesPolicy, this returns Result<ResourcesForTx<S>, BootloaderSubsystemError>
Expand All @@ -422,14 +435,21 @@ where
false, // is_deployment
transaction.calldata().len() as u64,
calldata_tokens,
intrinsic_cost,
L1_TX_INTRINSIC_L2_GAS,
L1_TX_INTRINSIC_PUBDATA,
L1_TX_INTRINSIC_NATIVE_COST,
)?;

// L1 transactions might have a gas limit < intrinsic cost,
// so we pick the min as minimal_gas_used.
let minimal_gas_used = intrinsic_cost.min(gas_limit);
// L1 transactions might have a gas limit < minimal_gas_used. This should be
// prevented by L1 validation, but we log and saturate if it happens.
if gas_limit < minimal_gas_used {
system_log!(
system,
"L1 tx gas limit below intrinsic cost, using saturated arithmetic instead"
);
}
// Pick the min to keep processing L1 txs even if the L1 validation is wrong.
let minimal_gas_used = minimal_gas_used.min(gas_limit);

Ok(ResourceAndFeeInfo {
resources,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,18 +322,19 @@ where
// Parse blobs, if any
// No need to feature gate this part, as blobs() should return an empty list
// for non-EIP4844 transactions.
let block_base_fee_per_blob_gas = system.get_blob_base_fee_per_gas();

#[cfg(not(feature = "eip-4844"))]
crate::require_internal!(
block_base_fee_per_blob_gas == U256::ONE,
"Blob base fee should be set to 1 if EIP 4844 is disabled",
system
)?;

let blobs = if let Some(blobs_list) = transaction.blobs() {
let tx_max_fee_per_blob_gas = transaction.max_fee_per_blob_gas().ok_or(internal_error!(
"Tx with blobs must define max_fee_per_blob_gas"
))?;
let block_base_fee_per_blob_gas = system.get_blob_base_fee_per_gas();

#[cfg(not(feature = "eip-4844"))]
crate::require_internal!(
block_base_fee_per_blob_gas == U256::ONE,
"Blob base fee should be set to 1 if EIP 4844 is disabled",
system
)?;

if &block_base_fee_per_blob_gas > tx_max_fee_per_blob_gas && !Config::SIMULATION {
return Err(TxError::Validation(
Expand Down
18 changes: 15 additions & 3 deletions basic_system/src/system_implementation/flat_storage_model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,15 +450,27 @@ impl<

fn get_account_diff<'a>(
&'a self,
_address: Self::AccountAddress<'a>,
address: Self::AccountAddress<'a>,
) -> Option<Self::AccountDiff<'a>> {
None
self.account_data_cache
.cache
.get(address.into())
.map(|item| {
let current = item.current().value();
(current.nonce, current.balance, current.bytecode_hash)
})
}
fn accounts_diffs_iterator<'a>(
&'a self,
) -> impl ExactSizeIterator<Item = (Self::AccountAddress<'a>, Self::AccountDiff<'a>)> + Clone
{
[].into_iter()
self.account_data_cache.cache.iter().map(|item| {
let current = item.current().value();
(
item.key().as_ref(),
(current.nonce, current.balance, current.bytecode_hash),
)
})
}

type StorageKey<'a>
Expand Down
2 changes: 1 addition & 1 deletion docs/system_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The hook accepts the following ABI-encoded parameters:
- `bytes32` - observable bytecode hash

Key features:
- Enforces EIP-158 by rejecting bytecode longer than 24576 bytes
- Enforces EIP-170 by rejecting bytecode longer than 24576 bytes
- Used exclusively for protocol upgrades approved by governance
- Does not publish full bytecode in pubdata to fit within gas/calldata limits
- Bytecodes are published separately via Ethereum calldata
Expand Down
2 changes: 1 addition & 1 deletion evm_interpreter/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ impl<'ee, S: EthereumLikeTypes> Interpreter<'ee, S> {
let deployed_code = return_values.returndata;
let mut error_after_constructor = None;
if deployed_code.len() > MAX_CODE_SIZE {
// EIP-158: reject code of length > 24576.
// EIP-170: reject code of length > 24576.
error_after_constructor = Some(EvmError::CreateContractSizeLimit)
} else if !deployed_code.is_empty() && deployed_code[0] == 0xEF {
// EIP-3541: reject code starting with 0xEF.
Expand Down
2 changes: 1 addition & 1 deletion forward_system/src/run/query_processors/tx_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub struct TxDataResponder<TS: TxSource> {
/// Note: we use different fields for next_tx and next_tx_format
/// so that they don't have to be consumed at the same time.
pub next_tx_format: Option<TxEncodingFormat>,
/// Cached next transaction format, populated after size query
/// Cached next transaction from, populated after size query
/// (if present)
pub next_tx_from: Option<B160>,
}
Expand Down
2 changes: 1 addition & 1 deletion forward_system/src/system/system_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl<O: IOOracle> SystemTypes for ForwardSystemTypes<O> {
Self::Resources,
EthereumLikeStorageAccessCostModel,
VecStackFactory,
0, // Stack limit (0 = unlimited)
0, // VecStackFactory ignores N (node size), so 0 is fine here
O, // Oracle implementation
FlatTreeWithAccountsUnderHashesStorageModel<
Self::Allocator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pub struct GenericTransientStorage<
cache: HistoryMap<K, V, A, ()>,
pub(crate) current_tx_number: u32,
phantom: PhantomData<SF>,
alloc: A,
}

impl<
Expand All @@ -36,15 +35,14 @@ impl<
cache: HistoryMap::new(allocator.clone()),
current_tx_number: 0,
phantom: PhantomData,
alloc: allocator.clone(),
}
}

pub fn begin_new_tx(&mut self) {
// Just discard old history
// Note: it will reset snapshots counter, old snapshots handlers can't be used anymore
// Note: We will reset it redundantly for first tx
self.cache = HistoryMap::new(self.alloc.clone());
self.cache.reset_for_new_tx();
self.current_tx_number += 1;
}

Expand Down
2 changes: 1 addition & 1 deletion system_hooks/src/call_hooks/contract_deployer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ where
evm_interpreter::charge_native_and_ergs::<S::Resources>(
resources,
HOOK_BASE_NATIVE_COST,
HOOK_BASE_ERGS_COST,
Ergs::empty(),
)?;

if calldata.len() < 4 {
Expand Down
6 changes: 4 additions & 2 deletions system_hooks/src/call_hooks/mint_base_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ where
ergs_to_pass: _,
input: calldata,
call_scratch_space: _,
nominal_token_value: _,
nominal_token_value,
caller,
callee,
callers_caller: _,
Expand All @@ -46,6 +46,8 @@ where
}

let mut error = false;
// This hook doesn't accept any native token value
error |= nominal_token_value != U256::ZERO;
let mut is_static = false;
match modifier {
CallModifier::Constructor => {
Expand Down Expand Up @@ -73,7 +75,7 @@ where
evm_interpreter::charge_native_and_ergs::<S::Resources>(
&mut resources,
HOOK_BASE_NATIVE_COST,
HOOK_BASE_ERGS_COST,
Ergs(0), // Do not charge EVM gas here, it is already charged in the system contract
)?;
// Calldata length shouldn't be able to overflow u32, due to gas
// limitations.
Expand Down
4 changes: 2 additions & 2 deletions system_hooks/src/call_hooks/set_bytecode_on_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ where
evm_interpreter::charge_native_and_ergs::<S::Resources>(
resources,
HOOK_BASE_NATIVE_COST,
HOOK_BASE_ERGS_COST,
Ergs(0), // Do not charge EVM gas here, it is already charged in the system contract
)?;

if is_static {
Expand Down Expand Up @@ -163,7 +163,7 @@ where

// Although this can be called as a part of protocol upgrade,
// we are checking the next invariants, just in case
// EIP-158: reject code of length > 24576.
// EIP-170: reject code of length > 24576.
if bytecode_length as usize > MAX_CODE_SIZE {
return Ok(Err(
"Set bytecode on address failure: called with invalid bytecode(length > 24576)",
Expand Down
3 changes: 0 additions & 3 deletions system_hooks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
system::{
base_system_functions::{
Bn254AddErrors, Bn254MulErrors, Bn254PairingCheckErrors, ModExpErrors,
P256VerifyErrors, RipeMd160Errors, Secp256k1ECRecoverErrors, Sha256Errors,

Check warning on line 52 in system_hooks/src/lib.rs

View workflow job for this annotation

GitHub Actions / eth_stf_run

unused import: `P256VerifyErrors`

Check warning on line 52 in system_hooks/src/lib.rs

View workflow job for this annotation

GitHub Actions / Run benchmarks

unused import: `P256VerifyErrors`

Check warning on line 52 in system_hooks/src/lib.rs

View workflow job for this annotation

GitHub Actions / Run benchmarks

unused import: `P256VerifyErrors`

Check warning on line 52 in system_hooks/src/lib.rs

View workflow job for this annotation

GitHub Actions / Run benchmarks

unused import: `P256VerifyErrors`

Check warning on line 52 in system_hooks/src/lib.rs

View workflow job for this annotation

GitHub Actions / Run benchmarks

unused import: `P256VerifyErrors`
},
errors::subsystem::Subsystem,
EthereumLikeTypes, System, SystemTypes, *,
Expand Down Expand Up @@ -343,8 +343,5 @@
/// Base cost for calling into a system hook
const HOOK_BASE_NATIVE_COST: u64 = 1000;

/// Base ergs cost for calling a system hook (100 gas)
const HOOK_BASE_ERGS_COST: Ergs = Ergs(100 * ERGS_PER_GAS);

/// Ergs cost per byte of bytecode for force deployments.
const SET_BYTECODE_DETAILS_EXTRA_ERGS_PER_BYTE: Ergs = Ergs(50 * ERGS_PER_GAS);
4 changes: 2 additions & 2 deletions tests/instances/system_hooks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,8 +856,8 @@ fn test_contract_deployer_gas_charging() {
vec![(code_hash, bytecode)],
);

// The hook should charge HOOK_BASE_ERGS_COST (100 gas) + extra for bytecode length
assert_eq!(gas_used, 2950);
// The hook should charge for bytecode length
assert_eq!(gas_used, 2850);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion tests/rig/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ impl Default for BlockContext {
gas_limit: MAX_BLOCK_GAS_LIMIT,
pubdata_limit: u64::MAX,
mix_hash: U256::ONE,
blob_fee: U256::MAX,
blob_fee: U256::ONE,
}
}
}
Expand Down
Loading
Loading