Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions graft/coreth/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion graft/coreth/core/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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
}
4 changes: 1 addition & 3 deletions graft/coreth/core/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions graft/coreth/tests/state_test_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
57 changes: 30 additions & 27 deletions graft/evm/firewood/account_trie.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,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 {
Expand Down Expand Up @@ -172,7 +173,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
}
Expand All @@ -188,14 +189,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 {
Expand All @@ -208,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
}
Expand All @@ -218,59 +221,59 @@ 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{
fw: a.fw,
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)),
Expand Down
3 changes: 1 addition & 2 deletions graft/evm/firewood/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
66 changes: 66 additions & 0 deletions graft/evm/firewood/recovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (C) 2019-2025, 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
}
13 changes: 5 additions & 8 deletions graft/evm/firewood/storage_trie.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,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
}
Loading