trie/bintrie: record inserted leaves for t8n (#34843)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

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 <fselmo2@gmail.com>
This commit is contained in:
Guillaume Ballet 2026-05-28 17:06:47 +02:00 committed by GitHub
parent 95320ffe69
commit 61342e9c01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 556 additions and 10 deletions

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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: &params.BlobScheduleConfig{
Cancun: params.DefaultCancunBlobConfig,
Prague: params.DefaultPragueBlobConfig,
Osaka: params.DefaultOsakaBlobConfig,
},
},
}
var bpo1BlobConfig = &params.BlobConfig{

126
trie/bintrie/recorder.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -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
}