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 }