Skip to content
Open
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
12 changes: 12 additions & 0 deletions crates/context/interface/src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ pub trait Cfg {

/// Returns the gas params for the EVM.
fn gas_params(&self) -> &GasParams;

/// Whether Soul Gas Token (SGT) is enabled for gas payment.
/// Default: false. Only used by OP Stack chains with SGT.
fn is_sgt_enabled(&self) -> bool {
false
}

/// Whether SGT is backed 1:1 by native token.
/// Default: true.
fn is_sgt_native_backed(&self) -> bool {
true
}
}

/// What bytecode analysis to perform
Expand Down
52 changes: 52 additions & 0 deletions crates/context/interface/src/journaled_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,46 @@ pub trait JournalTr {
_skip_cold_load: bool,
) -> Result<StateLoad<SStoreResult>, JournalLoadError<<Self::Database as Database>::Error>>;

/// Loads storage value without affecting warm/cold status.
///
/// Used for protocol-level operations (e.g., SGT gas payment) that should not
/// influence EIP-2929 gas metering during execution.
fn sload_no_warm(
&mut self,
address: Address,
key: StorageKey,
) -> Result<StorageValue, <Self::Database as Database>::Error> {
let _ = (address, key);
unimplemented!("sload_no_warm not implemented — required for SGT support")
}

/// Stores storage value without affecting warm/cold status.
///
/// Used for protocol-level operations (e.g., SGT gas payment) that should not
/// influence EIP-2929 gas metering during execution. Still journals the storage
/// change so reverts work correctly.
fn sstore_no_warm(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change adds new required methods to the JournalTr trait (sload_no_warm, sstore_no_warm, load_account_mut_no_warm, load_account_no_warm), but the repo still has another impl JournalTr that was not updated: examples/cheatcode_inspector/src/main.rs.

As a result, this is currently source-breaking for the workspace and cargo test --workspace fails with missing trait items for Backend.

I think this needs either:

  • updating all in-repo JournalTr implementations together, or
  • providing default trait implementations if the intent is to extend the interface in a non-breaking way.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in b82be88.

Copy link
Copy Markdown
Collaborator Author

@blockchaindevsh blockchaindevsh Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qzhodl After further thought, I think it's better to panic in the default trait implementations to ensure they're never called(committed in 5d00d01). WDYT?

&mut self,
address: Address,
key: StorageKey,
value: StorageValue,
) -> Result<(), <Self::Database as Database>::Error> {
let _ = (address, key, value);
unimplemented!("sstore_no_warm not implemented — required for SGT support")
}

/// Loads account mutably without affecting warm/cold status.
///
/// Used for protocol-level balance modifications (e.g., SGT native-backed balance
/// sync) that should not influence EIP-2929 gas metering during execution.
fn load_account_mut_no_warm(
&mut self,
address: Address,
) -> Result<Self::JournaledAccount<'_>, <Self::Database as Database>::Error> {
let _ = address;
unimplemented!("load_account_mut_no_warm not implemented — required for SGT support")
}

/// Loads transient storage value.
fn tload(&mut self, address: Address, key: StorageKey) -> StorageValue;

Expand Down Expand Up @@ -161,6 +201,18 @@ pub trait JournalTr {
address: Address,
) -> Result<StateLoad<&Account>, <Self::Database as Database>::Error>;

/// Loads the account without affecting warm/cold status.
///
/// Used for protocol-level operations (e.g., SGT) that should not influence
/// EIP-2929 gas metering during execution.
fn load_account_no_warm(
&mut self,
address: Address,
) -> Result<StateLoad<&Account>, <Self::Database as Database>::Error> {
let _ = address;
unimplemented!("load_account_no_warm not implemented — required for SGT support")
}

/// Loads the account code, use `load_account_with_code` instead.
#[inline]
#[deprecated(note = "Use `load_account_with_code` instead")]
Expand Down
24 changes: 18 additions & 6 deletions crates/context/interface/src/journaled_state/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,17 @@ impl<'a, DB: Database, ENTRY: JournalEntryTr> JournaledAccount<'a, DB, ENTRY> {
/// Loads the storage slot.
///
/// If storage is cold and skip_cold_load is true, it will return [`JournalLoadError::ColdLoadSkipped`] error.
/// If `no_warm` is true, the slot is loaded without marking it warm or pushing warming
/// journal entries (used for protocol-level operations like SGT that should not
/// influence EIP-2929 gas metering).
///
/// Does not erase the db error.
#[inline(never)]
pub fn sload_concrete_error(
&mut self,
key: StorageKey,
skip_cold_load: bool,
no_warm: bool,
) -> Result<StateLoad<&mut EvmStorageSlot>, JournalLoadError<DB::Error>> {
let is_newly_created = self.account.is_created();
let (slot, is_cold) = match self.account.storage.entry(key) {
Expand All @@ -186,7 +190,9 @@ impl<'a, DB: Database, ENTRY: JournalEntryTr> JournaledAccount<'a, DB, ENTRY> {
return Err(JournalLoadError::ColdLoadSkipped);
}
}
slot.mark_warm_with_transaction_id(self.transaction_id);
if !no_warm {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the no_warm fix is still incomplete at both the account level and the storage-slot level.

There are still two places where warming state leaks through:

  1. Account path (load_account_mut_optional)
    In the Vacant branch, a newly loaded account is inserted into journal state with the current transaction_id, but it is not kept cold when no_warm = true. As a result, a later normal access in the same transaction can observe the account as already warm, even though this protocol-level read was supposed to have no EIP-2929 side effects.

  2. Storage path (sload_concrete_error)
    In the Vacant branch, a newly loaded slot is inserted with EvmStorageSlot::new(value, self.transaction_id). That constructor sets transaction_id to the current tx and is_cold = false, so even when no_warm = true, the slot is effectively cached as warm for subsequent accesses in the same transaction.

Also, ENTRY::storage_warmed(self.address, key) is still pushed whenever is_cold is true, even under no_warm = true, so the function still records a warming side effect in the journal.

So the current change only avoids the explicit mark_warm_with_transaction_id(...) calls, but it does not fully preserve “no warming side effects” semantics.

I think no_warm should also:

  • keep newly inserted cold accounts cold in the Vacant account path,
  • keep newly inserted cold slots cold in the Vacant storage path, and
  • skip pushing warming journal entries when no_warm = true.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in b82be88.

slot.mark_warm_with_transaction_id(self.transaction_id);
}
(slot, is_cold)
}
Entry::Vacant(vac) => {
Expand All @@ -200,19 +206,23 @@ impl<'a, DB: Database, ENTRY: JournalEntryTr> JournaledAccount<'a, DB, ENTRY> {
if is_cold && skip_cold_load {
return Err(JournalLoadError::ColdLoadSkipped);
}

// if storage was cleared, we don't need to ping db.
let value = if is_newly_created {
StorageValue::ZERO
} else {
self.db.storage(self.address, key)?
};

let slot = vac.insert(EvmStorageSlot::new(value, self.transaction_id));
// When no_warm, don't set transaction_id so the slot stays
// cold to later normal accesses (is_cold_transaction_id).
let tid = if no_warm { 0 } else { self.transaction_id };
let slot = vac.insert(EvmStorageSlot::new(value, tid));
(slot, is_cold)
}
};

if is_cold {
if is_cold && !no_warm {
// add it to journal as cold loaded.
self.journal_entries
.push(ENTRY::storage_warmed(self.address, key));
Expand All @@ -224,6 +234,7 @@ impl<'a, DB: Database, ENTRY: JournalEntryTr> JournaledAccount<'a, DB, ENTRY> {
/// Stores the storage slot.
///
/// If storage is cold and skip_cold_load is true, it will return [`JournalLoadError::ColdLoadSkipped`] error.
/// If `no_warm` is true, storage is accessed without affecting warm/cold status.
///
/// Does not erase the db error.
#[inline]
Expand All @@ -232,12 +243,13 @@ impl<'a, DB: Database, ENTRY: JournalEntryTr> JournaledAccount<'a, DB, ENTRY> {
key: StorageKey,
new: StorageValue,
skip_cold_load: bool,
no_warm: bool,
) -> Result<StateLoad<SStoreResult>, JournalLoadError<DB::Error>> {
// touch the account so changes are tracked.
self.touch();

// assume that acc exists and load the slot.
let slot = self.sload_concrete_error(key, skip_cold_load)?;
let slot = self.sload_concrete_error(key, skip_cold_load, no_warm)?;

let ret = Ok(StateLoad::new(
SStoreResult {
Expand Down Expand Up @@ -465,7 +477,7 @@ impl<'a, DB: Database, ENTRY: JournalEntryTr> JournaledAccountTr
key: StorageKey,
skip_cold_load: bool,
) -> Result<StateLoad<&mut EvmStorageSlot>, JournalLoadErasedError> {
self.sload_concrete_error(key, skip_cold_load)
self.sload_concrete_error(key, skip_cold_load, false)
.map_err(|i| i.map(ErasedError::new))
}

Expand All @@ -477,7 +489,7 @@ impl<'a, DB: Database, ENTRY: JournalEntryTr> JournaledAccountTr
new: StorageValue,
skip_cold_load: bool,
) -> Result<StateLoad<SStoreResult>, JournalLoadErasedError> {
self.sstore_concrete_error(key, new, skip_cold_load)
self.sstore_concrete_error(key, new, skip_cold_load, false)
.map_err(|i| i.map(ErasedError::new))
}

Expand Down
17 changes: 17 additions & 0 deletions crates/context/src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ pub struct CfgEnv<SPEC = SpecId> {
pub limit_contract_initcode_size: Option<usize>,
/// Skips the nonce validation against the account's nonce
pub disable_nonce_check: bool,
/// Whether Soul Gas Token (SGT) is enabled for gas payment.
/// Only used by OP Stack chains with SGT deployed. Default: false.
pub sgt_enabled: bool,
/// Whether SGT is backed 1:1 by native token. Default: true.
pub sgt_is_native_backed: bool,
/// Blob max count. EIP-7840 Add blob schedule to EL config files.
///
/// If this config is not set, the check for max blobs will be skipped.
Expand Down Expand Up @@ -149,6 +154,8 @@ impl<SPEC> CfgEnv<SPEC> {
limit_contract_initcode_size: None,
spec,
disable_nonce_check: false,
sgt_enabled: false,
sgt_is_native_backed: true,
max_blobs_per_tx: None,
tx_gas_limit_cap: None,
blob_base_fee_update_fraction: None,
Expand Down Expand Up @@ -251,6 +258,8 @@ impl<SPEC> CfgEnv<SPEC> {
limit_contract_initcode_size: self.limit_contract_initcode_size,
spec,
disable_nonce_check: self.disable_nonce_check,
sgt_enabled: self.sgt_enabled,
sgt_is_native_backed: self.sgt_is_native_backed,
tx_gas_limit_cap: self.tx_gas_limit_cap,
max_blobs_per_tx: self.max_blobs_per_tx,
blob_base_fee_update_fraction: self.blob_base_fee_update_fraction,
Expand Down Expand Up @@ -502,6 +511,14 @@ impl<SPEC: Into<SpecId> + Clone> Cfg for CfgEnv<SPEC> {
fn gas_params(&self) -> &GasParams {
&self.gas_params
}

fn is_sgt_enabled(&self) -> bool {
self.sgt_enabled
}

fn is_sgt_native_backed(&self) -> bool {
self.sgt_is_native_backed
}
}

impl<SPEC: Default + Into<SpecId>> Default for CfgEnv<SPEC> {
Expand Down
35 changes: 34 additions & 1 deletion crates/context/src/journal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,32 @@ impl<DB: Database, ENTRY: JournalEntryTr> JournalTr for Journal<DB, ENTRY> {
.map_err(JournalLoadError::unwrap_db_error)
}

fn sload_no_warm(
&mut self,
address: Address,
key: StorageKey,
) -> Result<StorageValue, <Self::Database as Database>::Error> {
self.inner.sload_no_warm(&mut self.database, address, key)
}

fn sstore_no_warm(
&mut self,
address: Address,
key: StorageKey,
value: StorageValue,
) -> Result<(), <Self::Database as Database>::Error> {
self.inner
.sstore_no_warm(&mut self.database, address, key, value)
}

fn load_account_mut_no_warm(
&mut self,
address: Address,
) -> Result<Self::JournaledAccount<'_>, <Self::Database as Database>::Error> {
self.inner
.load_account_mut_no_warm(&mut self.database, address)
}

fn tload(&mut self, address: Address, key: StorageKey) -> StorageValue {
self.inner.tload(address, key)
}
Expand Down Expand Up @@ -256,14 +282,21 @@ impl<DB: Database, ENTRY: JournalEntryTr> JournalTr for Journal<DB, ENTRY> {
self.inner.load_account(&mut self.database, address)
}

fn load_account_no_warm(&mut self, address: Address) -> Result<StateLoad<&Account>, DB::Error> {
self.inner
.load_account_mut_optional(&mut self.database, address, false, true)
.map_err(JournalLoadError::unwrap_db_error)
.map(|s| s.map(|j| j.into_account()))
}

#[inline]
fn load_account_mut_skip_cold_load(
&mut self,
address: Address,
skip_cold_load: bool,
) -> Result<StateLoad<Self::JournaledAccount<'_>>, DB::Error> {
self.inner
.load_account_mut_optional(&mut self.database, address, skip_cold_load)
.load_account_mut_optional(&mut self.database, address, skip_cold_load, false)
.map_err(JournalLoadError::unwrap_db_error)
}

Expand Down
Loading