Skip to content

core/state, trie, triedb: split CachingDB into MPT and UBT databases (v1.17.4 sync, #34700 adoption) - #2343

Draft
pratikspatil024 wants to merge 3 commits into
ppatil-upstream-v1.17.3-part2from
ppatil-upstream-mptubt
Draft

core/state, trie, triedb: split CachingDB into MPT and UBT databases (v1.17.4 sync, #34700 adoption)#2343
pratikspatil024 wants to merge 3 commits into
ppatil-upstream-v1.17.3-part2from
ppatil-upstream-mptubt

Conversation

@pratikspatil024

Copy link
Copy Markdown
Member

Summary

Adopts the type split from go-ethereum ba215fd92 (#34700, cmd, core, trie,
triedb: split CachingDB into merkle + binary dbs
) as a hand-written port rather
than a merge resolution. 33 modified + 2 new files, +137/−299 of code.

This is an out-of-band adoption in the ongoing v1.17.4 sync, stacked between the
v1.17.3 merge PRs — the same pattern as the eth/70 PR (#2341) and the core/vm
catch-up (#2337).

Why it is a separate PR, and why now

#34700 produced 31 of batch 22's 34 conflicts and was deferred out of #2342 so
a hand-written restructure of consensus-critical core/state would not ride
inside a 20-commit merge commit.

The deferral was scheduled, not open-ended: #34763 applies the same MPT/UBT split
to core/state/reader.go in the very next batch
(batch 23), and #34843 follows in
batch 30. Declining the family permanently would fork core/state on the name of
its primary type for the rest of the sync — the compounding-divergence trap already
three deep on the eth-protocol surface.

Upstream frames #34700 as groundwork for #34004, the UBT state transition, which is
not in this sync's range at all — its only mention anywhere in the fetched
upstream history is #34700's own commit message.

The one decision: which half of #34700 to take

#34700 does two separable things. Only the first is adopted.

1. The type split — adopted. CachingDB becomes MPTDatabase + UBTDatabase,
selected by a new DatabaseType (TypeMPT / TypeUBT) with Type() on the
Database interface. IsVerkle becomes IsUBT on the trie interface, on
triedb.Database and in triedb.Config; triedb.VerkleDefaults becomes
UBTDefaults; types.EmptyVerkleHash folds into EmptyBinaryHash, which held the
same value. The core/state call sites stop reaching through to the trie database
and ask the state database its type instead (db.Type().Is(TypeUBT)).

This is where the future-merge value is, and it is the whole reason to do this now.

2. Runtime fork-boundary selection — declined. Upstream also rewrites
BlockChain.StateAt(root) to take a *types.Header, adds
StateAtForkBoundary(parent, header) for the last-MPT/first-UBT boundary, makes
HistoricState refuse UBT, and has ProcessBlock pick the implementation per
block from chainConfig.IsUBT(number, time) behind a local prewarmReader type
assertion. Declined because:

  • it is keyed on the timestamp fork fields Bor deleted in favour of block-based
    ones, so IsUBT(num, time) targets fields Bor does not have;
  • it ripples StateAt's signature through every caller across eth, internal,
    miner and the tracers;
  • it buys nothing while VerkleBlock is nil on every Bor preset.

Also declined: upstream's params/config.go rename (already declined in #2342
Bor has no VerkleTime to rename) and ChainOverrides.OverrideVerkle
OverrideUBT, which would rename the operator-facing --override.verkle CLI flag
and its ethconfig TOML key, and require regenerating gen_config.go, for a
dormant fork.

The rule, so the boundary is not ad hoc

Rename identifiers whose type or value is part of the split — the state
database, the trie interface, the triedb config flag. Keep identifiers that mean
the Verkle hardfork — Bor's fork register, chain presets, CLI flags and
params surface all still call that fork Verkle, and renaming them is a
hardfork-surface and operator-facing change with no conflict payoff, because the
sequels touch core/state, not params.

The visible consequence is a few mixed-vocabulary lines — EnableUBTAtGenesis
returning genesis.Config.EnableVerkleAtGenesis, and tests/block_test_util.go
setting IsUBT: gspec.Config.IsVerkleGenesis(). That is the boundary showing
through, not an oversight.

Where Bor's divergences landed

Every Bor-only member of CachingDB moved onto MPTDatabase, because Bor only
ever runs MPT and UBTDatabase is dormant code Bor never constructs outside
tooling and tests:

  • DisableSnapInReader / EnableSnapInReader and the useSnapInReader guard
  • ReaderTrieOnly (V2 parallel execution)
  • ReadersWithCacheStats and ReadersWithCacheStatsTriple, returning Bor's
    ReaderWithStats rather than upstream's Reader
  • ContractCodeWithPrefix
  • Snapshot(), which Bor carries on the Database interface where upstream does
    not. UBTDatabase implements it as return nil — a unified binary trie has no
    snapshot layer.

Two constructor deviations from upstream, both forced by pre-existing divergence:

  • Bor has no CodeDB. Upstream's constructors take (triedb, codedb); Bor's
    take (triedb, snap) for MPT and (triedb) for UBT, and each owns the inline
    codeCache / codeSizeCache pair that newCachingCodeReader needs.
  • NewDatabaseForTesting returns *MPTDatabase, not upstream's Database,
    because core/state/reader_test.go calls the Bor-only ReadersWithCacheStats.

NewDatabase becomes upstream's deprecated dispatcher, returning Database and
choosing from tdb.IsUBT(). Bor's footprint is 35 files against upstream's 67
because Bor's callers already held state.Database — the concrete type appeared in
exactly one production declaration (BlockChain.statedb), so only three sites
needed the concrete constructor.

Two things verified rather than assumed

The dropped OpenTrie transition-state check is behaviour-preserving on the
dispatcher path.
Bor's CachingDB.OpenTrie consulted the overlay transition
state when the triedb was verkle: panic if InTransition(), BinaryTrie if
Transitioned(), else fall through to a StateTrie. The split drops that.
overlay.LoadTransitionState(db, root, isVerkle) with no stored state returns
&TransitionState{Ended: isVerkle}, so for a UBT triedb Transitioned() was
already true and the old code already returned a BinaryTrie; OpenStorageTrie
likewise returned self, which is what UBTDatabase does.

The one place the narrowing is real is core/blockchain.go, which now builds
NewMPTDatabase unconditionally. That only matters for a hand-written genesis
setting enableVerkleAtGenesis: true — a configuration no Bor preset uses and no
test drives through NewBlockChain — and fixing it properly is the declined
fork-boundary plumbing, so it is recorded in needs-wiring.md rather than papered
over with invented dispatch.

CachingDB.TransitionStatePerRoot was dead. An lru.Cache of
*overlay.TransitionState sized 1000, declared and initialised and never read
the only two references in the tree were its declaration and its initialiser.
Upstream's MPTDatabase has no equivalent. Removed rather than carried into a
newly written file, where it would have pulled the overlay import in for nothing.

Executed tests

Beyond CI's standard gates:

  • go build ./..., go vet ./..., gofmt -l, go mod tidy — clean apart from the
    two pre-existing //nolint copylocks.
  • make lint (golangci-lint v2.11.4, the version CI runs) — 0 issues.
  • tests/bor integration suite — 622.3 s, exit 0.
  • Both fork meta-guards pass (TestReinforceMultiClientPreCompilesTest,
    TestV2ForkParity) — no Rules field name moved, since the params rename is
    declined.
  • go test green on core (171.0 s), eth (46.8 s), miner (199.0 s),
    consensus/bor (41.7 s), triedb/pathdb (44.5 s), core/state,
    core/state/snapshot, core/types, core/types/bal, trie, trie/bintrie,
    trie/trienode, triedb, all eth/... including every tracer package, tests,
    internal/ethapi, all other consensus/..., cmd/utils.
  • Pre-existing failures re-baselined in a detached worktree at e15a2b63b
    rather than assumed, because this change touches both packages — all eight fail
    identically before it: cmd/evm's TestT8n / TestEVMTracing / TestEvmRun /
    TestEvmRunRegEx, and cmd/geth's TestConsoleWelcome / TestCustomGenesis /
    TestCustomBackend / TestExport. TestCustomGenesis was worth confirming
    specifically, since this renames Genesis.IsVerkle and hashAlloc's parameter.

UBTDatabase is exercised, not merely compiled. Two existing tests reach it
through the NewDatabase dispatcher by opening a triedb with triedb.UBTDefaults:
TestVerklePrefetcher (covering OpenTrie's binary branch, OpenStorageTrie
returning self, the prefetcher's isUBT path, and the AccessEvents allocation in
NewWithReader) and TestStateDBCopyBinaryTrie (ported from #34758 in #2342).

Twin scan: ParallelStateDB carries no verkle/UBT branch and does not mirror
IntermediateRoot or handleDestruction, so the five statedb.go conversions to
Type().Is(...) have no V2 counterpart. No Bor-only implementor of
state.Database or state.Trie exists beyond HistoricDB — given Type()
returning TypeMPT with upstream's own TODO — and the three trie types.

Rollout notes

  • Not consensus-affecting. No fork gate was flipped; Amsterdam, Verkle/UBT and
    the binary trie all remain dormant (VerkleBlock nil on every preset), so
    UBTDatabase is never constructed on any Bor network. The core/state
    conversions are equivalence-preserving: Type().Is(TypeUBT) ⟺ the old
    TrieDB().IsVerkle() for every database the dispatcher can produce.
  • No coordinated upgrade required. Backwards-compatible. No database migration.
  • No operator-facing change — the --override.verkle flag and its TOML key are
    deliberately left alone.
  • Deferrals and the wiring a future UBT enable would need are recorded in
    docs/upstream-merges/v1.17.4/needs-wiring.md; the fork decision is in
    fork-register.md; full per-file reasoning in ledger.md. Those doc updates are
    written but ride in the milestone chores commit in v1.17.3 part 3, so this PR
    carries no doc changes.

Stacked PR — do not squash

This is part of a stack. Merge order matters and squash-merging any PR in it
breaks every PR above it
, because squashing rewrites commits into new SHAs and
the PRs above would then re-show all of this PR's changes and conflict against
their base. Team standard for upstream syncs is a merge commit, never squash.

upstream-merge-v1.17.4                       (base)
 └ ppatil-upstream-v1.16.9            #2308
    └ ppatil-upstream-v1.17.0         #2319
       └ ppatil-upstream-v1.17.1      #2325
          └ ppatil-upstream-v1.17.2   #2328
             └ ppatil-corevm-catchup  #2337
                └ ppatil-upstream-v1.17.3       #2340  (v1.17.3 part 1, batch 20)
                   └ ppatil-upstream-eth70      #2341  (eth/70 adoption)
                      └ ppatil-upstream-v1.17.3-part2  #2342  (batches 21-22)
                         └ ppatil-upstream-mptubt      <-- THIS PR (CachingDB split)
                            └ v1.17.3 part 3           (batches 23-26 + chores)
                               └ v1.17.4                (milestone 6/6)

Reviews are deliberately not being requested yet — the sync is mid-flight and
several decisions are still open. This will be marked ready once all six milestones
are complete.

Adopts the type split from geth ba215fd (#34700), deferred out of batch 22
where it produced 31 of the batch's 34 conflicts. Scheduled rather than dropped:
#34763 applies the same split to core/state/reader.go inside batch 23, and #34843
follows in batch 30, so batch 23 should not land against a non-split tree.
Declining the family would fork core/state on the name of its primary type for
the rest of the sync.

CachingDB becomes MPTDatabase and UBTDatabase, selected by DatabaseType, with
Type() on the Database interface. IsVerkle becomes IsUBT on the trie interface,
on triedb.Database and in triedb.Config, VerkleDefaults becomes UBTDefaults, and
the core/state call sites stop reaching through to the trie database: they ask the
state database its type instead. types.EmptyVerkleHash folds into EmptyBinaryHash,
which held the same value.

Every Bor divergence lands on MPTDatabase, because Bor only ever runs MPT and
UBTDatabase is dormant code Bor never constructs outside tooling and tests: the
snap-in-reader guard, ReaderTrieOnly, the two ReadersWithCacheStats variants
returning Bor's ReaderWithStats, ContractCodeWithPrefix, and Snapshot(), which
Bor carries on the Database interface where upstream does not. UBTDatabase
implements Snapshot() as nil since a unified binary trie has no snapshot layer.
Bor has no CodeDB, so the constructors take the snapshot and own the inline code
caches instead, and NewDatabaseForTesting returns the concrete MPT database
because reader_test.go needs a Bor-only method.

Only the type split is taken, not the runtime fork-boundary selection also in
that commit: StateAt growing a header parameter, StateAtForkBoundary, and
ProcessBlock choosing per block from chainConfig.IsUBT(number, time). That half is
keyed on the timestamp fork fields Bor deleted in favour of block-based ones, it
ripples StateAt's signature through every caller across eth, internal, miner and
the tracers, and it buys nothing while VerkleBlock is nil on every preset. The
params rename and the operator-facing --override.verkle flag stay for the same
reason: params is where the fork schedule lives, and the sequels touch core/state.
So the vocabulary is deliberately split — core, trie and triedb say UBT, params
says Verkle — and both halves are recorded in needs-wiring.md.

The dropped transition-state check in OpenTrie is not a behaviour change on the
dispatcher path: overlay.LoadTransitionState returns Ended set to the verkle flag
when nothing is stored, so a UBT trie database already took the binary branch. The
one place it is real, core/blockchain.go now building the MPT database
unconditionally, needs the declined fork-boundary plumbing to fix properly and is
recorded there rather than papered over with invented dispatch.

CachingDB.TransitionStatePerRoot was declared, initialised and never read; the
only two references in the tree were those two lines. Removed rather than carried
into a newly written file.

No fork gate was flipped. Amsterdam, Verkle/UBT and the binary trie remain
dormant.
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.13953% with 90 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.34%. Comparing base (84eee66) to head (49f8c39).

Files with missing lines Patch % Lines
core/state/database_mpt.go 50.47% 48 Missing and 4 partials ⚠️
core/state/database_ubt.go 75.60% 8 Missing and 2 partials ⚠️
core/genesis.go 36.36% 4 Missing and 3 partials ⚠️
triedb/pathdb/database.go 55.55% 1 Missing and 3 partials ⚠️
core/chain_makers.go 0.00% 2 Missing and 1 partial ⚠️
core/state/database_history.go 0.00% 3 Missing ⚠️
triedb/database.go 0.00% 3 Missing ⚠️
tests/block_test_util.go 0.00% 2 Missing ⚠️
core/state/statedb.go 80.00% 0 Missing and 1 partial ⚠️
core/state/trie_prefetcher.go 85.71% 1 Missing ⚠️
... and 4 more

❌ Your patch check has failed because the patch coverage (58.13%) 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-part2    #2343      +/-   ##
=================================================================
+ Coverage                          54.31%   54.34%   +0.02%     
=================================================================
  Files                                925      927       +2     
  Lines                             166726   166765      +39     
=================================================================
+ Hits                               90565    90625      +60     
+ Misses                             70436    70423      -13     
+ Partials                            5725     5717       -8     
Files with missing lines Coverage Δ
core/blockchain.go 62.73% <100.00%> (ø)
core/state/database.go 68.42% <100.00%> (+17.25%) ⬆️
core/state/reader.go 73.94% <100.00%> (ø)
core/state/state_object.go 81.18% <100.00%> (ø)
triedb/pathdb/history.go 65.67% <100.00%> (ø)
triedb/pathdb/lookup.go 91.82% <ø> (ø)
triedb/pathdb/reader.go 21.18% <100.00%> (ø)
core/state/statedb.go 69.93% <80.00%> (ø)
core/state/trie_prefetcher.go 75.32% <85.71%> (ø)
core/state_processor.go 60.97% <0.00%> (ø)
... and 11 more

... and 28 files with indirect coverage changes

Files with missing lines Coverage Δ
core/blockchain.go 62.73% <100.00%> (ø)
core/state/database.go 68.42% <100.00%> (+17.25%) ⬆️
core/state/reader.go 73.94% <100.00%> (ø)
core/state/state_object.go 81.18% <100.00%> (ø)
triedb/pathdb/history.go 65.67% <100.00%> (ø)
triedb/pathdb/lookup.go 91.82% <ø> (ø)
triedb/pathdb/reader.go 21.18% <100.00%> (ø)
core/state/statedb.go 69.93% <80.00%> (ø)
core/state/trie_prefetcher.go 75.32% <85.71%> (ø)
core/state_processor.go 60.97% <0.00%> (ø)
... and 11 more

... and 28 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.

Develop-drift cascade: carries develop's #2347 (Kurtosis e2e and
stateless-e2e on every PR base) and #2333 (complete witnesses under
BlockSTM v2) one hop further up the stack.

No conflict, but one silent break that only the compiler found. This hop
splits CachingDB into MPTDatabase and its UBT counterpart, which moves
ReadersWithCacheStatsTriple off the Database interface and onto
*MPTDatabase. develop's witness regeneration test reaches for it through
state.NewDatabase, which now returns the interface. Switched that one call
to state.NewMPTDatabase.

The substitution is exact rather than a narrowing: CachingDB was the MPT
database, the method moved across with an identical body, and the test
wants an MPT-backed production reader stack. The returned *MPTDatabase
still satisfies Database for the NewWithReader call below it.

Verified: build clean; vet clean apart from the pre-existing
parallel_state_processor.go:341 lock-copy finding; gofmt clean; #2333's
prewalk and read-set tests pass, and both regeneration tests pass with
241/241 real mainnet blocks round-tripped and no skips.
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.

1 participant