Repository navigation
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 probesevmone's
call_implperforms its EIP-7702 delegation lookup (copy_code) andits new-account surcharge check (
account_exists) on the CALL target beforecharging
CALL_VALUE_COST. REVM charges the 9000 first, and additionally skipsthe 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) nowanswer 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_rootverifies the post-state root againstthe 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 gateThis 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 insrc/host.rs), so droppingthem leaves
31f5326a4intact 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 coldload; 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:
before this gate, the bundle state after it, and the post-state root is
verified against the target header in every replay mode.
access-outside-the-proven-witness case, i.e. exactly the
31f5326a4class.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— tracingEVMC_BRIDGE_TRACE=1logs every account-touchingHostBackendcall;EVMC_BRIDGE_TRACE_DIRdumps both strict access sequences plus the reference'saccesses around the index where the subject stopped;
EVMC_BRIDGE_ACCESS_COUNTS=1(added indefcb61f2) prints only the counts.All off by default and never consulted on a decision path.
Verification
Measured, not inferred:
(25817835–25817856) passes. Both figures hold for the DTVM and the evmone
subject leg.
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.
subject != referencecounts in the batch are thefive read-one-less cases (2863/2864, 3559/3560, 4054/4055).
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 --libpasses inadapter-subject-backendsand inadapter-subject-backends/witness-db. A few witness-db tests needRETH_SUBJECT_BACKENDand friends set; without them they fail identically onthe 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 (theexceptional-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
treat the witnesses themselves as sound. Why geth is not affected by the
first defect is not confirmed. Its
gasCallalso callsStateDB.Empty(address)before charging, so the likeliest explanation is adifferent witness format carrying enough trie nodes for an exclusion proof —
but that is a hypothesis, not something we verified.
does run in every mode, but we did not construct a subject-only test case in
which an answer actually escapes.