Skip to content

fix(evmc-adapter): unproven cold-account probes, and an over-strict access gate - #1

Open
abmcar wants to merge 16 commits into
evmc-backend-and-replay-docsfrom
evmc-extra-access-fix
Open

abmcar wants to merge 16 commits into
evmc-backend-and-replay-docsfrom
evmc-extra-access-fix

Conversation

@abmcar

@abmcar abmcar commented Aug 28, 2026 •

Copy link
Copy Markdown
Owner

Eight of the ten blocks that failed strict differential witness replay on a
1000-block mainnet corpus (25817835–25818834) now pass, under both DTVM and
evmone. The remaining two are a DTVM JIT defect, not addressed here.

Two independent defects, plus the tracing that localised them. Base is
evmc-backend-and-replay-docs (7ced99bf0).


31f5326a4 — unproven cold-account probes

evmone's call_impl performs its EIP-7702 delegation lookup (copy_code) and
its new-account surcharge check (account_exists) on the CALL target before
charging CALL_VALUE_COST. REVM charges the 9000 first, and additionally skips
the cold load outright once the remaining gas cannot cover the cold-access
surcharge, so a CALL that is already out of gas never reads its target at all.

An execution witness is built from the canonical REVM execution, so those two
probes can name an account the witness does not cover — not even with an
exclusion proof. Walking the pre-state trie by hand for the account on block
25817857 hits a missing node at depth 3, so the witness can prove neither
existence nor non-existence. The bridge turned a question about a doomed
instruction into a hard witness failure: 5 of 1000 blocks (25817857, 25818392,
25818430, 25818453, 25818505), identically under DTVM and evmone.

The three read-only probes (account_exists, get_code, get_code_hash) now
answer as the non-existent, codeless account they describe, and only when the
underlying load reports that the address lies outside the proof. Every other
callback stays fatal, the error is not latched, and the predicate compares the
address named by the error, so incompleteness raised for a different account
is still fatal.

Why this is safe: the answers are mutually consistent, they can only raise the
instruction's cost, and every path on which they could still be observed —
actually performing the call, or reading that account's balance or storage —
loads the same unproven account again and fails closed there. On top of that,
WitnessDb::into_verified_post_state_root verifies the post-state root against
the target header in every replay mode, so an answer that did escape would
fail loudly rather than be silently accepted.

Verified on block 25817857: DTVM completes with 902 strict database accesses,
item-for-item equal to the REVM reference leg, post-state root verified.

Upstream evmone moved the 9000 charge ahead of the probes in v0.23; the pinned
0.18 and DTVM still carry the old order.


⚠ defcb61f2 + 762e33ed3 — relaxing the access-sequence gate

This is a judgment call, not a fact. It loosens a correctness gate. A
reviewer may reasonably disagree, and it can be reverted on its own: both
commits sit at the tip of this branch and touch only
witness-db/src/replay.rs (plus one diagnostic in src/host.rs), so dropping
them leaves 31f5326a4 intact and working.

The gate demanded that the subject's and the reference's strict database access
sequences be equal. The two commits together replace that with: the subject
must be an order-preserving subsequence of the reference, after splitting a
post-execution withdrawal tail off both sides and comparing that tail as a set.

The failure it fixes is the opposite of the one the gate was built to catch —
the subject reads less, not more. Our bridge answers the EIP-2929 warm-up
callbacks (access_account, access_storage) from the journal without a cold
load; REVM loads first and charges afterwards. So an instruction that runs out
of gas at the cold-access charge never issues the read on our side, while REVM
already issued it. On block 25818316 the reference makes 4055 accesses and DTVM
4054, the missing one a cold SLOAD whose 2100 gas the frame could not pay.
Blocks 25818085 and 25818686 fail the same way.

The second commit exists because block 25818085 trips both effects at once: one
missing cold SLOAD and the 16 withdrawal beneficiaries loaded in evmone's
own order. The pre-existing withdrawal-tail allowance requires equal lengths;
the subsequence rule does not tolerate reordering. Each allowance alone still
rejects the block, so they have to compose.

Argument for why relaxing this direction is safe — please scrutinise it:

  • Reading less cannot hide a wrong value. The execution result is compared
    before this gate, the bundle state after it, and the post-state root is
    verified against the target header in every replay mode.
  • Reading something the reference never read is still rejected — that is the
    access-outside-the-proven-witness case, i.e. exactly the 31f5326a4 class.
  • A withdrawal beneficiary the reference never loaded is still rejected.
  • Accesses within a block are distinct (the journal serves every repeat from
    memory), which is what makes the greedy match an exact subsequence test rather
    than an approximation.

What is genuinely given up: the gate no longer proves the two legs read the
same set. It now proves the subject read a subset. If you want the stronger
property back, revert these two commits.


f6e2d5fce — tracing

EVMC_BRIDGE_TRACE=1 logs every account-touching HostBackend call;
EVMC_BRIDGE_TRACE_DIR dumps both strict access sequences plus the reference's
accesses around the index where the subject stopped;
EVMC_BRIDGE_ACCESS_COUNTS=1 (added in defcb61f2) prints only the counts.
All off by default and never consulted on a decision path.


Verification

Measured, not inferred:

  • 8 of the 10 previously failing blocks pass; the 22-block normal control set
    (25817835–25817856) passes. Both figures hold for the DTVM and the evmone
    subject leg.
  • On the 22 normal blocks, subject and reference access counts are equal
    block for block
    , before and after these commits, on both engines. The extra
    accesses are path-triggered (a cold CALL target in a frame already doomed to
    OOG), not systematic — roughly 5 occurrences per 1000 blocks.
  • After the fixes, the only subject != reference counts in the batch are the
    five read-one-less cases (2863/2864, 3559/3560, 4054/4055).
  • Ruled out by measurement: the failing-block set is identical block for
    block
    across 3 adapter revisions × 2 evmone versions (v0.18, v0.21). Not an
    artefact of a recent adapter change or of an evmone version bump.
  • cargo test --lib passes in adapter-subject-backends and in
    adapter-subject-backends/witness-db. A few witness-db tests need
    RETH_SUBJECT_BACKEND and friends set; without them they fail identically on
    the unmodified base 7ced99bf0.

Not fixed here

Blocks 25818502 and 25818530 still fail under DTVM. That is a separate,
JIT-only DTVM defect — a null-pointer dereference inside JIT'd code that the
trap handler reports as out-of-bounds memory. Both blocks replay correctly under
RETH_SUBJECT_EVMC_OPTIONS="mode=interpreter". A partial DTVM-side fix (the
exceptional-halt gas rule) is upstream at DTVMStack/DTVM#605, and the
misclassification that let the crash pass as a consensus halt is at
DTVMStack/DTVM#604. The null dereference itself is still open: re-measured on a
fresh build carrying both DTVM changes, the two blocks still fail under JIT with
unchanged access counts at the fault (152 and 45).

Stated uncertainties

  • geth replays the same 1000-block corpus with zero failures, which is why we
    treat the witnesses themselves as sound. Why geth is not affected by the
    first defect is not confirmed.
    Its gasCall also calls
    StateDB.Empty(address) before charging, so the likeliest explanation is a
    different witness format carrying enough trie nodes for an exclusion proof —
    but that is a hypothesis, not something we verified.
  • The first fix's protection was reasoned through, and the post-state-root check
    does run in every mode, but we did not construct a subject-only test case in
    which an answer actually escapes.

claude added 4 commits August 28, 2026 07:42
Localising an unproven witness access needs to name the exact host callback
that asked for it. EVMC_BRIDGE_TRACE=1 logs every account-touching
HostBackend call, and makes the differential replay dump both strict database
access sequences (EVMC_BRIDGE_TRACE_DIR) plus the reference's accesses around
the index where the subject stopped, so the subject's extra read can be read
straight off the reference it should have matched.

Off by default and never consulted on a decision path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187PTseLY2NbzxKrnstDWwS
… proof

evmone's call_impl performs its EIP-7702 delegation lookup (copy_code) and its
new-account surcharge check (account_exists) on the CALL target *before*
charging CALL_VALUE_COST. REVM charges that 9000 first, and additionally skips
the cold load outright once the remaining gas cannot cover the cold-access
surcharge, so a CALL that is already out of gas never reads its target at all.

An execution witness is built from the canonical REVM execution, so those two
probes can name an account the witness does not cover - not even with an
exclusion proof, since the trie nodes along its path were never revealed. The
bridge then turned a question about a doomed instruction into a hard witness
failure: 5 of 1000 mainnet blocks replayed with DTVM, and the same blocks with
evmone, died on "account proof is incomplete for <addr>".

Answer the three read-only probes (account_exists, get_code, get_code_hash)
for such an account as the non-existent, codeless account they describe, and
leave every other callback fatal. The answers are mutually consistent, they
can only raise the instruction's cost, and any path on which they could still
be observed - performing the call, or reading the account's balance or
storage - loads the same unproven account again and fails closed there. The
post-state root is verified against the target header in every replay mode, so
an answer that did escape would still be caught loudly rather than silently
accepted.

The predicate compares the address named by the error, so an incompleteness
error raised for a different account stays fatal. It matches the rendered
message because EvmFactory fixes the Evm<DB: Database, I> signature and no
extra bound can be placed on DB::Error to carry this as typed data.

Verified on block 25817857: DTVM now completes with 902 strict database
accesses, exactly matching the REVM reference leg, and a verified post-state
root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187PTseLY2NbzxKrnstDWwS
The strict differential gate demanded that the two strict-database access
sequences be equal. That is stricter than the property it exists to enforce,
and it fails on the opposite of the defect it was built to catch.

The EVMC bridge answers the EIP-2929 warm-up callbacks (access_account,
access_storage) from the journal, without a cold load. REVM instead loads the
account or slot first and charges afterwards. So when an instruction runs out
of gas at the cold-access charge, REVM has already read the state and the
subject never does: on block 25818316 the reference makes 4055 accesses and
DTVM makes 4054, the missing one being a cold SLOAD whose 2100 gas the frame
could not pay (the bridge trace ends with access_storage on that slot and no
get_storage after it). Blocks 25818085 and 25818686 fail the same way.

Reading less cannot hide a wrong value - the execution result is compared
before this gate, the bundle state after it, and the post-state root is
verified against the target header in every replay mode. Reading something the
reference never read is the real failure, because that is an access outside
the proven witness. So require the subject's sequence to be an order-
preserving subsequence of the reference's instead of equal to it. Accesses
within a block are distinct, since the journal serves every repeat from
memory, which makes the greedy match an exact subsequence test.

The withdrawal-tail allowance is unchanged and still applies when the
subsequence test does not.

Also gates the access-count diagnostic behind its own
EVMC_BRIDGE_ACCESS_COUNTS, so before/after access counts can be collected
without the full per-callback trace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187PTseLY2NbzxKrnstDWwS
Block 25818085 shows both effects at once: evmone misses one cold SLOAD whose
2100 gas the frame could not pay, *and* loads the 16 withdrawal beneficiaries
in its own order. The withdrawal-tail rule requires equal lengths, and the
subsequence rule does not tolerate reordering, so each allowance alone still
rejects the block - 2863 accesses against the reference's 2864, diverging at
index 1933 and failing the greedy embedding 15 entries from the end.

Split the post-execution withdrawal tail off both sequences first - the
longest suffix of distinct Basic loads of withdrawal beneficiaries, whose
order is not execution semantics - and compare it as a set; then require the
remaining head to embed in order. A beneficiary the reference never loaded is
still rejected.

Also carries the status code and both gas fields into the nested-halt fault
text, which is what localised the DTVM defect behind blocks 25818502 and
25818530 ("status 9, gas_left 339011, gas_refund 0, child gas limit 440977").

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187PTseLY2NbzxKrnstDWwS
abmcar and others added 12 commits August 29, 2026 04:57
A branch pin stops being reproducible the moment the branch moves, and this
one carries a change the build cannot do without: 42d475f is upstream
paradigmxyz/revmc cf68a87f plus a single fork commit raising the JIT codegen
workers' stack to 16 MiB.

The platform default of roughly 2 MB is not enough for LLVM instruction
selection on complex mainnet contracts. Replaying blocks 25817835-25818834
through the standalone revmc adapter, which pinned the unpatched upstream
commit, aborted on 38 of 1000 blocks with "thread 'revmc-NN' has overflowed
its stack" -- clustered on the blocks holding the largest contracts, so the
loss was biased rather than random.
The standalone adapter pinned pure upstream paradigmxyz/revmc cf68a87f, whose
JIT codegen workers get the platform default stack (~2 MB). LLVM instruction
selection overflows that on the largest mainnet contracts: replaying blocks
25817835-25818834 aborted on 38 of 1000 with "thread 'revmc-NN' has overflowed
its stack", clustered on the blocks holding the biggest code, so the loss was
biased rather than random.

42d475f is that same upstream commit plus exactly one fork commit raising the
worker stack to 16 MiB (2 files, +9 lines, stack_size only). The reth workspace
already carried the fix through a branch pin; this crate has its own workspace
and did not. Pinned by sha, not branch, so the build stays reproducible.

Re-scanned after the re-pin: 38 failures drop to 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
Both of this adapter's differential gates required the subject's DB access
sequence to equal the reference's item for item. That is too strict in one
direction. evmone-family engines probe a call target before charging the 9000
transfer cost, while revm charges first and skips the cold load when the frame
cannot pay for it; when the frame is that short on gas, the subject legitimately
issues *fewer* loads than the reference. witness-db already carries this
allowance (see subject_accesses_within_reference there); this crate has its own
copy of the gate and was never updated, so two blocks in the 1000-block window
failed here alone.

Accept the subject's sequence when it is an order-preserving subsequence of the
reference's, with the withdrawal tail compared as a set -- reth's ordering of
beneficiary loads is not part of execution semantics. Applied to both gates: the
discovery replay and the measured one. Relaxing only the first merely moved the
error from "dynamic compile discovery differential mismatch" to "differential
mismatch".

Wrong values cannot hide behind a skipped read: the post-state root is still
verified against the target header, and reading *more* than the reference is
still rejected, which is the direction that would signal a missing witness.

Blocks 25818316 and 25818686 now report allBlocksMatch: true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
The harness doc covered the DTVM and evmone legs and the timing protocol, but
someone handed this repository still could not reproduce the five-engine
comparison: the geth leg was not described anywhere, the revmc pin's reason was
not written down, the DTVM module-cache bound that the corpus requires was
undocumented, and nothing said which set of blocks to run.

New sections:

- §9 covers geth. Its CLI derives from GPL code under geth's cmd/, so it ships
  as patches rather than source, the same way the DTVM and revmc changes do.
  Two variants exist and they do not measure the same region: stock times
  Setup + Execution + Validation, the fork times Execution alone, and the gap
  is 1.43x. Placing a stock number beside the rethSubjectExecute figures of §3
  compares different regions, so the variant and the schema field to check for
  it are spelled out.

- §10 covers the campaign: the corpus and its measured properties, what each
  leg needs, and why the comparison must run on the intersection of blocks all
  engines pass rather than on each engine's own surviving subset. The full
  1000x4 scan found 46 blocks failing at least one engine; the table gives the
  root cause and current state of every one of them, including the two that
  remain open.

Also added to existing sections: DTVM_EVM_MAX_MODULE_CACHE_SIZE in §5, since
this corpus holds 15,415 unique contracts against a default bound of 4096 and
the resulting eviction thrash is the difference between hours and hundreds of
hours; and in §8, why the revmc pin must carry the worker-stack commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
The revmc entry in the same document argues that a branch pin stops being
reproducible the moment the branch moves, and then the DTVM row named a branch.
`03b542e` is the tree the measured library was actually built from, and it is
now pushed to the fork, so the reader can reach it.

Also names what that commit carries beyond the cache branch: the module-cache
bound of §5, and the two engine fixes that went upstream as DTVMStack/DTVM#605
and paradigmxyz#604.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
Two corrections to §10 now that the campaign has run.

The defect table listed the JIT null dereference as open and told the reader to
run on a 998-block subset. It is fixed upstream (DTVMStack/DTVM#607) and a full
rescan under the fixed libraries returns 1000 OK, 0 failures. All 46 originally
failing blocks are closed and the four-engine intersection is the whole corpus.
The table's description of that defect was also wrong in a way worth fixing: it
is not about nested calls. EVM memory is allocated lazily, and the failure needs
a frame's first growth to happen inside a runtime helper rather than through
expandMemoryIR; depth 0 on a fresh instance qualifies.

New §10.5 says what the measured number actually contains, because the obvious
reading of it is wrong in two ways. First, the tight boundary is available after
all — replay-batch fills rethSubjectRunExecLoop, replay-block does not, which is
why it reads as null if you check the wrong tool — and moving to it makes DTVM
look worse rather than better, since the fixed overhead is a larger fraction of
REVM's smaller figure. Second, neither boundary isolates code generation: a
metrics build counts 2,248 module-cache lookups per block against 186 top-level
executions, each running a full bytecode memcmp under the mandatory strict
validation, and nested frames never reopen the timing window. The figure is
DTVM-through-the-bridge against native REVM, and the document now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
The sizing rule in §5 was wrong and it cost a full re-measurement. It said to count
the corpus's unique contracts and set the bound above that; the 1000-block corpus has
15,415, so the campaign ran at 16384. But the cache key is (code_address, revision,
memory-specialisation profile, code prefix, code size), so one contract occupies more
than one entry whenever it is entered under more than one memory profile. The real
peak exceeds 16384 and the cache evicted inside the measured passes.

Nothing reported this. --production-timing checks the single-VM invariant and the
correctness gate and nothing else; the hot-cache gate that would have caught it lives
in the diagnostic protocol, which we had chosen to skip because "unqualified" sounded
like a missing formality. It was not: rerunning the diagnostic protocol fails at the
very first block of G0 with seven evictions, seven misses and seven synchronous
compilations.

Same corpus, same disk cache, same binary, same protocol, one environment variable
changed: 217.23 ms per block becomes 87.15 ms. The spread collapses too, p10-p90 from
1.84-6.42x down to 1.27-1.53x against REVM, because the largest blocks touch the most
contracts and so overflowed hardest. Everything this project attributed to a
"working-set effect" was that eviction; an ordering experiment (block-major against
pass-major, both at the corrected bound) now differs by 0.5%.

§5 therefore says do not size by contract count, and says the diagnostic protocol is
the only thing that proves the bound is adequate. §10.4 says step 2 is not optional
and why. §10.5 carries the corrected results; §10.6's boundary figures are restated
against the clean run.

Also checked in so the numbers can be recomputed rather than believed:
docs/corpus/mainnet-25817835-25818834.tsv pins all 1000 blocks by hash and bundle
digest, and docs/results/five-engine-1000.tsv is the per-block per-engine table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
The campaign table named 03b542e. The library the numbers came from was built from
8403be3, one commit further on, and the difference is not cosmetic: 8403be3 is the
fix for the JIT null dereference (upstream DTVMStack/DTVM#607). Building 03b542e
does not give a smaller version of this result, it gives a build that cannot finish
the corpus — blocks 25818502 and 25818530 fault in generated code, and since
03b542e reclassifies that fault as an internal error rather than a consensus status,
the replay fails closed. A reproducer following the old table would get 998 blocks
and never form the intersection the campaign reports.

Replaced the prose with a table of what 8403be3 contains and where each piece
landed upstream, so a reader can see why every one of the five commits is load
bearing here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
…ource column

An audit walked the whole path as someone holding only the public branch. Its
findings, in the order they would bite:

The REVM 41 column mixed two measurements. The aggregator took rethRevmExecute from
the DTVM batch records (hot, median of eight measured passes) *and* from the evmone
replay-block records (cold, one fresh process per invocation), and took a median of
the union. Those are different measurements of the same engine. Fixed to the DTVM
records alone, which is what makes the DTVM/REVM pairing apples-to-apples in the
first place: 64.90 ms becomes 61.96, and docs/results/five-engine-1000.tsv is
regenerated.

Concurrency was not mentioned anywhere, while the shape of the work invites it —
the evmone and geth legs are one process per bundle. §10.4 now says every measured
invocation runs alone, with the calibration showing why: at 8 concurrent processes
on 56 cores the measured phase inflates 2.9% for geth and 4.8% for evmone, which
moves cross-engine ratios rather than only absolute numbers.

The qualification build could not be built from published materials.
patches/dtvm-evmc-phase-metrics.patch is written against DTVM 338d123 and does not
apply to the campaign pin, and the pre-applied branch is 338d123-based, which
predates the module-cache bound this corpus requires. So the one tool that proves a
timing run was eviction-free was unobtainable, right next to the warning saying it
is the only thing that can prove it. Published as abmcar/DTVM@f9e25be on
metrics/evm-phase-metrics-on-8403be3 and named in §10.2.

Repetition counts and extraction were defined only for the batch legs. New §10.2b
gives, per leg, the invocation, repetitions, the gate that admits a sample, the
field, and the aggregation — plus a jq for bare reports, which carry neither
timingUse nor a correctness wrapper.

Smaller: evmone was pinned to a v0.18.0 tag that does not exist in DTVMStack/evmone
(now the commit); deep capture needs a node that can reconstruct that state at all,
not just more time; the revmc build needs LLVM_SYS_221_PREFIX; a "§5 of the engine
notes" reference pointed at no document; §10.5's "about 4%" was left over from the
contaminated numbers and is 8.5%; and §10.0 now pins the harness commit, since this
file preached commit pinning while being addressed by a moving branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
§10.0 pinned the campaign to the branch head as of the commit that introduced the
pin, which is both self-referential and wrong: the numbers were produced by binaries
built from 62bae44, and every commit since has corrected documentation without
touching the harness. Separated the two, and said why they must stay separate —
rebuilding the harness to fix prose would invalidate the measurements the prose is
describing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
… use it

New dtvm_module_ms column and a "module only" row: 73.30 ms against the 87.15 ms
the cross-engine table uses. The four boundaries come from one run and are therefore
subtractive — reth's executor construction and state extraction, then entering DTVM,
then the module-cache lookup with its full-bytecode memcmp.

The number is easy to misuse, so the text says so plainly. Dividing it by REVM gives
1.32x, which is DTVM-favourable for a structural reason: the lookup was removed from
one side and REVM has no module cache to remove anything from, its equivalent
per-contract analysis being inside its execution and inseparable. The symmetric
comparison is tight against tight, 1.45x, and that is what the cross-engine claims
should rest on. 73.30 answers a different question — how fast the compiled module
itself is.

The module figures come from the diagnostic build. On the same blocks it runs 0.49%
slower than the production build (p10-p90 1.0000-1.0105), so mixing them costs half a
percent in a direction that does not flatter DTVM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
The DTVM row is now jitActiveWallNs net of module lookups — 73.30 ms — with
rethSubjectExecute kept as a second, explicitly cross-comparable row at 87.15 ms.
Every "vs DTVM" ratio is recomputed against the new primary, so evmone moves from
2.70x to 3.26x and geth from 2.52x to 3.07x.

Those larger ratios are not a better result, they are a narrower boundary, and §10.5
says so before it says anything else: the other four legs stop further out and have
no equivalent inner layer to strip, so the column is systematically DTVM-favourable.
For "how much faster is DTVM than REVM" the symmetric pair is tight against tight,
1.45x, and that is stated with the two asymmetric alternatives beside it.

This also opened a reproduction hole that is now closed. The published metrics build
f9e25be reports jitActiveWallNs *including* the nested module lookups, so following
the old instructions would not produce 73.30. abmcar/DTVM@ad16f40 on
metrics/lookup-instrumented carries the subtraction; §10.2 names it, §10.2b routes
the primary row through it, and both say plainly that f9e25be will not do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018osheVEXr1SywFtbYFjHAc
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.

2 participants