Skip to content

RFC: custom MegaTransactionError variants without abandoning OpHandler#321

Draft
mark0-cn wants to merge 1 commit into
megaeth-labs:mainfrom
mark0-cn:mark0/docs/mega-transaction-error-rfc
Draft

RFC: custom MegaTransactionError variants without abandoning OpHandler#321
mark0-cn wants to merge 1 commit into
megaeth-labs:mainfrom
mark0-cn:mark0/docs/mega-transaction-error-rfc

Conversation

@mark0-cn

Copy link
Copy Markdown

Summary

This is an RFC / discussion PR (draft, not meant to merge as-is) about lifting a known limitation:
MegaTransactionError (crates/mega-evm/src/evm/result.rs) is forced to be a type alias for op_revm::OpTransactionError, so mega-evm cannot surface its own transaction validation error variants — even though MegaHaltReason is already a first-class Base(..) + custom enum.

The only code change here corrects the stale TODO in result.rs: the previous note claimed a custom tx-error enum requires abandoning OpHandler and reimplementing every handler method. That is not the cheapest path. I'd like to agree on a direction before implementing.

Problem

MegaHaltReason (result.rs:62) is a proper enum: Base(OpHaltReason) plus mega variants (DataLimitExceeded, ComputeGasLimitExceeded, VolatileDataAccessOutOfGas, SystemTxInvalidCallee, …). Transaction validation errors get no such treatment — MegaTransactionError = OpTransactionError (result.rs:56).

Consequence: mega-specific validation failures are surfaced as untyped strings. The system-transaction callee-whitelist rejection returns FromStringError::from_string("Mega system transaction callee is not in the whitelist") (evm/execution.rs:104), landing in EVMError::Custom(String) — not structured, not matchable by embedders. There is even a MegaHaltReason::SystemTxInvalidCallee { callee } variant (result.rs:94) that is effectively unused because the real rejection path is a string.

Root cause (verified against revm 27.1.0 / op-revm 8.1.0)

  • op_revm::OpHandler<EVM, ERROR, FRAME> is generic over ERROR, and its Handler impl bounds it (op-revm-8.1.0/src/handler.rs:68-71):
    ERROR: EvmTrError<EVM> + From<OpTransactionError> + FromStringError + IsTxError
    and it calls ERROR::from(OpTransactionError::..) internally (e.g. handler.rs:396).
  • EvmTrError<EVM> is an auto-trait (blanket impl at revm-handler-8.1.0/src/handler.rs:37) requiring From<InvalidTransaction> + From<InvalidHeader> + From<DBError> + From<ContextError<DBError>> + FromStringError.
  • MegaHandler (evm/execution.rs:47-59,408-424) wraps OpHandler and delegate!s validate_env / validate_against_state_and_deduct_caller / reimburse_caller / refund to it, inheriting the From<OpTransactionError> bound. The concrete error is EVMError<DB::Error, MegaTransactionError> (evm/factory.rs:126, evm/interfaces.rs:125).
  • To make MegaTransactionError a custom enum we'd need EVMError<DB, MegaTransactionError>: From<OpTransactionError>. revm provides no generic From<TX> for EVMError (only From<InvalidTransaction> when TX: From<InvalidTransaction>, From<DBError>, From<ContextError>, FromStringError), and we cannot add impl From<OpTransactionError> for EVMError<DBError, MegaTransactionError> here: From and EVMError are foreign and DBError is an uncovered type parameter ⇒ orphan-rule violation.

Why MegaHaltReason escapes this: HaltReason is an associated type (type HaltReason = MegaHaltReason at execution.rs:424), set directly with explicit From/TryFrom impls — no generic bound forces a foreign conversion. The error type is a generic parameter with a From<OpTransactionError> bound, so the constraint propagates.

Options

Option A — Local newtype error adapter (recommended)

Keep delegating to OpHandler. Introduce a local newtype and make MegaTransactionError a real enum:

pub enum MegaTransactionError {
    Base(OpTransactionError),
    SystemTxInvalidCallee { callee: Address },
    // future mega validation variants
}

struct MegaError<DBError>(EVMError<DBError, MegaTransactionError>); // local ⇒ no orphan issue

Because MegaError is local, it can implement the whole handler-error surface, including the blocked conversion:
From<OpTransactionError> (→ Base), From<MegaTransactionError>, From<InvalidTransaction>, From<InvalidHeader>, From<DBError>, From<ContextError<DBError>>, FromStringError, IsTxError, Debug/Display/Error (EvmTrError then comes for free via its blanket). Instantiate MegaHandler<_, MegaError<DB::Error>, EthFrame<..>> and unwrap at the Handler::run boundary so the public ExecuteEvm::Error stays the natural EVMError<DB::Error, MegaTransactionError>.

  • Pros: No handler reimplementation; OpHandler delegation and all upstream validation logic stay intact. Mirrors the proven MegaHaltReason design. Mega validation failures (starting with SystemTxInvalidCallee) become structured.
  • Cons: ~9 mechanical trait impls on the newtype; the public error type changes (MegaTransactionError becomes an enum), a breaking change for embedders matching on it.
  • Directly refutes the current TODO — abandoning OpHandler is unnecessary.

Option B — Reimplement the handler without OpHandler (the current TODO's path)

Set ERROR = EVMError<DB, MegaTransactionError> directly (revm's blankets cover it once MegaTransactionError: From<InvalidTransaction>), and reimplement the four delegated methods, converting OpTransactionError → MegaTransactionError::Base explicitly via map_err (no From bound, no orphan issue).

  • Pros: No newtype; the error type is the plain EVMError<DB, MegaTransactionError>.
  • Cons: Must copy/maintain op-revm's validate_env / validate_against_state_and_deduct_caller / reimburse_caller / refund logic and keep it in sync across every op-revm bump — real, recurring drift risk.

Option C — Upstream change in op-revm

Ask op-revm to make the OpTransactionError → ERROR conversion injectable (assoc fn / closure / MapTxError) instead of a hard From<OpTransactionError> bound.

  • Pros: Smallest mega footprint; benefits other op-revm forks.
  • Cons: External timeline and coordination; out of our direct control.

Option D — Status quo

Keep the alias; represent mega validation failures as EVMError::Custom(String) or as halt reasons.

  • Pros: Zero work.
  • Cons: Lossy — no structured matching; SystemTxInvalidCallee stays defined-but-unused.

Recommendation

Option A. It unblocks structured mega validation errors with bounded, mechanical effort and no reimplementation of upstream handler logic, and it makes the tx-error story symmetric with MegaHaltReason. Option C is worth raising upstream in parallel as the longer-term cleanup.

Proposed scope of the first implementation

  1. Promote MegaTransactionError to Base(OpTransactionError) + SystemTxInvalidCallee { callee } (+ serde, Display, Error, From<InvalidTransaction>), mirroring MegaHaltReason.
  2. Add the MegaError newtype adapter + trait impls; thread it as the handler ERROR, unwrap at Handler::run.
  3. Route evm/execution.rs:104 (and the REX5 system-tx nonce/chain-id checks) through the new variant instead of from_string.
  4. Migration: enumerate downstream matchers (block executor, bin/mega-evme, bin/mega-t8n) and provide From/accessors; call out the breaking error-type change for embedders and coordinate a version bump.

Open questions

  • Does alloy_evm::Evm::Error / block-executor error conversion impose bounds beyond what EVMError<DB, MegaTransactionError> already satisfies via revm blankets? (Expected: no, given MegaTransactionError: Error + Display.)
  • Public surface: unwrap to EVMError<DB, MegaTransactionError> (cleaner type) vs. expose MegaError directly (fewer conversions)? I lean unwrap.
  • Which validation failures graduate to variants now vs. later (just SystemTxInvalidCallee, or also the REX5 system-tx nonce/chain-id checks)?
  • Backward-compat: error representation is not spec'd EVM behavior, but the public error type change is breaking for embedders — confirm the intended version-bump story.

Offer to implement

If the team is happy with Option A (or prefers B/C), I'd be glad to take this from RFC to implementation myself: the MegaTransactionError enum + MegaError adapter, rerouting the system-tx validation path, the downstream migration, and full test coverage (structured-variant round-trip à la the existing MegaHaltReason tests, plus the system-tx callee rejection path). Happy to split it into reviewable steps and iterate on the direction first.

Replace the stale TODO (which assumed a custom transaction-error enum requires
abandoning OpHandler) with an accurate note: OpHandler is generic over its error
type, so a local newtype error adapter can satisfy the From<OpTransactionError>
bound without reimplementing the handler. Full design options are discussed in
the RFC PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant