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
16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@ license = "MIT OR Apache-2.0"
autobins = false

[dependencies]
# Upstream revm v29.0.1 (no fork). The miner reward is skipped during parallel execution via a
# Upstream revm v40 (no fork). The miner reward is skipped during parallel execution via a
# `GrevmHandler` in deferred-beneficiary mode and self-computed at commit time, so the
# previous `Galxe/revm` fork (which only added a `lazy_reward` cfg flag + `ResultAndState` field)
# is no longer needed.
revm = { version = "29.0.1", default-features = false }
revm-database = { version = "7.0.5", default-features = false }
revm-state = { version = "7.0.5", default-features = false }
revm-primitives = { version = "20.2.1", default-features = false }
revm-inspector = { version = "10.0.1", default-features = false }
revm-context = { version = "9.1.0", default-features = false }
revm = { version = "40.0.3", default-features = false }
revm-database = { version = "15.0.0", default-features = false }
revm-state = { version = "12.0.0", default-features = false }
revm-primitives = { version = "24.0.0", default-features = false }
revm-inspector = { version = "21.0.3", default-features = false }
revm-context = { version = "18.0.3", default-features = false }

ahash = "0.8"
rayon = "1.10.0"
parking_lot = "0.12"

# Alloy — upstream (the `Galxe/alloy-evm` fork only repointed revm to the gravity fork and added a
# `..` to one `ResultAndState` destructure; both are unnecessary against upstream revm).
alloy-evm = { version = "0.21.3", default-features = false }
alloy-evm = { version = "0.36.0", default-features = false }

# async
futures = "0.3"
Expand Down
6 changes: 4 additions & 2 deletions docs/use-with-reth.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ where
for outcome in &results {
match outcome {
TxExecutionOutcome::Executed(result) => {
let _gas_used = result.gas_used();
let _gas_used = result.tx_gas_used();
}
TxExecutionOutcome::Skipped(reason) => {
eprintln!("transaction skipped: {reason:?}");
Expand Down Expand Up @@ -133,7 +133,9 @@ end-to-end harness (`src/test_utils/common/execute.rs`).
## Optional delegated-account policy

`DelegatedSafetyConfig` contains two Grevm/Gravity-specific, opt-in EIP-7702 policies. Both are
disabled by default to preserve stock revm/Ethereum execution semantics:
disabled by default to preserve stock revm/Ethereum execution semantics. They are automatically
inactive before Prague, so one block-scoped policy configuration can safely be reused while
replaying historical blocks:

- `forbid_delegated_create` makes `CREATE` and `CREATE2` halt as not activated while executing in a
delegated account's context.
Expand Down
3 changes: 1 addition & 2 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[toolchain]
channel = "1.88.0"
channel = "1.93.0"

# Note: we don't specify cargofmt in our toolchain because we rely on
# the nightly version of cargofmt and verify formatting in CI/CD.
components = ["cargo", "clippy", "rustc", "rust-docs", "rust-std", "rust-analyzer"]

9 changes: 6 additions & 3 deletions src/bin/fetch_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ mod rpc;
use std::path::PathBuf;

use grevm::test_utils::common::mainnet::{
self, BlockFixture, PreState, TxFixture, spec_for_timestamp, write_mainnet_block,
self, BlockFixture, PreState, TxFixture, spec_for_mainnet_block, write_mainnet_block,
};
use rpc::{Rpc, parse_block_number};
use serde_json::{Value, json};
Expand Down Expand Up @@ -62,8 +62,11 @@ fn main() -> Result<(), Error> {
}
let spec_id = spec_override.unwrap_or_else(|| {
let ts = block.get("timestamp").and_then(Value::as_str).unwrap_or("0x0");
spec_for_timestamp(u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0))
.to_string()
spec_for_mainnet_block(
block_number,
u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0),
)
.to_string()
});
let mut block_fixture = BlockFixture::from_rpc(block_number, chain_id, spec_id, &block)?;
// prestateTracer can't report block hashes (not account state); fetch the last 256 for
Expand Down
10 changes: 6 additions & 4 deletions src/bin/fetch_continuous.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use std::path::PathBuf;

use grevm::test_utils::common::{
execute,
mainnet::{self, BlockFixture, PreState, TxFixture, spec_for_timestamp, write_block},
mainnet::{self, BlockFixture, PreState, TxFixture, spec_for_mainnet_block, write_block},
};
use revm_context::TxEnv;
use rpc::{Rpc, parse_block_number};
Expand Down Expand Up @@ -69,9 +69,11 @@ fn main() -> Result<(), Error> {
return Err(format!("block {hex} not found").into());
}
let ts = block.get("timestamp").and_then(Value::as_str).unwrap_or("0x0");
let spec =
spec_for_timestamp(u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0))
.to_string();
let spec = spec_for_mainnet_block(
bn,
u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0),
)
.to_string();
let bf = BlockFixture::from_rpc(bn, chain_id, spec, &block)?;

let n_tx = if let Some(arr) = block.get("transactions").and_then(Value::as_array) {
Expand Down
11 changes: 7 additions & 4 deletions src/bin/replay_mainnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ use std::{
use grevm::test_utils::common::{
execute,
mainnet::{
self, AccountFixture, BlockFixture, MainnetBlock, PreState, TxFixture, spec_for_timestamp,
self, AccountFixture, BlockFixture, MainnetBlock, PreState, TxFixture,
spec_for_mainnet_block,
},
};
use revm_primitives::{Address, B256};
Expand Down Expand Up @@ -265,9 +266,11 @@ fn build_block(
caches: &mut Caches,
) -> Result<MainnetBlock, Error> {
let ts = block.get("timestamp").and_then(Value::as_str).unwrap_or("0x0");
let spec =
spec_for_timestamp(u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0))
.to_string();
let spec = spec_for_mainnet_block(
number,
u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0),
)
.to_string();
let mut bf = BlockFixture::from_rpc(number, chain_id, spec, block)?;
// Block hashes for BLOCKHASH, reusing the cross-block cache (fetches only the few new entries).
let (lo, hi) = (number.saturating_sub(256), number.saturating_sub(1));
Expand Down
4 changes: 2 additions & 2 deletions src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ impl<DB: DatabaseRef> ParallelTakeBundle for ParallelState<DB> {
mod tests {
use super::*;
use revm_database::{AccountStatus, TransitionAccount};
use revm_primitives::{Bytes, HashMap};
use revm_primitives::{AddressMap, Bytes};
use revm_state::AccountInfo;

fn transitions(count: usize) -> TransitionState {
Expand All @@ -142,7 +142,7 @@ mod tests {
},
)
})
.collect::<HashMap<_, _>>(),
.collect::<AddressMap<_>>(),
}
}

Expand Down
37 changes: 36 additions & 1 deletion src/delegated_safety/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
///
/// The switches are independent because delegated CREATE changes an EOA's nonce, while the
/// balance guard handles value movement that admission filtering cannot see without execution.
/// Both are opt-in and disabled by [`Default`], preserving upstream revm behavior.
/// Both are opt-in and disabled by [`Default`], preserving upstream revm behavior. Before Prague,
/// where EIP-7702 is not active, both switches are treated as disabled.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DelegatedSafetyConfig {
/// Halts CREATE and CREATE2 in an EIP-7702 delegated account's execution context with
Expand Down Expand Up @@ -39,4 +40,38 @@ impl DelegatedSafetyConfig {
pub const fn enabled() -> Self {
Self { forbid_delegated_create: true, reserve_delegated_balance: true }
}

/// Returns the effective policy for `spec`.
///
/// EIP-7702 activates in Prague. Keeping a policy configured while replaying older blocks is
/// supported, but it must not alter their instruction table or transaction handler.
pub const fn for_spec(self, spec: revm_primitives::hardfork::SpecId) -> Self {
if spec.is_enabled_in(revm_primitives::hardfork::SpecId::PRAGUE) {
self
} else {
Self::disabled()
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use revm_primitives::hardfork::SpecId;

#[test]
fn delegated_safety_is_inert_before_eip7702_activation() {
assert_eq!(
DelegatedSafetyConfig::enabled().for_spec(SpecId::CANCUN),
DelegatedSafetyConfig::disabled()
);
assert_eq!(
DelegatedSafetyConfig::enabled().for_spec(SpecId::PRAGUE),
DelegatedSafetyConfig::enabled()
);
assert_eq!(
DelegatedSafetyConfig::reserve_only().for_spec(SpecId::AMSTERDAM),
DelegatedSafetyConfig::reserve_only()
);
}
}
50 changes: 35 additions & 15 deletions src/delegated_safety/handler.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{ReserveJournalExt, ReservePlanner};
use metrics::counter;
use revm::{
context_interface::journaled_state::account::JournaledAccountTr,
handler::{EvmTr, EvmTrError, FrameResult, FrameTr, Handler, post_execution},
interpreter::{
CallOutcome, InitialAndFloorGas, InstructionResult, InterpreterResult,
Expand All @@ -10,7 +11,7 @@ use revm::{
use revm_context::{
BlockEnv, ContextTr, JournalTr, Transaction, TxEnv,
journaled_state::JournalCheckpoint,
result::{ExecutionResult, HaltReason, InvalidTransaction},
result::{ExecutionResult, HaltReason, InvalidTransaction, ResultGas},
};
use revm_primitives::Bytes;
use revm_state::EvmState;
Expand Down Expand Up @@ -217,13 +218,17 @@ where
type Error = ERROR;
type HaltReason = HaltReason;

fn pre_execution(&self, evm: &mut Self::Evm) -> Result<u64, Self::Error> {
fn pre_execution(
&self,
evm: &mut Self::Evm,
init_and_floor_gas: &mut InitialAndFloorGas,
) -> Result<u64, Self::Error> {
// This is revm's default `pre_execution` sequence. The trait has no hook after
// authorization processing, so these three calls are repeated here solely to place the
// reserve checkpoint at the transaction/execution boundary.
self.validate_against_state_and_deduct_caller(evm)?;
self.validate_against_state_and_deduct_caller(evm, init_and_floor_gas)?;
self.load_accounts(evm)?;
let eip7702_refund = self.apply_eip7702_auth_list(evm)?;
let eip7702_refund = self.apply_eip7702_auth_list(evm, init_and_floor_gas)?;

debug_assert!(self.execution_checkpoint.get().is_none());
self.execution_checkpoint.set(Some(evm.ctx().journal_mut().checkpoint()));
Expand All @@ -237,23 +242,34 @@ where
exec_result: &mut FrameResult,
init_and_floor_gas: InitialAndFloorGas,
eip7702_gas_refund: i64,
) -> Result<(), Self::Error> {
) -> Result<ResultGas, Self::Error> {
// Preserve the gas state before revm merges execution refunds with the authorization
// refund. A reserve violation rolls back execution, so only the independently earned
// EIP-7702 authorization refund may be reapplied to this snapshot.
let execution_gas = *exec_result.gas();

self.refund(evm, exec_result, eip7702_gas_refund);
// Match revm's default lifecycle: capture the pre-floor gas components. `ResultGas`
// applies the EIP-7623 floor when deriving `tx_gas_used`, while retaining the actual
// pre-floor spend and refund for downstream consumers.
let mut result_gas = post_execution::build_result_gas(
exec_result.instruction_result().is_halt(),
exec_result.gas(),
init_and_floor_gas,
);
self.eip7623_check_gas_floor(evm, exec_result, init_and_floor_gas);
self.reimburse_caller(evm, exec_result)?;
self.enforce_reserve(
if let Some(reserve_result_gas) = self.enforce_reserve(
evm,
exec_result,
execution_gas,
init_and_floor_gas,
eip7702_gas_refund,
)?;
self.beneficiary_mode.apply(evm, exec_result)
)? {
result_gas = reserve_result_gas;
}
self.beneficiary_mode.apply::<EVM, ERROR>(evm, exec_result)?;
Ok(result_gas)
}
}

Expand All @@ -277,7 +293,7 @@ where
execution_gas: revm::interpreter::Gas,
init_and_floor_gas: InitialAndFloorGas,
eip7702_gas_refund: i64,
) -> Result<(), ERROR> {
) -> Result<Option<ResultGas>, ERROR> {
let execution_checkpoint = self
.execution_checkpoint
.take()
Expand All @@ -301,12 +317,18 @@ where
// from the pre-refund execution gas. The first reimbursement was also reverted, so
// apply it again using this synthetic top-level REVERT result.
self.refund(evm, exec_result, eip7702_gas_refund);
let result_gas = post_execution::build_result_gas(
exec_result.instruction_result().is_halt(),
exec_result.gas(),
init_and_floor_gas,
);
self.eip7623_check_gas_floor(evm, exec_result, init_and_floor_gas);
self.reimburse_caller(evm, exec_result)?;
return Ok(Some(result_gas))
} else {
evm.ctx().journal_mut().checkpoint_commit();
}
Ok(())
Ok(None)
}

fn has_reserve_violation(
Expand Down Expand Up @@ -357,11 +379,9 @@ where
// Reproduce the nonce bump normally performed by revm's top-level CREATE frame. It must be a
// journaled mutation so a surrounding transaction/error rollback still has correct semantics.
let sender = evm.ctx_ref().tx().caller();
let account = evm.ctx().journal_mut().load_account(sender)?;
let Some(new_nonce) = account.data.info.nonce.checked_add(1) else {
let mut account = evm.ctx().journal_mut().load_account_mut(sender)?;
if !account.data.bump_nonce() {
return Err(InvalidTransaction::NonceOverflowInTransaction.into());
};
account.data.info.nonce = new_nonce;
evm.ctx().journal_mut().nonce_bump_journal_entry(sender);
}
Ok(())
}
28 changes: 12 additions & 16 deletions src/delegated_safety/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use revm::{
bytecode::opcode::{CREATE, CREATE2},
handler::instructions::EthInstructions,
interpreter::{
Host, Instruction, InstructionContext, InstructionResult,
Host, Instruction, InstructionContext, InstructionExecResult, InstructionResult,
instructions::contract,
interpreter::EthInterpreter,
interpreter_types::{InputsTr, InterpreterTypes, RuntimeFlag},
Expand All @@ -11,43 +11,39 @@ use revm::{
use revm_primitives::hardfork::SpecId;

/// Mainnet instructions with grevm-local CREATE/CREATE2 delegated-context guard.
pub(crate) fn gravity_instructions<CTX>() -> EthInstructions<EthInterpreter, CTX>
pub(crate) fn gravity_instructions<CTX>(spec: SpecId) -> EthInstructions<EthInterpreter, CTX>
where
CTX: Host,
{
let mut instructions = EthInstructions::new_mainnet();
instructions.insert_instruction(CREATE, Instruction::new(guarded_create::<_, false, _>, 0));
instructions.insert_instruction(CREATE2, Instruction::new(guarded_create::<_, true, _>, 0));
let mut instructions = EthInstructions::new_mainnet_with_spec(spec);
instructions.insert_instruction(CREATE, Instruction::new(guarded_create::<false, _, _>), 0);
instructions.insert_instruction(CREATE2, Instruction::new(guarded_create::<true, _, _>), 0);
instructions
}

fn guarded_create<WIRE: InterpreterTypes, const IS_CREATE2: bool, H: Host + ?Sized>(
fn guarded_create<const IS_CREATE2: bool, WIRE: InterpreterTypes, H: Host + ?Sized>(
context: InstructionContext<'_, H, WIRE>,
) {
) -> InstructionExecResult {
if context.interpreter.runtime_flag.is_static() {
context.interpreter.halt(InstructionResult::StateChangeDuringStaticCall);
return;
return Err(InstructionResult::StateChangeDuringStaticCall)
}

if IS_CREATE2 && !context.interpreter.runtime_flag.spec_id().is_enabled_in(SpecId::PETERSBURG) {
context.interpreter.halt(InstructionResult::NotActivated);
return;
return Err(InstructionResult::NotActivated)
}

// `target_address` is the account owning this execution context. For a 7702 call it remains
// the delegated EOA even though the interpreter executes bytecode loaded from its delegate.
let recipient = context.interpreter.input.target_address();
let Some(load) = context.host.load_account_delegated(recipient) else {
context.interpreter.halt(InstructionResult::FatalExternalError);
return;
return Err(InstructionResult::FatalExternalError)
};

// `Some(coldness)` means the target has an EIP-7702 delegation designator; the boolean itself
// only reports whether loading the delegate was cold and is irrelevant to this policy.
if load.is_delegate_account_cold.is_some() {
context.interpreter.halt(InstructionResult::NotActivated);
return;
return Err(InstructionResult::NotActivated)
}

contract::create::<WIRE, IS_CREATE2, H>(context);
contract::create::<IS_CREATE2, WIRE, H>(context)
}
4 changes: 3 additions & 1 deletion src/delegated_safety/reserve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,9 @@ fn balance_before_entry(
#[cfg(test)]
mod tests {
use super::*;
use revm_context::{JournalTr, journal::entry::SelfdestructionRevertStatus};
use revm::{
context::JournalTr, context_interface::journaled_state::entry::SelfdestructionRevertStatus,
};
use revm_database::EmptyDB;
use revm_primitives::address;
use revm_state::{Account, Bytecode};
Expand Down
Loading
Loading