fix(evm): make EnrichedMegaTx's cached tx_size/da_size reachable#331
fix(evm): make EnrichedMegaTx's cached tx_size/da_size reachable#331RealiCZ wants to merge 7 commits into
Conversation
The MegaTransactionExt override on EnrichedMegaTx<T> could never dispatch: the trait's tx_size/estimated_da_size methods carry a `where Self: Encodable2718` bound that EnrichedMegaTx never satisfied, so every caller silently recomputed from the inner transaction instead of reusing the stored fields. Add an Encodable2718 impl to satisfy the bound, and a new opt-in run_transaction_enriched/execute_mega_transaction_enriched entry point on MegaBlockExecutor that reads tx_size/da_size from the outer transaction (hitting EnrichedMegaTx's cache) instead of unwrapping to the raw inner tx. run_transaction itself is unchanged, so existing callers are unaffected. Adds a benchmark (crates/mega-evm/benches/enriched_tx.rs) and tests proving the fix changes real limit-enforcement behavior, not just a returned value.
…wrapping Add a doc comment explaining why run_transaction_with_sizes exists (shared body for run_transaction/run_transaction_enriched, sizes resolved by the caller). Also reflows enriched_tx.rs's module doc comment to satisfy cargo fmt's comment wrapping.
…on_enriched run_transaction_enriched feeds Tx-reported tx_size/da_size straight into pre_execution_check's consensus-relevant limit checks with no validation. EnrichedMegaTx::new is a public, fully trusting constructor, so a stale or understated cache could let a transaction bypass tx_da_size_limit, tx_encode_size_limit, or the block cumulative-size limits. Add a debug_assert cross-check against a fresh recompute in run_transaction_enriched (compiled out in release builds, so it doesn't cost the performance this method exists to deliver), and document the caller contract on both run_transaction_enriched and EnrichedMegaTx::new. Also documents on run_transaction why the standard BlockExecutor trait path can't take the enriched shortcut. Restructures the block-limit integration test to assert the debug_assert actually fires on a deliberately mismatched cached da_size, proving the safety net works, since the assert now makes the test's original "cached value silently wins" scenario panic by design.
… doc link The should_panic test for run_transaction_enriched's debug_assert cache cross-check failed under `cargo test --release`: debug_assert compiles out in release builds, so the deliberately mismatched cached da_size silently succeeds instead of panicking. Gate the test (and its now debug-only-used imports/helper) behind #[cfg(debug_assertions)] and document that this is the intended profile-dependent trade-off, not an oversight. Also fixes a broken rustdoc intra-doc link on run_transaction: BlockExecutor isn't in scope in this file (only referenced via the fully qualified alloy_evm::block::BlockExecutor path), so the unqualified link never resolved. cargo doc -p mega-evm --no-deps now has no new warnings from this change.
🧬 Mutation testing — ✅ PASSNothing to test — no mutants were generated on the changed lines. |
Codecov Report❌ Patch coverage is
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Label checkThe spec:stable and api:compatible labels look correct now. Missing comp:misc |
🧬 Mutation testing — ✅ PASSDiff mutation score: 100.0% (2/2 viable mutants killed)
No new test gaps introduced by this change. 🎉 |
There was a problem hiding this comment.
- Summary: focused re-review of the
EnrichedMegaTxcache-reachability fix, the newrun_transaction_enrichedentry point, and its test/bench surface. - Nice: the
debug_assertcross-check onrun_transaction_enrichedcloses the trust-boundary risk ofEnrichedMegaTx::newaccepting arbitrary sizes without paying anything in release, and the#[cfg(debug_assertions)] #[should_panic]test proves that safety net actually fires on a mismatch — profile-dependent trade-off is documented on the method rather than left as a footgun. - The
run_transaction/run_transaction_enriched/run_transaction_with_sizessplit cleanly separates the two size-resolution paths without changing the existingBlockExecutor-backed signature, and the mutation-test refresh replaces the suppressed-as-dead entry with a real assertion that the stored fields are what dispatches. - Inline comments: 1 anchorable finding on the new
Encodable2718 for EnrichedMegaTx<T>impl (type_flagdelegation is incomplete). - No still-open prior review threads.
…d path Delegate Encodable2718::type_flag on EnrichedMegaTx<T> to the inner transaction instead of relying on the trait default. Verified with a temp test that the default (computed via the existing Typed2718::ty() delegation) already returns the correct value for typed transactions -- so this isn't a behavior fix, just matching this file's own convention of exhaustively delegating rather than relying on trait defaults (all four other trait impls on EnrichedMegaTx already do this), and it's more robust against a future T that overrides type_flag non-trivially. Also closes a real patch-coverage gap: execute_mega_transaction_enriched (the alias) had no caller in any test, and run_transaction_enriched's only test deliberately panics via its debug_assert before ever reaching the success path. Add a happy-path test that constructs EnrichedMegaTx::new_slow (an accurate cache) and calls execute_mega_transaction_enriched, asserting it succeeds and the outcome carries the cached tx_size/da_size. Un-gates the create_envelope/EnrichedMegaTx/MegaTransactionExt test-only imports from #[cfg(debug_assertions)] since they're now also used unconditionally.
|
Thanks for the re-review. Addressed the inline Also went back and closed the codecov patch-coverage gap (14 lines, 67.4%) that was still open at that point:
|
There was a problem hiding this comment.
Clean
- Reviewed the EnrichedMegaTx cache-plumbing fix: new Encodable2718 for EnrichedMegaTx impl to satisfy the trait bound, additive run_transaction_enriched / execute_mega_transaction_enriched entry points that read cached sizes, debug_assert cross-check against a fresh recompute, and matching tests + benchmark.
- run_transaction (shared with BlockExecutor::execute_transaction_with_commit_condition) is left untouched, so existing callers keep the recomputing behavior — the new path is opt-in.
- Trust-boundary risk (public EnrichedMegaTx::new + no release-mode revalidation before consensus-relevant limit checks) is called out explicitly in the doc comments on both EnrichedMegaTx::new and run_transaction_enriched, and the debug_assert gives a debug/CI safety net at zero release cost.
- Tests: the debug-only should_panic test proves the safety net actually fires on a mismatched cache, and the release-safe happy-path test proves execute_mega_transaction_enriched returns an outcome carrying the cached values.
- No new actionable findings.
- Still open from earlier reviews: none — the prior type_flag delegation thread was addressed in 446020e and has now been resolved.
Fold the opt-in run_transaction_enriched/execute_mega_transaction_enriched entry points back into run_transaction/execute_mega_transaction. The method now bounds `MegaTransactionExt + Encodable2718` and resolves tx_size/da_size through the trait, so an EnrichedMegaTx reuses its cached fields while any other tx falls back to the recomputing default — the choice is made at compile time by trait dispatch, so callers no longer pick a variant. - The alloy_evm ExecutableTx path recomputes the sizes itself and calls the now-public run_transaction_with_sizes, since its tx cannot implement MegaTransactionExt. - Move the debug_assert size cross-check and the trust-boundary contract onto run_transaction (a no-op for bare txs, the safety net for cached overrides). - Add MegaTransactionExt for Recovered<&MegaTxEnvelope>, and Typed2718 / Encodable2718 / MegaTransactionExt for mega-evme's OverriddenTx, so replay callers satisfy the new bound. - Update the enriched tests and benchmark comments accordingly.
There was a problem hiding this comment.
- Summary: reviewed the refactor unifying
run_transactionand the removedrun_transaction_enrichedback into one entry point (fd4e93d), plus theOverriddenTx/Recovered<&MegaTxEnvelope>trait plumbing and the reworked block-executor tests. - Inline comments: 1 anchorable finding on the new
pub fn run_transaction_with_sizesdocstring. - Body-only items:
- PR description is stale after the final refactor commit. It still says "
run_transaction's existing signature is untouched" and describes a new opt-inrun_transaction_enriched/execute_mega_transaction_enrichedentry point, but the last commit unified those back intorun_transactionand addedMegaTransactionExt + Encodable2718bounds to it — a real API-surface change reviewers relying on the description would miss. Worth updating before the squash-merge, since the PR body is the default squash-commit body.
- PR description is stale after the final refactor commit. It still says "
- Still open from earlier reviews: none (prior type_flag delegation and CodeQL nonce threads are both resolved).
Positive notes on the refactor itself:
- Unifying
run_transactionandrun_transaction_enrichedvia aMegaTransactionExt + Encodable2718bound is a clean simplification — callers no longer pick a variant, and the trait-dispatched read oftx_size/da_sizeis the same code path for both cached and bare txs. - Adding the
debug_assertcross-check onrun_transaction(and pushing the trust-boundary doc onto it) is a nice consolidation of the safety net — a bare tx passes trivially, a cached override gets validated in debug/test builds. - The new
Encodable2718 for EnrichedMegaTx<T>andOverriddenTx<T>delegations look correct: they forward to the inner tx, so any consensus-relevant encoded-size read is faithful to the raw transaction.
| /// This is the escape hatch for callers whose `Tx` cannot implement [`MegaTransactionExt`] | ||
| /// (e.g. the `alloy_evm` `ExecutableTx`-constrained path): they resolve the sizes themselves | ||
| /// and pass them in. Prefer [`MegaBlockExecutor::run_transaction`] otherwise, which resolves | ||
| /// the sizes for you and cross-checks any cached values. |
There was a problem hiding this comment.
[Minor] run_transaction_with_sizes is now pub, but its docstring lacks the trust-boundary warning that run_transaction carries. Sizes flow directly into BlockLimiter::pre_execution_check (via its tx_encode_size_limit/tx_da_size_limit/block cumulative-size checks) with no validation and — unlike run_transaction — no debug_assert cross-check, so an external caller reaching for this as a mere "I already have the sizes, skip recomputation" optimization can silently bypass consensus-relevant limits.
The only in-crate caller besides run_transaction is execute_transaction_with_commit_condition in the same file (which recomputes fresh sizes on line 649–650), so the pub visibility is only meaningful for external callers. If pub is intentional, please duplicate (or explicitly link to) the "Correctness" section from run_transaction so callers see the same warning they would have seen if they had implemented MegaTransactionExt. Alternatively consider pub(crate) — external callers who want the shortcut can implement MegaTransactionExt and get the debug_assert safety net for free.
| /// the sizes for you and cross-checks any cached values. | |
| /// Shared body of [`MegaBlockExecutor::run_transaction`] and the `alloy_evm` | |
| /// block-execution path: `tx_size`/`da_size` are resolved by the caller (recomputed or read | |
| /// from a cache), this only consumes them. | |
| /// | |
| /// This is the escape hatch for callers whose `Tx` cannot implement [`MegaTransactionExt`] | |
| /// (e.g. the `alloy_evm` `ExecutableTx`-constrained path): they resolve the sizes themselves | |
| /// and pass them in. Prefer [`MegaBlockExecutor::run_transaction`] otherwise, which resolves | |
| /// the sizes for you and cross-checks any cached values. | |
| /// | |
| /// # Correctness | |
| /// | |
| /// `tx_size`/`da_size` feed directly into `BlockLimiter::pre_execution_check` (via its | |
| /// `tx_encode_size_limit`/`tx_da_size_limit`/block cumulative-size checks) with **no** | |
| /// validation against the real encoded transaction, and (unlike | |
| /// [`MegaBlockExecutor::run_transaction`]) **no** `debug_assert` cross-check. Callers MUST | |
| /// ensure the values accurately reflect `tx` — an understated value lets a transaction | |
| /// bypass a limit it should have been rejected by. Prefer implementing | |
| /// [`MegaTransactionExt`] on `Tx` and calling [`MegaBlockExecutor::run_transaction`] | |
| /// instead, which gains the cross-check safety net for free. |
William's unify-run_transaction refactor added MegaTransactionExt for Recovered<&MegaTxEnvelope> (the zero-copy shape run_transaction's new MegaTransactionExt bound needs), but no test called it. cargo-mutants diff-scoped gate reported it MISSED (tx_hash collapsing to Default::default() went uncaught) and codecov flagged the same lines as uncovered patch. Add a sibling test mirroring the existing Recovered<MegaTxEnvelope> (owned) coverage. Verified locally: mutation gate now shows 0 missed (was 1), and the previously-uncovered helpers.rs lines are covered.
There was a problem hiding this comment.
🧭 Re-review update
- Reviewed the incremental diff since 446020e: the refactor (fd4e93d) folds
run_transaction_enrichedback into a singlerun_transaction<Tx: MegaTransactionExt + Encodable2718 + Copy>— dispatching sizes through the trait soEnrichedMegaTxreuses its cache while a bareRecovered<...>uses the recomputing default, with thedebug_assertcross-check now sitting on the unified entry — plus the addedRecovered<&MegaTxEnvelope>test coverage (50589a9). The unification is a nice simplification: one entry point, choice made at compile time, and the trust-boundary contract lives with the primary API. - Verified: (a) removed mutation suppressions match the newly reachable overrides, and the fresh
EnrichedMegaTx/Recovered<&MegaTxEnvelope>mutation tests cover them; (b) thealloy_evm::execute_transaction_with_commit_conditionpath still recomputes and routes throughrun_transaction_with_sizes, so its size semantics are unchanged from before the PR; (c) newTyped2718/Encodable2718/MegaTransactionExtimpls onbin/mega-evmeOverriddenTx<T>correctly delegate to inner, so replay callers satisfy the new bounds without changing execution semantics. - No new actionable findings this round.
Still open from earlier reviews:
crates/mega-evm/src/block/executor.rs:424—run_transaction_with_sizesdocstring still lacks the trust-boundary warning thatrun_transactioncarries. Since the refactor kept itpubas the escape hatch for theExecutableTx-constrained path and it has nodebug_assertsafety net, external callers reaching for it as a size-shortcut can still silently bypasstx_encode_size_limit/tx_da_size_limit/block cumulative-size checks. Either duplicate/link the Correctness section fromrun_transaction, or narrow visibility topub(crate)— the only in-tree caller besidesrun_transactionisexecute_transaction_with_commit_conditionin the same file.
Summary
EnrichedMegaTx<T>'sMegaTransactionExtoverride fortx_size/estimated_da_sizecould never dispatch — the trait methods carry awhere Self: Encodable2718bound thatEnrichedMegaTxnever satisfied, so every caller silently recomputed from the raw inner transaction instead of reusing the precomputed fields.This defeated the entire purpose of the wrapper: mega-reth's sequencer hot path already precomputes
da_size/tx_size(often from a cheap mempool-cached value) and wraps it inEnrichedMegaTxexpecting it to be reused, butMegaBlockExecutor::run_transactionignored that and recomputed every time.Closes #324.
Fix:
impl Encodable2718 for EnrichedMegaTx<T>(delegating to the inner transaction) so the bound is satisfiable.run_transaction's existing signature is untouched — it's shared with the standardBlockExecutor::execute_transaction_with_commit_conditionpath, which is constrained byalloy_evm'sExecutableTxand can't requireMegaTransactionExt.run_transaction_enriched/execute_mega_transaction_enrichedentry point that readstx_size/da_sizefrom the outerTx(hittingEnrichedMegaTx's cache) instead of unwrapping to the raw inner transaction.crates/mega-evm/benches/enriched_tx.rs).Trust boundary hardening:
run_transaction_enrichedfeedsTx-reportedtx_size/da_sizedirectly intopre_execution_check's consensus-relevant limit checks with no validation, andEnrichedMegaTx::newis a public constructor that accepts arbitrary values. Added adebug_assertcross-check against a fresh recompute (compiled out in release, so it costs nothing in production) and documented the caller contract on bothrun_transaction_enrichedandEnrichedMegaTx::new.Test plan
EnrichedMegaTx's trait-dispatchedtx_size/estimated_da_size/tx_hashreturn the stored fields, not a recomputed value.MegaBlockExecutorintegration test proves the fix changes real behavior: with atx_da_size_limitset between a deliberately understated storedda_sizeand the real recomputed value, the same transaction is rejected underrun_transactionand — before thedebug_assertwas added — was accepted underrun_transaction_enriched.#[cfg(debug_assertions)],#[should_panic]) proves thedebug_assertsafety net actually catches that exact mismatch in debug/test builds. Verified it correctly does not run undercargo test --release(the assert compiles out there by design — documented as the intended profile-dependent trade-off, not an oversight).cargo mutants --in-diffgate locally against the full change: 0 missed mutants.cargo test -p mega-evm(debug and--release) andcargo test -p mega-evmepass.cargo fmt --all --check,cargo clippy --workspace --lib --examples --tests --benches --all-features --locked(debug and--release, checked for unused-import regressions from the newcfg(debug_assertions)gating),cargo sort --check,cargo doc -p mega-evm --no-deps(no new warnings), and theno_std/riscv64 target check all pass.cargo bench -p mega-evm --bench enriched_txrun locally to verify the new benchmark actually executes (not just compiles).