Skip to content

core, params, eth: merge geth v1.17.4 (v1.17.4 sync, milestone 6/6) - #2346

Draft
pratikspatil024 wants to merge 137 commits into
ppatil-upstream-v1.17.3-part3from
ppatil-upstream-v1.17.4
Draft

core, params, eth: merge geth v1.17.4 (v1.17.4 sync, milestone 6/6)#2346
pratikspatil024 wants to merge 137 commits into
ppatil-upstream-v1.17.3-part3from
ppatil-upstream-v1.17.4

Conversation

@pratikspatil024

Copy link
Copy Markdown
Member

Summary

Final milestone of the go-ethereum v1.17.4 sync: batches 27–33, closing the target. With this the merge work for the whole sync is complete — git merge-base --is-ancestor v1.17.4 HEAD passes, and all six milestone tags (v1.16.9, v1.17.0, v1.17.1, v1.17.2, v1.17.3, v1.17.4) are ancestors of this branch.

Seven batch merge commits plus two docs commits. Every conflict resolution is recorded in docs/upstream-merges/v1.17.4/ledger.md, every fork/EIP in fork-register.md, and everything deliberately not adopted in needs-wiring.md — nothing was dropped silently.

Batches 27 859b9fabb, 28 5df1648b0, 29 047e24c29, 30 51aa42761, 31 338ffb831, 32 8ce37d5ba, 33 f397229ee
Upstream commits 105 first-parent
Stacked on ppatil-upstream-v1.17.3-part3 (#2345)

The four decisions that carry the weight

EIP-8037, state-creation gas — adopted, and adoption was forced. 25 of its 36 files arrived auto-merged, including the entire gas substrate (core/gaspool.go, core/vm/gascosts.go, interpreter.go, jump_table.go, core/state_processor.go), so declining would have meant reverting all of them and re-deriving a model the conflicted files already depended on. No new fork, no activation height, no precompile changeparams/config.go, both runtime presets, both packaged genesis files and core/forkid are untouched, and ActivePrecompiles is unchanged. Behaviour is gated on Amsterdam, which is nil on every preset, and Bor's IsAmsterdam was already the block-based form.

The safety argument is an exact equivalence rather than an approximation: with StateGas == 0 — Bor's situation everywhere while Amsterdam is dormant — upstream's ExitHalt(0) consumes precisely the remaining regular gas that Bor's previous UseGas(RegularGas) consumed, ExitRevert() preserves the budget as Bor's old err != ErrExecutionReverted guard did, and ExitSuccess() is the identity. Bor's Ahmedabad 32 KB code-size cap survives upstream's relocation of the size check via a new evm.checkMaxCodeSize routed into all three of its new per-branch call sites.

One deletion in that commit looks alarming and is not: it removes the static-call write-protection guard from opCreate/opCreate2, but the same commit moves the check into gasCreate/gasCreate2/gasCreateEip3860/gasCreate2Eip3860 (+6 lines against −2), which the interpreter evaluates before the execution function. All four are wired in Bor's jump tables, and Bor wraps dynamicGas errors exactly as upstream does.

snap/2 — declined a fourth time, and the reason changed shape. It is no longer volume but landing site: Bor renamed eth/downloader/downloader.go to bor_downloader.go and diverged it to 2,570 lines against upstream's 1,252, so all four commits here touching upstream's copy (#34626, #35178, #35180, #35155) have nowhere to go. Adoption now means re-porting Bor's downloader onto upstream's snap/2 state machine plus replacing SnapSyncCommitHead, which is branch-and-PR work. The feature also remains unusable: it exists to serve block access lists, and Bor can neither produce one (AmsterdamBlock nil) nor build them under BlockSTM V2.

#35156, in-place stack operations — the second forced adoption, landing in the hottest code in the tree. Upstream added seven pointer-returning stack methods; Bor's Stack is a different implementation (a GEVM-ported fixed [1024]uint256.Int array with a leading counter). 21 call sites in instructions.go and 2 in eips.go auto-merged outside any conflict region, so the package could not compile without them; the seven were translated to Bor's shape.

#35124, blobpool GetBlobs cache — declined as one unit with a package move. The move of miner/ordering.gocore/txpool/txorder/ exists only to let the cache share the miner's selection logic, and the cache cannot run here: eth.blobTxPool is declared and never assigned (Bor removed the blob pool from its subpool list) and the Engine API consuming it is inherited-but-unused.

Standing scans

Both clean for the last five batches running. No file under core/stateless/ or eth/protocols/wit/ appears anywhere in this milestone's upstream range, and batch 33's range contains zero hunks mentioning witness at all. Bor's own witness lines did sit inside four conflict regions in batch 32, so their counts were baselined from HEAD before resolving and re-verified after — all eight files identical.

Six Bor-only implementers of upstream interfaces broke outside upstream's footprint and were fixed, five of them surfaced by the build: runEcrecoverWithCache, consensus/bor/statefull's system-call processor (a directory upstream doesn't have), eth/tracers/parity.go, a SubGasCheckGasLegacy rename, nine core/vm test call sites, and a GetRules accessor Bor had removed entirely.

Executed tests

Beyond CI's standard gates, run at this tip:

  • go test ./... — 149 packages pass, 3 fail, 85 carry no tests. All three failures and all twelve failing test names are in the documented pre-existing set, and the signatures were compared, not just the names: cmd/evm's four all report invalid eip number 1346; cmd/geth's five plus cmd/devp2p/internal/ethtest all show params.(*BorConfig).CalculatePeriod via miner.(*worker).newWorkLoop. That set has shrunk over this sync — TestPDBMethodParity and two core/vm timer flakes were fixed along the way.
  • make lint — 0 issues, which also confirms this milestone's four declines left no unused symbols.
  • go vet ./... and go vet -tags integration ./... — only two pre-existing lock-copy findings, re-baselined at the previous commit and reproducing byte-identically.
  • govulncheck — one called advisory, GO-2026-6061 (google.golang.org/grpc@v1.79.3). Inherited, not introduced: grpc is pinned to the identical version on origin/develop. The whole dependency delta against develop is seven lines, all traceable to recorded decisions.
  • EIP-8037 gas invarianceTestV2BlockSTMAllBlocks replayed 241 real mainnet blocks through the merged EVM with byte-identical state roots on both the serial and BlockSTM-V2 paths. Since gas used commits into receipts → receipt root → header, identical roots across 241 historical blocks mean identical gas. This is the empirical counterpart to the analytic argument above.
  • tests/bor — passes.
  • Two kurtosis devnets, on an image built from this tip. Standard: both nodes advancing in lockstep with identical finalized hashes, Heimdall consensus 502→1030, milestones 404→502, checkpoint acks 5→16, zero errors. Witness: validator producing on syncmode=full, RPC consuming on syncmode=stateless — 169 witnesses written by the producer and the identical set held by the consumer, wit/1+wit/2 negotiated, consumer tracking the producer's head exactly (a stateless node cannot import a block without its witness, so head parity is the end-to-end proof).
  • Milestone PR triage across all 105 upstream commits — no upstream addition needs Bor-side wiring. The only new geth CLI flag and the only new ethconfig.Config field both belong to declined snap/2 and are absent from the tree; no precompile registration moved; make docs regenerates byte-identically; gen_config.go needs no regeneration.

Rollout notes

Not consensus-affecting as merged. Every fork and EIP in this milestone is dormant: AmsterdamBlock is nil on every Bor preset, so EIP-8037's pricing, the EIP-7928 header field and EIP-7843's SlotNumber are all inert, and that dormancy is argued per item in fork-register.md. No coordinated upgrade is required for this branch on its own; enabling any of these forks is a separate, explicit decision with its own change, review and N-1/N/N+1 activation tests.

Merge as a merge commit — never squash. Squashing rewrites SHAs and breaks every PR stacked above this one, and it would also collapse upstream's per-commit authorship, which is the point of syncing by merge.

Three items are recorded for follow-up rather than fixed here:

  • #34947's cgroup memory-cap fix landed on the inherited geth CLI only. internal/cli/server/config.go:1522 still carries a near-verbatim copy of the same block calling gopsutil.VirtualMemory(), and that is the path a real bor server --config … startup takes, so containerised nodes still size cache from host memory. Porting it changes production cache sizing, so it wants its own review.
  • The EIP-7928/7843 header assertions live on the wrong engine for Bor. They are real and correctly block-based, but sit in consensus/beacon, which eth/ethconfig only builds around clique or an ethash faker. consensus/bor checks neither field. Harmless today and pre-existing, but whoever schedules Amsterdam must add them to consensus/bor.
  • A latent nil-params.BorConfig panic reached from the miner work loop breaks two inherited cmd/ packages and has now surfaced in three batches. CI never sees it because TESTALL excludes the whole cmd/ tree. Worth filing separately.

Reviews are deliberately not requested yet — the team decision is to review the whole stack as one batch once every milestone is complete.

rjl493456442 and others added 30 commits May 11, 2026 23:57
…827)

Every tracer that implements Stop/GetResult held a `reason error` field
that is written by Stop (called from the trace-timeout watchdog
goroutine in api.go) and read by GetResult (called by the RPC handler
main goroutine). These accesses were unsynchronized.
In the --create path, execFunc returns gasLeft as the second return
value, but the rest of the code treats this value as "gas used" (printed
as such, and compared in timedExec). This makes gas reporting incorrect
and can cause benchmark consistency checks to fail.
This is a refactoring PR to wrap all pre/post-execution system calls as
the exported functions, eliminating the duplicated system calls across
the codebase.

There are a few things unchanged but worths highlight:

- ChainMaker is left as unchanged, a significant rewrite is required
- BeaconRoot in header should be non-nil if Cancun is enabled

---------

Co-authored-by: jwasinger <j-wasinger@hotmail.com>
Fixes the regression caught by
https://hive.ethpandaops.io/#/test/generic/1778481210-e59b7465e1d04f7ed1b0200838584b16?testnumber=137.
engine.AssembleBlock explicitly expects withdrawals to be non-nil for
pre-Shanghai blocks as opposed to FinaliseAndAssemble which stripped off
the withdrawal.
In b2843a1, metrics check len(res) == len(hashes) but res is
pre-allocated with make(), so length is always equal. Partial hit metric
never fires. Count non-nil elements instead.

---------

Co-authored-by: Bosul Mun <bsbs8645@snu.ac.kr>
This PR introduces a separate transaction pool type for sparse blobpool.

In sparse blobpool, PooledTransactions message delivers transactions without
blobs, partial or full cells are downloaded by Cells message. Blobpool no longer
stores transactions with complete sidecars, and it stores transactions without
blobs, along with the corresponding cells. Because of this, a dedicated type
distinct from types.Transaction is required.

This PR introduces a type called `BlobTxForPool` and stores each sidecar field
independently, in order to bypass the assumption that a sidecar always exists as
a complete unit.

Reintroducing the conversion queue was considered, but was ultimately omitted
because type conversion should be sufficiently fast. With sparse blobpool, blob
-> cell computation would take about ~13ms per blob. Not sure whether this is
fast enough, but otherwise we can add the conversion queue later on the sparse
blobpool branch.
1. should use !reflect.DeepEqual.
2. big.NewInt(0).SetBits([]big.Word{}) work around for DeepEqual when
big.Int is zero, unpack return a []big.Word{}.
Passing `--v2=false` currently still selects the v2 binding generator
because the command checks whether the flag was set.

This switches generation to use the boolean flag value, so explicit
false continues to generate legacy bindings while `--v2` keeps selecting
v2.
This PR introduces OnGasChangeV2 tracing hook, as the pre-requisite for landing
EIP-8037.

---------

Co-authored-by: Sina M <1591639+s1na@users.noreply.github.com>
This PR extends the journal to track the pre-transaction values of
mutated balances, nonces, and code.

At the end of the transaction, these values are used to filter out no-op
changes, such as balance transitions from a-> b->a. These changes are
excluded from the block-level access list.

Additionally, there is a dedicated `bal.ConstructionBlockAccessList`
objects for gathering the state reads and writes within the current
transaction. These state writes will be keyed by the block accessList
index.

---------

Co-authored-by: jwasinger <j-wasinger@hotmail.com>
## Summary

The `--rpc.telemetry.sample-ratio` flag declares `Value: 1.0` and `geth
--help` advertises `(default: 1)`. In practice, however, omitting the
flag produces a sample ratio of `0`, causing
`sdktrace.TraceIDRatioBased(0)` to drop 100% of spans. Users who enable
`--rpc.telemetry` see the `OpenTelemetry trace export enabled` log line
and a clean startup, but no traces ever leave the process.

The root cause is the interaction between two pieces of code:

1. `cmd/utils/flags.go:setOpenTelemetry` (added in #34062) only copies
the flag value when `ctx.IsSet(...)` returns true:

   ```go
   if ctx.IsSet(RPCTelemetrySampleRatioFlag.Name) {
       tcfg.SampleRatio = ctx.Float64(RPCTelemetrySampleRatioFlag.Name)
   }
   ```

That is the right pattern for "don't clobber a config-file value with
the CLI default," but it implies that something else must initialise the
field when neither source sets it.

2. `node/defaults.go:DefaultConfig` never initialises
`OpenTelemetry.SampleRatio`, leaving it at the float64 zero value.

The result for the common CLI-only user (no TOML config) is `SampleRatio
= 0` → every span is silently dropped, despite the documented default of
1.

## Change

Seed `OpenTelemetry: OpenTelemetryConfig{SampleRatio: 1.0}` in
`node.DefaultConfig` so the documented default matches runtime behavior
and the `ctx.IsSet` guard in `setOpenTelemetry` continues to do what it
was designed to do.
Avoids every legacy tx hash query hitting the blob pool on the path of
BlobPool.GetRLP.
This PR fixes a bug in the current blobpool `Reset` function where it
used the Transaction type instead of blobTxForPool.

Decoding transactions fetched from the pool as Transaction type 
caused an error because the blobpool stores blobTxForPool types.
This method is similar to `eth_blobBaseFee` but returns the next base
fee.
Return blockchain rewind failures from debug_setHead instead of ignoring
them.
…4957)

This PR finally lands EIP-7928, collecting the block accessList during
the block execution and verifying against the block header.

---------

Co-authored-by: jwasinger <j-wasinger@hotmail.com>
Co-authored-by: Marius van der Wijden <m.vanderwijden@live.de>
This PR implements the serving side of the eth71 BAL exchange messages.
Until commit 4cd7092 also contained the requesting side, but since that
part still needs more work, I'm splitting it out into a separate PR.

The test injects BALs directly into rawdb. This can be removed once BAL 
generation is integrated into the chain maker.

---------

Co-authored-by: Felix Lange <fjl@twurst.com>
…4967)

Updates the static validation logic to cover additional edge cases
(reflecting the state of the latest devnet branch, except cleaned up
slightly).

---------

Co-authored-by: Gary Rong <garyrong0905@gmail.com>
It's a change to BAL json marshalling and t8n tooling to adhere the EELS
definition.
…33969)

Adds a fast path for ExecutionPayloadEnvelope and BlobAndProofListV*
that bypasses encoding/json's reflection and re-validation, which are
expensive for large payloads with many blobs. Also hand-rolls the
jsonrpcMessage wire encoding in the RPC codec to avoid a second
re-validation pass when writing responses to the connection.

Resolves #33814

---------

Co-authored-by: Marius van der Wijden <m.vanderwijden@live.de>
Co-authored-by: Felix Lange <fjl@twurst.com>
Co-authored-by: healthykim <bsbs8645@snu.ac.kr>
Co-authored-by: Felix Lange <fjl@twurst.com>
Fixes a regression where nil results from getBlobs were encoded as an empty array instead of null.


---------

Co-authored-by: Felix Lange <fjl@twurst.com>
cuiweixie and others added 26 commits June 12, 2026 23:09
This PR fixes an issue that when peers legitimately lack a requested
BAL, empty (0x80) is delivered and this BAL entry will be refetched 
over and over again. 

A `refused` tracker is added and catchUp will fail if this BAL is
unavailable against the entire peerset.
`TestTracingHTTPTimeout` still flakes in CI after #35101, failing at the
POST:

    --- FAIL: TestTracingHTTPTimeout (0.26s)
        tracing_test.go:633: request: Post "http://127.0.0.1:43497": EOF

The test sets a short server `WriteTimeout` and posts a blocking call.
`ContextRequestTimeout` leaves a fixed 100ms for the server to write its
timeout response before the HTTP write deadline cuts the connection.

I can't repro it locally, but my theory is that under load that write
can miss the window, so the connection is dropped and the client POST
returns `EOF`, failing the test before it inspects the span. This is the
only test exposed to it because it is the only one that configures a
`WriteTimeout`.

The EOF is benign: the server sets the timeout error on the SERVER span
before attempting the write, independent of whether the client receives
the response. Since that span status is all the test asserts,
`tryPostJSONRPC` tolerates the transport error instead of failing on it.
The stack primitives pop by value: pop() returns the 32-byte value
itself, so every popped operand is copied out of the stack arena before
it is used. The result side was already in place, peek returns a pointer
and binary ops write into the new stack top. This PR fixes the operand
side: pointer-returning primitives (popPtr, popPtrPeek, etc), with the
handlers rewritten to read operands directly from their arena slots.
Every popped operand paid the copy, whatever the op went on to do with
it, so this optimization covers the arithmetic and comparison ops as
much as JUMP, MSTORE, SSTORE and RETURN.

The copy is visible in the assembly. On arm64, master's opLt spends four
instructions moving the popped value through the frame, and the
comparison then reads it back from there:

LDP (R5), (R6, R7) ; load words 0 and 1 of the popped value from the
arena
    LDP  16(R5), (R5, R8)            ; load words 2 and 3
STP (R6, R7), vm.~r0-64(SP) ; store words 0 and 1 into a frame slot
    STP  (R5, R8), vm.~r0-48(SP)     ; store words 2 and 3

With popPtrPeek those four instructions are gone, the frame shrinks from
locals=0x58 to locals=0x18, and the function from 336 to 288 bytes. The
compiler cannot remove the copy itself: uint256.Int is a four-element
array, and Go's SSA does not promote arrays longer than one element to
registers, so a by-value pop pays this round trip no matter how far
inlining gets, for LT exactly as for ADD.

The CALL and CREATE families are deliberately not converted: a child
frame reuses the same stack arena, so parent pointers into popped slots
die when the child pushes. The rule is recorded on the primitives:
pointers stay valid until the next push or any sub call. Converting the
call family safely means materializing scalars before the child call,
left for later work with a call-heavy benchmark to justify it.

### Benchmarks

Measured with the benchmark suite from #35144 (the evm-bench contract
workloads and the block import benchmark), which is not part of this
PR's diff. Apple M4 Max, fixed iteration counts, n=10, all p=0.000. B/op
and allocs/op are statistically identical on every benchmark:

| benchmark | master | PR | vs master |
|---|---|---|---|
| Snailtracer | 60.0 ms | 54.1 ms | -9.8% |
| TenThousandHashes | 13.2 ms | 12.2 ms | -7.8% |
| ERC20Transfer | 11.7 ms | 11.0 ms | -5.5% |
| ERC20Mint | 7.49 ms | 7.02 ms | -6.2% |
| ERC20ApprovalTransfer | 8.92 ms | 8.44 ms | -5.4% |

This PR is independent of #35144 but plays nicely with it: the generated
dispatch there splices these handler bodies, so the in-place forms land
in its fast path too, where they measure larger.

### Testing

The rewritten handlers run on the interpreter's only execution path, so
correctness rests on references outside the change:

- **Consensus fixtures.** The full tests package passes: state tests,
the execution-spec families, blockchain tests.
- **Opcode testcases.** The JSON testcases compare individual opcode
results against committed expected values.
- **Tracer fixtures.** The tracetest reference files pin exact log and
return data shapes, covering the rewritten LOG and RETURN paths.
- **Cross-build differential.** A goevmlab campaign running this
branch's evm against master's evm over generated state tests across four
forks (Prague, Cancun, London, Osaka) with full trace comparison:
160,566 tests, zero divergences.

---------

Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
… (#35170)

sendInvalidTxs's *eth.TransactionsPacket case iterated `txs` — the
locally-sent invalid transactions, every one of which is in `invalids`
by construction — instead of the transactions actually carried by the
received packet. As a result the loop returned "received bad tx" on the
very first TransactionsPacket the peer sent, regardless of its contents,
and never inspected what was really propagated.

Iterate msg.Items() (the decoded contents of the received packet) so the
"node must not propagate invalid txs" conformance check tests the real
condition instead of producing a false negative.

---------

Co-authored-by: Bosul Mun <bsbs8645@snu.ac.kr>
This PR improves the slot reservation logic in the context of snap/2.

Geth has the mechanism to reserve roughly half the peer slots for peers 
supporting the snap protocol if snap syncing is needed by local node.

With the context of snap/2, this mechanism should be changed that:
we reserve the slot for the "usable snap peer", not blindly for peer
with snap extension enabled (such as legacy snap/1, which can't serve
the snap/2).
…181)

This PR introduces a new condition that if the local node falls behind
too much and the required BAL for catching up is very likely to be
unavailable, the entire snap sync will be restarting from scratch.

As the defined BAL retention window is weak-subjective-period which is
calculated dynamically. A more conservative threshold is used (90K
blocks) for robustness.

Apart from that, the BAL catchup will be divided into several spans and
apply one by one. It's essential to prevent the potential out-of-memory
panic of placing the entire BAL set in memory.
This PR does two things:

- Expose snap/2 specific sync progress fields
- Seed the sync progress after `loadSyncStatus `
This PR fixes an issue where flat states are continuously persisted
during downloadState, while the sync journal is only persisted at the
end of Sync.

As a result, an unclean shutdown can leave the on-disk flat state ahead
of the journal markers. Some persisted entries may be stale (storage
slots that should have been deleted), and these dangling entries are not
detected or fixed by subsequent state downloads.

To address this, this PR introduces a cleanup step before state
downloading begins. It removes all state entries that are not covered by
the persisted journal markers.
Adds `testing_commitBlockV1`. It is the write companion of `testing_buildBlockV1`:
it builds a block from the provided payload attributes and transactions on
top of the current canonical head, inserts it, and sets it as the new
head, returning the new head hash.

---------

Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
Since go 1.18 reflect has `reflect.Pointer` which replaces
`reflect.Ptr`. Newer versions of `govet` will alert. 
See also: https://pkg.go.dev/reflect#pkg-constants
The timer should wait the remaining time, not the elapsed time.
20 upstream first-parent commits, 27 conflicted files across 46 hunks. The batch
is dominated by two refactors of shared execution plumbing, and both were adopted
because declining them had become the expensive option rather than the safe one.

Witness generation, propagation and import are untouched. The stateless package,
the witness protocol and the downloader have no diff at all. Three files on the
guarded list did change, and every changed hunk in them was scanned for witness
symbols rather than trusted by filename, with no hits.

No fork gate moved and no new fork or EIP arrived. The chain config, the runtime
presets, the packaged genesis files, the jump table and the fork id calculation
are all unchanged, so neither fork meta-guard needed a new entry. The only
hardfork-adjacent change is upstream's move to a two-dimensional gas-change
tracing hook, which alters how a gas event is dispatched and not what any opcode
or precompile costs. Amsterdam, Verkle and the binary trie stay dormant.

The most consequential resolution is in the state processor's prologue, and it
would have compiled either way. Upstream's variable block auto-merged over bor's
and builds the block context with a nil author, where bor passes the author it
resolved. That value becomes the context's coinbase, and bor derives the block
author from the header signature rather than from the header's coinbase field, so
a silent nil there changes fee distribution and the coinbase opcode. Bor's own
side of the same conflict could not have compiled either, because it re-declared
the EVM that the merged variable block now creates. The resolution restores the
author in the merged block and drops the duplicated construction. This is the
same hazard as the deleted slice in an earlier batch, one step more dangerous,
because there the build failed and here it would not have.

The first refactor renames and re-types upstream's own block-level access list
field and replaces the journal's dirty-account counter with a richer per-account
mutation map. The old field name reads like bor's parallel-execution read set and
is not; it is upstream's, present at the merge base. By the time the conflicts
surfaced, the struct field, the getter, the constructor and the finalisation
aggregation had all auto-merged, leaving only the two references inside the
markers, so adopting cost one line per site while declining would have meant
reverting a twelve-file footprint. Bor's divergences around those lines are kept:
the multi-version read wrapper and the timer skips in the account getter, and the
storage mutex in the committed-state reader. The journal change stranded six
bor-only call sites with no conflict. Two iterate the dirty address set, which
the new map keys identically, and four are the direct bulk setters used during
parallel settlement, where the old call did nothing but ensure key membership and
the new one has the same effect. A bare entry is safe there because every access
list aggregation is guarded by its own per-field flag, so finalisation is restored
without recording access-list data, which is the behaviour parallel execution
already documents. The same refactor also promotes the transaction-context setter
into the state interface, so the parallel state database now implements it,
carrying only the index: it is handed its index at construction, attributes logs
at settlement, and builds no access list, so the hash and access index have
nothing to bind to. That implementation made an existing exemption stale.

The second refactor wraps the pre- and post-execution system calls into exported
helpers. It does not touch block finalisation or assembly, so the assembly
decision recorded earlier in this sync is undisturbed; the assembly rework lands
in the next batch. The helpers are adopted with their bodies rewritten for bor:
the tracing spans are removed because bor has no telemetry package at all, the
timestamp-based fork predicates become bor's block-based pair, and the arena
release is dropped as it has been throughout. The context argument is kept in both
signatures so the eleven call sites that already reference these helpers compile
unchanged and future merges stay aligned, even though it is now unused. Where
bor's semantics differ, the gate is preserved rather than the code: block
processing keeps its inlined request block, since the helper cannot express the
condition that suppresses those requests on bor chains and the block is
interleaved with state-sync tracing and bor's own finalisation signature. Three
lower-risk callers do route through the new helper, wrapped in that same
condition, which is equivalent because the fork test now lives inside the helper.

Elsewhere: the snap-sync head-hash write is re-seated where bor relocated the
head assignment, since upstream's new line targeted a site bor had emptied and
would otherwise have been lost; the blob pool combines bor's block-based fork
predicate with upstream's new pooled-transaction accessors, which were mandatory
because the code below them had already auto-merged; the virtual-host handler
adopts request-side case normalisation, the missing half of a pair whose map was
already lowercased at construction; and two upstream bug fixes are taken whose
bugs bor shared, an unconditional scheme overwrite and an inverted test assertion.

The base-fee accessor is an add-and-add where bor had already shipped the same
method, and bor's version is kept because its gate is the correct one: the
calculation returns the base fee of the block after its argument, so the fork test
belongs one block ahead. The matching public method and a mock converge onto
upstream instead, retiring two permanent divergences at no cost.

The simulated-block withdrawals case is a genuine combine where neither side alone
is correct. Upstream's new fix only attaches withdrawals once the relevant fork is
active but still dereferences the override unconditionally inside that branch,
relying on sanitisation that does not hold on bor, where the override stays nil.
Keeping bor's side would have dropped the fix. The resolution keeps both the fork
gate and the nil check.

Nine sites broke at build or vet with no conflict to announce them, including a
second landing site for the declined telemetry defaults that auto-merged silently.
Six of those are test files reachable only with a build tag, which is why this
batch adds a tag-enabled vet sweep to the per-batch gates: the previous milestone
shipped an unused import in exactly such a file, invisible to the ordinary build,
the untagged vet and the neighbouring integration package.

Two more bor-only drift guards fired and were updated, one requiring every state
method to exist on the parallel implementation or be exempted, the other requiring
every tracing hook to be classified. Two tests new in this batch assume upstream's
configuration shape and were adapted: one omits the post-merge marker that bor's
chain maker needs, and one reaches for the mainnet config where its seven siblings
use a purpose-built one with block-based fork keys, which on bor activates neither
blob transactions nor the blob schedule.

Six test failures are pre-existing and were confirmed by re-running at the merge
base and comparing the failure text rather than the test names. Two are timing
races between a five-millisecond abort and a ten-million-gas loop, which pass in
isolation at both revisions and fail only under parallel load at both. Four are
golden-output mismatches in the evm command, including a state root mismatch that
resolves to an unchanged root: the base computes the same value against the same
stale expectation.
20 upstream first-parent commits, 26 conflicted files. This is the batch the plan
flagged from the start, and it reached both of the points where this sync stops and
asks rather than resolving. Two clusters were declined with explicit approval, and
both are documented for the day they have to land here.

Witness generation, propagation and import are untouched, and this time that was the
decision rather than an observation. Upstream reworked witness serialization so the
external form reverses header order and sorts codes and state nodes, to match the
execution-spec ordering for a new engine method that carries a witness. That is
reachable here: bor's own wire format is a three-field encoding of its own, but the
decoder falls back to the external form when a peer sends the older five-field layout,
and the same conversion backs the debug witness endpoints. Adopting only the upstream
side would leave the encoder writing one order and the decoder assuming another on that
fallback path, which is an interop break rather than a formatting change. The
motivation does not reach bor either, since the engine API is inherited but unused and
the file implementing the witness variant of it was already deleted here. Declined, with
bor's context field preserved on both conversions. Upstream's new round-trip test for
the external form arrived as an add-and-add conflict and bor's existing tests were kept.

The second decline is the block access list construction cluster, five commits that
build the list through execution, serve it at a new protocol version, rework its JSON,
add static validation, and wire it into the transition tool. The construction commit
changes the consensus engine interface itself, adding an access index and a list to
Finalize. That is where bor diverges hardest: bor's Finalize takes and returns receipts
because it applies state sync events and appends the state sync receipt, and bor keeps
an assemble variant upstream no longer has. Upstream also builds the list
unconditionally per block while gating it inside the pre and post execution helpers, so
on bor it would allocate for every block and merge nothing, in exchange for changing the
most consensus-critical signature in the tree. The feature is dormant here, and it
cannot run under bor's parallel executor at all, which is already recorded. Declined as
one unit, since the later commits build on the first.

The resulting position is deliberate and stated plainly in the register: after the
previous batch and this one, bor records state accesses into a per-transaction access
list but never creates one. The recording sites are all behind a nil guard, and the list
is allocated only once the dormant fork is active. Nothing assembles, merges, validates
or serves a block-level list, so the header field stays empty and the verifier keeps
asserting exactly that. Scheduling the fork now requires the construction cluster and
the parallel-executor decision together, because neither alone produces a valid list.

Declining a cluster means reverting its whole footprint rather than its conflicts. The
union here is forty-seven paths against fourteen conflicts, so a conflict-only rollback
would have left roughly thirty files carrying declined code, almost all of it merged
silently. The footprint was computed from upstream history, every path restored to this
fork's tip or removed, and the kept commits touching those same files re-applied in
order afterwards: the engine payload types, the chain maker's slot number on bor's
block-based gate, and the rewind error propagation.

Three residues survived that sweep, each caught by a different gate. The payload data
codec was renamed by a later kept commit, so the declined access-list plumbing outlived
the rollback under a filename the footprint could not match, and was stripped by hand;
that is a general hazard when declining a commit whose files a later kept commit
renames. The engine API's blob endpoints gained tracing spans that merged in while this
fork's import block won, leaving undefined references; reverting the file would have
dropped three kept commits, so the spans were removed and the context parameters kept,
and a counter that existed only to fill a span attribute went with it while its
counterpart that drives the partial-response decision stayed. Four cache meters merged
in whose only consumer is a file this fork deleted long ago, which the build accepts and
the linter does not, so they were reverted too.

The largest adoption was forced rather than chosen. The JSON-RPC message's error field
merged from a struct pointer to pre-marshalled bytes, so every reader of its code and
message had to move to the new accessor and no side of those conflicts would have
compiled alone. The codec split into separate message and batch encoders, with
hand-written builders and a helper that preserves the existing content-length,
identity-transfer-encoding and flush behaviour for error responses exactly, only
relocated. This fork's divergences there were blank lines and one explicit discard on
the batch write, which was kept. The rework requires a new direct dependency for JSON
writing, which is worth a reviewer's attention since the supply-chain check will report
it.

Elsewhere: the rewind endpoint now returns its error instead of discarding it; the
generated payload envelope encoder was replaced by the hand-written one, which would
otherwise have been defined twice; a new seven-hundred-line engine benchmark was adapted
rather than dropped, moving two timestamp fork fields to their block-based equivalents
and removing context arguments from the engine calls that do not take one here; and
three modify-and-delete conflicts on files this fork had already deleted were all kept
deleted, each matching a decision already on record.

No fork gate moved and no new fork or EIP arrived. The chain config, the runtime presets,
the packaged genesis files, the jump table and the fork id are untouched, and the
protocol parameters file was inside the declined footprint and is back at this fork's
tip. Neither meta-guard needed an entry.
20 upstream first-parent commits, 25 conflicted files. After the two declines of the
previous batch this one is comparatively ordinary, and it is the first of the
milestone where both standing scans come back completely empty: no file under the
stateless package, the witness protocol, the state database, the blockchain, the
miner or the downloader is touched at all, and neither the chain config, the runtime
presets, the packaged genesis files, the precompile table, the jump table nor the
fork id calculation changed. No fork gate moved, no new fork or EIP arrived, and
neither meta-guard needed an entry.

One item needed a decision rather than a resolution, and for an unusual reason: this
fork already has the optimisation upstream is adding, at a different scope. Upstream
adds a process-global sharded cache of jump-destination bitmaps keyed by code hash,
with hit and miss meters, threaded as a new parameter through the prefetcher and the
processor. This fork already shares such a cache between the prefetcher goroutine and
the parallel execution workers, but per block: a fresh map is created for each block
and discarded afterwards. So the two are not duplicates, they differ in lifetime.
Upstream's would skip the analysis entirely after a contract is first seen rather
than once per block, and it is bounded where this fork's is not, which is plausibly
the larger win on a workload that concentrates on a small set of contracts.

Declined for this batch on scope grounds, with approval. Adopting the parameter
threading collides with this fork's diverged prefetcher signatures, and the variant
actually worth having — pointing the existing shared-cache config field at a
process-global bounded cache, which needs no signature changes at all — is a
design change to this fork rather than a merge resolution, and it belongs on its own
branch with before-and-after measurements on the execution hot path. It is recorded
as a benchmarked follow-up, including the two invariants to settle first and the
instruments to measure with. The new file was removed along with the decline, since
with nothing referencing it its unexported meters and constants are dead code the
linter reports, the same shape as the orphaned cache meters in the previous batch.

A decision from two batches ago was vindicated by upstream. That batch hit an
add-and-add on the backend's base fee accessor and kept this fork's version, arguing
that because the calculation returns the base fee of the block *after* its argument,
gating on the next block number was correct and upstream's gate on the current one
was subtly wrong at the fork boundary. Upstream has now fixed exactly that, arriving
at byte-identical logic, so the conflict resolved by converging onto upstream and the
divergence is retired at no cost. This fork was also already ahead on the STUN
dependency bump, carrying version three before the merge, so that conflict resolved
to this side and the module files kept their state.

The deprecated flag removal was accepted after two checks rather than on faith. None
of the fourteen removed flags is referenced outside the files upstream edits, and the
binary this fork ships is built from a different entry point with its flags declared
elsewhere, so the removal cannot change what operators pass to it. One hazard inside
the cluster: upstream's side of the miner-config hunk keeps a blob-cap flag this fork
declined long ago, so that hunk had to collapse to neither side instead of taking
upstream's. The removal of the USB parameter from the account-manager and signer
constructors was taken consistently across the clef command and the signer package.

Two residues surfaced from gates rather than from reading the diff. The legacy flag
file kept two imports whose only users upstream deleted, which the build caught, the
same class as the unused import behind a build tag that broke CI at the end of the
previous milestone. And the request handler ended up checking the method-name length
twice, because upstream moved the check to the top of the function while this fork's
copy survived further down; the inner one was removed in favour of upstream's single
early check.

Elsewhere: the engine API now imports a payload at genesis even when snap sync is
configured, combining upstream's new exception with this fork's own sync-mode
predicate; the HTTP handler stack gains the gzip opt-out next to this fork's
execution-pool field; two test mocks were combined rather than replaced; and two
modify-and-delete conflicts on files this fork does not carry, an upstream CI
workflow and the handler file from the declined snap protocol split, were both kept
deleted.

Verification covered every touched package with no failures. The command package for
the inherited geth binary fails seven tests on a panic in this fork's period
calculation reached from the miner work loop, which was reproduced at the previous
batch with an identical panic frame and is therefore pre-existing; that also clears
the flag removal of suspicion. Worth recording why CI is green on it: this fork's
test target excludes the whole command tree, so those tests have never been part of
the gate. The panic itself is a real latent bug in an inherited but unused surface,
where starting on a non-bor genesis reaches a nil consensus config, and is worth
filing separately.
20 upstream first-parent commits, 38 conflicted files. That is the largest conflict
count of the milestone, but it is misleading: the conflicts collapse into only nine
upstream commits, and the batch is really one structural adoption plus two extensions
of deferrals that already existed. Both standing scans come back empty for the second
batch running. No file under the stateless package, the witness protocol, the state
database, the miner or the downloader changed; the blockchain has exactly one changed
line, a freezer tail lookup that now names a tail group. Neither the chain config, the
runtime presets, the packaged genesis files, the precompile table, the jump table nor
the fork id calculation moved, no new fork or EIP arrived, and neither meta-guard
needed an entry.

The miner work loop is worth stating explicitly, since it conflicted and it is the
file on the no-go list that matters most. The conflict was telemetry attribute types
only, an integer helper replacing a widened one, from a tracing commit this fork
cannot carry. The file is now byte-identical to its previous state. A file overlapping
is not the feature overlapping.

The structural item is upstream's flat-file storage for finalized block access lists.
Despite the title it is not an access-list feature: it is a general rework of the
freezer, replacing a per-table prunable boolean with a named tail group so that
different groups within one freezer can keep independent tails, adding the ability to
attach a new table to an existing freezer, and changing the ancient-store interface so
that reading and truncating a tail both take a group name. The access-list table rides
on top of that.

The general layer was not really a choice. The interface change had already merged
cleanly, and with it the table wrapper, the in-memory freezer, the ancient utilities,
the shared test suite and all six callers in the path-based trie database: thirteen of
the twenty-four files in the footprint. Neither side compiled alone, the same shape as
the forced adoption two batches ago. Declining would have meant reverting an interface
change across thirteen cleanly merged files and then paying for it on every future
freezer commit.

The real decision was the access-list table itself, and it was taken in full with
approval. Nothing in this fork writes such a list, so the table holds a nil
placeholder per frozen block until the construction cluster deferred two batches ago
is adopted. The upgrade path was verified rather than assumed: the repair routine
takes the common head as the minimum over non-empty tables and fast-forwards a freshly
added empty table to it, so an existing database gains the new table at its current
head with no backfill and no resync. Upstream ships a test for exactly that, and it
passes here. The ongoing cost is index only, six bytes per frozen block for a table
that stays empty. Taking the general layer while stripping the table would have bought
that back in exchange for a permanent divergence in three storage-layer functions that
would conflict on every future freezer change and would have to be undone anyway once
the fork is scheduled.

Two adaptations were needed for this fork. It carries two extra chain tables, total
difficulty and the state-sync receipt, both previously prunable; under the group model
every prunable table must name a group, and since both are written per block alongside
bodies and receipts they join the block-data group. That reproduces the previous
semantics exactly, one shared tail across all prunable chain tables, while the new
table gets its own group so it can be pruned independently. Separately, this fork's
freezer carries an offset for pruned-ancient stores, and the repair and validate
routines were rewritten wholesale upstream and merged cleanly, so they were audited
line by line: all seventeen uses of the offset survive, including the one inside a
conflict, and the offset test passes.

Two deferrals were extended, both with approval. The archive-format rewrite deferred
long ago received a spec update; almost all of it lands in a package this fork does
not carry, which arrived as modify-and-delete conflicts and was removed again, while
the files it does carry keep their existing form because every change in them
presupposes the deferred interface layer and a flag this fork lacks. And the deferred
snap protocol split gained a skeleton: a three-thousand-line parallel copy of the
syncer, wired to nothing. Removed, on the same reasoning as the global cache file in
the previous batch, since unreferenced code is dead weight the linter reports. The
batch two ahead carries the sync logic that would consume it, so the decision will be
revisited there as one unit.

The tracing work was declined as usual, but one real fix was hiding inside it. The
HTTP transport merged cleanly and pulled in an import of a package this fork does not
have, which would not have compiled; the same region also carries an independent fix
that always sets the content length on responses, whose stated precondition this fork
gained two batches ago. So that file was combined rather than reverted: the fix and
the context threading are kept, only the span and its import are stripped, and the
error path keeps the identity transfer encoding and the explicit flush that were
hand-preserved earlier. The service registry produced another vindication: its
conflict shows the merge base with a three-value lookup introduced by a tracing commit
this fork declined, while both sides now return a single value. Upstream has converged
back onto this fork's shape.

Upstream also moved three chain-head subscriptions from run loops into constructors.
In all three the constructor half merged cleanly while the loop half conflicted, so
keeping the previous form was not an option; it would have subscribed twice in the
transaction pool. This fork has a deliberate divergence here, a defensive nil check
around unsubscribe added by its own commit in exactly these places, and that check is
preserved, including on the one deferred statement upstream wrote without it.

Two interface implementers outside upstream's footprint were found by the twin scan
before resolving rather than by a gate. One is a generated mock that exists only here;
it was regenerated with the command recorded in its own header rather than hand-edited,
and exactly eight lines changed. The other is a test double carrying a compile-time
interface assertion, which would have broken the test build.

Two upstream fixtures were adapted. One called a write helper with upstream's argument
count where this fork's takes two more. The other, the new-table alignment test, needed
the offset argument and, less obviously, had to populate this fork's two extra tables:
they share the block-data group, the repair routine takes the maximum tail within a
group, and leaving them empty would fast-forward the whole group past the very data the
test reads back. A comment records why. That property is safe in production because
every block-data table is written together for each frozen block, and on a fresh
database all of them are empty so the head stays at zero.

Verification covered every touched package with no failures, including the integration
suite. The state-test fixtures are not present in this working copy so those tests
skip locally, which is why the vet pass with the integration tag matters: it is what
proves the tagged files still compile.
20 upstream first-parent commits, 34 conflicted files. They collapse into ten upstream
commits, and nineteen of the thirty-four belong to a single one: upstream has removed
its stand-alone signing tool. That makes this the only batch of the sync that is
overwhelmingly a deletion, a little over ten thousand lines of it. Both standing scans
come back clean for the third batch running. No file under the stateless package, the
witness protocol, the state database, the blockchain or the downloader changed; the only
change anywhere under the miner is one line of a test failure message, an off-by-one in
an index from the sweep that fixed incorrect variables in error strings. Neither the
chain config, the runtime presets, the packaged genesis files, the precompile table, the
jump table nor the fork id calculation moved, no new fork or EIP arrived, and neither
meta-guard needed an entry.

The tool removal was accepted, but on evidence rather than on the fact that upstream did
it. Three things were checked. This fork never builds the binary: its makefile, its CI
build script, its release configuration and its docker files contain no reference to it
at all, so no release artifact has ever contained it. Nothing outside the tool itself
imports the packages being deleted: a first search looked alarming, because the external
account backend and an ABI dump command both appeared to match, but that was a prefix
match on a subpackage of shared type definitions which upstream keeps, along with the
four-byte database. And the capability people actually care about is untouched, because
the external signer backend is an RPC client that talks to any signer implementing the
protocol; the deleted binary was one implementation of it, not the mechanism. Worth
noting that this fork had modified two of the deleted files only one batch earlier, in
the deprecated-flag removal, so that work is discarded here.

The dependency tidy corroborated the removal independently: three table-formatting and
terminal-width libraries dropped out of the secondary module's graph, which were the
deleted tool's console dependencies. Two documentation files also conflicted, but both
are complete rewrites on this side, so both keep their existing content; a search for
the tool's name appears to hit in them at first, but only because the conflict regions
in the working tree still contain upstream's text.

The batch's real find was a bor-only interface broken by a clean merge. Upstream changed
a balance-reading API method to take its block parameter by pointer, so that it can be
omitted and defaulted to the latest block, and that file merged without conflict. This
fork, however, declares its own interface that the consensus engine uses to reach those
methods, and it still named the value form; the concrete type stopped satisfying it and
the config package failed to compile at four construction sites. The build caught it.
The fix carries no behavioural risk: that method is never actually invoked anywhere in
the tree, existing only so the concrete API type remains assignable, and the interface's
two other methods already took the pointer form, so the change is also the locally
consistent one. The generated mock beside it was regenerated with the command recorded
in its own header rather than hand-edited.

The snapshot generation shutdown race fix was adopted wholesale. It replaces a
channel-of-channels abort handshake with a cancel and done pair plus an explicit stop
method, and deletes a wrapper error type. Adopting was straightforward because this
fork's divergence in every one of those eight conflicts is blank-line placement, plus a
single linter directive attached to the type upstream deletes. It was confirmed first
that nothing outside the package touches the old mechanism; the only other match in the
tree is an unrelated local variable of this fork's own parallel-execution abort type.

One conflict was a genuine combine on a fork-gated path. Upstream now drops a
reorged-out legacy blob transaction after the relevant fork instead of upgrading its
sidecar, and the merge had already pulled in the new comment saying exactly that,
directly above the old code that still performed the upgrade — so the function
contradicted itself. The resolution takes upstream's drop-and-error behaviour with this
fork's block-based fork predicate rather than upstream's timestamp-based one, which is
the standing rule for every fork gate in this sync.

For once a tracing commit could be taken. Every one so far has been declined because
this fork has no equivalent of upstream's telemetry wrapper package, but the propagator
option added to the RPC client depends only on the upstream OpenTelemetry propagation
package, and this fork does carry OpenTelemetry along with its own tracing helpers. The
distinction is worth stating plainly: what is missing is the wrapper, not the library.
The accompanying test file was still kept deleted.

A validation tightening on block access list encoding was kept out, extending an earlier
decline: this fork's copy of that file is an older shape, without the per-slot validate
method the fix edits, because the JSON encoding and static validation commits were both
declined three batches ago as part of one cluster.

Elsewhere: the console binding for a new transaction-pool clearing method was added at
upstream's exact position, since its backend merged cleanly; a websocket request on a
non-matching path now returns a not-found status instead of closing silently; an
array-parsing error that reported the wrong character was fixed; and a test helper this
fork had generalised keeps its generic signature while taking upstream's spelling
correction.

Three upstream test fixtures needed this fork's signatures, all caught by the vet pass
rather than the build: a new API test was combined with the two existing local tests
instead of replacing them and had its server constructor given this fork's
execution-pool arguments; a new pool test was given the extra interrupt argument this
fork's pending-transaction method takes, passing nil, which the legacy subpool handles
explicitly; and a duplicated import was removed where this fork groups it differently.

Verification covered every touched package with no failures, including the integration
suite. One inherited command package panics on a nil consensus config reached from the
miner work loop. That is the same latent bug found two batches ago behind seven failures
in the other inherited command, and it was re-checked rather than assumed: the package
was run at the previous batch's commit in a throwaway worktree and panics with an
identical frame. Neither the miner nor the params package is touched here. The finding
widens what is known about that bug — it affects any inherited command that starts a
node with a miner on a non-bor genesis — and since this fork's test target excludes the
whole command tree, CI has never exercised either case. Still worth filing separately.
20 upstream first-parent commits, 39 conflicted files collapsing into ten commits. The
batch has two halves that pull in opposite directions: the state-creation gas EIP had to
be taken whole because two thirds of its footprint arrived already merged, while the
BAL-based state sync protocol was declined for the fourth time. Both standing scans come
back clean for the fourth batch running. Nothing under the stateless package or the
witness protocol appears anywhere in the range, and no upstream hunk in it mentions
witness at all. The hardfork surface does move this time, and the reasoning is recorded
in full in the fork register: no new fork, no activation height, no precompile added,
removed or re-gated, and the behaviour is gated on a fork that is nil on every network
preset. This fork's gate for it was already the block-based form, so no conversion was
needed.

The gas EIP was adopted because declining had stopped being an option: twenty-five of its
thirty-six files auto-merged, including the whole gas-accounting substrate — the block gas
pool, the budget type, the interpreter, the jump table and the test harness — so refusing
it would have meant reverting all of them and re-deriving a model the conflicted files
already depend on. That makes this the fourth forced adoption of the sync and by far the
largest. The substantive change is a gas reservoir: every frame now snapshots its state
gas on entry and returns an exit-form budget rather than the running one. Adopting that on
a consensus path needed an equivalence argument, and it is exact — with state gas at zero,
which is this fork's situation everywhere because state gas is only ever charged in the
dormant branch, the halt form consumes precisely the remaining regular gas the old code
consumed, the revert form preserves it exactly as the old guard did, and the success form
is the identity.

Two divergences needed care rather than resolution. This fork raises the deployed-code
size cap on its own fork and, unlike upstream, assigns the error instead of returning so
the deployment gas is still charged; upstream meanwhile deleted the single size check and
pushed it into each of three gas-charging branches, deliberately before the charge in the
new fork's branch. Both properties are preserved by routing all three of upstream's new
call sites through a small helper that applies this fork's cap first. Upstream's early
return is gas-equivalent to the assign-and-continue flow on every path reachable here,
because the deployment gas is charged before the size check returns; the only difference
is that the code is no longer written before the error surfaces, which is unobservable
since the caller reverts the snapshot either way.

The second is a deletion that looks alarming and is not. The same commit removes the
static-call write-protection guard from both contract-creation opcodes, and the creation
helper does not check it, which read alone would permit contract creation inside a static
call. It does not, because the commit moves the check into the four creation gas
functions — six added lines against two removed — and the interpreter evaluates dynamic
gas before the execution function. All four are wired in this fork's jump tables and
overrides, and this fork wraps a dynamic-gas error exactly as upstream does, so the
observable behaviour matches. Keeping the old guard would have been unreachable code that
also diverged from upstream's error text.

The transaction-level half needed rewiring instead. Upstream extracted a settlement
method, which merged cleanly, and deleted both the frozen initial budget and the
gas-used accessor, leaving six callers of a method that no longer exists. All six were
repointed at the locals the new method returns while keeping every local divergence in
that function: the parallel-execution fee flags, the burnt-contract redirect with its
nil-config guard, the balance snapshots that feed the execution result, the signed
big-integer tip, and this fork's arm of the transaction gas cap. Both fork gates in the
file stay block-based; a second timestamp-form call site had auto-merged and was corrected.

The sync protocol decline was approved as one unit and rests on a structural argument that
did not exist at the two earlier deferrals. This fork renamed the downloader and diverged
it to twice upstream's size, so all four commits here that touch upstream's downloader
have no landing site at all — adoption is no longer porting a handler split but re-porting
a two-and-a-half-thousand-line downloader onto upstream's new state machine, plus
replacing a commit-head method its own interface requires. Meanwhile the feature remains
unusable, because it exists to serve block access lists and this fork can neither produce
one nor build them under parallel execution. Nine files were removed and twenty-six
reverted; every removal was checked against the previous commit first so none could be a
local file lost by accident, and the auto-merged config field and command-line flag were
stripped by hand. Three salvage candidates were examined and all three rejected on
evidence — one is plumbing feeding only the declined progress counter, one would have added
three permanently-zero fields to user-facing sync output, and the third turns out to be an
orphan rather than a fix this fork is missing, because its commit-head path already
regenerates the snapshot and resumes paused maintenance.

The blob-cache commit was declined as one unit with a package move, because the move exists
only to let the cache share the miner's selection logic. The cache cannot run here: the blob
pool field is declared and never assigned, since the pool was removed from the subpool list
outright, and the engine API that consumes it is inherited but unused, so adopting would
have wrapped a nil pool. The move would also have been expensive for nothing — this fork's
ordering file is a third larger, its constructor takes an extra interrupt parameter, it adds
four methods, and about twenty call sites in the block-building path use the unexported
local type.

The stack refactor was the second forced adoption and the one that lands in the hottest
code in the tree. Upstream added seven methods returning pointers into the stack instead of
copying values, and this fork's stack is a different implementation entirely — a fixed array
with a leading counter, ported from another interpreter, against upstream's shared-backing
design. Declining was unavailable because twenty-one call sites in one file and two in
another auto-merged outside any conflict region, so the package could not compile without
them. The seven were translated, which is mechanical and semantically identical, and
upstream kept the value-copy pop so the remaining thirteen local callers still compile. All
twenty-four conflicts in the instruction file plus one elsewhere turned out to be pure
blank-line divergences — not assumed, but established by a resolver that compares against
the merge base with blank lines stripped and refuses any hunk whose substance differs. It
flagged none there and flagged exactly the eleven that genuinely needed hand resolution.

The container memory-limit fix was adopted, and it carries a finding worth acting on
separately: the library it replaces is still used in this fork's own server configuration,
in a near-verbatim copy of the very block upstream fixed — same clamp, same allowance, same
warning — and that is the path a normal configured startup takes. The fix therefore landed
only on the inherited command-line path. Porting it changes cache sizing on the production
startup path, so it is recorded as a follow-up rather than smuggled in here. A conformance
test fix was ported rather than taken or dropped: upstream's version depends on a lazy
decoding wrapper this fork does not carry, but the false negative it fixes exists here too,
so it was expressed against the local packet shape. The block-building test API change is
the fourth orphaned change on a feature declined twenty batches ago.

Six local implementers of upstream interfaces broke outside upstream's footprint, five of
them surfaced by the build. An ecrecover cache fast path whose own comment says it mirrors
the precompile runner got exactly the edit upstream made to the function it mirrors; the
state-sync system-call processor, in a directory upstream does not have at all, took the new
zero state budget; the tracer moved to the new intrinsic-gas signature; a prefetch loop
followed a pure rename; nine local virtual-machine tests took the new argument; and a
generated helper called an accessor this fork had removed entirely, so the predicate was
written in the local idiom instead of reintroducing it — which is also the block-based form
the standing rule requires.

Verification covers every touched package, plus the integration-tagged vet pass, both
module tidies and the linter, which reports no issues and so also confirms the declines left
no orphaned symbols. Two vet findings and two failing inherited command packages are
pre-existing, all re-baselined at the previous commit rather than assumed, and all
reproducing identically — the two packages carry the still-unfiled nil consensus-config panic
reached from the miner work loop, which this fork's test target excludes from CI entirely. Two local tests failed on the first run
and both were genuine regressions from this batch, each re-baselined and fixed. One was a
timing race the stack optimisation outran: gas parity is exact on both paths, but the fast
dispatch now burns the test's budget in under six milliseconds against a five-millisecond
sleep, so the budget was raised in the two tests that race a wall clock. The other was a
fork-parity guard doing its job — the gas EIP adds the first state-processor-level branch on
the new fork, and before amending the expectation it was verified that both execution paths
reach it through the same four shared system-call helpers, so no divergence exists; the
asymmetry is where the helper is defined.
The final batch, and the smallest of the sync: 4 upstream first-parent commits, 15 files
offered, 14 taken, 3 conflicts over two commits. The boundary is the release tag itself, so
this lands the target the sync has been working toward. Both standing scans come back clean
for the fifth batch running, and this time trivially — no file under the stateless package
or the witness protocol appears in the range, and the range contains no hunk mentioning
witness in any file at all. Not one entry on the hardfork surface list is touched either:
no chain config, no runtime preset, no packaged genesis, no precompile table, no jump table,
no fork id. Nothing moves that either meta-guard tracks.

Two of the three conflicts belong to a tree-wide sweep replacing a deprecated reflection
constant with its current name. Ten of its twelve files merged cleanly; the two that did not
diverge from the merge base by a blank line each, which was established rather than assumed
by the same resolver used in the previous batch — it refuses and flags any hunk whose
substance differs, and it flagged neither. One local file the sweep could not reach, in a
command directory upstream does not have, was completed by hand. That one compiles either
way, because the old name is a true alias in the standard library and even carries an
automated-fix directive, but an all-tree sweep that leaves a single file on the deprecated
spelling is one the next linter run re-opens.

The third conflict is the version file, and it was kept local again. The batch ends at
upstream's release tag, which sets a patch number and marks the build stable. This follows
the precedent set two milestones ago rather than the one from the middle of the sync, and the
distinction is real: the earlier take-theirs was a release-*cycle* commit opening a new line,
whereas a release *tag* has been declined every time one has appeared. The evidence behind
that precedent was re-gathered rather than trusted — every milestone tip of this sync still
reads the local values, and the two branches that do not are both explained, one being the
first batch's release-cycle auto-merge and the other the base branch cut before the sync
started. This also retires a stale plan item: the bump had been carried as something for the
chores commit to settle, and it is not — it is simply not taken. What does become actionable
is the question that decision parked, namely what this file should read for a tree whose base
really is this release; until now that was hypothetical. It stays untouched here because it
is a release decision rather than a merge one.

One commit produced no change at all, and that was verified rather than assumed, because a
footprint reporting "unchanged" is otherwise indistinguishable from a change that silently
failed to apply. It fixes a discovery iterator waiting the elapsed interval instead of the
remaining one, and this tree already had the corrected expression, introduced locally by an
earlier lint cleanup — so all three sides agreed and the merge was genuinely a no-op. That
makes the fifth time in this sync that upstream has converged onto something already present
here. The remaining commit, a log message in the path-based trie's obsolete-index cleanup,
merged without incident.

Verification is the full per-batch tier: build, both vet passes including the
integration-tagged one, formatting over every touched file, both module tidies, and the
linter with no issues. Every touched package passes, along with the bor test suite. No
regressions and no new failures; the only vet output is the two pre-existing lock-copy
findings in files this batch does not touch, already re-baselined in earlier batches.

With this the merge work for the sync is complete: the branch now carries upstream history
through the release tag, in 33 batches, each a single merge commit with its reasoning
recorded.
Documentation only; no code changes. Covers batches 27 through 33, closing the
final milestone. With these the merge work for the whole sync is complete: the
branch carries upstream history through the release tag, in 33 batches, each a
single merge commit with its reasoning recorded.

Four decisions in this milestone carry most of the weight, and three of them are
adoptions that were not optional.

The state-creation gas EIP was taken whole. Twenty-five of its thirty-six files
arrived already merged, including the entire gas-accounting substrate — the block
gas pool, the budget type, the interpreter and the jump table — so declining it
would have meant reverting all of them and re-deriving a model the conflicted
files already depended on. Its behaviour is gated on a fork that is nil on every
network preset, and the register records the dormancy argument in three parts:
the new opcode pricing lives only in that fork's instruction set, the new gas
reservoir is a no-op while state gas is zero, and the system-call allowance is
gated too. The equivalence at zero state gas is exact rather than approximate,
which is what made adoption safe on a consensus path.

The stack refactor was likewise undeclinable, and it lands in the hottest code in
the tree. Upstream replaced value-copying stack pops with pointer-returning ones,
and this fork's stack is a different implementation with the same purpose. Enough
call sites merged cleanly that the package could not compile without the new
methods, so they were translated. Worth knowing for the next sync: this made one
dispatch path fast enough to outrun a five-millisecond timer that two local tests
race, which is a property of those tests rather than of the change.

The BAL-based state sync protocol was declined a fourth time, and the reason has
changed shape. It is no longer a question of volume but of landing site: this fork
renamed its downloader and diverged it to twice upstream's size, so the four
commits here that touch upstream's copy have nowhere to go. Adoption now means
re-porting a two-and-a-half-thousand-line downloader onto upstream's new state
machine, which is branch-and-PR work rather than merge work. The feature also
remains unusable, since it exists to serve block access lists that this fork can
neither produce nor build under parallel execution.

The blob-cache commit was declined together with a package move, because the move
exists only to let the cache share the miner's transaction selection. The cache
cannot run here — the pool it wraps is declared and never assigned — and the move
would have touched about twenty call sites in the block-building path to no end.

Two items in the backlog are worth reading before the next sync rather than after.
A container memory-limit fix was adopted, but it fixes the inherited command-line
path while this fork's own server configuration still carries a near-verbatim copy
of the same block, and that copy is what a normal configured startup uses; porting
it changes cache sizing in production, so it is filed rather than smuggled in. And
the register now notes that the deployed-code size cap has two overlapping sources
here, one local and one upstream, whose interaction needs explicit boundary tests
before the upstream fork is ever scheduled.

The version file is deliberately not bumped. That follows the precedent from two
milestones ago rather than the one from the middle of the sync, and it is not a
deferral — a release tag's bump has been declined every time one appeared. What
does now need an answer, and is a release decision rather than a merge one, is
what that file should read for a tree whose base really is this release.
Documentation only; no code changes. Records what was run at the milestone tip
after the final batch and the chores commit, and what it showed.

The whole-tree test run passes 149 packages against 3 failures, and all three,
along with every failing test name, are in the long-documented pre-existing set.
The signatures were compared rather than just the names: the four transition-tool
failures all report the same invalid eip number they have reported for many
milestones, and the five console/genesis/export failures plus the devp2p
conformance package all show the same nil consensus-config panic reached from the
miner work loop. Worth noting the direction of travel — that pre-existing set has
shrunk over this sync rather than grown, since a state-method parity test was
fixed at the second milestone and two interpreter timer flakes were fixed in the
penultimate batch. The linter reports no issues, which also confirms this
milestone's several declines left no unused symbols behind, and both module
tidies are no-ops.

The vulnerability scan reports a single called advisory, in the gRPC dependency.
It is inherited from the development branch rather than introduced here, and that
was verified rather than asserted: the dependency is pinned to the identical
version on both. The sync's entire dependency delta against that branch is seven
lines, every one of them traceable to a recorded decision.

The milestone's headline adoption needed an empirical check, not just an
argument. Taking the state-creation gas EIP was forced by how much of it arrived
already merged, and the safety case for it was analytic: with state gas at zero,
which is this fork's situation everywhere while the gating fork is dormant,
upstream's new exit forms reduce exactly to the previous behaviour. An analytic
argument on a consensus path deserves evidence, and the tree already contains the
right instrument. Replaying 241 real mainnet blocks through the merged virtual
machine reproduces byte-identical state roots on both the serial and parallel
paths. Since gas used is committed into receipts, receipts into the receipt root,
and that root into the header, identical roots across 241 historical blocks mean
identical gas. Building a second image from the pre-merge base to diff block by
block was considered and is unnecessary; this is the stronger instrument and it
is parameterised by real chain data.

The block-access-list dormancy claim needed narrowing, and the narrowing matters
at enable time. Nothing on any path here writes the header field, so it stays
nil as the register has said since it arrived. But the assertion that it stays
nil lives on the inherited beacon engine, which is only ever constructed around
the clique or ethash engines — never on this fork's networks, whose headers are
verified by its own engine, and that engine checks neither this field nor the
slot-number field beside it. Harmless today, pre-existing rather than introduced
here, and with no effect on state since the field is covered by the header hash.
It becomes a requirement whenever the fork is scheduled, and is recorded as one.

Two devnets ran on an image built from this tip, and both pass. The standard one
shows both nodes advancing in lockstep with identical finalized hashes,
consensus, milestones and checkpoint acknowledgements all rising, and no errors
in either execution log. The witness one took two attempts, and the first failure
is recorded because it is a trap rather than a typo: the sync-with-witness
participant setting is not a witness toggle, it switches the node's entire sync
mode to stateless, which starts from the latest checkpoint — so on a
genesis-fresh chain, applying it to the block producer deadlocks the network at
block zero waiting for a checkpoint only that producer could create. With
production and consumption split across the two nodes it passes, and the witness
path is evidenced directly rather than by log grep: the producer wrote 169
witnesses and the stateless consumer holds the identical set, the witness
protocol capabilities are negotiated between the peers, and the consumer tracks
the producer's head exactly — which is the end-to-end proof, since a stateless
node cannot import a block without its witness.

Two measurement traps are recorded alongside the results, because both nearly
produced false findings. The consensus client answers two of the obvious counter
endpoints with not-implemented payloads whose numeric error codes a naive parse
reads as data, reporting frozen counters that are in fact climbing. And grepping
execution logs for the word witness is worthless here — it returned a higher
count on the broken run than on the healthy one, because the relevant logger runs
on a two-minute interval.

The milestone's upstream-change triage comes back empty. Each class that would
need wiring on this side was checked directly rather than by reading commit
subjects: the only new command-line flag and the only new config field both
belong to the declined sync protocol and are absent from the tree entirely, no
precompile registration moved, the generated command-line documentation
regenerates byte-identically, and the generated config marshaller needs no
regeneration because its input never changed.
Operator decision, taken after the final batch and the chores commit were already
made. The constants now match upstream's own v1.17.4 release commit exactly; the
only remaining difference from that file is a locally added comment line.

The merge-time resolution had kept the previous values, and that was the right
call as a *merge* decision — a release tag's bump has been declined at every
milestone of this sync, and the evidence for it was re-gathered rather than
trusted. What it deliberately left open was the release question the merge could
not answer: what this file should read once the tree's base genuinely is this
release. It now does, so that question was put and answered. The earlier
reasoning is kept in the ledger rather than rewritten, because the sequence is
the useful record — the merge declined the bump, the release question was raised
explicitly, and the release answer was to take it.

Two consequences are recorded in the ledger alongside it.

The commit messages of the final batch and of the chores commit both say the
version file is deliberately not bumped, and both are now out of date. Neither
can be amended: rewriting either hash would break every pull request stacked
above this branch, which is a far worse outcome than a stale sentence. The ledger
entry is authoritative where they disagree.

The change is safe but not entirely invisible, and the one place it surfaces is
worth naming rather than discovering later. The default miner extra-data packs
the major, minor and patch numbers into a single word, so that word moves with
the patch bump, and extra-data lands in the block header. It only ever appears as
content of the fixed-length vanity prefix, though, because this fork's consensus
engine truncates the header's extra field to exactly the vanity length and then
appends the validator bytes and seal itself. Vanity content is producer-chosen
and constrained only by length, no validity rule reads it, and every real
deployment configures its own. So the effect is cosmetic, and on a
default-configured node it is an improvement: the advertised base version becomes
the true one.

Verified: the tree builds, formatting is clean, and the packages whose tests
touch the version string or extra-data all pass. Nothing in the tree embeds the
previous string, and the console tests derive their expectation from these same
constants.
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 54.29023% with 1156 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.96%. Comparing base (0f70159) to head (f399939).

Files with missing lines Patch % Lines
core/vm/evm.go 21.76% 102 Missing and 13 partials ⚠️
core/vm/gas_table.go 7.82% 102 Missing and 4 partials ⚠️
core/txpool/blobpool/blobpool.go 61.45% 57 Missing and 17 partials ⚠️
core/state_transition.go 66.66% 48 Missing and 19 partials ⚠️
core/state/statedb.go 22.95% 43 Missing and 4 partials ⚠️
accounts/usbwallet/ledger.go 0.00% 41 Missing ⚠️
beacon/engine/bapl_encode.go 0.00% 40 Missing ⚠️
core/vm/gascosts.go 64.07% 31 Missing and 6 partials ⚠️
core/rawdb/freezer.go 65.00% 24 Missing and 11 partials ⚠️
core/rawdb/accessors_snapshot.go 0.00% 31 Missing ⚠️
... and 75 more

❌ Your patch check has failed because the patch coverage (54.29%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@                        Coverage Diff                        @@
##           ppatil-upstream-v1.17.3-part3    #2346      +/-   ##
=================================================================
+ Coverage                          54.38%   54.96%   +0.57%     
=================================================================
  Files                                932      930       -2     
  Lines                             167713   168109     +396     
=================================================================
+ Hits                               91211    92393    +1182     
+ Misses                             70724    69808     -916     
- Partials                            5778     5908     +130     
Files with missing lines Coverage Δ
accounts/abi/pack.go 80.76% <100.00%> (ø)
accounts/abi/reflect.go 89.93% <100.00%> (ø)
accounts/abi/unpack.go 80.41% <100.00%> (ø)
beacon/engine/ed_codec.go 59.85% <ø> (ø)
beacon/engine/epe_decode.go 75.00% <ø> (ø)
beacon/engine/pa_codec.go 0.00% <ø> (ø)
consensus/bor/statefull/processor.go 78.62% <100.00%> (ø)
core/evm.go 76.08% <100.00%> (+2.36%) ⬆️
core/filtermaps/matcher.go 83.09% <ø> (ø)
core/rawdb/accessors_indexes.go 17.55% <ø> (ø)
... and 132 more

... and 27 files with indirect coverage changes

Files with missing lines Coverage Δ
accounts/abi/pack.go 80.76% <100.00%> (ø)
accounts/abi/reflect.go 89.93% <100.00%> (ø)
accounts/abi/unpack.go 80.41% <100.00%> (ø)
beacon/engine/ed_codec.go 59.85% <ø> (ø)
beacon/engine/epe_decode.go 75.00% <ø> (ø)
beacon/engine/pa_codec.go 0.00% <ø> (ø)
consensus/bor/statefull/processor.go 78.62% <100.00%> (ø)
core/evm.go 76.08% <100.00%> (+2.36%) ⬆️
core/filtermaps/matcher.go 83.09% <ø> (ø)
core/rawdb/accessors_indexes.go 17.55% <ø> (ø)
... and 132 more

... and 27 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Final hop of the develop-drift cascade, bringing develop's #2347 (Kurtosis
e2e and stateless-e2e on every PR base) and #2333 (complete witnesses under
BlockSTM v2) to the top of the stack.

Clean at this hop; inherits the three witness resolutions made below, at
ppatil-upstream-v1.17.2, ppatil-upstream-mptubt and
ppatil-upstream-v1.17.3-part3.

The cascade shape matters and is worth recording: develop was merged once
into the base branch as a fast-forward, then carried hop by hop up the
chain. Merging develop independently into each of the eleven branches would
have paid the same resolutions eleven times and put develop content into
the upper diffs that is absent from their bases.

Cross-check on the result: every witness-relevant file here is byte
identical to a throwaway worktree in which develop was merged directly into
this branch in one step and verified before the cascade began — statedb.go,
reader.go, reader_witness.go, parallel_state_processor.go,
v2_method_parity_test.go and the three witness test files. Eleven hops and
one merge converge on the same content.

Verified: build clean; vet clean apart from the pre-existing
parallel_state_processor.go:341 lock-copy finding; both regeneration tests
pass with 241/241 real mainnet blocks round-tripped and no skips; core/state,
core/stateless and eth/protocols/wit pass in full.
Ledger section for the twelve-hop cascade that carried develop's #2333
(complete witnesses under BlockSTM v2) and #2347 (Kurtosis e2e and
stateless-e2e on every PR base) up the stack: why it could not wait, why the
merges cascade instead of fanning out, the single conflict and the reasoning
that settled it, the three breaks git reported as clean, and the
verification at each adaptation hop.

Two things in it are worth more than the narrative. The first is the
argument for dropping develop's `witnessStats` field rather than keeping it:
upstream #34106 relocated witness statistics into `stateless.Witness`, so
the field has no writer and no reader here, while the metric itself survives
with identical attribution and wider coverage. The consequence — counters
reading higher than on develop because prewalk-collected nodes are now
counted — is recorded so nobody has to rediscover it from a dashboard. The
second is the cross-check: every witness-relevant file in the finished
cascade is byte-identical to a throwaway worktree where develop was merged
directly into the top branch in one step, which is the evidence that no
hop's resolution drifted. That check is cheap and worth repeating on any
future cascade.

The needs-wiring row is a genuine gap rather than a note. `ubtTrieReader`
has no `CollectStateWitness` and appears in neither `findTrieReader`'s type
switch nor `collectStateWitnessFromReader`'s, so a UBT-backed reader chain
collects no witness at all. It predates this cascade and is dormant while
UBT is off, but it fails silently rather than loudly, and the existing
prewalk and read-set tests cannot see it because every case there builds an
MPT reader. It needs settling before UBT is enabled, not during.
Ancestry only. The newTrieReader point-cache fix this carries was
already present here, so the merge records the relationship without
changing a byte.

That is the reason it exists. Without it this branch would not contain
its predecessor, and a stacked pull request whose head does not contain
its base misreports its own diff and turns an eventual merge into an
argument.

Deliberately not re-verified, because there is nothing new to verify:
the merge result's tree is identical to this branch's previous tree,
which is the tree that already passed build, full-tree vet, #2333's
prewalk and read-set tests, and CI. A merge with no tree delta cannot
break what that tree established.
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.