From 61342e9c01b90d8ccb3c51af78f2c8e9c0b7dd38 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 28 May 2026 17:06:47 +0200 Subject: [PATCH] trie/bintrie: record inserted leaves for t8n (#34843) Because the UBT doesn't differentiate slots from accounts, the content of the tree can not be exported as a `GenesisAlloc`, which means that `evm t8n` can not intergrate it. We have tried integrating the new format into execution-specs, but this is very hard to maintain because the team doesn't see it as a priority and their own repository is seeing a lot of churn. This PR adds the ability to capture the structure of what is being inserted in the tree, so that the information isn't lost and it can be dumped in the t8n context. --------- Co-authored-by: felipe --- cmd/evm/internal/t8ntool/execution.go | 22 +- cmd/evm/internal/t8ntool/transition.go | 48 ++++- core/state/database_ubt.go | 29 ++- tests/init.go | 28 +++ trie/bintrie/recorder.go | 126 +++++++++++ trie/bintrie/recorder_test.go | 277 +++++++++++++++++++++++++ trie/bintrie/trie.go | 36 +++- 7 files changed, 556 insertions(+), 10 deletions(-) create mode 100644 trie/bintrie/recorder.go create mode 100644 trie/bintrie/recorder_test.go 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 }