diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index 39dfbf772b..85a581eba6 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -418,9 +418,24 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
return statedb, execRs, body, nil
}
+// newPrestateTrieDBConfig returns the triedb config used to construct the
+// prestate. UBT mode requires the path-based backend; the legacy hash-based
+// backend cannot decode UBT-encoded nodes.
+func newPrestateTrieDBConfig(isBintrie bool) *triedb.Config {
+ if isBintrie {
+ cfg := *triedb.UBTDefaults
+ cfg.Preimages = true
+ return &cfg
+ }
+ return &triedb.Config{Preimages: true}
+}
+
func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, isBintrie bool) *state.StateDB {
- tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsUBT: isBintrie})
+ tdb := triedb.NewDatabase(db, newPrestateTrieDBConfig(isBintrie))
sdb := state.NewDatabase(tdb, nil)
+ if isBintrie {
+ sdb.(*state.UBTDatabase).EnableAllocRecording()
+ }
root := types.EmptyRootHash
if isBintrie {
@@ -458,8 +473,11 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, isBintrie bool
// MakePreStateStreaming is like MakePreState, but decodes the alloc from disk
// one account at a time so the full map is never held in memory.
func MakePreStateStreaming(db ethdb.Database, allocPath string, isBintrie bool) (*state.StateDB, error) {
- tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsUBT: isBintrie})
+ tdb := triedb.NewDatabase(db, newPrestateTrieDBConfig(isBintrie))
sdb := state.NewDatabase(tdb, nil)
+ if isBintrie {
+ sdb.(*state.UBTDatabase).EnableAllocRecording()
+ }
root := types.EmptyRootHash
if isBintrie {
diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go
index 89b703d3b8..6c6667e409 100644
--- a/cmd/evm/internal/t8ntool/transition.go
+++ b/cmd/evm/internal/t8ntool/transition.go
@@ -30,6 +30,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
"github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/overlay"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
@@ -243,14 +244,55 @@ func Transition(ctx *cli.Context) error {
collector = make(Alloc)
s.DumpToCollector(collector, nil)
default:
- btleaves = make(map[common.Hash]hexutil.Bytes)
- if err := s.DumpBinTrieLeaves(btleaves); err != nil {
- return err
+ udb, ok := s.Database().(*state.UBTDatabase)
+ if !ok {
+ return NewError(ErrorEVM, errors.New("expected UBTDatabase in binary trie mode"))
+ }
+ rec := udb.AllocRecorder()
+ if rec == nil {
+ return NewError(ErrorEVM, errors.New("UBT alloc recorder was not enabled"))
+ }
+ collector = Alloc(rec.Alloc())
+ if err := mergeUnmigratedBaseAlloc(udb, s.IntermediateRoot(false), collector); err != nil {
+ return NewError(ErrorEVM, fmt.Errorf("failed to merge base MPT alloc: %v", err))
}
}
return dispatchOutput(ctx, baseDir, result, collector, allocOutput, body, btleaves)
}
+func mergeUnmigratedBaseAlloc(udb *state.UBTDatabase, currentRoot common.Hash, dst Alloc) error {
+ ts := overlay.LoadTransitionState(udb.TrieDB().Disk(), currentRoot, true)
+ if !ts.InTransition() {
+ return nil
+ }
+ if ts.BaseRoot == (common.Hash{}) || ts.BaseRoot == types.EmptyRootHash {
+ return nil
+ }
+ mptDB := state.NewMPTDatabase(udb.TrieDB(), nil)
+ sdb, err := state.New(ts.BaseRoot, mptDB)
+ if err != nil {
+ return fmt.Errorf("open base MPT at %x: %w", ts.BaseRoot, err)
+ }
+ if _, err := sdb.DumpToCollector(mergeAlloc(dst), nil); err != nil {
+ return fmt.Errorf("walk base MPT at %x: %w", ts.BaseRoot, err)
+ }
+ return nil
+}
+
+type mergeAlloc Alloc
+
+func (m mergeAlloc) OnRoot(common.Hash) {}
+
+func (m mergeAlloc) OnAccount(addr *common.Address, da state.DumpAccount) {
+ if addr == nil {
+ return
+ }
+ if _, exists := m[*addr]; exists {
+ return
+ }
+ m[*addr] = dumpAccountToTypesAccount(da)
+}
+
// writeStreamedAlloc writes the post-state alloc to path one account at a
// time, producing the same JSON shape as saveFile on an Alloc map.
func writeStreamedAlloc(path string, s *state.StateDB) error {
diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go
index 16579f6d6a..d9b2f07a77 100644
--- a/core/state/database_ubt.go
+++ b/core/state/database_ubt.go
@@ -27,10 +27,26 @@ import (
// It provides the same functionality as MPTDatabase but uses unified binary
// trie for state hashing instead of Merkle Patricia Tries.
type UBTDatabase struct {
- triedb *triedb.Database
- codedb *CodeDB
+ triedb *triedb.Database
+ codedb *CodeDB
+ recorder *bintrie.Recorder
}
+// EnableAllocRecording installs an alloc recorder shared across every binary
+// trie opened from this database. The recorder captures account, storage, and
+// code writes keyed by their original (unhashed) addresses, which is required
+// for tooling like evm t8n to render the post-state as a types.GenesisAlloc.
+func (db *UBTDatabase) EnableAllocRecording() *bintrie.Recorder {
+ if db.recorder == nil {
+ db.recorder = bintrie.NewRecorder()
+ }
+ return db.recorder
+}
+
+// AllocRecorder returns the attached recorder, or nil if recording was never
+// enabled on this database.
+func (db *UBTDatabase) AllocRecorder() *bintrie.Recorder { return db.recorder }
+
// Type returns Binary, indicating this database is backed by a Universal Binary Trie.
func (db *UBTDatabase) Type() DatabaseType { return TypeUBT }
@@ -96,7 +112,14 @@ func (db *UBTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Rea
// OpenTrie opens the main account trie at a specific root hash.
func (db *UBTDatabase) OpenTrie(root common.Hash) (Trie, error) {
- return bintrie.NewBinaryTrie(root, db.triedb, db.triedb.BinTrieGroupDepth())
+ tr, err := bintrie.NewBinaryTrie(root, db.triedb, db.triedb.BinTrieGroupDepth())
+ if err != nil {
+ return nil, err
+ }
+ if db.recorder != nil {
+ tr.SetRecorder(db.recorder)
+ }
+ return tr, nil
}
// OpenStorageTrie opens the storage trie of an account. In binary trie mode,
diff --git a/tests/init.go b/tests/init.go
index 3db988a993..2550eb1231 100644
--- a/tests/init.go
+++ b/tests/init.go
@@ -776,6 +776,34 @@ var Forks = map[string]*params.ChainConfig{
ShanghaiTime: u64(0),
UBTTime: u64(0),
},
+ "Binary": {
+ ChainID: big.NewInt(1),
+ HomesteadBlock: big.NewInt(0),
+ EIP150Block: big.NewInt(0),
+ EIP155Block: big.NewInt(0),
+ EIP158Block: big.NewInt(0),
+ ByzantiumBlock: big.NewInt(0),
+ ConstantinopleBlock: big.NewInt(0),
+ PetersburgBlock: big.NewInt(0),
+ IstanbulBlock: big.NewInt(0),
+ MuirGlacierBlock: big.NewInt(0),
+ BerlinBlock: big.NewInt(0),
+ LondonBlock: big.NewInt(0),
+ ArrowGlacierBlock: big.NewInt(0),
+ MergeNetsplitBlock: big.NewInt(0),
+ TerminalTotalDifficulty: big.NewInt(0),
+ ShanghaiTime: u64(0),
+ CancunTime: u64(0),
+ PragueTime: u64(0),
+ OsakaTime: u64(0),
+ UBTTime: u64(0),
+ DepositContractAddress: params.MainnetChainConfig.DepositContractAddress,
+ BlobScheduleConfig: ¶ms.BlobScheduleConfig{
+ Cancun: params.DefaultCancunBlobConfig,
+ Prague: params.DefaultPragueBlobConfig,
+ Osaka: params.DefaultOsakaBlobConfig,
+ },
+ },
}
var bpo1BlobConfig = ¶ms.BlobConfig{
diff --git a/trie/bintrie/recorder.go b/trie/bintrie/recorder.go
new file mode 100644
index 0000000000..e6c757b106
--- /dev/null
+++ b/trie/bintrie/recorder.go
@@ -0,0 +1,126 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+)
+
+// Recorder maintains the inverse of the binary-trie key transform: it captures
+// every mutation applied to a BinaryTrie keyed by the original address (and,
+// for storage, the original slot key) so the post-state can be rendered as a
+// types.GenesisAlloc.
+type Recorder struct {
+ accounts map[common.Address]*types.Account
+}
+
+// NewRecorder returns an empty Recorder.
+func NewRecorder() *Recorder {
+ return &Recorder{accounts: make(map[common.Address]*types.Account)}
+}
+
+// entry returns the existing account entry, or creates a fresh one.
+func (r *Recorder) entry(addr common.Address) *types.Account {
+ if acc, ok := r.accounts[addr]; ok {
+ return acc
+ }
+ acc := &types.Account{}
+ r.accounts[addr] = acc
+ return acc
+}
+
+// RecordAccount upserts the nonce and balance for addr. Existing storage and
+// code on the entry are preserved.
+func (r *Recorder) RecordAccount(addr common.Address, acc *types.StateAccount) {
+ e := r.entry(addr)
+ e.Nonce = acc.Nonce
+ if acc.Balance != nil {
+ e.Balance = acc.Balance.ToBig()
+ } else {
+ e.Balance = nil
+ }
+}
+
+// RecordStorage records a storage write. A zero value removes the slot.
+func (r *Recorder) RecordStorage(addr common.Address, key, value []byte) {
+ k := bytesToHash(key)
+ v := bytesToHash(value)
+ e := r.entry(addr)
+ if (v == common.Hash{}) {
+ if e.Storage != nil {
+ delete(e.Storage, k)
+ if len(e.Storage) == 0 {
+ e.Storage = nil
+ }
+ }
+ return
+ }
+ if e.Storage == nil {
+ e.Storage = make(map[common.Hash]common.Hash)
+ }
+ e.Storage[k] = v
+}
+
+// RecordCode records the contract code for addr. Empty code clears the field.
+func (r *Recorder) RecordCode(addr common.Address, code []byte) {
+ e := r.entry(addr)
+ if len(code) == 0 {
+ e.Code = nil
+ return
+ }
+ e.Code = common.CopyBytes(code)
+}
+
+// RecordDeleteAccount drops addr entirely from the recorded set.
+func (r *Recorder) RecordDeleteAccount(addr common.Address) {
+ delete(r.accounts, addr)
+}
+
+// RecordDeleteStorage clears a single storage slot for addr.
+func (r *Recorder) RecordDeleteStorage(addr common.Address, key []byte) {
+ r.RecordStorage(addr, key, nil)
+}
+
+// Alloc returns the recorded post-state as a types.GenesisAlloc. The returned
+// map shares storage with the recorder; callers must not mutate it concurrently
+// with further Record calls.
+func (r *Recorder) Alloc() types.GenesisAlloc {
+ out := make(types.GenesisAlloc, len(r.accounts))
+ for addr, a := range r.accounts {
+ out[addr] = *a
+ }
+ return out
+}
+
+// Has reports whether addr has been recorded.
+func (r *Recorder) Has(addr common.Address) bool {
+ _, ok := r.accounts[addr]
+ return ok
+}
+
+// bytesToHash left-pads short slices into a common.Hash, matching the
+// normalization performed by BinaryTrie.UpdateStorage on values.
+func bytesToHash(b []byte) common.Hash {
+ var h common.Hash
+ if len(b) >= common.HashLength {
+ copy(h[:], b[:common.HashLength])
+ } else {
+ copy(h[common.HashLength-len(b):], b)
+ }
+ return h
+}
diff --git a/trie/bintrie/recorder_test.go b/trie/bintrie/recorder_test.go
new file mode 100644
index 0000000000..57a7232dfd
--- /dev/null
+++ b/trie/bintrie/recorder_test.go
@@ -0,0 +1,277 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/trie"
+ "github.com/holiman/uint256"
+)
+
+func newRecorderTestTrie() *BinaryTrie {
+ return &BinaryTrie{
+ store: newNodeStore(),
+ tracer: trie.NewPrevalueTracer(),
+ }
+}
+
+// TestRecorderCapturesAccountWrite verifies the recorder mirrors a single
+// UpdateAccount call into the resulting GenesisAlloc.
+func TestRecorderCapturesAccountWrite(t *testing.T) {
+ tr := newRecorderTestTrie()
+ rec := NewRecorder()
+ tr.SetRecorder(rec)
+
+ addr := common.HexToAddress("0x1111111111111111111111111111111111111111")
+ acc := &types.StateAccount{
+ Nonce: 7,
+ Balance: uint256.NewInt(42),
+ CodeHash: common.HexToHash("aa").Bytes(),
+ }
+ if err := tr.UpdateAccount(addr, acc, 0); err != nil {
+ t.Fatalf("UpdateAccount: %v", err)
+ }
+
+ alloc := rec.Alloc()
+ got, ok := alloc[addr]
+ if !ok {
+ t.Fatalf("address %x missing from alloc", addr)
+ }
+ if got.Nonce != 7 {
+ t.Errorf("nonce: got %d want 7", got.Nonce)
+ }
+ if got.Balance == nil || got.Balance.Uint64() != 42 {
+ t.Errorf("balance: got %v want 42", got.Balance)
+ }
+}
+
+// TestRecorderStorageRoundTrip verifies that storage writes are recorded with
+// the original (unhashed) slot keys.
+func TestRecorderStorageRoundTrip(t *testing.T) {
+ tr := newRecorderTestTrie()
+ rec := NewRecorder()
+ tr.SetRecorder(rec)
+
+ addr := common.HexToAddress("0x2222222222222222222222222222222222222222")
+ acc := &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}
+ if err := tr.UpdateAccount(addr, acc, 0); err != nil {
+ t.Fatalf("UpdateAccount: %v", err)
+ }
+
+ slot := common.HexToHash("00000000000000000000000000000000000000000000000000000000000000ff")
+ value := common.HexToHash("00000000000000000000000000000000000000000000000000000000deadbeef")
+ if err := tr.UpdateStorage(addr, slot[:], value[:]); err != nil {
+ t.Fatalf("UpdateStorage: %v", err)
+ }
+
+ alloc := rec.Alloc()
+ got := alloc[addr]
+ if got.Storage == nil {
+ t.Fatalf("storage map nil")
+ }
+ if got.Storage[slot] != value {
+ t.Errorf("storage[%x] = %x, want %x", slot, got.Storage[slot], value)
+ }
+}
+
+// TestRecorderDeleteStorage verifies that writing a zero value (or calling
+// DeleteStorage) removes the slot from the recorded set, matching MPT-dump
+// semantics.
+func TestRecorderDeleteStorage(t *testing.T) {
+ tr := newRecorderTestTrie()
+ rec := NewRecorder()
+ tr.SetRecorder(rec)
+
+ addr := common.HexToAddress("0x3333333333333333333333333333333333333333")
+ if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil {
+ t.Fatalf("UpdateAccount: %v", err)
+ }
+
+ slotKept := common.HexToHash("00000000000000000000000000000000000000000000000000000000000000ff")
+ slotGone := common.HexToHash("0100000000000000000000000000000000000000000000000000000000000001")
+ if err := tr.UpdateStorage(addr, slotKept[:], common.HexToHash("01").Bytes()); err != nil {
+ t.Fatalf("UpdateStorage(kept): %v", err)
+ }
+ if err := tr.UpdateStorage(addr, slotGone[:], common.HexToHash("02").Bytes()); err != nil {
+ t.Fatalf("UpdateStorage(gone): %v", err)
+ }
+ if err := tr.DeleteStorage(addr, slotGone[:]); err != nil {
+ t.Fatalf("DeleteStorage: %v", err)
+ }
+
+ alloc := rec.Alloc()
+ if _, exists := alloc[addr].Storage[slotGone]; exists {
+ t.Errorf("deleted slot still present")
+ }
+ if _, exists := alloc[addr].Storage[slotKept]; !exists {
+ t.Errorf("retained slot missing")
+ }
+}
+
+// TestRecorderDeleteAccount verifies an account removed via DeleteAccount
+// disappears from the alloc entirely, including its storage.
+func TestRecorderDeleteAccount(t *testing.T) {
+ tr := newRecorderTestTrie()
+ rec := NewRecorder()
+ tr.SetRecorder(rec)
+
+ addrKept := common.HexToAddress("0x4444444444444444444444444444444444444444")
+ addrGone := common.HexToAddress("0x5555555555555555555555555555555555555555")
+ for _, a := range []common.Address{addrKept, addrGone} {
+ if err := tr.UpdateAccount(a, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil {
+ t.Fatalf("UpdateAccount(%x): %v", a, err)
+ }
+ }
+ slot := common.HexToHash("0100000000000000000000000000000000000000000000000000000000000001")
+ if err := tr.UpdateStorage(addrGone, slot[:], common.HexToHash("0a").Bytes()); err != nil {
+ t.Fatalf("UpdateStorage: %v", err)
+ }
+ if err := tr.DeleteAccount(addrGone); err != nil {
+ t.Fatalf("DeleteAccount: %v", err)
+ }
+
+ alloc := rec.Alloc()
+ if _, exists := alloc[addrGone]; exists {
+ t.Errorf("deleted account still present in alloc")
+ }
+ if _, exists := alloc[addrKept]; !exists {
+ t.Errorf("untouched account missing from alloc")
+ }
+}
+
+// TestRecorderDeleteThenRecreate verifies that recreating an account after a
+// delete starts from a fresh entry — old storage and code do not bleed into
+// the new account.
+func TestRecorderDeleteThenRecreate(t *testing.T) {
+ tr := newRecorderTestTrie()
+ rec := NewRecorder()
+ tr.SetRecorder(rec)
+
+ addr := common.HexToAddress("0x6666666666666666666666666666666666666666")
+ slot := common.HexToHash("0100000000000000000000000000000000000000000000000000000000000001")
+
+ if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(100)}, 0); err != nil {
+ t.Fatalf("UpdateAccount #1: %v", err)
+ }
+ if err := tr.UpdateStorage(addr, slot[:], common.HexToHash("0a").Bytes()); err != nil {
+ t.Fatalf("UpdateStorage: %v", err)
+ }
+ if err := tr.UpdateContractCode(addr, common.Hash{}, []byte{0x60, 0x00}); err != nil {
+ t.Fatalf("UpdateContractCode: %v", err)
+ }
+ if err := tr.DeleteAccount(addr); err != nil {
+ t.Fatalf("DeleteAccount: %v", err)
+ }
+ if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 7, Balance: uint256.NewInt(9999)}, 0); err != nil {
+ t.Fatalf("UpdateAccount #2: %v", err)
+ }
+
+ alloc := rec.Alloc()
+ got := alloc[addr]
+ if got.Nonce != 7 {
+ t.Errorf("nonce after recreate: got %d want 7", got.Nonce)
+ }
+ if got.Balance == nil || got.Balance.Uint64() != 9999 {
+ t.Errorf("balance after recreate: got %v want 9999", got.Balance)
+ }
+ if len(got.Storage) != 0 {
+ t.Errorf("recreated account has stale storage: %v", got.Storage)
+ }
+ if len(got.Code) != 0 {
+ t.Errorf("recreated account has stale code: %x", got.Code)
+ }
+}
+
+// TestRecorderCodeOverwrite verifies that a second UpdateContractCode call
+// replaces the previously-recorded code.
+func TestRecorderCodeOverwrite(t *testing.T) {
+ tr := newRecorderTestTrie()
+ rec := NewRecorder()
+ tr.SetRecorder(rec)
+
+ addr := common.HexToAddress("0x7777777777777777777777777777777777777777")
+ if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil {
+ t.Fatalf("UpdateAccount: %v", err)
+ }
+ first := []byte{0x60, 0x01}
+ second := []byte{0x60, 0x02, 0x60, 0x03}
+ if err := tr.UpdateContractCode(addr, common.Hash{}, first); err != nil {
+ t.Fatalf("UpdateContractCode #1: %v", err)
+ }
+ if err := tr.UpdateContractCode(addr, common.Hash{}, second); err != nil {
+ t.Fatalf("UpdateContractCode #2: %v", err)
+ }
+
+ alloc := rec.Alloc()
+ if !bytes.Equal(alloc[addr].Code, second) {
+ t.Errorf("code: got %x want %x", alloc[addr].Code, second)
+ }
+}
+
+// TestRecorderPartialUpdatePreservesStorage verifies that a nonce/balance
+// update on an account does not wipe its previously-recorded storage or code.
+func TestRecorderPartialUpdatePreservesStorage(t *testing.T) {
+ tr := newRecorderTestTrie()
+ rec := NewRecorder()
+ tr.SetRecorder(rec)
+
+ addr := common.HexToAddress("0x8888888888888888888888888888888888888888")
+ if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil {
+ t.Fatalf("UpdateAccount: %v", err)
+ }
+ slot := common.HexToHash("0100000000000000000000000000000000000000000000000000000000000001")
+ if err := tr.UpdateStorage(addr, slot[:], common.HexToHash("0a").Bytes()); err != nil {
+ t.Fatalf("UpdateStorage: %v", err)
+ }
+ code := []byte{0x60, 0x05}
+ if err := tr.UpdateContractCode(addr, common.Hash{}, code); err != nil {
+ t.Fatalf("UpdateContractCode: %v", err)
+ }
+ // Bump nonce only; storage and code should survive.
+ if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 2, Balance: uint256.NewInt(1)}, len(code)); err != nil {
+ t.Fatalf("UpdateAccount #2: %v", err)
+ }
+
+ alloc := rec.Alloc()
+ got := alloc[addr]
+ if got.Nonce != 2 {
+ t.Errorf("nonce: got %d want 2", got.Nonce)
+ }
+ if got.Storage[slot] == (common.Hash{}) {
+ t.Errorf("storage was cleared by partial update")
+ }
+ if !bytes.Equal(got.Code, code) {
+ t.Errorf("code was cleared by partial update")
+ }
+}
+
+// TestRecorderDisabledByDefault confirms that without SetRecorder the trie
+// performs no recording (sanity check that hooks are gated).
+func TestRecorderDisabledByDefault(t *testing.T) {
+ tr := newRecorderTestTrie()
+ if tr.Recorder() != nil {
+ t.Fatal("Recorder() should be nil before SetRecorder")
+ }
+ addr := common.HexToAddress("0x9999999999999999999999999999999999999999")
+ if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil {
+ t.Fatalf("UpdateAccount: %v", err)
+ }
+}
diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go
index e3436e3df1..0d0c0e0e70 100644
--- a/trie/bintrie/trie.go
+++ b/trie/bintrie/trie.go
@@ -111,12 +111,22 @@ type BinaryTrie struct {
reader *trie.Reader
tracer *trie.PrevalueTracer
groupDepth int // Number of levels per serialized group (1-8, default 8)
+ recorder *Recorder
}
func (t *BinaryTrie) GroupDepth() int {
return t.groupDepth
}
+// SetRecorder attaches an alloc recorder to the trie. Subsequent mutating
+// operations will report the original (unhashed) account, storage, and code
+// writes to the recorder so the post-state can be exported as a GenesisAlloc.
+// Pass nil to detach.
+func (t *BinaryTrie) SetRecorder(r *Recorder) { t.recorder = r }
+
+// Recorder returns the currently attached alloc recorder, or nil.
+func (t *BinaryTrie) Recorder() *Recorder { return t.recorder }
+
// ToDot converts the binary trie to a DOT language representation. Useful for debugging.
func (t *BinaryTrie) ToDot() string {
t.store.computeHash(t.store.root)
@@ -255,7 +265,13 @@ func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount,
values[BasicDataLeafKey] = basicData[:]
values[CodeHashLeafKey] = acc.CodeHash[:]
- return t.store.InsertValuesAtStem(stem, values, t.nodeResolver)
+ if err := t.store.InsertValuesAtStem(stem, values, t.nodeResolver); err != nil {
+ return err
+ }
+ if t.recorder != nil {
+ t.recorder.RecordAccount(addr, acc)
+ }
+ return nil
}
// UpdateStem updates the values for the given stem key.
@@ -279,6 +295,9 @@ func (t *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) er
if err != nil {
return fmt.Errorf("UpdateStorage (%x) error: %v", address, err)
}
+ if t.recorder != nil {
+ t.recorder.RecordStorage(address, key, value)
+ }
return nil
}
@@ -293,7 +312,13 @@ func (t *BinaryTrie) DeleteAccount(addr common.Address) error {
values[BasicDataLeafKey] = zero[:]
values[CodeHashLeafKey] = zero[:]
- return t.store.InsertValuesAtStem(stem, values, t.nodeResolver)
+ if err := t.store.InsertValuesAtStem(stem, values, t.nodeResolver); err != nil {
+ return err
+ }
+ if t.recorder != nil {
+ t.recorder.RecordDeleteAccount(addr)
+ }
+ return nil
}
// DeleteStorage removes any existing value for key from the trie. If a node was not
@@ -305,6 +330,9 @@ func (t *BinaryTrie) DeleteStorage(addr common.Address, key []byte) error {
if err != nil {
return fmt.Errorf("DeleteStorage (%x) error: %v", addr, err)
}
+ if t.recorder != nil {
+ t.recorder.RecordDeleteStorage(addr, key)
+ }
return nil
}
@@ -352,6 +380,7 @@ func (t *BinaryTrie) Copy() *BinaryTrie {
reader: t.reader,
tracer: t.tracer.Copy(),
groupDepth: t.groupDepth,
+ recorder: t.recorder,
}
}
@@ -390,6 +419,9 @@ func (t *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Has
}
}
}
+ if t.recorder != nil {
+ t.recorder.RecordCode(addr, code)
+ }
return nil
}