From 76925ee64b20db1aec9f258409da8a14e003e327 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Mon, 15 Dec 2025 14:54:17 -0500 Subject: [PATCH 01/18] perf: Share proposal between hash and commit --- graft/coreth/core/blockchain.go | 12 +- graft/coreth/core/genesis_test.go | 4 +- graft/coreth/tests/state_test_util.go | 3 +- graft/evm/firewood/account_trie.go | 72 +- graft/evm/firewood/hash_test.go | 3 +- graft/evm/firewood/recovery.go | 82 +++ graft/evm/firewood/storage_trie.go | 30 +- graft/evm/firewood/triedb.go | 779 +++++++++++----------- graft/subnet-evm/core/blockchain.go | 12 +- graft/subnet-evm/core/genesis_test.go | 4 +- graft/subnet-evm/tests/state_test_util.go | 3 +- 11 files changed, 551 insertions(+), 453 deletions(-) create mode 100644 graft/evm/firewood/recovery.go diff --git a/graft/coreth/core/blockchain.go b/graft/coreth/core/blockchain.go index fddf901fabdf..60dfb74a8bb2 100644 --- a/graft/coreth/core/blockchain.go +++ b/graft/coreth/core/blockchain.go @@ -231,12 +231,12 @@ func (c *CacheConfig) triedbConfig() *triedb.Config { } config.DBOverride = firewood.Config{ - ChainDataDir: c.ChainDataDir, - CleanCacheSize: c.TrieCleanLimit * 1024 * 1024, - FreeListCacheEntries: firewood.Defaults.FreeListCacheEntries, - Revisions: uint(c.StateHistory), // must be at least 2 - ReadCacheStrategy: ffi.CacheAllReads, - ArchiveMode: !c.Pruning, + DatabasePath: c.ChainDataDir, + CacheSizeBytes: uint(c.TrieCleanLimit) * 1024 * 1024, + FreeListCacheEntries: 40_000, // same as default + RevisionsInMemory: uint(c.StateHistory), // must be at least 2 + CacheStrategy: ffi.CacheAllReads, + Archive: !c.Pruning, }.BackendConstructor } return config diff --git a/graft/coreth/core/genesis_test.go b/graft/coreth/core/genesis_test.go index 37e4aaf1e180..c777f80900c9 100644 --- a/graft/coreth/core/genesis_test.go +++ b/graft/coreth/core/genesis_test.go @@ -304,9 +304,7 @@ func newDbConfig(t *testing.T, scheme string) *triedb.Config { case rawdb.PathScheme: return &triedb.Config{DBOverride: pathdb.Defaults.BackendConstructor} case customrawdb.FirewoodScheme: - fwCfg := firewood.Defaults - // Create a unique temporary directory for each test - fwCfg.ChainDataDir = t.TempDir() + fwCfg := firewood.DefaultConfig(t.TempDir()) return &triedb.Config{DBOverride: fwCfg.BackendConstructor} default: t.Fatalf("unknown scheme %s", scheme) diff --git a/graft/coreth/tests/state_test_util.go b/graft/coreth/tests/state_test_util.go index a90044765a22..a4363c560fb1 100644 --- a/graft/coreth/tests/state_test_util.go +++ b/graft/coreth/tests/state_test_util.go @@ -68,8 +68,7 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, snapshotter bo case rawdb.PathScheme: tconf.DBOverride = pathdb.Defaults.BackendConstructor case customrawdb.FirewoodScheme: - cfg := firewood.Defaults - cfg.ChainDataDir = tempdir + cfg := firewood.DefaultConfig(tempdir) tconf.DBOverride = cfg.BackendConstructor default: panic("unknown trie database scheme" + scheme) diff --git a/graft/evm/firewood/account_trie.go b/graft/evm/firewood/account_trie.go index c1952a548cd0..50f9c5fc3f89 100644 --- a/graft/evm/firewood/account_trie.go +++ b/graft/evm/firewood/account_trie.go @@ -1,5 +1,18 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them 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 libevm additions are distributed in the hope that they 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 firewood @@ -20,13 +33,14 @@ import ( var _ state.Trie = (*accountTrie)(nil) -// accountTrie implements state.Trie for managing account states. -// There are a couple caveats to the current implementation: -// 1. `Commit` is not used as expected in the state package. The `StorageTrie` doesn't return -// values, and we thus rely on the `accountTrie`. -// 2. The `Hash` method actually creates the proposal, since Firewood cannot calculate -// the hash of the trie without committing it. It is immediately dropped, and this -// can likely be optimized. +// accountTrie implements [state.Trie] for managing account states. +// Although it fulfills the [state.Trie] interface, it has some important differences: +// 1. [accountTrie.Commit] is not used as expected in the state package. The `StorageTrie` doesn't return +// values, and we thus rely on the `accountTrie`. Additionally, no [trienode.NodeSet] is +// actually constructed, since Firewood manages nodes internally and the list of changes +// is not needed externally. +// 2. The [accountTrie.Hash] method actually creates the [ffi.Proposal], since Firewood cannot calculate +// the hash of the trie without committing it. // // Note this is not concurrent safe. type accountTrie struct { @@ -172,7 +186,7 @@ func (a *accountTrie) DeleteAccount(addr common.Address) error { // Queue the key for deletion a.dirtyKeys[string(key)] = nil a.updateKeys = append(a.updateKeys, key) - a.updateValues = append(a.updateValues, nil) // Nil value indicates deletion + a.updateValues = append(a.updateValues, nil) // Must use nil to indicate deletion a.hasChanges = true // Mark that there are changes to commit return nil } @@ -188,14 +202,16 @@ func (a *accountTrie) DeleteStorage(addr common.Address, key []byte) error { // Queue the key for deletion a.dirtyKeys[string(combinedKey[:])] = nil a.updateKeys = append(a.updateKeys, combinedKey[:]) - a.updateValues = append(a.updateValues, nil) // Nil value indicates deletion + a.updateValues = append(a.updateValues, nil) // Must use nil to indicate deletion a.hasChanges = true // Mark that there are changes to commit return nil } // Hash returns the current hash of the state trie. -// This will create a proposal and drop it, so it is not efficient to call for each transaction. +// This will create the necessary proposals to guarantee that the changes can +// later be committed. All new proposals will be tracked by the [TrieDB]. // If there are no changes since the last call, the cached root is returned. +// On error, the zero hash is returned. func (a *accountTrie) Hash() common.Hash { hash, err := a.hash() if err != nil { @@ -218,51 +234,51 @@ func (a *accountTrie) hash() (common.Hash, error) { return a.root, nil } -// Commit returns the new root hash of the trie and a NodeSet containing all modified accounts and storage slots. -// The format of the NodeSet is different than in go-ethereum's trie implementation due to Firewood's design. -// This boolean is ignored, as it is a relic of the StateTrie implementation. +// Commit returns the new root hash of the trie and an empty [trienode.NodeSet]. +// The boolean input is ignored, as it is a relic of the StateTrie implementation. +// If the changes are not yet already tracked by the [TrieDB], they are created. func (a *accountTrie) Commit(bool) (common.Hash, *trienode.NodeSet, error) { // Get the hash of the trie. + // Ensures all changes are tracked by the Database. hash, err := a.hash() if err != nil { return common.Hash{}, nil, err } - // Create the NodeSet. This will be sent to `triedb.Update` later. - nodeset := trienode.NewNodeSet(common.Hash{}) - for i, key := range a.updateKeys { - nodeset.AddNode(key, &trienode.Node{ - Blob: a.updateValues[i], - }) - } - - return hash, nodeset, nil + set := trienode.NewNodeSet(common.Hash{}) + return hash, set, nil } // UpdateContractCode implements state.Trie. -// Contract code is controlled by rawdb, so we don't need to do anything here. +// Contract code is controlled by `rawdb`, so we don't need to do anything here. +// This always returns nil. func (*accountTrie) UpdateContractCode(common.Address, common.Hash, []byte) error { return nil } // GetKey implements state.Trie. -// This should not be used, since any user should not be accessing by raw key. +// Preimages are not yet supported in Firewood. +// It always returns nil. func (*accountTrie) GetKey([]byte) []byte { return nil } // NodeIterator implements state.Trie. // Firewood does not support iterating over internal nodes. +// This always returns an error. func (*accountTrie) NodeIterator([]byte) (trie.NodeIterator, error) { return nil, errors.New("NodeIterator not implemented for Firewood") } // Prove implements state.Trie. -// Firewood does not yet support providing key proofs. +// Firewood does not support providing key proofs. +// This always returns an error. func (*accountTrie) Prove([]byte, ethdb.KeyValueWriter) error { return errors.New("Prove not implemented for Firewood") } +// Copy creates a deep copy of the [accountTrie]. +// The [database.Reader] is shared, since it is read-only. func (a *accountTrie) Copy() *accountTrie { // Create a new AccountTrie with the same root and reader newTrie := &accountTrie{ @@ -270,7 +286,7 @@ func (a *accountTrie) Copy() *accountTrie { parentRoot: a.parentRoot, root: a.root, reader: a.reader, // Share the same reader - hasChanges: a.hasChanges, + hasChanges: true, // Mark as having changes to ensure re-hashing dirtyKeys: make(map[string][]byte, len(a.dirtyKeys)), updateKeys: make([][]byte, len(a.updateKeys)), updateValues: make([][]byte, len(a.updateValues)), diff --git a/graft/evm/firewood/hash_test.go b/graft/evm/firewood/hash_test.go index b160dbe7e41c..9489b9d6a3fd 100644 --- a/graft/evm/firewood/hash_test.go +++ b/graft/evm/firewood/hash_test.go @@ -77,8 +77,7 @@ func newFuzzState(t *testing.T) *fuzzState { }) firewoodMemdb := rawdb.NewMemoryDatabase() - fwCfg := Defaults // copy the defaults - fwCfg.ChainDataDir = t.TempDir() // Use a temporary directory for the Firewood + fwCfg := DefaultConfig(t.TempDir()) firewoodState := state.NewDatabaseWithConfig( firewoodMemdb, &triedb.Config{ diff --git a/graft/evm/firewood/recovery.go b/graft/evm/firewood/recovery.go new file mode 100644 index 000000000000..6aeaf69a1ba7 --- /dev/null +++ b/graft/evm/firewood/recovery.go @@ -0,0 +1,82 @@ +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them 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 libevm additions are distributed in the hope that they 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 +// . + +// The firewood package provides a [triedb.DBOverride] backed by [Firewood]. +// +// [Firewood]: https://github.com/ava-labs/firewood +package firewood + +import ( + "encoding/binary" + "fmt" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/ethdb" +) + +const ( + committedBlockHashKey = "committedFirewoodBlockHash" + committedHeightKey = "committedFirewoodHeight" +) + +// ReadCommittedBlockHash retrieves the most recently committed block hash from the key-value store. +func ReadCommittedBlockHashes(kvStore ethdb.Database) (map[common.Hash]struct{}, error) { + data, _ := kvStore.Get([]byte(committedBlockHashKey)) // ignore not found error + if len(data)%common.HashLength != 0 { + return nil, fmt.Errorf("invalid committed block hash length: expected multiple of %d, got %d", common.HashLength, len(data)) + } + hashes := make(map[common.Hash]struct{}) + if len(data) == 0 { + hashes[common.Hash{}] = struct{}{} + return hashes, nil + } + for i := 0; i < len(data); i += common.HashLength { + hash := common.BytesToHash(data[i : i+common.HashLength]) + hashes[hash] = struct{}{} + } + return hashes, nil +} + +// WriteCommittedBlockHash writes the most recently committed block hash to the key-value store. +func WriteCommittedBlockHashes(kvStore ethdb.Database, hashes map[common.Hash]struct{}) error { + contents := make([]byte, 0, len(hashes)*common.HashLength) + for hash := range hashes { + contents = append(contents, hash.Bytes()...) + } + if err := kvStore.Put([]byte(committedBlockHashKey), contents); err != nil { + return fmt.Errorf("error writing committed block hashes: %w", err) + } + return nil +} + +// ReadCommittedHeight retrieves the most recently committed height from the key-value store. +func ReadCommittedHeight(kvStore ethdb.Database) uint64 { + data, _ := kvStore.Get([]byte(committedHeightKey)) + if len(data) != 8 { + return 0 + } + return binary.BigEndian.Uint64(data) +} + +// WriteCommittedHeight writes the most recently committed height to the key-value store. +func WriteCommittedHeight(kvStore ethdb.Database, height uint64) error { + enc := make([]byte, 8) + binary.BigEndian.PutUint64(enc, height) + if err := kvStore.Put([]byte(committedHeightKey), enc); err != nil { + return fmt.Errorf("error writing committed height: %w", err) + } + return nil +} diff --git a/graft/evm/firewood/storage_trie.go b/graft/evm/firewood/storage_trie.go index 54bad04aedda..beb7930dc57e 100644 --- a/graft/evm/firewood/storage_trie.go +++ b/graft/evm/firewood/storage_trie.go @@ -1,5 +1,18 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them 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 libevm additions are distributed in the hope that they 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 firewood @@ -23,22 +36,19 @@ func newStorageTrie(accountTrie *accountTrie) *storageTrie { } } -// Actual commit is handled by the account trie. -// Return the old storage root as if there was no change since Firewood -// will manage the hash calculations without it. -// All changes are managed by the account trie. +// Commit is a no-op for storage tries, as all changes are managed by the account trie. +// It always returns a nil NodeSet and zero hash. func (*storageTrie) Commit(bool) (common.Hash, *trienode.NodeSet, error) { return common.Hash{}, nil, nil } -// Firewood doesn't require tracking storage roots inside of an account. -// They will be updated in place when hashing of the proposal takes place. +// Hash returns an empty hash, as the storage roots are managed internally to Firewood. func (*storageTrie) Hash() common.Hash { return common.Hash{} } -// Copy should never be called on a storage trie, as it is just a wrapper around the account trie. -// Each storage trie should be re-opened with the account trie separately. +// Copy returns nil, as storage tries do not need to be copied separately. +// All usage of a copied storage trie should first ensure it is non-nil. func (*storageTrie) Copy() *storageTrie { return nil } diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 85f8d8e34ce9..20f562547dd7 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -1,6 +1,22 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. +// Copyright 2025 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them 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 libevm additions are distributed in the hope that they 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 +// . +// The firewood package provides a [triedb.DBOverride] backed by [Firewood]. +// +// [Firewood]: https://github.com/ava-labs/firewood package firewood import ( @@ -13,13 +29,13 @@ import ( "time" "github.com/ava-labs/firewood-go-ethhash/ffi" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/libevm/stateconf" "github.com/ava-labs/libevm/log" - "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/libevm/trie/trienode" "github.com/ava-labs/libevm/trie/triestate" "github.com/ava-labs/libevm/triedb" @@ -29,566 +45,547 @@ import ( const firewoodDir = "firewood" var ( - _ proposable = (*ffi.Database)(nil) - _ proposable = (*ffi.Proposal)(nil) - - // FFI triedb operation metrics - ffiProposeCount = metrics.GetOrRegisterCounter("firewood/triedb/propose/count", nil) - ffiProposeTimer = metrics.GetOrRegisterCounter("firewood/triedb/propose/time", nil) - ffiCommitCount = metrics.GetOrRegisterCounter("firewood/triedb/commit/count", nil) - ffiCommitTimer = metrics.GetOrRegisterCounter("firewood/triedb/commit/time", nil) - ffiCleanupTimer = metrics.GetOrRegisterCounter("firewood/triedb/cleanup/time", nil) - ffiOutstandingProposals = metrics.GetOrRegisterGauge("firewood/triedb/propose/outstanding", nil) - - // FFI Trie operation metrics - ffiHashCount = metrics.GetOrRegisterCounter("firewood/triedb/hash/count", nil) - ffiHashTimer = metrics.GetOrRegisterCounter("firewood/triedb/hash/time", nil) - ffiReadCount = metrics.GetOrRegisterCounter("firewood/triedb/read/count", nil) - ffiReadTimer = metrics.GetOrRegisterCounter("firewood/triedb/read/time", nil) + _ triedb.DBConstructor = Config{}.BackendConstructor + _ triedb.DBOverride = (*TrieDB)(nil) ) -type proposable interface { - // Propose creates a new proposal from the current state with the given keys and values. - Propose(keys, values [][]byte) (*ffi.Proposal, error) +// TrieDB is a triedb.DBOverride implementation backed by Firewood. +// It acts as a HashDB for backwards compatibility with most of the blockchain code. +type TrieDB struct { + // The underlying Firewood database, used for storing proposals and revisions. + // This is exported as read-only, with knowledge that the consumer will not close it + // and the latest state can be modified at any time. + Firewood *ffi.Database + + kvStore ethdb.Database + + proposals +} + +type proposals struct { + sync.RWMutex + + byStateRoot map[common.Hash][]*proposal + // The proposal tree tracks the structure of the current proposals, and which proposals are children of which. + // This is used to ensure that we can dereference proposals correctly and commit the correct ones + // in the case of duplicate state roots. + // The root of the tree is stored here, and represents the top-most layer on disk. + tree *proposal + // possible temporarily holds proposals created during a trie update. + // This is cleared after the update is complete and the proposals have been sent to the database. + possible map[unverifiedKey]*proposal +} + +type unverifiedKey struct { + parentBlockHash, root common.Hash +} + +// A proposal carries a Firewood FFI proposal (i.e. Rust-owned memory). +// The Firewood library adds a finalizer to the proposal handle to ensure that +// the memory is freed when the Go object is garbage collected. However, because +// we form a tree of proposals, the `proposal.Proposal` field may be the only +// reference to a given proposal. To ensure that all proposals in the tree +// can be freed in a finalizer, this cannot be included in the tree structure. +type proposal struct { + *proposalMeta + handle *ffi.Proposal } -// ProposalContext represents a proposal in the Firewood database. -// This tracks all outstanding proposals to allow dereferencing upon commit. -type ProposalContext struct { - Proposal *ffi.Proposal - Hashes map[common.Hash]struct{} // All corresponding block hashes - Root common.Hash - Block uint64 - Parent *ProposalContext - Children []*ProposalContext +type proposalMeta struct { + parent *proposalMeta + children []*proposalMeta + blockHashes map[common.Hash]struct{} // All corresponding block hashes + root common.Hash + height uint64 } +// Config provides necessary parameters for creating a Firewood database. type Config struct { - ChainDataDir string - CleanCacheSize int // Size of the clean cache in bytes - FreeListCacheEntries uint // Number of free list entries to cache - Revisions uint // Number of revisions to keep in memory (must be >= 2) - ReadCacheStrategy ffi.CacheStrategy - ArchiveMode bool + DatabasePath string // directory where the database files will be stored + CacheSizeBytes uint + FreeListCacheEntries uint + RevisionsInMemory uint // must be >= 2 + CacheStrategy ffi.CacheStrategy + Archive bool // whether to write keep all historical revisions on disk } -// Note that `FilePath` is not specified, and must always be set by the user. -var Defaults = Config{ - CleanCacheSize: 1024 * 1024, // 1MB - FreeListCacheEntries: 40_000, - Revisions: 100, - ReadCacheStrategy: ffi.CacheAllReads, +// DefaultConfig returns a default Config with the given directory. +// The default config is: +// - CacheSizeBytes: 1MB +// - FreeListCacheEntries: 40,000 +// - MaxRevisions: 100 +// - CacheStrategy: [ffi.CacheAllReads] +func DefaultConfig(dir string) Config { + return Config{ + DatabasePath: dir, + CacheSizeBytes: 1024 * 1024, // 1MB + FreeListCacheEntries: 40_000, + RevisionsInMemory: 100, + CacheStrategy: ffi.CacheAllReads, + } } -func (c Config) BackendConstructor(ethdb.Database) triedb.DBOverride { - db, err := New(c) +// BackendConstructor implements the [triedb.DBConstructor] interface. +// It creates a new Firewood database with the given configuration. +// Any error during creation will cause the program to exit. +func (c Config) BackendConstructor(disk ethdb.Database) triedb.DBOverride { + db, err := New(c, disk) if err != nil { - log.Crit("firewood: error creating database", "error", err) + log.Crit("firewood: creating database", "error", err) } return db } -type TrieDB struct { - fwDisk *ffi.Database // The underlying Firewood database, used for storing proposals and revisions. - proposalLock sync.RWMutex - // proposalMap provides O(1) access by state root to all proposals stored in the proposalTree - proposalMap map[common.Hash][]*ProposalContext - // The proposal tree tracks the structure of the current proposals, and which proposals are children of which. - // This is used to ensure that we can dereference proposals correctly and commit the correct ones - // in the case of duplicate state roots. - // The root of the tree is stored here, and represents the top-most layer on disk. - proposalTree *ProposalContext -} - -// New creates a new Firewood database with the given disk database and configuration. -// Any error during creation will cause the program to exit. -func New(config Config) (*TrieDB, error) { - path := filepath.Join(config.ChainDataDir, firewoodDir) - if err := validatePath(path); err != nil { +// New creates a new Firewood database with the given configuration. +// The database will not be opened on error. +func New(config Config, disk ethdb.Database) (*TrieDB, error) { + height := ReadCommittedHeight(disk) + blockHashes, err := ReadCommittedBlockHashes(disk) + if err != nil { return nil, err } + if err := validateDir(config.DatabasePath); err != nil { + return nil, err + } + path := filepath.Join(config.DatabasePath, firewoodDir) options := []ffi.Option{ - ffi.WithNodeCacheEntries(uint(config.CleanCacheSize / 256)), // TODO(#4750): is 256 bytes per node a good estimate? + ffi.WithNodeCacheEntries(config.CacheSizeBytes / 256), // TODO(#4750): is 256 bytes per node a good estimate? ffi.WithFreeListCacheEntries(config.FreeListCacheEntries), - ffi.WithRevisions(config.Revisions), - ffi.WithReadCacheStrategy(config.ReadCacheStrategy), + ffi.WithRevisions(config.RevisionsInMemory), + ffi.WithReadCacheStrategy(config.CacheStrategy), } - if config.ArchiveMode { + if config.Archive { options = append(options, ffi.WithRootStore()) } fw, err := ffi.New(path, options...) if err != nil { - return nil, err + return nil, fmt.Errorf("opening firewood database: %w", err) } - currentRoot, err := fw.Root() + intialRoot, err := fw.Root() if err != nil { + if closeErr := fw.Close(context.Background()); closeErr != nil { + return nil, fmt.Errorf("%w: error while closing: %w", err, closeErr) + } return nil, err } return &TrieDB{ - fwDisk: fw, - proposalMap: make(map[common.Hash][]*ProposalContext), - proposalTree: &ProposalContext{ - Root: common.Hash(currentRoot), + Firewood: fw, + kvStore: disk, + proposals: proposals{ + byStateRoot: make(map[common.Hash][]*proposal), + tree: &proposal{ + proposalMeta: &proposalMeta{ + root: common.Hash(intialRoot), + blockHashes: blockHashes, + height: height, + }, + }, + possible: make(map[unverifiedKey]*proposal), }, }, nil } -func validatePath(path string) error { - if path == "" { - return errors.New("firewood database file path must be set") +func validateDir(dir string) error { + if dir == "" { + return errors.New("chain data directory must be set") } - // Check that the directory exists - dir := filepath.Dir(path) switch info, err := os.Stat(dir); { case os.IsNotExist(err): log.Info("Database directory not found, creating", "path", dir) - if err := os.MkdirAll(dir, 0o755); err != nil { - return fmt.Errorf("error creating database directory: %w", err) + if err := os.MkdirAll(dir, 0o750); err != nil { + return fmt.Errorf("creating database directory: %v", err) } return nil case err != nil: - return fmt.Errorf("error checking database directory: %w", err) + return fmt.Errorf("os.Stat() on database directory: %v", err) case !info.IsDir(): - return fmt.Errorf("database directory path is not a directory: %s", dir) + return fmt.Errorf("database directory path is not a directory: %q", dir) } return nil } // Scheme returns the scheme of the database. -// This is only used in some API calls -// and in StateDB to avoid iterating through deleted storage tries. -// WARNING: If cherry-picking anything from upstream that uses this, -// it must be overwritten to use something like: -// `_, ok := db.(*Database); if !ok { return "" }` -// to recognize the Firewood database. +// However, to avoid a slow deletion in `libevm` `StateDB`, it returns [rawdb.HashScheme]. func (*TrieDB) Scheme() string { return rawdb.HashScheme } // Initialized checks whether a non-empty genesis block has been written. func (t *TrieDB) Initialized(common.Hash) bool { - root, err := t.fwDisk.Root() + root, err := t.Firewood.Root() if err != nil { - log.Error("firewood: error getting current root", "error", err) + log.Error("get current root", "error", err) return false } - // If the current root isn't empty, then unless the database is empty, we have a genesis block recorded. + // If the current root isn't empty, then unless the genesis block is empty, + // the database is initialized. return common.Hash(root) != types.EmptyRootHash } -// Update takes a root and a set of keys-values and creates a new proposal. -// It will not be committed until the Commit method is called. -// This function should be called even if there are no changes to the state to ensure proper tracking of block hashes. -func (t *TrieDB) Update(root common.Hash, parentRoot common.Hash, block uint64, nodes *trienode.MergedNodeSet, _ *triestate.Set, opts ...stateconf.TrieDBUpdateOption) error { - // We require block hashes to be provided for all blocks in production. - // However, many tests cannot reasonably provide a block hash for genesis, so we allow it to be omitted. - parentHash, hash, ok := stateconf.ExtractTrieDBUpdatePayload(opts...) - if !ok { - log.Error("firewood: no block hash provided for block %d", block) - } +// Size returns the storage size of diff layer nodes above the persistent disk +// layer and the dirty nodes buffered within the disk layer +// Only used for metrics and Commit intervals in APIs. +// This will be implemented in the firewood database eventually. +// Currently, Firewood stores all revisions in disk and proposals in memory. +func (*TrieDB) Size() (common.StorageSize, common.StorageSize) { + return 0, 0 +} - // The rest of the operations except key-value arranging must occur with a lock - t.proposalLock.Lock() - defer t.proposalLock.Unlock() +// Reference is no-op because proposals are only referenced when created. +// Additionally, internal nodes do not need tracked by consumers. +func (*TrieDB) Reference(common.Hash, common.Hash) {} - // Check if this proposal already exists. - // During reorgs, we may have already created this proposal. - // Additionally, we may have already created this proposal with a different block hash. - if existingProposals, ok := t.proposalMap[root]; ok { - for _, existing := range existingProposals { - // If the block hash is already tracked, we can skip proposing this again. - if _, exists := existing.Hashes[hash]; exists { - log.Debug("firewood: proposal already exists", "root", root.Hex(), "parent", parentRoot.Hex(), "block", block, "hash", hash.Hex()) - return nil - } - // We already have this proposal, but should create a new context with the correct hash. - // This solves the case of a unique block hash, but the same underlying proposal. - if _, exists := existing.Parent.Hashes[parentHash]; exists { - log.Debug("firewood: proposal already exists, updating hash", "root", root.Hex(), "parent", parentRoot.Hex(), "block", block, "hash", hash.Hex()) - existing.Hashes[hash] = struct{}{} - return nil - } - } +// Dereference is no-op because proposals will be removed automatically. +// Additionally, internal nodes do not need tracked by consumers. +func (*TrieDB) Dereference(common.Hash) {} + +// Cap is a no-op because it isn't supported by Firewood. +func (*TrieDB) Cap(common.StorageSize) error { + return nil +} + +// Close closes the database, freeing all associated resources. +// This may hang for a short period while waiting for finalizers to complete. +// If it does not close as expected, this indicates that there are still references +// to proposals or revisions in memory, and an error will be returned. +// The database should not be used after calling Close, but it is safe to call multiple times. +func (t *TrieDB) Close() error { + p := &t.proposals + p.Lock() + defer p.Unlock() + + if p.tree == nil { + return nil // already closed + } + + // All remaining proposals can explicitly be dropped. + for _, child := range p.tree.children { + p.removeProposalAndChildren(child) } + p.tree = nil + p.byStateRoot = nil + p.possible = nil - keys, values := arrangeKeyValuePairs(nodes) // may return nil, nil if no changes - return t.propose(root, parentRoot, hash, parentHash, block, keys, values) + // We must provide a context to close since it may hang while waiting for the finalizers to complete. + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return t.Firewood.Close(ctx) } -// propose creates a new proposal for every possible parent with the given keys and values. -// If the parent cannot be found, an error will be returned. -// -// To avoid having to create a new proposal for each valid state root, the block hashes are -// provided to ensure uniqueness. When this method is called, we can guarantee that the proposalContext -// must be created and tracked. -// -// Should only be accessed with the proposal lock held. -func (t *TrieDB) propose(root common.Hash, parentRoot common.Hash, hash common.Hash, parentHash common.Hash, block uint64, keys [][]byte, values [][]byte) error { - // Find the parent proposal with the correct hash. - // We assume the number of proposals at a given root is small, so we can iterate through them. - for _, parentProposal := range t.proposalMap[parentRoot] { - // If we know this proposal cannot be the parent, we can skip it. - // Since the only possible block that won't have a parent hash is block 1, - // and that will always be proposed from the database root, - // we can guarantee that the parent hash will be present in one of the proposals. - if _, exists := parentProposal.Hashes[parentHash]; !exists { - continue - } - log.Debug("firewood: proposing from parent proposal", "parent", parentProposal.Root.Hex(), "root", root.Hex(), "height", block) - p, err := createProposal(parentProposal.Proposal, root, keys, values) - if err != nil { - return err - } - pCtx := &ProposalContext{ - Proposal: p, - Hashes: map[common.Hash]struct{}{hash: {}}, - Root: root, - Block: block, - Parent: parentProposal, - } +// Update updates the database to the given root at the given height. +// The parent block hash and block hash must be provided in the options. +// A proposal must have already been created from [accountTrie.Commit] with the same root, +// parent root, and height. +// If no such proposal exists, an error will be returned. +func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.MergedNodeSet, _ *triestate.Set, opts ...stateconf.TrieDBUpdateOption) error { + // We require block hashes to be provided for all blocks in production. + // However, many tests cannot reasonably provide a block blockHash for genesis, so we allow it to be omitted. + parentBlockHash, blockHash, ok := stateconf.ExtractTrieDBUpdatePayload(opts...) + if !ok { + return fmt.Errorf("firewood: no block hash provided for block %d", height) + } + + // The rest of the operations except key-value arranging must occur with a lock + t.proposals.Lock() + defer t.proposals.Unlock() - t.proposalMap[root] = append(t.proposalMap[root], pCtx) - parentProposal.Children = append(parentProposal.Children, pCtx) + if t.proposals.exists(root, blockHash, parentBlockHash) { return nil } - // Since we were unable to find a parent proposal with the given parent hash, - // we must create a new proposal from the database root. - // We must avoid the case in which we are reexecuting blocks upon startup, and haven't yet stored the parent block. - if _, exists := t.proposalTree.Hashes[parentHash]; t.proposalTree.Block != 0 && !exists { - return fmt.Errorf("firewood: parent hash %s not found for block %s at height %d", parentHash.Hex(), hash.Hex(), block) - } else if t.proposalTree.Root != parentRoot { - return fmt.Errorf("firewood: parent root %s does not match proposal tree root %s for root %s at height %d", parentRoot.Hex(), t.proposalTree.Root.Hex(), root.Hex(), block) + p, ok := t.possible[unverifiedKey{parentBlockHash: parentBlockHash, root: root}] + // Now, all unused proposals have no other references, since we didn't store them + // in the proposal map or tree, so they will be garbage collected. + // Any proposals with a different root were mistakenly created, so they can be freed as well. + t.proposals.possible = make(map[unverifiedKey]*proposal) + if !ok { + return fmt.Errorf("no proposal found for block %d, root %s, hash %s", height, root.Hex(), blockHash.Hex()) } - log.Debug("firewood: proposing from database root", "root", root.Hex(), "height", block) - p, err := createProposal(t.fwDisk, root, keys, values) - if err != nil { - return err - } - pCtx := &ProposalContext{ - Proposal: p, - Hashes: map[common.Hash]struct{}{hash: {}}, // This may be common.Hash{} for genesis blocks. - Root: root, - Block: block, - Parent: t.proposalTree, + switch { + case p.root != root: + return fmt.Errorf("proposal root mismatch, expected %x, got %x", root, p.root) + case p.parent.root != parent: + return fmt.Errorf("parent root mismatch, expected %#x, got %x", parent, p.parent.root) + case p.height != height: + return fmt.Errorf("height mismatch, expected %d, got %d", height, p.height) } - t.proposalMap[root] = append(t.proposalMap[root], pCtx) - t.proposalTree.Children = append(t.proposalTree.Children, pCtx) + + // Track the proposal context in the tree and map. + p.parent.children = append(p.parent.children, p.proposalMeta) + t.proposals.byStateRoot[root] = append(t.proposals.byStateRoot[root], p) + p.blockHashes[blockHash] = struct{}{} return nil } +func (ps *proposals) exists(root, block, parentBlock common.Hash) bool { + // Check if this proposal already exists. + // During reorgs, we may have already created this proposal. + // Additionally, we may have already created this proposal with a different block hash. + proposals, ok := ps.byStateRoot[root] + if !ok { + return false + } + + for _, p := range proposals { + // If the block hash is already tracked, we can skip proposing this again. + if _, ok := p.blockHashes[block]; ok { + log.Debug("proposal already exists", "root", root.Hex(), "parent", parentBlock.Hex(), "block", block, "hash", block.Hex()) + return true + } + + // We already have this proposal, but should create a new context with the correct hash. + // This solves the case of a unique block hash, but the same underlying proposal. + if _, ok := p.parent.blockHashes[parentBlock]; ok { + log.Debug("proposal already exists, updating hash", "root", root.Hex(), "parent", parentBlock.Hex(), "block", block, "hash", block.Hex()) + p.blockHashes[block] = struct{}{} + return true + } + } + + return false +} + // Commit persists a proposal as a revision to the database. // // Any time this is called, we expect either: // 1. The root is the same as the current root of the database (empty block during bootstrapping) // 2. We have created a valid propsal with that root, and it is of height +1 above the proposal tree root. -// Additionally, this should be unique. +// Additionally, this will be unique. // // Afterward, we know that no other proposal at this height can be committed, so we can dereference all // children in the the other branches of the proposal tree. func (t *TrieDB) Commit(root common.Hash, report bool) error { - // We need to lock the proposal tree to prevent concurrent writes. - t.proposalLock.Lock() - defer t.proposalLock.Unlock() - - // Find the proposal with the given root. - var pCtx *ProposalContext - for _, possible := range t.proposalMap[root] { - if possible.Parent.Root == t.proposalTree.Root && possible.Parent.Block == t.proposalTree.Block { - // We found the proposal with the correct parent. - if pCtx != nil { - // This should never happen, as we ensure that we don't create duplicate proposals in `propose`. - return fmt.Errorf("firewood: multiple proposals found for %s", root.Hex()) - } - pCtx = possible - } - } - if pCtx == nil { - return fmt.Errorf("firewood: committable proposal not found for %s", root.Hex()) + t.proposals.Lock() + defer t.proposals.Unlock() + + p, err := t.proposals.findProposalToCommitWhenLocked(root) + if err != nil { + return err } - start := time.Now() - // Commit the proposal to the database. - if err := pCtx.Proposal.Commit(); err != nil { - t.dereference(pCtx) // no longer committable - return fmt.Errorf("firewood: error committing proposal %s: %w", root.Hex(), err) + if err := p.handle.Commit(); err != nil { + return fmt.Errorf("committing proposal %s: %w", root.Hex(), err) } - ffiCommitCount.Inc(1) - ffiCommitTimer.Inc(time.Since(start).Milliseconds()) - ffiOutstandingProposals.Dec(1) - // Now that the proposal is committed, we should clean up the proposal tree on return. - defer t.cleanupCommittedProposal(pCtx) + p.handle = nil // The proposal has been committed. - // Assert that the root of the database matches the committed proposal root. - currentRoot, err := t.fwDisk.Root() + newRoot, err := t.Firewood.Root() if err != nil { - return fmt.Errorf("firewood: error getting current root after commit: %w", err) + return fmt.Errorf("getting current root after commit: %w", err) } - - currentRootHash := common.Hash(currentRoot) - if currentRootHash != root { - return fmt.Errorf("firewood: current root %s does not match expected root %s", currentRootHash.Hex(), root.Hex()) + if common.Hash(newRoot) != root { + return fmt.Errorf("root after commit (%x) does not match expected root %x", newRoot, root) } + var logFn = log.Debug if report { - log.Info("Persisted proposal to firewood database", "root", root) - } else { - log.Debug("Persisted proposal to firewood database", "root", root) + logFn = log.Info } - return nil -} - -// Size returns the storage size of diff layer nodes above the persistent disk -// layer and the dirty nodes buffered within the disk layer -// Only used for metrics and Commit intervals in APIs. -// This will be implemented in the firewood database eventually. -// Currently, Firewood stores all revisions in disk and proposals in memory. -func (*TrieDB) Size() (common.StorageSize, common.StorageSize) { - return 0, 0 -} - -// Reference is a no-op. -func (*TrieDB) Reference(common.Hash, common.Hash) {} + logFn("Persisted proposal to firewood database", "root", root) -// Dereference is a no-op since Firewood handles unused state roots internally. -func (*TrieDB) Dereference(common.Hash) {} + // On success, we should dereference all children of the committed proposal. + // By removing all uncommittable proposals from the tree and map, + // we ensure that there are no more references. + t.cleanupCommittedProposal(p) -// Firewood does not support this. -func (*TrieDB) Cap(common.StorageSize) error { + // Update the committed block hashes and height on disk to enable recovery. + if err := WriteCommittedBlockHashes(t.kvStore, p.blockHashes); err != nil { + return err + } + if err := WriteCommittedHeight(t.kvStore, p.height); err != nil { + return err + } return nil } -func (t *TrieDB) Close() error { - t.proposalLock.Lock() - defer t.proposalLock.Unlock() +func (ps *proposals) findProposalToCommitWhenLocked(root common.Hash) (*proposal, error) { + var candidate *proposal - // before closing, we must deference any outstanding proposals to free the - // memory owned by firewood (outside of go's memory management) - for _, pCtx := range t.proposalTree.Children { - t.dereference(pCtx) + for _, p := range ps.byStateRoot[root] { + if p.parent.root != ps.tree.root || p.parent.height != ps.tree.height { + continue + } + if candidate != nil { + // This should never happen, as we ensure that we don't create duplicate proposals in `propose`. + return nil, fmt.Errorf("firewood: multiple proposals found for root %#x", root) + } + candidate = p } - t.proposalMap = nil - t.proposalTree.Children = nil - - // Close the database - // This may block momentarily while finalizers for Firewood objects run. - return t.fwDisk.Close(context.Background()) + if candidate == nil { + return nil, fmt.Errorf("firewood: committable proposal not found for %d:%#x", ps.tree.height+1, root) + } + return candidate, nil } // createProposal creates a new proposal from the given layer -// If there are no changes, it will return nil. -func createProposal(layer proposable, root common.Hash, keys, values [][]byte) (p *ffi.Proposal, err error) { - // If there's an error after creating the proposal, we must drop it. - defer func() { - if err != nil && p != nil { - if dropErr := p.Drop(); dropErr != nil { - // We should still return the original error. - log.Error("firewood: error dropping proposal after error", "root", root.Hex(), "error", dropErr) - } - p = nil - } - }() - - if len(keys) != len(values) { - return nil, fmt.Errorf("firewood: keys and values must have the same length, got %d keys and %d values", len(keys), len(values)) +func (t *TrieDB) createProposal(parent *proposal, keys, values [][]byte) (*proposal, error) { + propose := t.Firewood.Propose + if h := parent.handle; h != nil { + propose = h.Propose } - - start := time.Now() - p, err = layer.Propose(keys, values) + handle, err := propose(keys, values) if err != nil { - return nil, fmt.Errorf("firewood: unable to create proposal for root %s: %w", root.Hex(), err) + return nil, fmt.Errorf("firewood: create proposal from parent root %s: %w", parent.root.Hex(), err) } - ffiProposeCount.Inc(1) - ffiProposeTimer.Inc(time.Since(start).Milliseconds()) - ffiOutstandingProposals.Inc(1) - currentRoot, err := p.Root() - if err != nil { - return nil, fmt.Errorf("firewood: error getting root of proposal %s: %w", root, err) + // Edge case: genesis block + block := parent.height + 1 + if _, ok := parent.blockHashes[common.Hash{}]; ok && parent.root == types.EmptyRootHash { + block = 0 } - currentRootHash := common.Hash(currentRoot) - if root != currentRootHash { - return nil, fmt.Errorf("firewood: proposed root %s does not match expected root %s", currentRootHash.Hex(), root.Hex()) + p := &proposal{ + handle: handle, + proposalMeta: &proposalMeta{ + blockHashes: make(map[common.Hash]struct{}), + parent: parent.proposalMeta, + height: block, + }, } + root, err := handle.Root() + if err != nil { + return nil, fmt.Errorf("firewood: getting root of proposal: %w", err) + } + p.root = common.Hash(root) + return p, nil } // cleanupCommittedProposal dereferences the proposal and removes it from the proposal map. // It also recursively dereferences all children of the proposal. -func (t *TrieDB) cleanupCommittedProposal(pCtx *ProposalContext) { - start := time.Now() - oldChildren := t.proposalTree.Children - t.proposalTree = pCtx - t.proposalTree.Parent = nil - - t.removeProposalFromMap(pCtx) - - for _, childCtx := range oldChildren { - // Don't dereference the recently commit proposal. - if childCtx != pCtx { - t.dereference(childCtx) +func (ps *proposals) cleanupCommittedProposal(p *proposal) { + oldChildren := ps.tree.children + ps.tree = p + ps.tree.parent = nil + ps.tree.handle = nil + + ps.removeProposalFromMap(p.proposalMeta, false) + + for _, child := range oldChildren { + if child != p.proposalMeta { + ps.removeProposalAndChildren(child) } } - ffiCleanupTimer.Inc(time.Since(start).Milliseconds()) } // Internally removes all references of the proposal from the database. // Should only be accessed with the proposal lock held. -// Consumer must not be iterating the proposal map at this root. -func (t *TrieDB) dereference(pCtx *ProposalContext) { +func (ps *proposals) removeProposalAndChildren(p *proposalMeta) { // Base case: if there are children, we need to dereference them as well. - for _, child := range pCtx.Children { - t.dereference(child) + for _, child := range p.children { + ps.removeProposalAndChildren(child) } - pCtx.Children = nil // Remove the proposal from the map. - t.removeProposalFromMap(pCtx) - - // Drop the proposal in the backend. - if err := pCtx.Proposal.Drop(); err != nil { - log.Error("firewood: error dropping proposal", "root", pCtx.Root.Hex(), "error", err) - } - ffiOutstandingProposals.Dec(1) + ps.removeProposalFromMap(p, true) } // removeProposalFromMap removes the proposal from the proposal map. // The proposal lock must be held when calling this function. -func (t *TrieDB) removeProposalFromMap(pCtx *ProposalContext) { - rootList := t.proposalMap[pCtx.Root] +func (ps *proposals) removeProposalFromMap(meta *proposalMeta, drop bool) { + rootList := ps.byStateRoot[meta.root] for i, p := range rootList { - if p == pCtx { // pointer comparison - guaranteed to be unique + if p.proposalMeta == meta { // pointer comparison - guaranteed to be unique rootList[i] = rootList[len(rootList)-1] rootList[len(rootList)-1] = nil rootList = rootList[:len(rootList)-1] + + if drop { + if err := p.handle.Drop(); err != nil { + log.Error("while dropping proposal", "root", meta.root, "height", meta.height, "err", err) + } + } break } } if len(rootList) == 0 { - delete(t.proposalMap, pCtx.Root) + delete(ps.byStateRoot, meta.root) } else { - t.proposalMap[pCtx.Root] = rootList - } -} - -// Reader retrieves a node reader belonging to the given state root. -// An error will be returned if the requested state is not available. -func (t *TrieDB) Reader(root common.Hash) (database.Reader, error) { - revision, err := t.fwDisk.Revision(ffi.Hash(root)) - if err != nil { - return nil, fmt.Errorf("firewood: unable to retrieve from root %s: %w", root.Hex(), err) + ps.byStateRoot[meta.root] = rootList } - return &reader{revision: revision}, nil -} - -// reader is a state reader of Database which implements the Reader interface. -type reader struct { - revision *ffi.Revision -} - -// Node retrieves the trie node with the given node hash. No error will be -// returned if the node is not found. -func (reader *reader) Node(_ common.Hash, path []byte, _ common.Hash) ([]byte, error) { - // This function relies on Firewood's internal locking to ensure concurrent reads are safe. - // This is safe even if a proposal is being committed concurrently. - start := time.Now() - result, err := reader.revision.Get(path) - if metrics.EnabledExpensive { - ffiReadCount.Inc(1) - ffiReadTimer.Inc(time.Since(start).Milliseconds()) - } - return result, err } // getProposalHash calculates the hash if the set of keys and values are // proposed from the given parent root. func (t *TrieDB) getProposalHash(parentRoot common.Hash, keys, values [][]byte) (common.Hash, error) { + if len(keys) != len(values) { + return common.Hash{}, fmt.Errorf("keys and values must have the same length, got %d keys and %d values", len(keys), len(values)) + } + // This function only reads from existing tracked proposals, so we can use a read lock. - t.proposalLock.RLock() - defer t.proposalLock.RUnlock() + t.proposals.RLock() + defer t.proposals.RUnlock() var ( - p *ffi.Proposal - err error + count int // number of proposals created. + root common.Hash // The resulting root hash, should match between proposals ) - start := time.Now() - if t.proposalTree.Root == parentRoot { + if t.proposals.tree.root == parentRoot { // Propose from the database root. - p, err = t.fwDisk.Propose(keys, values) + p, err := t.createProposal(t.proposals.tree, keys, values) + root = p.root if err != nil { - return common.Hash{}, fmt.Errorf("firewood: error proposing from root %s: %w", parentRoot.Hex(), err) + return common.Hash{}, fmt.Errorf("proposing from root %s: %w", parentRoot.Hex(), err) } - } else { - // Find any proposal with the given parent root. - // Since we are only using the proposal to find the root hash, - // we can use the first proposal found. - proposals, ok := t.proposalMap[parentRoot] - if !ok || len(proposals) == 0 { - return common.Hash{}, fmt.Errorf("firewood: no proposal found for parent root %s", parentRoot.Hex()) + for parentHash := range t.proposals.tree.blockHashes { + t.possible[unverifiedKey{parentBlockHash: parentHash, root: p.root}] = p } - rootProposal := proposals[0].Proposal + count++ + } - p, err = rootProposal.Propose(keys, values) + // Find any proposal with the given parent root. + // Since we are only using the proposal to find the root hash, + // we can use the first proposal found. + for _, parent := range t.proposals.byStateRoot[parentRoot] { + p, err := t.createProposal(parent, keys, values) if err != nil { - return common.Hash{}, fmt.Errorf("firewood: error proposing from parent proposal %s: %w", parentRoot.Hex(), err) + return common.Hash{}, fmt.Errorf("proposing from root %s: %w", parentRoot.Hex(), err) } - } - ffiHashCount.Inc(1) - ffiHashTimer.Inc(time.Since(start).Milliseconds()) - - // We succesffuly created a proposal, so we must drop it after use. - defer func() { - if err := p.Drop(); err != nil { - log.Error("firewood: error dropping proposal after hash computation", "parentRoot", parentRoot.Hex(), "error", err) + if root != (common.Hash{}) && p.root != root { + return common.Hash{}, fmt.Errorf("inconsistent proposal roots found for parent root %s: %#x and %#x", parentRoot.Hex(), root, p.root) + } + root = p.root + for parentHash := range parent.blockHashes { + t.possible[unverifiedKey{parentBlockHash: parentHash, root: root}] = p } - }() + count++ + } - root, err := p.Root() - if err != nil { - return common.Hash{}, err + // This should never occur, as to process a block, there must be a revision to read from. + if count == 0 { + return common.Hash{}, fmt.Errorf("no proposals found with parent root %s", parentRoot.Hex()) } - return common.Hash(root), nil + + return root, nil } -func arrangeKeyValuePairs(nodes *trienode.MergedNodeSet) ([][]byte, [][]byte) { - if nodes == nil { - return nil, nil // No changes to propose +// Reader retrieves a node reader belonging to the given state root. +// An error will be returned if the requested state is not available. +func (t *TrieDB) Reader(root common.Hash) (database.Reader, error) { + revision, err := t.Firewood.Revision(ffi.Hash(root)) + if err != nil { + return nil, fmt.Errorf("retrieve revision at root %s: %w", root.Hex(), err) } - // Create key-value pairs for the nodes in bytes. - var ( - acctKeys [][]byte - acctValues [][]byte - storageKeys [][]byte - storageValues [][]byte - ) + return &reader{revision: revision}, nil +} - flattenedNodes := nodes.Flatten() - - for _, nodeset := range flattenedNodes { - for str, node := range nodeset { - if len(str) == common.HashLength { - // This is an account node. - acctKeys = append(acctKeys, []byte(str)) - acctValues = append(acctValues, node.Blob) - } else { - storageKeys = append(storageKeys, []byte(str)) - storageValues = append(storageValues, node.Blob) - } - } - } +// reader is a state reader of Database which implements the Reader interface. +type reader struct { + revision *ffi.Revision +} - // We need to do all storage operations first, so prefix-deletion works for accounts. - return append(storageKeys, acctKeys...), append(storageValues, acctValues...) +// Node retrieves the trie node with the given node hash. No error will be +// returned if the node is not found. +func (r *reader) Node(_ common.Hash, path []byte, _ common.Hash) ([]byte, error) { + return r.revision.Get(path) } diff --git a/graft/subnet-evm/core/blockchain.go b/graft/subnet-evm/core/blockchain.go index 4405d0b3c19e..e8650e592802 100644 --- a/graft/subnet-evm/core/blockchain.go +++ b/graft/subnet-evm/core/blockchain.go @@ -243,12 +243,12 @@ func (c *CacheConfig) triedbConfig() *triedb.Config { } config.DBOverride = firewood.Config{ - ChainDataDir: c.ChainDataDir, - CleanCacheSize: c.TrieCleanLimit * 1024 * 1024, - FreeListCacheEntries: firewood.Defaults.FreeListCacheEntries, - Revisions: uint(c.StateHistory), // must be at least 2 - ReadCacheStrategy: ffi.CacheAllReads, - ArchiveMode: !c.Pruning, + DatabasePath: c.ChainDataDir, + CacheSizeBytes: uint(c.TrieCleanLimit) * 1024 * 1024, + FreeListCacheEntries: 40_000, // same as default + RevisionsInMemory: uint(c.StateHistory), // must be at least 2 + CacheStrategy: ffi.CacheAllReads, + Archive: !c.Pruning, }.BackendConstructor } return config diff --git a/graft/subnet-evm/core/genesis_test.go b/graft/subnet-evm/core/genesis_test.go index 856ae1e2af6f..6cae15b22231 100644 --- a/graft/subnet-evm/core/genesis_test.go +++ b/graft/subnet-evm/core/genesis_test.go @@ -370,9 +370,7 @@ func newDbConfig(t *testing.T, scheme string) *triedb.Config { case rawdb.PathScheme: return &triedb.Config{DBOverride: pathdb.Defaults.BackendConstructor} case customrawdb.FirewoodScheme: - fwCfg := firewood.Defaults - // Create a unique temporary directory for each test - fwCfg.ChainDataDir = t.TempDir() + fwCfg := firewood.DefaultConfig(t.TempDir()) return &triedb.Config{DBOverride: fwCfg.BackendConstructor} default: t.Fatalf("unknown scheme %s", scheme) diff --git a/graft/subnet-evm/tests/state_test_util.go b/graft/subnet-evm/tests/state_test_util.go index e6b6675c38b9..fa3100c8c054 100644 --- a/graft/subnet-evm/tests/state_test_util.go +++ b/graft/subnet-evm/tests/state_test_util.go @@ -480,8 +480,7 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, snapshotter bo case rawdb.PathScheme: tconf.DBOverride = pathdb.Defaults.BackendConstructor case customrawdb.FirewoodScheme: - cfg := firewood.Defaults - cfg.ChainDataDir = tempdir + cfg := firewood.DefaultConfig(tempdir) tconf.DBOverride = cfg.BackendConstructor default: panic("unknown trie database scheme" + scheme) From c6ec619adee106c67df0be92aaf5eb02f2d474ee Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Mon, 15 Dec 2025 15:20:40 -0500 Subject: [PATCH 02/18] fix: Empty genesis --- graft/coreth/core/genesis.go | 13 ++++++++++++- graft/evm/firewood/triedb.go | 10 ++++------ graft/subnet-evm/core/genesis.go | 13 ++++++++++++- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/graft/coreth/core/genesis.go b/graft/coreth/core/genesis.go index 0134c494ff37..f7e7faac59a1 100644 --- a/graft/coreth/core/genesis.go +++ b/graft/coreth/core/genesis.go @@ -39,6 +39,7 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap3" "github.com/ava-labs/avalanchego/graft/coreth/triedb/pathdb" + "github.com/ava-labs/avalanchego/graft/evm/firewood" "github.com/ava-labs/avalanchego/vms/evm/acp226" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/hexutil" @@ -338,8 +339,13 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo if _, err := statedb.Commit(0, false, stateconf.WithTrieDBUpdateOpts(triedbOpt)); err != nil { panic(fmt.Sprintf("unable to commit genesis block to statedb: %v", err)) } + if root == types.EmptyRootHash && isFirewood(triedb) { + if err := triedb.Update(root, root, 0, nil, nil, triedbOpt); err != nil { + panic(fmt.Sprintf("unable to update genesis block in triedb: %v", err)) + } + } // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash { + if root != types.EmptyRootHash || isFirewood(triedb) { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) } @@ -398,3 +404,8 @@ func ReadBlockByHash(db ethdb.Reader, hash common.Hash) *types.Block { } return rawdb.ReadBlock(db, hash, *blockNumber) } + +func isFirewood(db *triedb.Database) bool { + _, ok := db.Backend().(*firewood.TrieDB) + return ok +} diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 20f562547dd7..d2d4d094e753 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -29,7 +29,6 @@ import ( "time" "github.com/ava-labs/firewood-go-ethhash/ffi" - "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" @@ -77,7 +76,7 @@ type proposals struct { } type unverifiedKey struct { - parentBlockHash, root common.Hash + parentBlockHash, root common.Hash //nolint:unused // It is used as a map key } // A proposal carries a Firewood FFI proposal (i.e. Rust-owned memory). @@ -198,11 +197,10 @@ func validateDir(dir string) error { case os.IsNotExist(err): log.Info("Database directory not found, creating", "path", dir) if err := os.MkdirAll(dir, 0o750); err != nil { - return fmt.Errorf("creating database directory: %v", err) + return fmt.Errorf("creating database directory: %w", err) } - return nil case err != nil: - return fmt.Errorf("os.Stat() on database directory: %v", err) + return fmt.Errorf("os.Stat() on database directory: %w", err) case !info.IsDir(): return fmt.Errorf("database directory path is not a directory: %q", dir) } @@ -385,7 +383,7 @@ func (t *TrieDB) Commit(root common.Hash, report bool) error { return fmt.Errorf("root after commit (%x) does not match expected root %x", newRoot, root) } - var logFn = log.Debug + logFn := log.Debug if report { logFn = log.Info } diff --git a/graft/subnet-evm/core/genesis.go b/graft/subnet-evm/core/genesis.go index 186d7e535246..3f3b4344c20d 100644 --- a/graft/subnet-evm/core/genesis.go +++ b/graft/subnet-evm/core/genesis.go @@ -34,6 +34,7 @@ import ( "math/big" "time" + "github.com/ava-labs/avalanchego/graft/evm/firewood" "github.com/ava-labs/avalanchego/graft/subnet-evm/core/extstate" "github.com/ava-labs/avalanchego/graft/subnet-evm/params" "github.com/ava-labs/avalanchego/graft/subnet-evm/plugin/evm/customrawdb" @@ -376,8 +377,13 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo if _, err := statedb.Commit(0, false, stateconf.WithTrieDBUpdateOpts(triedbOpt)); err != nil { panic(fmt.Sprintf("unable to commit genesis block to statedb: %v", err)) } + if root == types.EmptyRootHash && isFirewood(triedb) { + if err := triedb.Update(root, root, 0, nil, nil, triedbOpt); err != nil { + panic(fmt.Sprintf("unable to update genesis block in triedb: %v", err)) + } + } // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash { + if root != types.EmptyRootHash || isFirewood(triedb) { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) } @@ -457,3 +463,8 @@ func ReadBlockByHash(db ethdb.Reader, hash common.Hash) *types.Block { } return rawdb.ReadBlock(db, hash, *blockNumber) } + +func isFirewood(db *triedb.Database) bool { + _, ok := db.Backend().(*firewood.TrieDB) + return ok +} From 98dfe7fe7372501b11d36b122431a68d758d14ce Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Mon, 15 Dec 2025 15:32:43 -0500 Subject: [PATCH 03/18] fix: license headers --- graft/evm/firewood/account_trie.go | 17 ++--------------- graft/evm/firewood/recovery.go | 20 ++------------------ graft/evm/firewood/storage_trie.go | 17 ++--------------- graft/evm/firewood/triedb.go | 20 ++------------------ 4 files changed, 8 insertions(+), 66 deletions(-) diff --git a/graft/evm/firewood/account_trie.go b/graft/evm/firewood/account_trie.go index 50f9c5fc3f89..6b6a58cb1db4 100644 --- a/graft/evm/firewood/account_trie.go +++ b/graft/evm/firewood/account_trie.go @@ -1,18 +1,5 @@ -// Copyright 2025 the libevm authors. -// -// The libevm additions to go-ethereum are free software: you can redistribute -// them and/or modify them 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 libevm additions are distributed in the hope that they 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 -// . +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. package firewood diff --git a/graft/evm/firewood/recovery.go b/graft/evm/firewood/recovery.go index 6aeaf69a1ba7..0e95c2de1a26 100644 --- a/graft/evm/firewood/recovery.go +++ b/graft/evm/firewood/recovery.go @@ -1,22 +1,6 @@ -// Copyright 2025 the libevm authors. -// -// The libevm additions to go-ethereum are free software: you can redistribute -// them and/or modify them 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 libevm additions are distributed in the hope that they 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 -// . +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. -// The firewood package provides a [triedb.DBOverride] backed by [Firewood]. -// -// [Firewood]: https://github.com/ava-labs/firewood package firewood import ( diff --git a/graft/evm/firewood/storage_trie.go b/graft/evm/firewood/storage_trie.go index beb7930dc57e..dc6b6a480ab2 100644 --- a/graft/evm/firewood/storage_trie.go +++ b/graft/evm/firewood/storage_trie.go @@ -1,18 +1,5 @@ -// Copyright 2025 the libevm authors. -// -// The libevm additions to go-ethereum are free software: you can redistribute -// them and/or modify them 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 libevm additions are distributed in the hope that they 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 -// . +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. package firewood diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index d2d4d094e753..9682c06523fd 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -1,22 +1,6 @@ -// Copyright 2025 the libevm authors. -// -// The libevm additions to go-ethereum are free software: you can redistribute -// them and/or modify them 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 libevm additions are distributed in the hope that they 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 -// . +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. -// The firewood package provides a [triedb.DBOverride] backed by [Firewood]. -// -// [Firewood]: https://github.com/ava-labs/firewood package firewood import ( From 9961f98331707c53013a737ac498341f5c2a2577 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Wed, 17 Dec 2025 15:50:44 -0500 Subject: [PATCH 04/18] style: rename --- graft/evm/firewood/triedb.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 9682c06523fd..b2e882a3ee94 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -56,10 +56,10 @@ type proposals struct { tree *proposal // possible temporarily holds proposals created during a trie update. // This is cleared after the update is complete and the proposals have been sent to the database. - possible map[unverifiedKey]*proposal + possible map[possibleKey]*proposal } -type unverifiedKey struct { +type possibleKey struct { parentBlockHash, root common.Hash //nolint:unused // It is used as a map key } @@ -167,7 +167,7 @@ func New(config Config, disk ethdb.Database) (*TrieDB, error) { height: height, }, }, - possible: make(map[unverifiedKey]*proposal), + possible: make(map[possibleKey]*proposal), }, }, nil } @@ -282,11 +282,11 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer return nil } - p, ok := t.possible[unverifiedKey{parentBlockHash: parentBlockHash, root: root}] + p, ok := t.possible[possibleKey{parentBlockHash: parentBlockHash, root: root}] // Now, all unused proposals have no other references, since we didn't store them // in the proposal map or tree, so they will be garbage collected. // Any proposals with a different root were mistakenly created, so they can be freed as well. - t.proposals.possible = make(map[unverifiedKey]*proposal) + t.proposals.possible = make(map[possibleKey]*proposal) if !ok { return fmt.Errorf("no proposal found for block %d, root %s, hash %s", height, root.Hex(), blockHash.Hex()) } @@ -520,7 +520,7 @@ func (t *TrieDB) getProposalHash(parentRoot common.Hash, keys, values [][]byte) return common.Hash{}, fmt.Errorf("proposing from root %s: %w", parentRoot.Hex(), err) } for parentHash := range t.proposals.tree.blockHashes { - t.possible[unverifiedKey{parentBlockHash: parentHash, root: p.root}] = p + t.possible[possibleKey{parentBlockHash: parentHash, root: root}] = p } count++ } @@ -538,7 +538,7 @@ func (t *TrieDB) getProposalHash(parentRoot common.Hash, keys, values [][]byte) } root = p.root for parentHash := range parent.blockHashes { - t.possible[unverifiedKey{parentBlockHash: parentHash, root: root}] = p + t.possible[possibleKey{parentBlockHash: parentHash, root: root}] = p } count++ } From 7f911f4d7ad38eb7cc4f05a51ccd99f8bca6f985 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Thu, 18 Dec 2025 17:07:18 -0500 Subject: [PATCH 05/18] chore: metrics and cleanup --- graft/evm/firewood/account_trie.go | 2 +- graft/evm/firewood/triedb.go | 86 ++++++++++++++++++------------ 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/graft/evm/firewood/account_trie.go b/graft/evm/firewood/account_trie.go index 6b6a58cb1db4..e62e63319965 100644 --- a/graft/evm/firewood/account_trie.go +++ b/graft/evm/firewood/account_trie.go @@ -211,7 +211,7 @@ func (a *accountTrie) Hash() common.Hash { func (a *accountTrie) hash() (common.Hash, error) { // If we haven't already hashed, we need to do so. if a.hasChanges { - root, err := a.fw.getProposalHash(a.parentRoot, a.updateKeys, a.updateValues) + root, err := a.fw.createProposals(a.parentRoot, a.updateKeys, a.updateValues) if err != nil { return common.Hash{}, err } diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index b2e882a3ee94..d0918249f8d5 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -19,6 +19,7 @@ import ( "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/libevm/stateconf" "github.com/ava-labs/libevm/log" + "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/libevm/trie/trienode" "github.com/ava-labs/libevm/trie/triestate" "github.com/ava-labs/libevm/triedb" @@ -30,19 +31,28 @@ const firewoodDir = "firewood" var ( _ triedb.DBConstructor = Config{}.BackendConstructor _ triedb.DBOverride = (*TrieDB)(nil) + + hashCount = metrics.GetOrRegisterCounter("firewood/triedb/hash/count", nil) + hashTimer = metrics.GetOrRegisterCounter("firewood/triedb/hash/time", nil) + commitCount = metrics.GetOrRegisterCounter("firewood/triedb/commit/count", nil) + commitTimer = metrics.GetOrRegisterCounter("firewood/triedb/commit/time", nil) + proposeOnDiskCount = metrics.GetOrRegisterCounter("firewood/triedb/propose/disk/count", nil) + proposeOnProposeCount = metrics.GetOrRegisterCounter("firewood/triedb/propose/proposal/count", nil) + explicitlyDroppedCount = metrics.GetOrRegisterCounter("firewood/triedb/drop/count", nil) ) // TrieDB is a triedb.DBOverride implementation backed by Firewood. // It acts as a HashDB for backwards compatibility with most of the blockchain code. type TrieDB struct { + proposals + // The underlying Firewood database, used for storing proposals and revisions. // This is exported as read-only, with knowledge that the consumer will not close it // and the latest state can be modified at any time. Firewood *ffi.Database + // kvStore is used for storing recovery information. kvStore ethdb.Database - - proposals } type proposals struct { @@ -206,16 +216,11 @@ func (t *TrieDB) Initialized(common.Hash) bool { return false } - // If the current root isn't empty, then unless the genesis block is empty, - // the database is initialized. return common.Hash(root) != types.EmptyRootHash } -// Size returns the storage size of diff layer nodes above the persistent disk -// layer and the dirty nodes buffered within the disk layer -// Only used for metrics and Commit intervals in APIs. -// This will be implemented in the firewood database eventually. -// Currently, Firewood stores all revisions in disk and proposals in memory. +// Size is a no-op because Firewood does not track storage size. +// All memory management is handled internally by Firewood. func (*TrieDB) Size() (common.StorageSize, common.StorageSize) { return 0, 0 } @@ -271,17 +276,13 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer // However, many tests cannot reasonably provide a block blockHash for genesis, so we allow it to be omitted. parentBlockHash, blockHash, ok := stateconf.ExtractTrieDBUpdatePayload(opts...) if !ok { - return fmt.Errorf("firewood: no block hash provided for block %d", height) + return fmt.Errorf("no block hash provided for block %d", height) } // The rest of the operations except key-value arranging must occur with a lock t.proposals.Lock() defer t.proposals.Unlock() - if t.proposals.exists(root, blockHash, parentBlockHash) { - return nil - } - p, ok := t.possible[possibleKey{parentBlockHash: parentBlockHash, root: root}] // Now, all unused proposals have no other references, since we didn't store them // in the proposal map or tree, so they will be garbage collected. @@ -291,6 +292,10 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer return fmt.Errorf("no proposal found for block %d, root %s, hash %s", height, root.Hex(), blockHash.Hex()) } + // If we have already created an identical proposal, we can skip adding it again. + if t.proposals.exists(root, blockHash, parentBlockHash) { + return nil + } switch { case p.root != root: return fmt.Errorf("proposal root mismatch, expected %x, got %x", root, p.root) @@ -308,10 +313,10 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer return nil } +// Check if this proposal already exists. +// During reorgs, we may have already tracked this block hash. +// Additionally, we may have coincidentally created an identical proposal with a different block hash. func (ps *proposals) exists(root, block, parentBlock common.Hash) bool { - // Check if this proposal already exists. - // During reorgs, we may have already created this proposal. - // Additionally, we may have already created this proposal with a different block hash. proposals, ok := ps.byStateRoot[root] if !ok { return false @@ -324,8 +329,7 @@ func (ps *proposals) exists(root, block, parentBlock common.Hash) bool { return true } - // We already have this proposal, but should create a new context with the correct hash. - // This solves the case of a unique block hash, but the same underlying proposal. + // We have an identical proposal, but should ensure the hash is tracked with this proposal. if _, ok := p.parent.blockHashes[parentBlock]; ok { log.Debug("proposal already exists, updating hash", "root", root.Hex(), "parent", parentBlock.Hex(), "block", block, "hash", block.Hex()) p.blockHashes[block] = struct{}{} @@ -346,6 +350,12 @@ func (ps *proposals) exists(root, block, parentBlock common.Hash) bool { // Afterward, we know that no other proposal at this height can be committed, so we can dereference all // children in the the other branches of the proposal tree. func (t *TrieDB) Commit(root common.Hash, report bool) error { + start := time.Now() + defer func() { + commitTimer.Inc(time.Since(start).Milliseconds()) + commitCount.Inc(1) + }() + t.proposals.Lock() defer t.proposals.Unlock() @@ -373,9 +383,8 @@ func (t *TrieDB) Commit(root common.Hash, report bool) error { } logFn("Persisted proposal to firewood database", "root", root) - // On success, we should dereference all children of the committed proposal. - // By removing all uncommittable proposals from the tree and map, - // we ensure that there are no more references. + // On success, we should remove all children of the committed proposal. + // They will never be committed. t.cleanupCommittedProposal(p) // Update the committed block hashes and height on disk to enable recovery. @@ -397,13 +406,13 @@ func (ps *proposals) findProposalToCommitWhenLocked(root common.Hash) (*proposal } if candidate != nil { // This should never happen, as we ensure that we don't create duplicate proposals in `propose`. - return nil, fmt.Errorf("firewood: multiple proposals found for root %#x", root) + return nil, fmt.Errorf("multiple proposals found for root %#x", root) } candidate = p } if candidate == nil { - return nil, fmt.Errorf("firewood: committable proposal not found for %d:%#x", ps.tree.height+1, root) + return nil, fmt.Errorf("committable proposal not found for %d:%#x", ps.tree.height+1, root) } return candidate, nil } @@ -413,10 +422,13 @@ func (t *TrieDB) createProposal(parent *proposal, keys, values [][]byte) (*propo propose := t.Firewood.Propose if h := parent.handle; h != nil { propose = h.Propose + proposeOnProposeCount.Inc(1) + } else { + proposeOnDiskCount.Inc(1) } handle, err := propose(keys, values) if err != nil { - return nil, fmt.Errorf("firewood: create proposal from parent root %s: %w", parent.root.Hex(), err) + return nil, fmt.Errorf("create proposal from parent root %s: %w", parent.root.Hex(), err) } // Edge case: genesis block @@ -436,7 +448,7 @@ func (t *TrieDB) createProposal(parent *proposal, keys, values [][]byte) (*propo root, err := handle.Root() if err != nil { - return nil, fmt.Errorf("firewood: getting root of proposal: %w", err) + return nil, fmt.Errorf("getting root of proposal: %w", err) } p.root = common.Hash(root) @@ -451,6 +463,7 @@ func (ps *proposals) cleanupCommittedProposal(p *proposal) { ps.tree.parent = nil ps.tree.handle = nil + // Since this propose has been committed, it doesn't need dropped. ps.removeProposalFromMap(p.proposalMeta, false) for _, child := range oldChildren { @@ -461,19 +474,18 @@ func (ps *proposals) cleanupCommittedProposal(p *proposal) { } // Internally removes all references of the proposal from the database. +// Frees the associated Rust memory for the proposal and all its children. // Should only be accessed with the proposal lock held. func (ps *proposals) removeProposalAndChildren(p *proposalMeta) { - // Base case: if there are children, we need to dereference them as well. for _, child := range p.children { ps.removeProposalAndChildren(child) } - - // Remove the proposal from the map. ps.removeProposalFromMap(p, true) } -// removeProposalFromMap removes the proposal from the proposal map. +// removeProposalFromMap removes the proposal from the state root map. // The proposal lock must be held when calling this function. +// The Rust memory is explicitly freed if drop is true. func (ps *proposals) removeProposalFromMap(meta *proposalMeta, drop bool) { rootList := ps.byStateRoot[meta.root] for i, p := range rootList { @@ -483,6 +495,7 @@ func (ps *proposals) removeProposalFromMap(meta *proposalMeta, drop bool) { rootList = rootList[:len(rootList)-1] if drop { + explicitlyDroppedCount.Inc(1) if err := p.handle.Drop(); err != nil { log.Error("while dropping proposal", "root", meta.root, "height", meta.height, "err", err) } @@ -497,14 +510,21 @@ func (ps *proposals) removeProposalFromMap(meta *proposalMeta, drop bool) { } } -// getProposalHash calculates the hash if the set of keys and values are +// createProposals calculates the hash if the set of keys and values are // proposed from the given parent root. -func (t *TrieDB) getProposalHash(parentRoot common.Hash, keys, values [][]byte) (common.Hash, error) { +// All proposals created will be tracked for future use. +func (t *TrieDB) createProposals(parentRoot common.Hash, keys, values [][]byte) (common.Hash, error) { + start := time.Now() + defer func() { + hashTimer.Inc(time.Since(start).Milliseconds()) + hashCount.Inc(1) + }() + if len(keys) != len(values) { return common.Hash{}, fmt.Errorf("keys and values must have the same length, got %d keys and %d values", len(keys), len(values)) } - // This function only reads from existing tracked proposals, so we can use a read lock. + // Must prevent a simultaneous `Commit`, as it alters the proposal tree/disk state. t.proposals.RLock() defer t.proposals.RUnlock() From 2eea57667b4b7dd0cfb9da7d9f8887286d08f1d0 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Thu, 18 Dec 2025 17:28:31 -0500 Subject: [PATCH 06/18] fix: copilot comments --- graft/evm/firewood/account_trie.go | 2 +- graft/evm/firewood/triedb.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graft/evm/firewood/account_trie.go b/graft/evm/firewood/account_trie.go index e62e63319965..20a903347df0 100644 --- a/graft/evm/firewood/account_trie.go +++ b/graft/evm/firewood/account_trie.go @@ -273,7 +273,7 @@ func (a *accountTrie) Copy() *accountTrie { parentRoot: a.parentRoot, root: a.root, reader: a.reader, // Share the same reader - hasChanges: true, // Mark as having changes to ensure re-hashing + hasChanges: a.hasChanges, dirtyKeys: make(map[string][]byte, len(a.dirtyKeys)), updateKeys: make([][]byte, len(a.updateKeys)), updateValues: make([][]byte, len(a.updateValues)), diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index d0918249f8d5..c2d567dac0b4 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -325,13 +325,13 @@ func (ps *proposals) exists(root, block, parentBlock common.Hash) bool { for _, p := range proposals { // If the block hash is already tracked, we can skip proposing this again. if _, ok := p.blockHashes[block]; ok { - log.Debug("proposal already exists", "root", root.Hex(), "parent", parentBlock.Hex(), "block", block, "hash", block.Hex()) + log.Debug("proposal already exists", "root", root.Hex(), "parentBlock", parentBlock.Hex(), "block", block.Hex()) return true } // We have an identical proposal, but should ensure the hash is tracked with this proposal. if _, ok := p.parent.blockHashes[parentBlock]; ok { - log.Debug("proposal already exists, updating hash", "root", root.Hex(), "parent", parentBlock.Hex(), "block", block, "hash", block.Hex()) + log.Debug("proposal already exists, updating hash", "root", root.Hex(), "parentBlock", parentBlock.Hex(), "block", block.Hex()) p.blockHashes[block] = struct{}{} return true } From 468547746c988c764c7cbcce7612bb0ee6f0c260 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Mon, 5 Jan 2026 12:10:12 -0500 Subject: [PATCH 07/18] fix: remove race in possible map --- graft/evm/firewood/triedb.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index c2d567dac0b4..ed11ee54d278 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -53,6 +53,12 @@ type TrieDB struct { // kvStore is used for storing recovery information. kvStore ethdb.Database + + // possible temporarily holds proposals created during a trie update. + // This is cleared after the update is complete and the proposals have been sent to the database. + // It's unexpected for mulitple updates to this to occur simultaneously, but a lock is used to ensure safety. + possible map[possibleKey]*proposal + possibleLock sync.Mutex } type proposals struct { @@ -64,9 +70,6 @@ type proposals struct { // in the case of duplicate state roots. // The root of the tree is stored here, and represents the top-most layer on disk. tree *proposal - // possible temporarily holds proposals created during a trie update. - // This is cleared after the update is complete and the proposals have been sent to the database. - possible map[possibleKey]*proposal } type possibleKey struct { @@ -177,8 +180,8 @@ func New(config Config, disk ethdb.Database) (*TrieDB, error) { height: height, }, }, - possible: make(map[possibleKey]*proposal), }, + possible: make(map[possibleKey]*proposal), }, nil } @@ -247,6 +250,8 @@ func (t *TrieDB) Close() error { p := &t.proposals p.Lock() defer p.Unlock() + t.possibleLock.Lock() + defer t.possibleLock.Unlock() if p.tree == nil { return nil // already closed @@ -258,7 +263,7 @@ func (t *TrieDB) Close() error { } p.tree = nil p.byStateRoot = nil - p.possible = nil + t.possible = nil // We must provide a context to close since it may hang while waiting for the finalizers to complete. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -282,12 +287,14 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer // The rest of the operations except key-value arranging must occur with a lock t.proposals.Lock() defer t.proposals.Unlock() + t.possibleLock.Lock() + defer t.possibleLock.Unlock() p, ok := t.possible[possibleKey{parentBlockHash: parentBlockHash, root: root}] // Now, all unused proposals have no other references, since we didn't store them // in the proposal map or tree, so they will be garbage collected. // Any proposals with a different root were mistakenly created, so they can be freed as well. - t.proposals.possible = make(map[possibleKey]*proposal) + clear(t.possible) if !ok { return fmt.Errorf("no proposal found for block %d, root %s, hash %s", height, root.Hex(), blockHash.Hex()) } @@ -527,6 +534,8 @@ func (t *TrieDB) createProposals(parentRoot common.Hash, keys, values [][]byte) // Must prevent a simultaneous `Commit`, as it alters the proposal tree/disk state. t.proposals.RLock() defer t.proposals.RUnlock() + t.possibleLock.Lock() + defer t.possibleLock.Unlock() var ( count int // number of proposals created. From e295fda1881b746f9847c3f1d44000938d8c3477 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Tue, 6 Jan 2026 16:50:32 -0500 Subject: [PATCH 08/18] fix: wrong order of error handling --- graft/evm/firewood/triedb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 1485bbc29d0e..8449b5d9fd7f 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -543,10 +543,10 @@ func (t *TrieDB) createProposals(parentRoot common.Hash, keys, values [][]byte) if t.proposals.tree.root == parentRoot { // Propose from the database root. p, err := t.createProposal(t.proposals.tree, keys, values) - root = p.root if err != nil { return common.Hash{}, fmt.Errorf("proposing from root %s: %w", parentRoot.Hex(), err) } + root = p.root for parentHash := range t.proposals.tree.blockHashes { t.possible[possibleKey{parentBlockHash: parentHash, root: root}] = p } From 7a5f6aa6d6a4c5c3f06fdac2f50ceee073f01e3f Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Fri, 9 Jan 2026 15:40:08 -0500 Subject: [PATCH 09/18] chore: lint --- graft/evm/firewood/triedb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 8449b5d9fd7f..94849cea3f0f 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -56,7 +56,7 @@ type TrieDB struct { // possible temporarily holds proposals created during a trie update. // This is cleared after the update is complete and the proposals have been sent to the database. - // It's unexpected for mulitple updates to this to occur simultaneously, but a lock is used to ensure safety. + // It's unexpected for multiple updates to this to occur simultaneously, but a lock is used to ensure safety. possible map[possibleKey]*proposal possibleLock sync.Mutex } From e2e94a02c3d3e9237d36bce9de32c2194f0760d0 Mon Sep 17 00:00:00 2001 From: Austin Larson <78000745+alarso16@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:25:07 -0500 Subject: [PATCH 10/18] refactor: Use Blockchain state to populate existing (#4835) --- graft/coreth/core/blockchain.go | 6 +++ graft/coreth/core/genesis.go | 16 +++---- graft/evm/firewood/recovery.go | 66 ----------------------------- graft/evm/firewood/triedb.go | 39 ++++++++--------- graft/subnet-evm/core/blockchain.go | 6 +++ graft/subnet-evm/core/genesis.go | 16 +++---- 6 files changed, 39 insertions(+), 110 deletions(-) delete mode 100644 graft/evm/firewood/recovery.go diff --git a/graft/coreth/core/blockchain.go b/graft/coreth/core/blockchain.go index 48e05541375d..3b0a6e6f113d 100644 --- a/graft/coreth/core/blockchain.go +++ b/graft/coreth/core/blockchain.go @@ -1851,6 +1851,9 @@ func (bc *BlockChain) reprocessState(current *types.Block, reexec uint64) error // If the state is already available and the acceptor tip is up to date, skip re-processing. if bc.HasState(current.Root()) && acceptorTipUpToDate { + if t, ok := bc.triedb.Backend().(*firewood.TrieDB); ok { + t.SetHashAndHeight(current.Hash(), current.NumberU64()) + } log.Info("Skipping state reprocessing", "root", current.Root()) return nil } @@ -1902,6 +1905,9 @@ func (bc *BlockChain) reprocessState(current *types.Block, reexec uint64) error ) // Note: we add 1 since in each iteration, we attempt to re-execute the next block. log.Info("Re-executing blocks to generate state for last accepted block", "from", current.NumberU64()+1, "to", origin) + if t, ok := bc.triedb.Backend().(*firewood.TrieDB); ok { + t.SetHashAndHeight(current.Hash(), current.NumberU64()) + } var roots []common.Hash for current.NumberU64() < origin { // TODO: handle canceled context diff --git a/graft/coreth/core/genesis.go b/graft/coreth/core/genesis.go index fea4deccdf50..bf257a4ae33a 100644 --- a/graft/coreth/core/genesis.go +++ b/graft/coreth/core/genesis.go @@ -339,16 +339,15 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo if _, err := statedb.Commit(0, false, stateconf.WithTrieDBUpdateOpts(triedbOpt)); err != nil { panic(fmt.Sprintf("unable to commit genesis block to statedb: %v", err)) } - if root == types.EmptyRootHash && isFirewood(triedb) { - if err := triedb.Update(root, root, 0, nil, nil, triedbOpt); err != nil { - panic(fmt.Sprintf("unable to update genesis block in triedb: %v", err)) - } - } // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash || isFirewood(triedb) { + if root != types.EmptyRootHash { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) } + } else { + if t, ok := triedb.Backend().(*firewood.TrieDB); ok { + t.SetHashAndHeight(block.Hash(), 0) + } } return block } @@ -404,8 +403,3 @@ func ReadBlockByHash(db ethdb.Reader, hash common.Hash) *types.Block { } return rawdb.ReadBlock(db, hash, *blockNumber) } - -func isFirewood(db *triedb.Database) bool { - _, ok := db.Backend().(*firewood.TrieDB) - return ok -} diff --git a/graft/evm/firewood/recovery.go b/graft/evm/firewood/recovery.go deleted file mode 100644 index 499bc971c334..000000000000 --- a/graft/evm/firewood/recovery.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package firewood - -import ( - "encoding/binary" - "fmt" - - "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/ethdb" -) - -const ( - committedBlockHashKey = "committedFirewoodBlockHash" - committedHeightKey = "committedFirewoodHeight" -) - -// ReadCommittedBlockHash retrieves the most recently committed block hash from the key-value store. -func ReadCommittedBlockHashes(kvStore ethdb.Database) (map[common.Hash]struct{}, error) { - data, _ := kvStore.Get([]byte(committedBlockHashKey)) // ignore not found error - if len(data)%common.HashLength != 0 { - return nil, fmt.Errorf("invalid committed block hash length: expected multiple of %d, got %d", common.HashLength, len(data)) - } - hashes := make(map[common.Hash]struct{}) - if len(data) == 0 { - hashes[common.Hash{}] = struct{}{} - return hashes, nil - } - for i := 0; i < len(data); i += common.HashLength { - hash := common.BytesToHash(data[i : i+common.HashLength]) - hashes[hash] = struct{}{} - } - return hashes, nil -} - -// WriteCommittedBlockHash writes the most recently committed block hash to the key-value store. -func WriteCommittedBlockHashes(kvStore ethdb.Database, hashes map[common.Hash]struct{}) error { - contents := make([]byte, 0, len(hashes)*common.HashLength) - for hash := range hashes { - contents = append(contents, hash.Bytes()...) - } - if err := kvStore.Put([]byte(committedBlockHashKey), contents); err != nil { - return fmt.Errorf("error writing committed block hashes: %w", err) - } - return nil -} - -// ReadCommittedHeight retrieves the most recently committed height from the key-value store. -func ReadCommittedHeight(kvStore ethdb.Database) uint64 { - data, _ := kvStore.Get([]byte(committedHeightKey)) - if len(data) != 8 { - return 0 - } - return binary.BigEndian.Uint64(data) -} - -// WriteCommittedHeight writes the most recently committed height to the key-value store. -func WriteCommittedHeight(kvStore ethdb.Database, height uint64) error { - enc := make([]byte, 8) - binary.BigEndian.PutUint64(enc, height) - if err := kvStore.Put([]byte(committedHeightKey), enc); err != nil { - return fmt.Errorf("error writing committed height: %w", err) - } - return nil -} diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 94849cea3f0f..b5aae4ecd4db 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -51,9 +51,6 @@ type TrieDB struct { // and the latest state can be modified at any time. Firewood *ffi.Database - // kvStore is used for storing recovery information. - kvStore ethdb.Database - // possible temporarily holds proposals created during a trie update. // This is cleared after the update is complete and the proposals have been sent to the database. // It's unexpected for multiple updates to this to occur simultaneously, but a lock is used to ensure safety. @@ -123,8 +120,8 @@ func DefaultConfig(dir string) TrieDBConfig { // BackendConstructor implements the [triedb.DBConstructor] interface. // It creates a new Firewood database with the given configuration. // Any error during creation will cause the program to exit. -func (c TrieDBConfig) BackendConstructor(disk ethdb.Database) triedb.DBOverride { - db, err := New(c, disk) +func (c TrieDBConfig) BackendConstructor(ethdb.Database) triedb.DBOverride { + db, err := New(c) if err != nil { log.Crit("firewood: creating database", "error", err) } @@ -133,13 +130,7 @@ func (c TrieDBConfig) BackendConstructor(disk ethdb.Database) triedb.DBOverride // New creates a new Firewood database with the given configuration. // The database will not be opened on error. -func New(config TrieDBConfig, disk ethdb.Database) (*TrieDB, error) { - height := ReadCommittedHeight(disk) - blockHashes, err := ReadCommittedBlockHashes(disk) - if err != nil { - return nil, err - } - +func New(config TrieDBConfig) (*TrieDB, error) { if err := validateDir(config.DatabaseDir); err != nil { return nil, err } @@ -167,16 +158,17 @@ func New(config TrieDBConfig, disk ethdb.Database) (*TrieDB, error) { return nil, err } + blockHashes := make(map[common.Hash]struct{}) + blockHashes[common.Hash{}] = struct{}{} return &TrieDB{ Firewood: fw, - kvStore: disk, proposals: proposals{ byStateRoot: make(map[common.Hash][]*proposal), tree: &proposal{ proposalMeta: &proposalMeta{ root: common.Hash(intialRoot), blockHashes: blockHashes, - height: height, + height: 0, }, }, }, @@ -204,6 +196,17 @@ func validateDir(dir string) error { return nil } +// SetHashAndHeight sets the committed block hashes and height in memory. +// This must be called at startup to initialize the in-memory state, unless +// explicitly committing a genesis block. +func (t *TrieDB) SetHashAndHeight(blockHash common.Hash, height uint64) { + t.Lock() + defer t.Unlock() + clear(t.tree.blockHashes) + t.tree.blockHashes[blockHash] = struct{}{} + t.tree.height = height +} + // Scheme returns the scheme of the database. // However, to avoid a slow deletion in `libevm` `StateDB`, it returns [rawdb.HashScheme]. func (*TrieDB) Scheme() string { @@ -392,14 +395,6 @@ func (t *TrieDB) Commit(root common.Hash, report bool) error { // On success, we should remove all children of the committed proposal. // They will never be committed. t.cleanupCommittedProposal(p) - - // Update the committed block hashes and height on disk to enable recovery. - if err := WriteCommittedBlockHashes(t.kvStore, p.blockHashes); err != nil { - return err - } - if err := WriteCommittedHeight(t.kvStore, p.height); err != nil { - return err - } return nil } diff --git a/graft/subnet-evm/core/blockchain.go b/graft/subnet-evm/core/blockchain.go index cbcb1280da70..1db6ae93e451 100644 --- a/graft/subnet-evm/core/blockchain.go +++ b/graft/subnet-evm/core/blockchain.go @@ -1882,6 +1882,9 @@ func (bc *BlockChain) reprocessState(current *types.Block, reexec uint64) error // If the state is already available and the acceptor tip is up to date, skip re-processing. if bc.HasState(current.Root()) && acceptorTipUpToDate { + if t, ok := bc.triedb.Backend().(*firewood.TrieDB); ok { + t.SetHashAndHeight(current.Hash(), current.NumberU64()) + } log.Info("Skipping state reprocessing", "root", current.Root()) return nil } @@ -1933,6 +1936,9 @@ func (bc *BlockChain) reprocessState(current *types.Block, reexec uint64) error ) // Note: we add 1 since in each iteration, we attempt to re-execute the next block. log.Info("Re-executing blocks to generate state for last accepted block", "from", current.NumberU64()+1, "to", origin) + if t, ok := bc.triedb.Backend().(*firewood.TrieDB); ok { + t.SetHashAndHeight(current.Hash(), current.NumberU64()) + } var roots []common.Hash for current.NumberU64() < origin { // TODO: handle canceled context diff --git a/graft/subnet-evm/core/genesis.go b/graft/subnet-evm/core/genesis.go index 7c5bafd1186f..ff2f4b6cd434 100644 --- a/graft/subnet-evm/core/genesis.go +++ b/graft/subnet-evm/core/genesis.go @@ -377,16 +377,15 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo if _, err := statedb.Commit(0, false, stateconf.WithTrieDBUpdateOpts(triedbOpt)); err != nil { panic(fmt.Sprintf("unable to commit genesis block to statedb: %v", err)) } - if root == types.EmptyRootHash && isFirewood(triedb) { - if err := triedb.Update(root, root, 0, nil, nil, triedbOpt); err != nil { - panic(fmt.Sprintf("unable to update genesis block in triedb: %v", err)) - } - } // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash || isFirewood(triedb) { + if root != types.EmptyRootHash { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) } + } else { + if t, ok := triedb.Backend().(*firewood.TrieDB); ok { + t.SetHashAndHeight(block.Hash(), block.NumberU64()) + } } return block } @@ -463,8 +462,3 @@ func ReadBlockByHash(db ethdb.Reader, hash common.Hash) *types.Block { } return rawdb.ReadBlock(db, hash, *blockNumber) } - -func isFirewood(db *triedb.Database) bool { - _, ok := db.Backend().(*firewood.TrieDB) - return ok -} From b37c3141990f5b684ee643e42b58f3ce7a8fe716 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Thu, 15 Jan 2026 12:29:34 -0500 Subject: [PATCH 11/18] chore: rename function --- graft/evm/firewood/triedb.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index b5aae4ecd4db..94c1c895f9ff 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -302,7 +302,7 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer } // If we have already created an identical proposal, we can skip adding it again. - if t.proposals.exists(root, blockHash, parentBlockHash) { + if t.proposals.existsOrTrack(root, blockHash, parentBlockHash) { return nil } switch { @@ -322,10 +322,10 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer return nil } -// Check if this proposal already exists. +// Check if this proposal already existsOrTrack. // During reorgs, we may have already tracked this block hash. // Additionally, we may have coincidentally created an identical proposal with a different block hash. -func (ps *proposals) exists(root, block, parentBlock common.Hash) bool { +func (ps *proposals) existsOrTrack(root, block, parentBlock common.Hash) bool { proposals, ok := ps.byStateRoot[root] if !ok { return false From 629474849dc89c4b217794e049e98daa2b8bf0e1 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Thu, 15 Jan 2026 16:19:49 -0500 Subject: [PATCH 12/18] test: Add basic tests --- graft/evm/firewood/triedb.go | 46 +++--- graft/evm/firewood/triedb_test.go | 247 ++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 25 deletions(-) create mode 100644 graft/evm/firewood/triedb_test.go diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 94c1c895f9ff..545bbfea998f 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -39,6 +39,8 @@ var ( proposeOnDiskCount = metrics.GetOrRegisterCounter("firewood/triedb/propose/disk/count", nil) proposeOnProposeCount = metrics.GetOrRegisterCounter("firewood/triedb/propose/proposal/count", nil) explicitlyDroppedCount = metrics.GetOrRegisterCounter("firewood/triedb/drop/count", nil) + + errNoProposalFound = errors.New("no proposal found") ) // TrieDB is a triedb.DBOverride implementation backed by Firewood. @@ -50,16 +52,10 @@ type TrieDB struct { // This is exported as read-only, with knowledge that the consumer will not close it // and the latest state can be modified at any time. Firewood *ffi.Database - - // possible temporarily holds proposals created during a trie update. - // This is cleared after the update is complete and the proposals have been sent to the database. - // It's unexpected for multiple updates to this to occur simultaneously, but a lock is used to ensure safety. - possible map[possibleKey]*proposal - possibleLock sync.Mutex } type proposals struct { - sync.RWMutex + sync.Mutex byStateRoot map[common.Hash][]*proposal // The proposal tree tracks the structure of the current proposals, and which proposals are children of which. @@ -67,6 +63,11 @@ type proposals struct { // in the case of duplicate state roots. // The root of the tree is stored here, and represents the top-most layer on disk. tree *proposal + + // possible temporarily holds proposals created during a trie update. + // This is cleared after the update is complete and the proposals have been sent to the database. + // It's unexpected for multiple updates to this to occur simultaneously, but a lock is used to ensure safety. + possible map[possibleKey]*proposal } type possibleKey struct { @@ -123,7 +124,7 @@ func DefaultConfig(dir string) TrieDBConfig { func (c TrieDBConfig) BackendConstructor(ethdb.Database) triedb.DBOverride { db, err := New(c) if err != nil { - log.Crit("firewood: creating database", "error", err) + log.Crit("creating firewood database", "error", err) } return db } @@ -147,10 +148,10 @@ func New(config TrieDBConfig) (*TrieDB, error) { fw, err := ffi.New(path, options...) if err != nil { - return nil, fmt.Errorf("opening firewood database: %w", err) + return nil, fmt.Errorf("opening database: %w", err) } - intialRoot, err := fw.Root() + initialRoot, err := fw.Root() if err != nil { if closeErr := fw.Close(context.Background()); closeErr != nil { return nil, fmt.Errorf("%w: error while closing: %w", err, closeErr) @@ -166,16 +167,17 @@ func New(config TrieDBConfig) (*TrieDB, error) { byStateRoot: make(map[common.Hash][]*proposal), tree: &proposal{ proposalMeta: &proposalMeta{ - root: common.Hash(intialRoot), + root: common.Hash(initialRoot), blockHashes: blockHashes, height: 0, }, }, + possible: make(map[possibleKey]*proposal), }, - possible: make(map[possibleKey]*proposal), }, nil } +// validateDir ensures that the given directory exists and is a directory. func validateDir(dir string) error { if dir == "" { return errors.New("chain data directory must be set") @@ -184,7 +186,7 @@ func validateDir(dir string) error { switch info, err := os.Stat(dir); { case os.IsNotExist(err): log.Info("Database directory not found, creating", "path", dir) - if err := os.MkdirAll(dir, 0o750); err != nil { + if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("creating database directory: %w", err) } case err != nil: @@ -252,8 +254,6 @@ func (t *TrieDB) Close() error { p := &t.proposals p.Lock() defer p.Unlock() - t.possibleLock.Lock() - defer t.possibleLock.Unlock() if p.tree == nil { return nil // already closed @@ -289,8 +289,6 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer // The rest of the operations except key-value arranging must occur with a lock t.proposals.Lock() defer t.proposals.Unlock() - t.possibleLock.Lock() - defer t.possibleLock.Unlock() p, ok := t.possible[possibleKey{parentBlockHash: parentBlockHash, root: root}] // Now, all unused proposals have no other references, since we didn't store them @@ -298,11 +296,11 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer // Any proposals with a different root were mistakenly created, so they can be freed as well. clear(t.possible) if !ok { - return fmt.Errorf("no proposal found for block %d, root %s, hash %s", height, root.Hex(), blockHash.Hex()) + return fmt.Errorf("%w for block %d, root %s, hash %s", errNoProposalFound, height, root.Hex(), blockHash.Hex()) } // If we have already created an identical proposal, we can skip adding it again. - if t.proposals.existsOrTrack(root, blockHash, parentBlockHash) { + if t.proposals.exists(root, blockHash, parentBlockHash) { return nil } switch { @@ -322,10 +320,10 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer return nil } -// Check if this proposal already existsOrTrack. +// Check if this proposal already exists. // During reorgs, we may have already tracked this block hash. // Additionally, we may have coincidentally created an identical proposal with a different block hash. -func (ps *proposals) existsOrTrack(root, block, parentBlock common.Hash) bool { +func (ps *proposals) exists(root, block, parentBlock common.Hash) bool { proposals, ok := ps.byStateRoot[root] if !ok { return false @@ -526,10 +524,8 @@ func (t *TrieDB) createProposals(parentRoot common.Hash, keys, values [][]byte) } // Must prevent a simultaneous `Commit`, as it alters the proposal tree/disk state. - t.proposals.RLock() - defer t.proposals.RUnlock() - t.possibleLock.Lock() - defer t.possibleLock.Unlock() + t.proposals.Lock() + defer t.proposals.Unlock() var ( count int // number of proposals created. diff --git a/graft/evm/firewood/triedb_test.go b/graft/evm/firewood/triedb_test.go new file mode 100644 index 000000000000..ffd3bc0a04e7 --- /dev/null +++ b/graft/evm/firewood/triedb_test.go @@ -0,0 +1,247 @@ +// Copyright (C) 2019, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package firewood + +import ( + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/libevm/stateconf" + "github.com/ava-labs/libevm/trie/trienode" + "github.com/ava-labs/libevm/triedb" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +func newTestDatabase(t *testing.T) state.Database { + t.Helper() + fwConfig := DefaultConfig(t.TempDir()) + triedbConfig := &triedb.Config{ + DBOverride: fwConfig.BackendConstructor, + } + internalState := state.NewDatabaseWithConfig( + rawdb.NewMemoryDatabase(), + triedbConfig, + ) + tdb := internalState.TrieDB().Backend().(*TrieDB) + t.Cleanup(func() { + require.NoError(t, tdb.Close()) + }) + + return NewStateAccessor(internalState, tdb) +} + +func TestCommitEmptyGenesis(t *testing.T) { + db := newTestDatabase(t) + triedb := db.TrieDB() + + tr, err := db.OpenTrie(types.EmptyRootHash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + + root := tr.Hash() + require.Equal(t, types.EmptyRootHash, root) + + root, nodes, err := tr.Commit(true) + require.NoErrorf(t, err, "%T.Commit()", tr) + require.Equal(t, types.EmptyRootHash, root) + + mergedNodes := trienode.NewMergedNodeSet() + require.NoErrorf(t, mergedNodes.Merge(nodes), "%T.Merge()", mergedNodes) + + require.NoErrorf( + t, + triedb.Update( + types.EmptyRootHash, + types.EmptyRootHash, + 0, mergedNodes, nil, + stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), + ), + "%T.Update()", triedb, + ) + + require.NoErrorf(t, triedb.Commit(types.EmptyRootHash, true), "%T.Commit()", triedb) +} + +func generateAccount(addr common.Address) types.StateAccount { + return types.StateAccount{ + Balance: uint256.NewInt(0).SetBytes(addr[:]), + } +} + +func verifyAccount(t *testing.T, tr state.Trie, addr common.Address, expected types.StateAccount) { + t.Helper() + acct, err := tr.GetAccount(addr) + require.NoErrorf(t, err, "%T.GetAccount(%s)", tr, addr) + require.Equalf(t, expected.Balance, acct.Balance, "%T.GetAccount(%s) balance", tr, addr) + require.Equalf(t, expected.Nonce, acct.Nonce, "%T.GetAccount(%s) nonce", tr, addr) +} + +func TestAccountPersistence(t *testing.T) { + db := newTestDatabase(t) + triedb := db.TrieDB() + + tr, err := db.OpenTrie(types.EmptyRootHash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + require.NotNil(t, tr) + + addr := common.HexToAddress("1234") + acct := generateAccount(addr) + require.NoErrorf(t, tr.UpdateAccount(addr, &acct), "%T.UpdateAccount()", tr) + verifyAccount(t, tr, addr, acct) + + hash := tr.Hash() + require.NotEqual(t, types.EmptyRootHash, hash) + + root, nodes, err := tr.Commit(true) + require.NoErrorf(t, err, "%T.Commit()", tr) + require.Equal(t, hash, root) + + mergedNodes := trienode.NewMergedNodeSet() + require.NoErrorf(t, mergedNodes.Merge(nodes), "%T.Merge()", mergedNodes) + + require.NoErrorf( + t, + triedb.Update( + hash, + types.EmptyRootHash, + 0, mergedNodes, nil, + stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), + ), + "%T.Update()", triedb, + ) + + // We should be able to read the account before and after committing. + tr, err = db.OpenTrie(hash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + verifyAccount(t, tr, addr, acct) + + require.NoErrorf(t, triedb.Commit(hash, true), "%T.Commit()", triedb) + tr, err = db.OpenTrie(hash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + verifyAccount(t, tr, addr, acct) +} + +func TestStoragePersistence(t *testing.T) { + db := newTestDatabase(t) + triedb := db.TrieDB() + + tr, err := db.OpenTrie(types.EmptyRootHash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + require.NotNil(t, tr) + + addr := common.HexToAddress("1234") + acct := generateAccount(addr) + require.NoErrorf(t, tr.UpdateAccount(addr, &acct), "%T.UpdateAccount()", tr) + + key := []byte{1} + value := []byte{2} + // A separate Storage Trie is expected to be used. + require.NoErrorf(t, tr.UpdateStorage(addr, key, value), "%T.UpdateStorage()", tr) + + // Retrievable before hashing + storedValue, err := tr.GetStorage(addr, key) + require.NoErrorf(t, err, "%T.GetStorage()", tr) + require.Equalf(t, value, storedValue, "%T.GetStorage() value", tr) + + hash := tr.Hash() + require.NotEqual(t, types.EmptyRootHash, hash) + + root, nodes, err := tr.Commit(true) + require.NoErrorf(t, err, "%T.Commit()", tr) + require.Equal(t, hash, root) + + mergedNodes := trienode.NewMergedNodeSet() + require.NoErrorf(t, mergedNodes.Merge(nodes), "%T.Merge()", mergedNodes) + + require.NoErrorf( + t, + triedb.Update( + hash, + types.EmptyRootHash, + 0, mergedNodes, nil, + stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), + ), + "%T.Update()", triedb, + ) + + // We should be able to read the storage before and after committing. + tr, err = db.OpenTrie(hash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + verifyAccount(t, tr, addr, acct) + + storedValue, err = tr.GetStorage(addr, key) + require.NoErrorf(t, err, "%T.GetStorage()", tr) + require.Equalf(t, value, storedValue, "%T.GetStorage() value", tr) + + require.NoErrorf(t, triedb.Commit(hash, true), "%T.Commit()", triedb) + tr, err = db.OpenTrie(hash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + verifyAccount(t, tr, addr, acct) + + storedValue, err = tr.GetStorage(addr, key) + require.NoErrorf(t, err, "%T.GetStorage()", tr) + require.Equalf(t, value, storedValue, "%T.GetStorage() value", tr) +} + +// Ensure that even if other tries are hashed, others can still be persisted via Update. +// Additionally, the now stale tries should not be accessible. +func TestParallelHashing(t *testing.T) { + db := newTestDatabase(t) + triedb := db.TrieDB() + + tr1, err := db.OpenTrie(types.EmptyRootHash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + require.NotNil(t, tr1) + + addr1 := common.HexToAddress("1234") + acct1 := generateAccount(addr1) + require.NoErrorf(t, tr1.UpdateAccount(addr1, &acct1), "%T.UpdateAccount()", tr1) + + tr2, err := db.OpenTrie(types.EmptyRootHash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + require.NotNil(t, tr2) + + addr2 := common.HexToAddress("5678") + acct2 := generateAccount(addr2) + require.NoErrorf(t, tr2.UpdateAccount(addr2, &acct2), "%T.UpdateAccount()", tr2) + + hash1 := tr1.Hash() + hash2 := tr2.Hash() + require.NotEqual(t, hash1, hash2) + + // Commit both tries + root1, nodes1, err := tr1.Commit(true) + require.NoErrorf(t, err, "%T.Commit()", tr1) + require.Equal(t, hash1, root1) + root2, nodes2, err := tr2.Commit(true) + require.NoErrorf(t, err, "%T.Commit()", tr2) + require.Equal(t, hash2, root2) + + mergedNodes1 := trienode.NewMergedNodeSet() + require.NoErrorf(t, mergedNodes1.Merge(nodes1), "%T.Merge()", mergedNodes1) + mergedNodes2 := trienode.NewMergedNodeSet() + require.NoErrorf(t, mergedNodes2.Merge(nodes2), "%T.Merge()", mergedNodes2) + + require.NoErrorf( + t, + triedb.Update( + hash1, + types.EmptyRootHash, + 0, mergedNodes1, nil, + stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), + ), + "%T.Update()", triedb, + ) + + err = triedb.Update( + hash2, + types.EmptyRootHash, + 0, mergedNodes2, nil, + stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), + ) + require.ErrorIsf(t, err, errNoProposalFound, "%T.Update()", triedb) +} From 93f3d7edf3cfd155a4b36facb81054d56cc3a5c5 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Fri, 16 Jan 2026 11:24:14 -0500 Subject: [PATCH 13/18] test: Allow mistakes to be corrected on Update --- graft/evm/firewood/triedb.go | 20 ++++---- graft/evm/firewood/triedb_test.go | 77 +++++++++++++++++++++---------- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 545bbfea998f..4ea0b009cc2c 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -40,7 +40,8 @@ var ( proposeOnProposeCount = metrics.GetOrRegisterCounter("firewood/triedb/propose/proposal/count", nil) explicitlyDroppedCount = metrics.GetOrRegisterCounter("firewood/triedb/drop/count", nil) - errNoProposalFound = errors.New("no proposal found") + errNoProposalFound = errors.New("no proposal found") + errUnexpectedProposalFound = errors.New("unexpected proposal found") ) // TrieDB is a triedb.DBOverride implementation backed by Firewood. @@ -291,32 +292,33 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer defer t.proposals.Unlock() p, ok := t.possible[possibleKey{parentBlockHash: parentBlockHash, root: root}] - // Now, all unused proposals have no other references, since we didn't store them - // in the proposal map or tree, so they will be garbage collected. - // Any proposals with a different root were mistakenly created, so they can be freed as well. - clear(t.possible) if !ok { return fmt.Errorf("%w for block %d, root %s, hash %s", errNoProposalFound, height, root.Hex(), blockHash.Hex()) } // If we have already created an identical proposal, we can skip adding it again. if t.proposals.exists(root, blockHash, parentBlockHash) { + // All unused proposals can be cleared, since we are already tracking an identical one. + clear(t.possible) return nil } switch { case p.root != root: - return fmt.Errorf("proposal root mismatch, expected %x, got %x", root, p.root) + return fmt.Errorf("%w: expected root %#x, got %#x", errUnexpectedProposalFound, root, p.root) case p.parent.root != parent: - return fmt.Errorf("parent root mismatch, expected %#x, got %x", parent, p.parent.root) + return fmt.Errorf("%w: expected parent root %#x, got %#x", errUnexpectedProposalFound, parent, p.parent.root) case p.height != height: - return fmt.Errorf("height mismatch, expected %d, got %d", height, p.height) + return fmt.Errorf("%w: expected height %d, got %d", errUnexpectedProposalFound, height, p.height) } // Track the proposal context in the tree and map. p.parent.children = append(p.parent.children, p.proposalMeta) t.proposals.byStateRoot[root] = append(t.proposals.byStateRoot[root], p) p.blockHashes[blockHash] = struct{}{} - + // Now, all unused proposals have no other references, since we didn't store them + // in the proposal map or tree, so they will be garbage collected. + // Any proposals with a different root were mistakenly created, so they can be freed as well. + clear(t.possible) return nil } diff --git a/graft/evm/firewood/triedb_test.go b/graft/evm/firewood/triedb_test.go index ffd3bc0a04e7..412667bbf489 100644 --- a/graft/evm/firewood/triedb_test.go +++ b/graft/evm/firewood/triedb_test.go @@ -11,7 +11,6 @@ import ( "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/libevm/stateconf" - "github.com/ava-labs/libevm/trie/trienode" "github.com/ava-labs/libevm/triedb" "github.com/holiman/uint256" "github.com/stretchr/testify/require" @@ -45,19 +44,16 @@ func TestCommitEmptyGenesis(t *testing.T) { root := tr.Hash() require.Equal(t, types.EmptyRootHash, root) - root, nodes, err := tr.Commit(true) + root, _, err = tr.Commit(true) require.NoErrorf(t, err, "%T.Commit()", tr) require.Equal(t, types.EmptyRootHash, root) - mergedNodes := trienode.NewMergedNodeSet() - require.NoErrorf(t, mergedNodes.Merge(nodes), "%T.Merge()", mergedNodes) - require.NoErrorf( t, triedb.Update( types.EmptyRootHash, types.EmptyRootHash, - 0, mergedNodes, nil, + 0, nil, nil, stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), ), "%T.Update()", triedb, @@ -96,19 +92,16 @@ func TestAccountPersistence(t *testing.T) { hash := tr.Hash() require.NotEqual(t, types.EmptyRootHash, hash) - root, nodes, err := tr.Commit(true) + root, _, err := tr.Commit(true) require.NoErrorf(t, err, "%T.Commit()", tr) require.Equal(t, hash, root) - mergedNodes := trienode.NewMergedNodeSet() - require.NoErrorf(t, mergedNodes.Merge(nodes), "%T.Merge()", mergedNodes) - require.NoErrorf( t, triedb.Update( hash, types.EmptyRootHash, - 0, mergedNodes, nil, + 0, nil, nil, stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), ), "%T.Update()", triedb, @@ -150,19 +143,16 @@ func TestStoragePersistence(t *testing.T) { hash := tr.Hash() require.NotEqual(t, types.EmptyRootHash, hash) - root, nodes, err := tr.Commit(true) + root, _, err := tr.Commit(true) require.NoErrorf(t, err, "%T.Commit()", tr) require.Equal(t, hash, root) - mergedNodes := trienode.NewMergedNodeSet() - require.NoErrorf(t, mergedNodes.Merge(nodes), "%T.Merge()", mergedNodes) - require.NoErrorf( t, triedb.Update( hash, types.EmptyRootHash, - 0, mergedNodes, nil, + 0, nil, nil, stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), ), "%T.Update()", triedb, @@ -214,24 +204,19 @@ func TestParallelHashing(t *testing.T) { require.NotEqual(t, hash1, hash2) // Commit both tries - root1, nodes1, err := tr1.Commit(true) + root1, _, err := tr1.Commit(true) require.NoErrorf(t, err, "%T.Commit()", tr1) require.Equal(t, hash1, root1) - root2, nodes2, err := tr2.Commit(true) + root2, _, err := tr2.Commit(true) require.NoErrorf(t, err, "%T.Commit()", tr2) require.Equal(t, hash2, root2) - mergedNodes1 := trienode.NewMergedNodeSet() - require.NoErrorf(t, mergedNodes1.Merge(nodes1), "%T.Merge()", mergedNodes1) - mergedNodes2 := trienode.NewMergedNodeSet() - require.NoErrorf(t, mergedNodes2.Merge(nodes2), "%T.Merge()", mergedNodes2) - require.NoErrorf( t, triedb.Update( hash1, types.EmptyRootHash, - 0, mergedNodes1, nil, + 0, nil, nil, stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), ), "%T.Update()", triedb, @@ -240,8 +225,50 @@ func TestParallelHashing(t *testing.T) { err = triedb.Update( hash2, types.EmptyRootHash, - 0, mergedNodes2, nil, + 0, nil, nil, stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), ) require.ErrorIsf(t, err, errNoProposalFound, "%T.Update()", triedb) } + +func TestUpdateWithWrongParameters(t *testing.T) { + db := newTestDatabase(t) + triedb := db.TrieDB() + + tr, err := db.OpenTrie(types.EmptyRootHash) + require.NoErrorf(t, err, "%T.OpenTrie()", db) + require.NotNil(t, tr) + + addr := common.HexToAddress("1234") + acct := generateAccount(addr) + require.NoErrorf(t, tr.UpdateAccount(addr, &acct), "%T.UpdateAccount()", tr) + + hash := tr.Hash() + require.NotEqual(t, types.EmptyRootHash, hash) + + root, _, err := tr.Commit(true) + require.NoErrorf(t, err, "%T.Commit()", tr) + require.Equal(t, hash, root) + + // "Accidentally" provide the wrong height + err = triedb.Update( + root, + types.EmptyRootHash, + 42, nil, nil, + stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), + ) + require.ErrorIsf(t, err, errUnexpectedProposalFound, "%T.Update()", triedb) + + // Providing the correct parameters can recover + require.NoErrorf( + t, + triedb.Update( + root, + types.EmptyRootHash, + 0, nil, nil, + stateconf.WithTrieDBUpdatePayload(common.Hash{}, common.Hash{1}), + ), + "%T.Update()", triedb, + ) + require.NoErrorf(t, triedb.Commit(root, true), "%T.Commit()", triedb) +} From 015675b13bf5ebf5024d7b70ef0a53850847b96b Mon Sep 17 00:00:00 2001 From: Austin Larson <78000745+alarso16@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:23:11 -0500 Subject: [PATCH 14/18] Firewood v0.1.0 (#4890) --- firewood/syncer/syncer.go | 2 +- firewood/syncer/syncer_test.go | 10 +++--- go.mod | 2 +- go.sum | 8 +++-- graft/coreth/go.mod | 2 +- graft/coreth/go.sum | 8 +++-- graft/evm/firewood/account_trie.go | 56 +++++++++++++----------------- graft/evm/firewood/triedb.go | 19 +++++----- graft/evm/go.mod | 2 +- graft/evm/go.sum | 8 +++-- graft/subnet-evm/go.mod | 2 +- graft/subnet-evm/go.sum | 8 +++-- 12 files changed, 66 insertions(+), 61 deletions(-) diff --git a/firewood/syncer/syncer.go b/firewood/syncer/syncer.go index 0fe633bc6544..23a424fa9cf7 100644 --- a/firewood/syncer/syncer.go +++ b/firewood/syncer/syncer.go @@ -138,6 +138,6 @@ func (*database) CommitChangeProof(context.Context, maybe.Maybe[[]byte], struct{ func (db *database) Clear() error { // Prefix delete key of length 0. - _, err := db.db.Update([][]byte{{}}, [][]byte{nil}) + _, err := db.db.Update([]ffi.BatchOp{ffi.PrefixDelete([]byte{})}) return err } diff --git a/firewood/syncer/syncer_test.go b/firewood/syncer/syncer_test.go index b360aab20e81..1cff42333911 100644 --- a/firewood/syncer/syncer_test.go +++ b/firewood/syncer/syncer_test.go @@ -104,7 +104,7 @@ func testSync(t *testing.T, seed int64, clientKeys int, serverKeys int) { // Note that each key/value pair may not be unique, so the resulting database may have fewer than [numKeys] entries. func generateDB(t *testing.T, numKeys int, seed int64) *ffi.Database { t.Helper() - db, err := ffi.New(t.TempDir()) + db, err := ffi.New(t.TempDir(), ffi.EthereumNodeHashing) require.NoError(t, err) require.NotNil(t, db) @@ -114,8 +114,7 @@ func generateDB(t *testing.T, numKeys int, seed int64) *ffi.Database { var ( r = rand.New(rand.NewSource(seed)) // #nosec G404 - keys = make([][]byte, numKeys) - vals = make([][]byte, numKeys) + ops = make([]ffi.BatchOp, numKeys) minLength = 1 maxLength = 64 ) @@ -132,11 +131,10 @@ func generateDB(t *testing.T, numKeys int, seed int64) *ffi.Database { _, err = r.Read(val) require.NoError(t, err, "read never errors") - keys = append(keys, key) - vals = append(vals, val) + ops = append(ops, ffi.Put(key, val)) } - _, err = db.Update(keys, vals) + _, err = db.Update(ops) require.NoError(t, err) return db diff --git a/go.mod b/go.mod index 980c1d314be3..a2a5f2139176 100644 --- a/go.mod +++ b/go.mod @@ -99,7 +99,7 @@ require github.com/ava-labs/avalanchego/graft/evm v0.0.0-00010101000000-00000000 require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect - github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 + github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect diff --git a/go.sum b/go.sum index 181c530d4056..3c560ae4623e 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= +github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 h1:Tt65C051dK7rE9VcJbi6zfit/ubsAqjC/H2vYMDmXfY= +github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0/go.mod h1:DkSDp/7LjADrNJ0Aj2dgFDPFTtn+9herXcABeZubmQk= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 h1:S6oFasZsplNmw8B2S8cMJQMa62nT5ZKGzZRdCpd+5qQ= @@ -703,6 +703,10 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= diff --git a/graft/coreth/go.mod b/graft/coreth/go.mod index 43cd98ebcef7..b48308700532 100644 --- a/graft/coreth/go.mod +++ b/graft/coreth/go.mod @@ -10,7 +10,7 @@ go 1.24.11 require ( github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761 github.com/ava-labs/avalanchego/graft/evm v0.0.0-00010101000000-000000000000 - github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 + github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 diff --git a/graft/coreth/go.sum b/graft/coreth/go.sum index da5cc7bf1904..a5a4b94c4d14 100644 --- a/graft/coreth/go.sum +++ b/graft/coreth/go.sum @@ -26,8 +26,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= +github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 h1:Tt65C051dK7rE9VcJbi6zfit/ubsAqjC/H2vYMDmXfY= +github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0/go.mod h1:DkSDp/7LjADrNJ0Aj2dgFDPFTtn+9herXcABeZubmQk= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -513,6 +513,10 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= diff --git a/graft/evm/firewood/account_trie.go b/graft/evm/firewood/account_trie.go index 3085faf97803..8514f3918b29 100644 --- a/graft/evm/firewood/account_trie.go +++ b/graft/evm/firewood/account_trie.go @@ -6,6 +6,7 @@ package firewood import ( "errors" + "github.com/ava-labs/firewood-go-ethhash/ffi" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/core/types" @@ -31,14 +32,13 @@ var _ state.Trie = (*accountTrie)(nil) // // Note this is not concurrent safe. type accountTrie struct { - fw *TrieDB - parentRoot common.Hash - root common.Hash - reader database.Reader - dirtyKeys map[string][]byte // Store dirty changes - updateKeys [][]byte - updateValues [][]byte - hasChanges bool + fw *TrieDB + parentRoot common.Hash + root common.Hash + reader database.Reader + dirtyKeys map[string][]byte // Store dirty changes + updateOps []ffi.BatchOp + hasChanges bool } func newAccountTrie(root common.Hash, db *TrieDB) (*accountTrie, error) { @@ -139,8 +139,7 @@ func (a *accountTrie) UpdateAccount(addr common.Address, account *types.StateAcc return err } a.dirtyKeys[string(key)] = data - a.updateKeys = append(a.updateKeys, key) - a.updateValues = append(a.updateValues, data) + a.updateOps = append(a.updateOps, ffi.Put(key, data)) a.hasChanges = true // Mark that there are changes to commit return nil } @@ -161,8 +160,7 @@ func (a *accountTrie) UpdateStorage(addr common.Address, key []byte, value []byt // Queue the keys and values for later commit a.dirtyKeys[string(combinedKey[:])] = data - a.updateKeys = append(a.updateKeys, combinedKey[:]) - a.updateValues = append(a.updateValues, data) + a.updateOps = append(a.updateOps, ffi.Put(combinedKey[:], data)) a.hasChanges = true // Mark that there are changes to commit return nil } @@ -172,9 +170,8 @@ func (a *accountTrie) DeleteAccount(addr common.Address) error { key := crypto.Keccak256Hash(addr.Bytes()).Bytes() // Queue the key for deletion a.dirtyKeys[string(key)] = nil - a.updateKeys = append(a.updateKeys, key) - a.updateValues = append(a.updateValues, nil) // Must use nil to indicate deletion - a.hasChanges = true // Mark that there are changes to commit + a.updateOps = append(a.updateOps, ffi.PrefixDelete(key)) // Remove all storage + a.hasChanges = true // Mark that there are changes to commit return nil } @@ -188,9 +185,8 @@ func (a *accountTrie) DeleteStorage(addr common.Address, key []byte) error { // Queue the key for deletion a.dirtyKeys[string(combinedKey[:])] = nil - a.updateKeys = append(a.updateKeys, combinedKey[:]) - a.updateValues = append(a.updateValues, nil) // Must use nil to indicate deletion - a.hasChanges = true // Mark that there are changes to commit + a.updateOps = append(a.updateOps, ffi.Delete(combinedKey[:])) + a.hasChanges = true // Mark that there are changes to commit return nil } @@ -211,7 +207,7 @@ func (a *accountTrie) Hash() common.Hash { func (a *accountTrie) hash() (common.Hash, error) { // If we haven't already hashed, we need to do so. if a.hasChanges { - root, err := a.fw.createProposals(a.parentRoot, a.updateKeys, a.updateValues) + root, err := a.fw.createProposals(a.parentRoot, a.updateOps) if err != nil { return common.Hash{}, err } @@ -269,14 +265,13 @@ func (*accountTrie) Prove([]byte, ethdb.KeyValueWriter) error { func (a *accountTrie) Copy() *accountTrie { // Create a new AccountTrie with the same root and reader newTrie := &accountTrie{ - fw: a.fw, - parentRoot: a.parentRoot, - root: a.root, - reader: a.reader, // Share the same reader - hasChanges: a.hasChanges, - dirtyKeys: make(map[string][]byte, len(a.dirtyKeys)), - updateKeys: make([][]byte, len(a.updateKeys)), - updateValues: make([][]byte, len(a.updateValues)), + fw: a.fw, + parentRoot: a.parentRoot, + root: a.root, + reader: a.reader, // Share the same reader + hasChanges: a.hasChanges, + dirtyKeys: make(map[string][]byte, len(a.dirtyKeys)), + updateOps: make([]ffi.BatchOp, len(a.updateOps)), } // Deep copy dirtyKeys map @@ -284,11 +279,8 @@ func (a *accountTrie) Copy() *accountTrie { newTrie.dirtyKeys[k] = append([]byte{}, v...) } - // Deep copy updateKeys and updateValues slices - for i := range a.updateKeys { - newTrie.updateKeys[i] = append([]byte{}, a.updateKeys[i]...) - newTrie.updateValues[i] = append([]byte{}, a.updateValues[i]...) - } + // Copy updateOps slice + newTrie.updateOps = append([]ffi.BatchOp{}, a.updateOps...) return newTrie } diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 4ea0b009cc2c..e57e418b26c6 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -146,8 +146,11 @@ func New(config TrieDBConfig) (*TrieDB, error) { if config.Archive { options = append(options, ffi.WithRootStore()) } + if metrics.EnabledExpensive { + options = append(options, ffi.WithExpensiveMetrics()) + } - fw, err := ffi.New(path, options...) + fw, err := ffi.New(path, ffi.EthereumNodeHashing, options...) if err != nil { return nil, fmt.Errorf("opening database: %w", err) } @@ -419,7 +422,7 @@ func (ps *proposals) findProposalToCommitWhenLocked(root common.Hash) (*proposal } // createProposal creates a new proposal from the given layer -func (t *TrieDB) createProposal(parent *proposal, keys, values [][]byte) (*proposal, error) { +func (t *TrieDB) createProposal(parent *proposal, ops []ffi.BatchOp) (*proposal, error) { propose := t.Firewood.Propose if h := parent.handle; h != nil { propose = h.Propose @@ -427,7 +430,7 @@ func (t *TrieDB) createProposal(parent *proposal, keys, values [][]byte) (*propo } else { proposeOnDiskCount.Inc(1) } - handle, err := propose(keys, values) + handle, err := propose(ops) if err != nil { return nil, fmt.Errorf("create proposal from parent root %s: %w", parent.root.Hex(), err) } @@ -514,17 +517,13 @@ func (ps *proposals) removeProposalFromMap(meta *proposalMeta, drop bool) { // createProposals calculates the hash if the set of keys and values are // proposed from the given parent root. // All proposals created will be tracked for future use. -func (t *TrieDB) createProposals(parentRoot common.Hash, keys, values [][]byte) (common.Hash, error) { +func (t *TrieDB) createProposals(parentRoot common.Hash, ops []ffi.BatchOp) (common.Hash, error) { start := time.Now() defer func() { hashTimer.Inc(time.Since(start).Milliseconds()) hashCount.Inc(1) }() - if len(keys) != len(values) { - return common.Hash{}, fmt.Errorf("keys and values must have the same length, got %d keys and %d values", len(keys), len(values)) - } - // Must prevent a simultaneous `Commit`, as it alters the proposal tree/disk state. t.proposals.Lock() defer t.proposals.Unlock() @@ -535,7 +534,7 @@ func (t *TrieDB) createProposals(parentRoot common.Hash, keys, values [][]byte) ) if t.proposals.tree.root == parentRoot { // Propose from the database root. - p, err := t.createProposal(t.proposals.tree, keys, values) + p, err := t.createProposal(t.proposals.tree, ops) if err != nil { return common.Hash{}, fmt.Errorf("proposing from root %s: %w", parentRoot.Hex(), err) } @@ -550,7 +549,7 @@ func (t *TrieDB) createProposals(parentRoot common.Hash, keys, values [][]byte) // Since we are only using the proposal to find the root hash, // we can use the first proposal found. for _, parent := range t.proposals.byStateRoot[parentRoot] { - p, err := t.createProposal(parent, keys, values) + p, err := t.createProposal(parent, ops) if err != nil { return common.Hash{}, fmt.Errorf("proposing from root %s: %w", parentRoot.Hex(), err) } diff --git a/graft/evm/go.mod b/graft/evm/go.mod index ce38fc8a06e3..bb7fedd00ebf 100644 --- a/graft/evm/go.mod +++ b/graft/evm/go.mod @@ -5,7 +5,7 @@ go 1.24.11 require ( github.com/VictoriaMetrics/fastcache v1.12.1 github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761 - github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 + github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 github.com/gorilla/rpc v1.2.0 github.com/holiman/bloomfilter/v2 v2.0.3 diff --git a/graft/evm/go.sum b/graft/evm/go.sum index c16622cb5916..fc4bb29203cb 100644 --- a/graft/evm/go.sum +++ b/graft/evm/go.sum @@ -17,8 +17,8 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= +github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 h1:Tt65C051dK7rE9VcJbi6zfit/ubsAqjC/H2vYMDmXfY= +github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0/go.mod h1:DkSDp/7LjADrNJ0Aj2dgFDPFTtn+9herXcABeZubmQk= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -340,6 +340,10 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= diff --git a/graft/subnet-evm/go.mod b/graft/subnet-evm/go.mod index 6f5ebc1007af..4f4050149e5e 100644 --- a/graft/subnet-evm/go.mod +++ b/graft/subnet-evm/go.mod @@ -17,7 +17,7 @@ require ( github.com/antithesishq/antithesis-sdk-go v0.3.8 github.com/ava-labs/avalanchego v1.14.1-antithesis-docker-image-fix github.com/ava-labs/avalanchego/graft/evm v0.0.0-00010101000000-000000000000 - github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 + github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 diff --git a/graft/subnet-evm/go.sum b/graft/subnet-evm/go.sum index 5f623548adb5..bb9e892b3b44 100644 --- a/graft/subnet-evm/go.sum +++ b/graft/subnet-evm/go.sum @@ -30,8 +30,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/ava-labs/avalanchego/graft/coreth v0.0.0-20251203215505-70148edc6eca h1:zZIQZhOqKe82SUvEx7IeRVoahjyKI0gfouHPQkvEHeI= github.com/ava-labs/avalanchego/graft/coreth v0.0.0-20251203215505-70148edc6eca/go.mod h1:y+/5DAxCTLAXdWRxAYN1V8DV0DIF7uHhOOeNa9oASuU= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= +github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 h1:Tt65C051dK7rE9VcJbi6zfit/ubsAqjC/H2vYMDmXfY= +github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0/go.mod h1:DkSDp/7LjADrNJ0Aj2dgFDPFTtn+9herXcABeZubmQk= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -571,6 +571,10 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= From d5dff005158c46ed99140bca0a28e1603d061ee4 Mon Sep 17 00:00:00 2001 From: Austin Larson <78000745+alarso16@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:31:51 -0500 Subject: [PATCH 15/18] Revert "Firewood v0.1.0" (#4892) --- firewood/syncer/syncer.go | 2 +- firewood/syncer/syncer_test.go | 10 +++--- go.mod | 2 +- go.sum | 8 ++--- graft/coreth/go.mod | 2 +- graft/coreth/go.sum | 8 ++--- graft/evm/firewood/account_trie.go | 56 +++++++++++++++++------------- graft/evm/firewood/triedb.go | 19 +++++----- graft/evm/go.mod | 2 +- graft/evm/go.sum | 8 ++--- graft/subnet-evm/go.mod | 2 +- graft/subnet-evm/go.sum | 8 ++--- 12 files changed, 61 insertions(+), 66 deletions(-) diff --git a/firewood/syncer/syncer.go b/firewood/syncer/syncer.go index 23a424fa9cf7..0fe633bc6544 100644 --- a/firewood/syncer/syncer.go +++ b/firewood/syncer/syncer.go @@ -138,6 +138,6 @@ func (*database) CommitChangeProof(context.Context, maybe.Maybe[[]byte], struct{ func (db *database) Clear() error { // Prefix delete key of length 0. - _, err := db.db.Update([]ffi.BatchOp{ffi.PrefixDelete([]byte{})}) + _, err := db.db.Update([][]byte{{}}, [][]byte{nil}) return err } diff --git a/firewood/syncer/syncer_test.go b/firewood/syncer/syncer_test.go index 1cff42333911..b360aab20e81 100644 --- a/firewood/syncer/syncer_test.go +++ b/firewood/syncer/syncer_test.go @@ -104,7 +104,7 @@ func testSync(t *testing.T, seed int64, clientKeys int, serverKeys int) { // Note that each key/value pair may not be unique, so the resulting database may have fewer than [numKeys] entries. func generateDB(t *testing.T, numKeys int, seed int64) *ffi.Database { t.Helper() - db, err := ffi.New(t.TempDir(), ffi.EthereumNodeHashing) + db, err := ffi.New(t.TempDir()) require.NoError(t, err) require.NotNil(t, db) @@ -114,7 +114,8 @@ func generateDB(t *testing.T, numKeys int, seed int64) *ffi.Database { var ( r = rand.New(rand.NewSource(seed)) // #nosec G404 - ops = make([]ffi.BatchOp, numKeys) + keys = make([][]byte, numKeys) + vals = make([][]byte, numKeys) minLength = 1 maxLength = 64 ) @@ -131,10 +132,11 @@ func generateDB(t *testing.T, numKeys int, seed int64) *ffi.Database { _, err = r.Read(val) require.NoError(t, err, "read never errors") - ops = append(ops, ffi.Put(key, val)) + keys = append(keys, key) + vals = append(vals, val) } - _, err = db.Update(ops) + _, err = db.Update(keys, vals) require.NoError(t, err) return db diff --git a/go.mod b/go.mod index a2a5f2139176..980c1d314be3 100644 --- a/go.mod +++ b/go.mod @@ -99,7 +99,7 @@ require github.com/ava-labs/avalanchego/graft/evm v0.0.0-00010101000000-00000000 require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect - github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 + github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect diff --git a/go.sum b/go.sum index 3c560ae4623e..181c530d4056 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 h1:Tt65C051dK7rE9VcJbi6zfit/ubsAqjC/H2vYMDmXfY= -github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0/go.mod h1:DkSDp/7LjADrNJ0Aj2dgFDPFTtn+9herXcABeZubmQk= +github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= +github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 h1:S6oFasZsplNmw8B2S8cMJQMa62nT5ZKGzZRdCpd+5qQ= @@ -703,10 +703,6 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= diff --git a/graft/coreth/go.mod b/graft/coreth/go.mod index b48308700532..43cd98ebcef7 100644 --- a/graft/coreth/go.mod +++ b/graft/coreth/go.mod @@ -10,7 +10,7 @@ go 1.24.11 require ( github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761 github.com/ava-labs/avalanchego/graft/evm v0.0.0-00010101000000-000000000000 - github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 + github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 diff --git a/graft/coreth/go.sum b/graft/coreth/go.sum index a5a4b94c4d14..da5cc7bf1904 100644 --- a/graft/coreth/go.sum +++ b/graft/coreth/go.sum @@ -26,8 +26,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 h1:Tt65C051dK7rE9VcJbi6zfit/ubsAqjC/H2vYMDmXfY= -github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0/go.mod h1:DkSDp/7LjADrNJ0Aj2dgFDPFTtn+9herXcABeZubmQk= +github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= +github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -513,10 +513,6 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= diff --git a/graft/evm/firewood/account_trie.go b/graft/evm/firewood/account_trie.go index 8514f3918b29..3085faf97803 100644 --- a/graft/evm/firewood/account_trie.go +++ b/graft/evm/firewood/account_trie.go @@ -6,7 +6,6 @@ package firewood import ( "errors" - "github.com/ava-labs/firewood-go-ethhash/ffi" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/core/types" @@ -32,13 +31,14 @@ var _ state.Trie = (*accountTrie)(nil) // // Note this is not concurrent safe. type accountTrie struct { - fw *TrieDB - parentRoot common.Hash - root common.Hash - reader database.Reader - dirtyKeys map[string][]byte // Store dirty changes - updateOps []ffi.BatchOp - hasChanges bool + fw *TrieDB + parentRoot common.Hash + root common.Hash + reader database.Reader + dirtyKeys map[string][]byte // Store dirty changes + updateKeys [][]byte + updateValues [][]byte + hasChanges bool } func newAccountTrie(root common.Hash, db *TrieDB) (*accountTrie, error) { @@ -139,7 +139,8 @@ func (a *accountTrie) UpdateAccount(addr common.Address, account *types.StateAcc return err } a.dirtyKeys[string(key)] = data - a.updateOps = append(a.updateOps, ffi.Put(key, data)) + a.updateKeys = append(a.updateKeys, key) + a.updateValues = append(a.updateValues, data) a.hasChanges = true // Mark that there are changes to commit return nil } @@ -160,7 +161,8 @@ func (a *accountTrie) UpdateStorage(addr common.Address, key []byte, value []byt // Queue the keys and values for later commit a.dirtyKeys[string(combinedKey[:])] = data - a.updateOps = append(a.updateOps, ffi.Put(combinedKey[:], data)) + a.updateKeys = append(a.updateKeys, combinedKey[:]) + a.updateValues = append(a.updateValues, data) a.hasChanges = true // Mark that there are changes to commit return nil } @@ -170,8 +172,9 @@ func (a *accountTrie) DeleteAccount(addr common.Address) error { key := crypto.Keccak256Hash(addr.Bytes()).Bytes() // Queue the key for deletion a.dirtyKeys[string(key)] = nil - a.updateOps = append(a.updateOps, ffi.PrefixDelete(key)) // Remove all storage - a.hasChanges = true // Mark that there are changes to commit + a.updateKeys = append(a.updateKeys, key) + a.updateValues = append(a.updateValues, nil) // Must use nil to indicate deletion + a.hasChanges = true // Mark that there are changes to commit return nil } @@ -185,8 +188,9 @@ func (a *accountTrie) DeleteStorage(addr common.Address, key []byte) error { // Queue the key for deletion a.dirtyKeys[string(combinedKey[:])] = nil - a.updateOps = append(a.updateOps, ffi.Delete(combinedKey[:])) - a.hasChanges = true // Mark that there are changes to commit + a.updateKeys = append(a.updateKeys, combinedKey[:]) + a.updateValues = append(a.updateValues, nil) // Must use nil to indicate deletion + a.hasChanges = true // Mark that there are changes to commit return nil } @@ -207,7 +211,7 @@ func (a *accountTrie) Hash() common.Hash { func (a *accountTrie) hash() (common.Hash, error) { // If we haven't already hashed, we need to do so. if a.hasChanges { - root, err := a.fw.createProposals(a.parentRoot, a.updateOps) + root, err := a.fw.createProposals(a.parentRoot, a.updateKeys, a.updateValues) if err != nil { return common.Hash{}, err } @@ -265,13 +269,14 @@ func (*accountTrie) Prove([]byte, ethdb.KeyValueWriter) error { func (a *accountTrie) Copy() *accountTrie { // Create a new AccountTrie with the same root and reader newTrie := &accountTrie{ - fw: a.fw, - parentRoot: a.parentRoot, - root: a.root, - reader: a.reader, // Share the same reader - hasChanges: a.hasChanges, - dirtyKeys: make(map[string][]byte, len(a.dirtyKeys)), - updateOps: make([]ffi.BatchOp, len(a.updateOps)), + fw: a.fw, + parentRoot: a.parentRoot, + root: a.root, + reader: a.reader, // Share the same reader + hasChanges: a.hasChanges, + dirtyKeys: make(map[string][]byte, len(a.dirtyKeys)), + updateKeys: make([][]byte, len(a.updateKeys)), + updateValues: make([][]byte, len(a.updateValues)), } // Deep copy dirtyKeys map @@ -279,8 +284,11 @@ func (a *accountTrie) Copy() *accountTrie { newTrie.dirtyKeys[k] = append([]byte{}, v...) } - // Copy updateOps slice - newTrie.updateOps = append([]ffi.BatchOp{}, a.updateOps...) + // Deep copy updateKeys and updateValues slices + for i := range a.updateKeys { + newTrie.updateKeys[i] = append([]byte{}, a.updateKeys[i]...) + newTrie.updateValues[i] = append([]byte{}, a.updateValues[i]...) + } return newTrie } diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index e57e418b26c6..4ea0b009cc2c 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -146,11 +146,8 @@ func New(config TrieDBConfig) (*TrieDB, error) { if config.Archive { options = append(options, ffi.WithRootStore()) } - if metrics.EnabledExpensive { - options = append(options, ffi.WithExpensiveMetrics()) - } - fw, err := ffi.New(path, ffi.EthereumNodeHashing, options...) + fw, err := ffi.New(path, options...) if err != nil { return nil, fmt.Errorf("opening database: %w", err) } @@ -422,7 +419,7 @@ func (ps *proposals) findProposalToCommitWhenLocked(root common.Hash) (*proposal } // createProposal creates a new proposal from the given layer -func (t *TrieDB) createProposal(parent *proposal, ops []ffi.BatchOp) (*proposal, error) { +func (t *TrieDB) createProposal(parent *proposal, keys, values [][]byte) (*proposal, error) { propose := t.Firewood.Propose if h := parent.handle; h != nil { propose = h.Propose @@ -430,7 +427,7 @@ func (t *TrieDB) createProposal(parent *proposal, ops []ffi.BatchOp) (*proposal, } else { proposeOnDiskCount.Inc(1) } - handle, err := propose(ops) + handle, err := propose(keys, values) if err != nil { return nil, fmt.Errorf("create proposal from parent root %s: %w", parent.root.Hex(), err) } @@ -517,13 +514,17 @@ func (ps *proposals) removeProposalFromMap(meta *proposalMeta, drop bool) { // createProposals calculates the hash if the set of keys and values are // proposed from the given parent root. // All proposals created will be tracked for future use. -func (t *TrieDB) createProposals(parentRoot common.Hash, ops []ffi.BatchOp) (common.Hash, error) { +func (t *TrieDB) createProposals(parentRoot common.Hash, keys, values [][]byte) (common.Hash, error) { start := time.Now() defer func() { hashTimer.Inc(time.Since(start).Milliseconds()) hashCount.Inc(1) }() + if len(keys) != len(values) { + return common.Hash{}, fmt.Errorf("keys and values must have the same length, got %d keys and %d values", len(keys), len(values)) + } + // Must prevent a simultaneous `Commit`, as it alters the proposal tree/disk state. t.proposals.Lock() defer t.proposals.Unlock() @@ -534,7 +535,7 @@ func (t *TrieDB) createProposals(parentRoot common.Hash, ops []ffi.BatchOp) (com ) if t.proposals.tree.root == parentRoot { // Propose from the database root. - p, err := t.createProposal(t.proposals.tree, ops) + p, err := t.createProposal(t.proposals.tree, keys, values) if err != nil { return common.Hash{}, fmt.Errorf("proposing from root %s: %w", parentRoot.Hex(), err) } @@ -549,7 +550,7 @@ func (t *TrieDB) createProposals(parentRoot common.Hash, ops []ffi.BatchOp) (com // Since we are only using the proposal to find the root hash, // we can use the first proposal found. for _, parent := range t.proposals.byStateRoot[parentRoot] { - p, err := t.createProposal(parent, ops) + p, err := t.createProposal(parent, keys, values) if err != nil { return common.Hash{}, fmt.Errorf("proposing from root %s: %w", parentRoot.Hex(), err) } diff --git a/graft/evm/go.mod b/graft/evm/go.mod index bb7fedd00ebf..ce38fc8a06e3 100644 --- a/graft/evm/go.mod +++ b/graft/evm/go.mod @@ -5,7 +5,7 @@ go 1.24.11 require ( github.com/VictoriaMetrics/fastcache v1.12.1 github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761 - github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 + github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 github.com/gorilla/rpc v1.2.0 github.com/holiman/bloomfilter/v2 v2.0.3 diff --git a/graft/evm/go.sum b/graft/evm/go.sum index fc4bb29203cb..c16622cb5916 100644 --- a/graft/evm/go.sum +++ b/graft/evm/go.sum @@ -17,8 +17,8 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 h1:Tt65C051dK7rE9VcJbi6zfit/ubsAqjC/H2vYMDmXfY= -github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0/go.mod h1:DkSDp/7LjADrNJ0Aj2dgFDPFTtn+9herXcABeZubmQk= +github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= +github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -340,10 +340,6 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= diff --git a/graft/subnet-evm/go.mod b/graft/subnet-evm/go.mod index 4f4050149e5e..6f5ebc1007af 100644 --- a/graft/subnet-evm/go.mod +++ b/graft/subnet-evm/go.mod @@ -17,7 +17,7 @@ require ( github.com/antithesishq/antithesis-sdk-go v0.3.8 github.com/ava-labs/avalanchego v1.14.1-antithesis-docker-image-fix github.com/ava-labs/avalanchego/graft/evm v0.0.0-00010101000000-000000000000 - github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 + github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 diff --git a/graft/subnet-evm/go.sum b/graft/subnet-evm/go.sum index bb9e892b3b44..5f623548adb5 100644 --- a/graft/subnet-evm/go.sum +++ b/graft/subnet-evm/go.sum @@ -30,8 +30,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/ava-labs/avalanchego/graft/coreth v0.0.0-20251203215505-70148edc6eca h1:zZIQZhOqKe82SUvEx7IeRVoahjyKI0gfouHPQkvEHeI= github.com/ava-labs/avalanchego/graft/coreth v0.0.0-20251203215505-70148edc6eca/go.mod h1:y+/5DAxCTLAXdWRxAYN1V8DV0DIF7uHhOOeNa9oASuU= -github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0 h1:Tt65C051dK7rE9VcJbi6zfit/ubsAqjC/H2vYMDmXfY= -github.com/ava-labs/firewood-go-ethhash/ffi v0.1.0/go.mod h1:DkSDp/7LjADrNJ0Aj2dgFDPFTtn+9herXcABeZubmQk= +github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= +github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -571,10 +571,6 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= From ea1c6db22fc80a666df2a2581fbbb94b73a67b30 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Wed, 21 Jan 2026 13:38:16 -0500 Subject: [PATCH 16/18] refactor: Commit instead of SetHashAndHeight in genesis --- graft/coreth/core/genesis.go | 16 +++++++++++----- graft/evm/firewood/triedb.go | 8 ++++++-- graft/subnet-evm/core/genesis.go | 16 +++++++++++----- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/graft/coreth/core/genesis.go b/graft/coreth/core/genesis.go index 7258594224b7..5290a44c9359 100644 --- a/graft/coreth/core/genesis.go +++ b/graft/coreth/core/genesis.go @@ -339,15 +339,21 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo if _, err := statedb.Commit(0, false, stateconf.WithTrieDBUpdateOpts(triedbOpt)); err != nil { panic(fmt.Sprintf("unable to commit genesis block to statedb: %v", err)) } + + // Firewood requires `Update` and `Commit`, even if the state is empty. + _, isFirewood := triedb.Backend().(*firewood.TrieDB) + if root == types.EmptyRootHash && isFirewood { + // Ensure the Firewood TrieDB is aware of the genesis block. + if err := triedb.Update(root, common.Hash{}, 0, nil, nil, triedbOpt); err != nil { + panic(fmt.Sprintf("unable to update firewood triedb with genesis block: %v", err)) + } + } + // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash { + if root != types.EmptyRootHash || isFirewood { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) } - } else { - if t, ok := triedb.Backend().(*firewood.TrieDB); ok { - t.SetHashAndHeight(block.Hash(), 0) - } } return block } diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index 4ea0b009cc2c..ef6e48d49cb6 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -200,8 +200,8 @@ func validateDir(dir string) error { } // SetHashAndHeight sets the committed block hashes and height in memory. -// This must be called at startup to initialize the in-memory state, unless -// explicitly committing a genesis block. +// This must be called at startup to initialize the in-memory state if the +// database is non-empty (e.g. restart, state sync) func (t *TrieDB) SetHashAndHeight(blockHash common.Hash, height uint64) { t.Lock() defer t.Unlock() @@ -279,6 +279,8 @@ func (t *TrieDB) Close() error { // A proposal must have already been created from [accountTrie.Commit] with the same root, // parent root, and height. // If no such proposal exists, an error will be returned. +// +// Unlike for HashDB and PathDB, `Commit` must be called even if if the root is unchanged. func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.MergedNodeSet, _ *triestate.Set, opts ...stateconf.TrieDBUpdateOption) error { // We require block hashes to be provided for all blocks in production. // However, many tests cannot reasonably provide a block blockHash for genesis, so we allow it to be omitted. @@ -358,6 +360,8 @@ func (ps *proposals) exists(root, block, parentBlock common.Hash) bool { // // Afterward, we know that no other proposal at this height can be committed, so we can dereference all // children in the the other branches of the proposal tree. +// +// Unlike for HashDB and PathDB, `Commit` must be called even if if the root is unchanged. func (t *TrieDB) Commit(root common.Hash, report bool) error { start := time.Now() defer func() { diff --git a/graft/subnet-evm/core/genesis.go b/graft/subnet-evm/core/genesis.go index 14b46591b899..bf8790febf54 100644 --- a/graft/subnet-evm/core/genesis.go +++ b/graft/subnet-evm/core/genesis.go @@ -383,15 +383,21 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo if _, err := statedb.Commit(0, false, stateconf.WithTrieDBUpdateOpts(triedbOpt)); err != nil { panic(fmt.Sprintf("unable to commit genesis block to statedb: %v", err)) } + + // Firewood requires `Update` and `Commit`, even if the state is empty. + _, isFirewood := triedb.Backend().(*firewood.TrieDB) + if root == types.EmptyRootHash && isFirewood { + // Ensure the Firewood TrieDB is aware of the genesis block. + if err := triedb.Update(root, common.Hash{}, 0, nil, nil, triedbOpt); err != nil { + panic(fmt.Sprintf("unable to update firewood triedb with genesis block: %v", err)) + } + } + // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash { + if root != types.EmptyRootHash || isFirewood { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) } - } else { - if t, ok := triedb.Backend().(*firewood.TrieDB); ok { - t.SetHashAndHeight(block.Hash(), block.NumberU64()) - } } return block } From 29c20b7b3de32882223562cbda6d8913c668bb03 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Wed, 21 Jan 2026 13:50:46 -0500 Subject: [PATCH 17/18] fix: use empty root hash --- graft/coreth/core/genesis.go | 2 +- graft/subnet-evm/core/genesis.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graft/coreth/core/genesis.go b/graft/coreth/core/genesis.go index 5290a44c9359..7625e6cb7411 100644 --- a/graft/coreth/core/genesis.go +++ b/graft/coreth/core/genesis.go @@ -344,7 +344,7 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo _, isFirewood := triedb.Backend().(*firewood.TrieDB) if root == types.EmptyRootHash && isFirewood { // Ensure the Firewood TrieDB is aware of the genesis block. - if err := triedb.Update(root, common.Hash{}, 0, nil, nil, triedbOpt); err != nil { + if err := triedb.Update(types.EmptyRootHash, types.EmptyRootHash, 0, nil, nil, triedbOpt); err != nil { panic(fmt.Sprintf("unable to update firewood triedb with genesis block: %v", err)) } } diff --git a/graft/subnet-evm/core/genesis.go b/graft/subnet-evm/core/genesis.go index bf8790febf54..1c861c86637a 100644 --- a/graft/subnet-evm/core/genesis.go +++ b/graft/subnet-evm/core/genesis.go @@ -388,7 +388,7 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo _, isFirewood := triedb.Backend().(*firewood.TrieDB) if root == types.EmptyRootHash && isFirewood { // Ensure the Firewood TrieDB is aware of the genesis block. - if err := triedb.Update(root, common.Hash{}, 0, nil, nil, triedbOpt); err != nil { + if err := triedb.Update(types.EmptyRootHash, types.EmptyRootHash, 0, nil, nil, triedbOpt); err != nil { panic(fmt.Sprintf("unable to update firewood triedb with genesis block: %v", err)) } } From 65db2c573fcdd07bd525d0048987c2af69c19eb3 Mon Sep 17 00:00:00 2001 From: Austin Larson Date: Wed, 21 Jan 2026 15:58:19 -0500 Subject: [PATCH 18/18] refactor: Infer empty genesis state --- graft/coreth/core/genesis.go | 13 +------------ graft/evm/firewood/triedb.go | 7 +++++++ graft/subnet-evm/core/genesis.go | 13 +------------ 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/graft/coreth/core/genesis.go b/graft/coreth/core/genesis.go index 7625e6cb7411..603a90a2eee4 100644 --- a/graft/coreth/core/genesis.go +++ b/graft/coreth/core/genesis.go @@ -38,7 +38,6 @@ import ( "github.com/ava-labs/avalanchego/graft/coreth/params" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/customtypes" "github.com/ava-labs/avalanchego/graft/coreth/plugin/evm/upgrade/ap3" - "github.com/ava-labs/avalanchego/graft/evm/firewood" "github.com/ava-labs/avalanchego/graft/evm/triedb/pathdb" "github.com/ava-labs/avalanchego/vms/evm/acp226" "github.com/ava-labs/libevm/common" @@ -339,18 +338,8 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo if _, err := statedb.Commit(0, false, stateconf.WithTrieDBUpdateOpts(triedbOpt)); err != nil { panic(fmt.Sprintf("unable to commit genesis block to statedb: %v", err)) } - - // Firewood requires `Update` and `Commit`, even if the state is empty. - _, isFirewood := triedb.Backend().(*firewood.TrieDB) - if root == types.EmptyRootHash && isFirewood { - // Ensure the Firewood TrieDB is aware of the genesis block. - if err := triedb.Update(types.EmptyRootHash, types.EmptyRootHash, 0, nil, nil, triedbOpt); err != nil { - panic(fmt.Sprintf("unable to update firewood triedb with genesis block: %v", err)) - } - } - // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash || isFirewood { + if root != types.EmptyRootHash { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) } diff --git a/graft/evm/firewood/triedb.go b/graft/evm/firewood/triedb.go index ef6e48d49cb6..ea9569d5b377 100644 --- a/graft/evm/firewood/triedb.go +++ b/graft/evm/firewood/triedb.go @@ -294,6 +294,13 @@ func (t *TrieDB) Update(root, parent common.Hash, height uint64, _ *trienode.Mer defer t.proposals.Unlock() p, ok := t.possible[possibleKey{parentBlockHash: parentBlockHash, root: root}] + // It's possible that we are committing a proposal on top of an empty genesis block in testing. + // In this case, we can still find the proposal by looking for the empty block hash + if !ok && height == 1 && parent == types.EmptyRootHash { + p, ok = t.possible[possibleKey{parentBlockHash: common.Hash{}, root: root}] + p.height = 1 + p.parent.blockHashes[parentBlockHash] = struct{}{} + } if !ok { return fmt.Errorf("%w for block %d, root %s, hash %s", errNoProposalFound, height, root.Hex(), blockHash.Hex()) } diff --git a/graft/subnet-evm/core/genesis.go b/graft/subnet-evm/core/genesis.go index 1c861c86637a..baa565a85e74 100644 --- a/graft/subnet-evm/core/genesis.go +++ b/graft/subnet-evm/core/genesis.go @@ -34,7 +34,6 @@ import ( "math/big" "time" - "github.com/ava-labs/avalanchego/graft/evm/firewood" "github.com/ava-labs/avalanchego/graft/evm/triedb/pathdb" "github.com/ava-labs/avalanchego/graft/subnet-evm/core/extstate" "github.com/ava-labs/avalanchego/graft/subnet-evm/params" @@ -383,18 +382,8 @@ func (g *Genesis) toBlock(db ethdb.Database, triedb *triedb.Database) *types.Blo if _, err := statedb.Commit(0, false, stateconf.WithTrieDBUpdateOpts(triedbOpt)); err != nil { panic(fmt.Sprintf("unable to commit genesis block to statedb: %v", err)) } - - // Firewood requires `Update` and `Commit`, even if the state is empty. - _, isFirewood := triedb.Backend().(*firewood.TrieDB) - if root == types.EmptyRootHash && isFirewood { - // Ensure the Firewood TrieDB is aware of the genesis block. - if err := triedb.Update(types.EmptyRootHash, types.EmptyRootHash, 0, nil, nil, triedbOpt); err != nil { - panic(fmt.Sprintf("unable to update firewood triedb with genesis block: %v", err)) - } - } - // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash || isFirewood { + if root != types.EmptyRootHash { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) }