diff --git a/core/blockchain.go b/core/blockchain.go index c8655cb339..4188d7f6c4 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -34,6 +34,7 @@ import ( "fmt" "io" "math/big" + "path/filepath" "runtime" "strings" "sync" @@ -48,7 +49,6 @@ import ( "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/plugin/evm/customtypes" - "github.com/ava-labs/coreth/triedb/firewood" "github.com/ava-labs/coreth/triedb/hashdb" "github.com/ava-labs/coreth/triedb/pathdb" "github.com/ava-labs/libevm/common" @@ -61,6 +61,7 @@ import ( "github.com/ava-labs/libevm/ethdb" "github.com/ava-labs/libevm/event" "github.com/ava-labs/libevm/libevm/stateconf" + "github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/libevm/triedb" @@ -176,6 +177,9 @@ const ( // trieCleanCacheStatsNamespace is the namespace to surface stats from the trie // clean cache's underlying fastcache. trieCleanCacheStatsNamespace = "hashdb/memcache/clean/fastcache" + + // firewoodPath is the default path for firewood database. + firewoodPath = "firewood" ) // CacheConfig contains the configuration values for the trie database @@ -223,19 +227,19 @@ func (c *CacheConfig) triedbConfig() *triedb.Config { DirtyCacheSize: c.TrieDirtyLimit * 1024 * 1024, }.BackendConstructor } - if c.StateScheme == customrawdb.FirewoodScheme { + if c.StateScheme == firewood.Scheme { // ChainDataDir may not be set during some tests, where this path won't be called. if c.ChainDataDir == "" { log.Crit("Chain data directory must be specified for Firewood") } 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: filepath.Join(c.ChainDataDir, firewoodPath), + CacheSizeBytes: uint(c.TrieCleanLimit * 1024 * 1024), + FreeListCacheEntries: 40_000, // same as Firewood default + RevisionsInMemory: uint(c.StateHistory), // must be at least 2 + CacheStrategy: ffi.CacheAllReads, + Archive: !c.Pruning, }.BackendConstructor } return config @@ -263,7 +267,7 @@ func DefaultCacheConfigWithScheme(scheme string) *CacheConfig { config := *DefaultCacheConfig config.StateScheme = scheme // TODO: remove this once if Firewood supports snapshots - if config.StateScheme == customrawdb.FirewoodScheme { + if config.StateScheme == firewood.Scheme { config.SnapshotLimit = 0 // no snapshot allowed for firewood } return &config @@ -1789,7 +1793,7 @@ func (bc *BlockChain) commitWithSnap( // Because Firewood relies on tracking block hashes in a tree, we need to notify the // database that this block is empty. - if bc.CacheConfig().StateScheme == customrawdb.FirewoodScheme && root == parentRoot { + if bc.triedb.Scheme() == firewood.Scheme && root == parentRoot { if err := bc.triedb.Update(root, parentRoot, current.NumberU64(), nil, nil, triedbOpt); err != nil { return common.Hash{}, fmt.Errorf("failed to update trie for block %s: %w", current.Hash(), err) } @@ -1956,7 +1960,7 @@ func (bc *BlockChain) reprocessState(current *types.Block, reexec uint64) error log.Info("Historical state regenerated", "block", current.NumberU64(), "elapsed", time.Since(start), "nodes", nodes, "preimages", imgs) // Firewood requires processing each root individually. - if bc.CacheConfig().StateScheme == customrawdb.FirewoodScheme { + if triedb.Scheme() == firewood.Scheme { for _, root := range roots { if err := triedb.Commit(root, true); err != nil { return err diff --git a/core/blockchain_repair_test.go b/core/blockchain_repair_test.go index c493e367d7..04404c064a 100644 --- a/core/blockchain_repair_test.go +++ b/core/blockchain_repair_test.go @@ -37,13 +37,13 @@ import ( "github.com/ava-labs/coreth/consensus/dummy" "github.com/ava-labs/coreth/params" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap3" "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/core/vm" "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/libevm/triedb/firewood" ethparams "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/triedb" "github.com/stretchr/testify/require" @@ -508,7 +508,7 @@ func testLongReorgedDeepRepair(t *testing.T, snapshots bool) { } func testRepair(t *testing.T, tt *rewindTest, snapshots bool) { - for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme, customrawdb.FirewoodScheme} { + for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme, firewood.Scheme} { t.Run(scheme, func(t *testing.T) { testRepairWithScheme(t, tt, snapshots, scheme) }) @@ -520,7 +520,7 @@ func testRepairWithScheme(t *testing.T, tt *rewindTest, snapshots bool, scheme s //log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) // fmt.Println(tt.dump(true)) - if scheme == customrawdb.FirewoodScheme && snapshots { + if scheme == firewood.Scheme && snapshots { t.Skip("Firewood scheme does not support snapshots") } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 801647611e..6686a98ea0 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -46,6 +46,7 @@ import ( "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/eth/tracers/logger" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/triedb/firewood" ethparams "github.com/ava-labs/libevm/params" ) @@ -73,7 +74,7 @@ var ( } // Firewood should only be included for snapshot disabled tests. - schemes = []string{rawdb.HashScheme, customrawdb.FirewoodScheme} + schemes = []string{rawdb.HashScheme, firewood.Scheme} ) func newGwei(n int64) *big.Int { @@ -728,7 +729,7 @@ func testUngracefulAsyncShutdown(t *testing.T, scheme string, snapshotEnabled bo // TestCanonicalHashMarker tests all the canonical hash markers are updated/deleted // correctly in case reorg is called. func TestCanonicalHashMarker(t *testing.T) { - for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme, customrawdb.FirewoodScheme} { + for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme, firewood.Scheme} { t.Run(scheme, func(t *testing.T) { testCanonicalHashMarker(t, scheme) }) diff --git a/core/extstate/database.go b/core/extstate/database.go index 96b0a5fe2b..771cbead2a 100644 --- a/core/extstate/database.go +++ b/core/extstate/database.go @@ -6,9 +6,8 @@ package extstate import ( "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/ava-labs/libevm/triedb" - - "github.com/ava-labs/coreth/triedb/firewood" ) func NewDatabaseWithConfig(db ethdb.Database, config *triedb.Config) state.Database { diff --git a/core/extstate/database_test.go b/core/extstate/database_test.go index 00cc402e70..e62a3a2a13 100644 --- a/core/extstate/database_test.go +++ b/core/extstate/database_test.go @@ -15,12 +15,12 @@ import ( "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm/stateconf" + "github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/ava-labs/libevm/trie/trienode" "github.com/ava-labs/libevm/triedb" "github.com/holiman/uint256" "github.com/stretchr/testify/require" - "github.com/ava-labs/coreth/triedb/firewood" "github.com/ava-labs/coreth/triedb/hashdb" ) @@ -81,8 +81,7 @@ func newFuzzState(t *testing.T) *fuzzState { }) firewoodMemdb := rawdb.NewMemoryDatabase() - fwCfg := firewood.Defaults // copy the defaults - fwCfg.ChainDataDir = t.TempDir() // Use a temporary directory for the Firewood + fwCfg := firewood.DefaultConfig(t.TempDir()) // Use a temporary directory for Firewood firewoodState := NewDatabaseWithConfig( firewoodMemdb, &triedb.Config{ diff --git a/core/extstate/firewood_database.go b/core/extstate/firewood_database.go index bd9d08f032..79ede30ba8 100644 --- a/core/extstate/firewood_database.go +++ b/core/extstate/firewood_database.go @@ -8,8 +8,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/state" - - "github.com/ava-labs/coreth/triedb/firewood" + "github.com/ava-labs/libevm/libevm/triedb/firewood" ) var ( diff --git a/core/genesis.go b/core/genesis.go index 0beed54d7b..0807a1e339 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -48,6 +48,7 @@ import ( "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/libevm/triedb/firewood" "github.com/ava-labs/libevm/log" ethparams "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/trie" @@ -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 && triedb.Scheme() == firewood.Scheme { + 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 || triedb.Scheme() == firewood.Scheme { if err := triedb.Commit(root, true); err != nil { panic(fmt.Sprintf("unable to commit genesis block: %v", err)) } diff --git a/core/genesis_test.go b/core/genesis_test.go index cd806ec302..81e1aa6ebf 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -38,10 +38,8 @@ import ( "github.com/ava-labs/coreth/consensus/dummy" "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/params/extras" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/plugin/evm/upgrade/ap3" "github.com/ava-labs/coreth/precompile/contracts/warp" - "github.com/ava-labs/coreth/triedb/firewood" "github.com/ava-labs/coreth/triedb/pathdb" "github.com/ava-labs/coreth/utils" "github.com/ava-labs/libevm/common" @@ -49,6 +47,7 @@ import ( "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/triedb/firewood" ethparams "github.com/ava-labs/libevm/params" "github.com/ava-labs/libevm/trie" "github.com/ava-labs/libevm/triedb" @@ -69,7 +68,7 @@ func TestGenesisBlockForTesting(t *testing.T) { } func TestSetupGenesis(t *testing.T) { - for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme, customrawdb.FirewoodScheme} { + for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme, firewood.Scheme} { t.Run(scheme, func(t *testing.T) { testSetupGenesis(t, scheme) }) @@ -303,10 +302,9 @@ func newDbConfig(t *testing.T, scheme string) *triedb.Config { return triedb.HashDefaults case rawdb.PathScheme: return &triedb.Config{DBOverride: pathdb.Defaults.BackendConstructor} - case customrawdb.FirewoodScheme: - fwCfg := firewood.Defaults + case firewood.Scheme: // 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/core/state_manager.go b/core/state_manager.go index 0f1b5844d0..999b6ea59f 100644 --- a/core/state_manager.go +++ b/core/state_manager.go @@ -30,10 +30,10 @@ package core import ( "fmt" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/triedb/firewood" ) // flushWindow is the distance to the [commitInterval] when we start @@ -60,7 +60,7 @@ type TrieDB interface { func NewTrieWriter(db TrieDB, config *CacheConfig) TrieWriter { // Firewood should only be used in pruning mode, but we shouldn't explicitly manage this. - if config.Pruning && config.StateScheme != customrawdb.FirewoodScheme { + if config.Pruning && config.StateScheme != firewood.Scheme { cm := &cappedMemoryTrieWriter{ TrieDB: db, memoryCap: common.StorageSize(config.TrieDirtyLimit) * 1024 * 1024, diff --git a/eth/api_debug.go b/eth/api_debug.go index 19b88ce16b..1c7d4aeeeb 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -34,7 +34,6 @@ import ( "time" "github.com/ava-labs/coreth/internal/ethapi" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/rpc" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/hexutil" @@ -42,6 +41,7 @@ import ( "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/rlp" "github.com/ava-labs/libevm/trie" @@ -386,5 +386,5 @@ func (api *DebugAPI) GetAccessibleState(from, to rpc.BlockNumber) (uint64, error } func (api *DebugAPI) isFirewood() bool { - return api.eth.blockchain.CacheConfig().StateScheme == customrawdb.FirewoodScheme + return api.eth.blockchain.CacheConfig().StateScheme == firewood.Scheme } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 8542ff14b2..27e942f024 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -36,7 +36,6 @@ import ( "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/coreth/eth/tracers" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/state" @@ -226,12 +225,8 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro // provided, it would be preferable to start from a fresh state, if we have it // on disk. func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) { - // Check if we're using firewood backend by typecasting - // firewoodDB.Scheme() == rawdb.HashScheme, so typecasting is necessary - isFirewood := eth.blockchain.CacheConfig().StateScheme == customrawdb.FirewoodScheme - // Use `hashState` if the state can be recomputed from the live database. - if eth.blockchain.TrieDB().Scheme() == rawdb.HashScheme && !isFirewood { + if eth.blockchain.TrieDB().Scheme() == rawdb.HashScheme { return eth.hashState(ctx, block, reexec, base, readOnly, preferDisk) } return eth.pathState(block) diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index c81f897dd8..ab926de5f0 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -43,7 +43,6 @@ import ( "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/internal/ethapi" "github.com/ava-labs/coreth/params" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/rpc" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/hexutil" @@ -54,6 +53,7 @@ import ( "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/eth/tracers/logger" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/triedb/firewood" ethparams "github.com/ava-labs/libevm/params" "golang.org/x/exp/slices" ) @@ -62,7 +62,7 @@ var ( errStateNotFound = errors.New("state not found") errBlockNotFound = errors.New("block not found") - schemes = []string{rawdb.HashScheme, customrawdb.FirewoodScheme} + schemes = []string{rawdb.HashScheme, firewood.Scheme} ) type testBackend struct { @@ -101,7 +101,7 @@ func newTestBackend(t *testing.T, n int, gspec *core.Genesis, scheme string, gen StateHistory: 100, // Sufficient history for testing ChainDataDir: t.TempDir(), } - if scheme == customrawdb.FirewoodScheme { + if scheme == firewood.Scheme { cacheConfig.SnapshotLimit = 0 // Firewood does not support snapshots } diff --git a/eth/tracers/tracers_test.go b/eth/tracers/tracers_test.go index 5bc7a203f9..c3d0da8c86 100644 --- a/eth/tracers/tracers_test.go +++ b/eth/tracers/tracers_test.go @@ -34,7 +34,6 @@ import ( "github.com/ava-labs/coreth/core" "github.com/ava-labs/coreth/params" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/tests" "github.com/ava-labs/libevm/common" @@ -43,6 +42,7 @@ import ( "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/eth/tracers/logger" + "github.com/ava-labs/libevm/libevm/triedb/firewood" ) func TestMain(m *testing.M) { @@ -52,7 +52,7 @@ func TestMain(m *testing.M) { } func BenchmarkPrestateTracer(b *testing.B) { - for _, scheme := range []string{rawdb.HashScheme, customrawdb.FirewoodScheme} { + for _, scheme := range []string{rawdb.HashScheme, firewood.Scheme} { b.Run(scheme, func(b *testing.B) { benchmarkTransactionTrace(b, scheme) }) diff --git a/go.mod b/go.mod index 4a320c9292..404aff0be3 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ 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.15 - github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 + github.com/ava-labs/libevm v1.13.15-0.20251125171320-8b74e7640434 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.1.0 github.com/go-cmd/cmd v1.4.3 diff --git a/go.sum b/go.sum index 8f661467bf..4247aac37f 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761 h1:FrsqYm5 github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761/go.mod h1:Ntq3RBvDQzNjy14NU3RC2Jf1A9pzfM5RVQ30Gwx/6IM= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15 h1:NAVjEu508HwdgbxH/xQxMQoBUgYUn9RQf0VeCrhtYMY= github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= -github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 h1:hQ15IJxY7WOKqeJqCXawsiXh0NZTzmoQOemkWHz7rr4= -github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= +github.com/ava-labs/libevm v1.13.15-0.20251125171320-8b74e7640434 h1:Ly9cgzVr0UX9FPQr2NAOoea4Pl88VCbZ/TdOB/1SHVA= +github.com/ava-labs/libevm v1.13.15-0.20251125171320-8b74e7640434/go.mod h1:icYQYbiJV0w2pBEWw5iUNnbyGsnCiGAlpvuA3fj5CkA= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 5bb615447d..c7556971f5 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -42,7 +42,6 @@ import ( "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/rpc" - "github.com/ava-labs/coreth/triedb/firewood" "github.com/ava-labs/libevm/accounts" "github.com/ava-labs/libevm/accounts/keystore" "github.com/ava-labs/libevm/accounts/scwallet" @@ -54,6 +53,7 @@ import ( "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/eth/tracers/logger" + "github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/rlp" "github.com/ava-labs/libevm/trie" diff --git a/plugin/evm/customrawdb/database_ext.go b/plugin/evm/customrawdb/database_ext.go index c301a5bfff..b562b740ca 100644 --- a/plugin/evm/customrawdb/database_ext.go +++ b/plugin/evm/customrawdb/database_ext.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/triedb/firewood" ) // InspectDatabase traverses the entire database and checks the size @@ -66,13 +67,13 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { // ParseStateSchemeExt parses the state scheme from the provided string. func ParseStateSchemeExt(provided string, disk ethdb.Database) (string, error) { // Check for custom scheme - if provided == FirewoodScheme { + if provided == firewood.Scheme { if diskScheme := rawdb.ReadStateScheme(disk); diskScheme != "" { // Valid scheme on disk mismatched return "", fmt.Errorf("state scheme %s already set on disk, can't use Firewood", diskScheme) } // If no conflicting scheme is found, is valid. - return FirewoodScheme, nil + return firewood.Scheme, nil } // Check for valid eth scheme diff --git a/plugin/evm/customrawdb/schema_ext.go b/plugin/evm/customrawdb/schema_ext.go index d13f2f7e24..7800573a06 100644 --- a/plugin/evm/customrawdb/schema_ext.go +++ b/plugin/evm/customrawdb/schema_ext.go @@ -51,5 +51,3 @@ var ( // and is equal to [syncPerformedPrefix] + block number as uint64. syncPerformedKeyLength = len(syncPerformedPrefix) + wrappers.LongLen ) - -var FirewoodScheme = "firewood" diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 5a27d49526..7feecd3f54 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -40,6 +40,7 @@ import ( "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/triedb/firewood" "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/libevm/rlp" @@ -68,7 +69,6 @@ import ( "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/params/extras" "github.com/ava-labs/coreth/plugin/evm/config" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/plugin/evm/extension" "github.com/ava-labs/coreth/plugin/evm/gossip" "github.com/ava-labs/coreth/plugin/evm/message" @@ -391,7 +391,7 @@ func (vm *VM) Initialize( vm.ethConfig.SkipTxIndexing = vm.config.SkipTxIndexing vm.ethConfig.StateScheme = vm.config.StateScheme - if vm.ethConfig.StateScheme == customrawdb.FirewoodScheme { + if vm.ethConfig.StateScheme == firewood.Scheme { log.Warn("Firewood state scheme is enabled") log.Warn("This is untested in production, use at your own risk") // Firewood does not support iterators, so the snapshot cannot be constructed @@ -513,7 +513,7 @@ func (vm *VM) initializeMetrics() error { return err } - if vm.config.MetricsExpensiveEnabled && vm.config.StateScheme == customrawdb.FirewoodScheme { + if vm.config.MetricsExpensiveEnabled && vm.config.StateScheme == firewood.Scheme { if err := ffi.StartMetrics(); err != nil { return fmt.Errorf("failed to start firewood metrics collection: %w", err) } @@ -591,7 +591,7 @@ func (vm *VM) initializeStateSync(lastAcceptedHeight uint64) error { // used by the node when processing blocks. // However, Firewood does not support multiple TrieDBs, so we use the same one. evmTrieDB := vm.eth.BlockChain().TrieDB() - if vm.ethConfig.StateScheme != customrawdb.FirewoodScheme { + if vm.ethConfig.StateScheme != firewood.Scheme { evmTrieDB = triedb.NewDatabase( vm.chaindb, &triedb.Config{ diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 50cae9cdcc..8249260ee8 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -33,6 +33,7 @@ import ( "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/trie" "github.com/holiman/uint256" @@ -47,7 +48,6 @@ import ( "github.com/ava-labs/coreth/params" "github.com/ava-labs/coreth/params/paramstest" "github.com/ava-labs/coreth/plugin/evm/customheader" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/plugin/evm/customtypes" "github.com/ava-labs/coreth/plugin/evm/extension" "github.com/ava-labs/coreth/plugin/evm/message" @@ -211,7 +211,7 @@ func testVMUpgrades(t *testing.T, scheme string) { func TestBuildEthTxBlock(t *testing.T) { // This test is done for all schemes to ensure the VM can be started with any scheme. - for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme, customrawdb.FirewoodScheme} { + for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme, firewood.Scheme} { t.Run(scheme, func(t *testing.T) { testBuildEthTxBlock(t, scheme) }) diff --git a/plugin/evm/vmtest/test_vm.go b/plugin/evm/vmtest/test_vm.go index d98bb24b6d..fb7f7e794e 100644 --- a/plugin/evm/vmtest/test_vm.go +++ b/plugin/evm/vmtest/test_vm.go @@ -17,16 +17,16 @@ import ( "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/libevm/core/rawdb" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/stretchr/testify/require" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" "github.com/ava-labs/coreth/plugin/evm/extension" avalancheatomic "github.com/ava-labs/avalanchego/chains/atomic" commoneng "github.com/ava-labs/avalanchego/snow/engine/common" ) -var Schemes = []string{rawdb.HashScheme, customrawdb.FirewoodScheme} +var Schemes = []string{rawdb.HashScheme, firewood.Scheme} type TestVMConfig struct { IsSyncing bool @@ -110,7 +110,7 @@ func ResetMetrics(snowCtx *snow.Context) { func OverrideSchemeConfig(scheme string, configJSON string) (string, error) { // If the scheme is not Firewood, return the configJSON as is - if scheme != customrawdb.FirewoodScheme { + if scheme != firewood.Scheme { return configJSON, nil } @@ -123,7 +123,7 @@ func OverrideSchemeConfig(scheme string, configJSON string) (string, error) { } // Set Firewood-specific configuration flags (these will override any existing values) - configMap["state-scheme"] = customrawdb.FirewoodScheme + configMap["state-scheme"] = firewood.Scheme configMap["snapshot-cache"] = 0 configMap["pruning-enabled"] = true configMap["state-sync-enabled"] = false diff --git a/scripts/eth-allowed-packages.txt b/scripts/eth-allowed-packages.txt index 1652e4a518..dc7d7fb308 100644 --- a/scripts/eth-allowed-packages.txt +++ b/scripts/eth-allowed-packages.txt @@ -33,6 +33,7 @@ "github.com/ava-labs/libevm/libevm/legacy" "github.com/ava-labs/libevm/libevm/options" "github.com/ava-labs/libevm/libevm/stateconf" +"github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/ava-labs/libevm/log" "github.com/ava-labs/libevm/metrics" "github.com/ava-labs/libevm/rlp" diff --git a/tests/state_test_util.go b/tests/state_test_util.go index ddc1c3b1e7..723b4fc86a 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -29,11 +29,10 @@ package tests import ( "os" + "path/filepath" "github.com/ava-labs/coreth/core/extstate" "github.com/ava-labs/coreth/core/state/snapshot" - "github.com/ava-labs/coreth/plugin/evm/customrawdb" - "github.com/ava-labs/coreth/triedb/firewood" "github.com/ava-labs/coreth/triedb/hashdb" "github.com/ava-labs/coreth/triedb/pathdb" "github.com/ava-labs/libevm/common" @@ -41,6 +40,7 @@ import ( "github.com/ava-labs/libevm/core/state" "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/ethdb" + "github.com/ava-labs/libevm/libevm/triedb/firewood" "github.com/ava-labs/libevm/triedb" "github.com/holiman/uint256" ) @@ -67,9 +67,8 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, snapshotter bo tconf.DBOverride = hashdb.Defaults.BackendConstructor case rawdb.PathScheme: tconf.DBOverride = pathdb.Defaults.BackendConstructor - case customrawdb.FirewoodScheme: - cfg := firewood.Defaults - cfg.ChainDataDir = tempdir + case firewood.Scheme: + cfg := firewood.DefaultConfig(filepath.Join(tempdir, "firewood")) tconf.DBOverride = cfg.BackendConstructor default: panic("unknown trie database scheme" + scheme) diff --git a/triedb/firewood/account_trie.go b/triedb/firewood/account_trie.go deleted file mode 100644 index 7be34f34e8..0000000000 --- a/triedb/firewood/account_trie.go +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package firewood - -import ( - "errors" - - "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/crypto" - "github.com/ava-labs/libevm/ethdb" - "github.com/ava-labs/libevm/log" - "github.com/ava-labs/libevm/rlp" - "github.com/ava-labs/libevm/trie" - "github.com/ava-labs/libevm/trie/trienode" - "github.com/ava-labs/libevm/triedb/database" -) - -// 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. -// -// Note this is not concurrent safe. -type AccountTrie struct { - fw *Database - 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 *Database) (*AccountTrie, error) { - reader, err := db.Reader(root) - if err != nil { - return nil, err - } - return &AccountTrie{ - fw: db, - parentRoot: root, - reader: reader, - dirtyKeys: make(map[string][]byte), - hasChanges: true, // Start with hasChanges true to allow computing the proposal hash - }, nil -} - -// GetAccount returns the state account associated with an address. -// - If the account has been updated, the new value is returned. -// - If the account has been deleted, (nil, nil) is returned. -// - If the account does not exist, (nil, nil) is returned. -func (a *AccountTrie) GetAccount(addr common.Address) (*types.StateAccount, error) { - key := crypto.Keccak256Hash(addr.Bytes()).Bytes() - - // First check if there's a pending update for this account - if updateValue, exists := a.dirtyKeys[string(key)]; exists { - // If the value is empty, it indicates deletion - // Invariant: All encoded values have length > 0 - if len(updateValue) == 0 { - return nil, nil - } - // Decode and return the updated account - account := new(types.StateAccount) - err := rlp.DecodeBytes(updateValue, account) - return account, err - } - - // No pending update found, read from the underlying reader - accountBytes, err := a.reader.Node(common.Hash{}, key, common.Hash{}) - if err != nil { - return nil, err - } - - if accountBytes == nil { - return nil, nil - } - - // Decode the account node - account := new(types.StateAccount) - err = rlp.DecodeBytes(accountBytes, account) - return account, err -} - -// GetStorage returns the value associated with a storage key for a given account address. -// - If the storage slot has been updated, the new value is returned. -// - If the storage slot has been deleted, (nil, nil) is returned. -// - If the storage slot does not exist, (nil, nil) is returned. -func (a *AccountTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) { - // If the account has been deleted, we should return nil - accountKey := crypto.Keccak256Hash(addr.Bytes()).Bytes() - if val, exists := a.dirtyKeys[string(accountKey)]; exists && len(val) == 0 { - return nil, nil - } - - var combinedKey [2 * common.HashLength]byte - storageKey := crypto.Keccak256Hash(key).Bytes() - copy(combinedKey[:common.HashLength], accountKey) - copy(combinedKey[common.HashLength:], storageKey) - - // Check if there's a pending update for this storage slot - if updateValue, exists := a.dirtyKeys[string(combinedKey[:])]; exists { - // If the value is empty, it indicates deletion - if len(updateValue) == 0 { - return nil, nil - } - // Decode and return the updated storage value - _, decoded, _, err := rlp.Split(updateValue) - return decoded, err - } - - // No pending update found, read from the underlying reader - storageBytes, err := a.reader.Node(common.Hash{}, combinedKey[:], common.Hash{}) - if err != nil || storageBytes == nil { - return nil, err - } - - // Decode the storage value - _, decoded, _, err := rlp.Split(storageBytes) - return decoded, err -} - -// UpdateAccount replaces or creates the state account associated with an address. -// This new value will be returned for subsequent `GetAccount` calls. -func (a *AccountTrie) UpdateAccount(addr common.Address, account *types.StateAccount) error { - // Queue the keys and values for later commit - key := crypto.Keccak256Hash(addr.Bytes()).Bytes() - data, err := rlp.EncodeToBytes(account) - if err != nil { - return err - } - a.dirtyKeys[string(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 -} - -// UpdateStorage replaces or creates the value associated with a storage key for a given account address. -// This new value will be returned for subsequent `GetStorage` calls. -func (a *AccountTrie) UpdateStorage(addr common.Address, key []byte, value []byte) error { - var combinedKey [2 * common.HashLength]byte - accountKey := crypto.Keccak256Hash(addr.Bytes()).Bytes() - storageKey := crypto.Keccak256Hash(key).Bytes() - copy(combinedKey[:common.HashLength], accountKey) - copy(combinedKey[common.HashLength:], storageKey) - - data, err := rlp.EncodeToBytes(value) - if err != nil { - return err - } - - // 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.hasChanges = true // Mark that there are changes to commit - return nil -} - -// DeleteAccount removes the state account associated with an address. -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) // Nil value indicates deletion - a.hasChanges = true // Mark that there are changes to commit - return nil -} - -// DeleteStorage removes the value associated with a storage key for a given account address. -func (a *AccountTrie) DeleteStorage(addr common.Address, key []byte) error { - var combinedKey [2 * common.HashLength]byte - accountKey := crypto.Keccak256Hash(addr.Bytes()).Bytes() - storageKey := crypto.Keccak256Hash(key).Bytes() - copy(combinedKey[:common.HashLength], accountKey) - copy(combinedKey[common.HashLength:], storageKey) - - // 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.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. -// If there are no changes since the last call, the cached root is returned. -func (a *AccountTrie) Hash() common.Hash { - hash, err := a.hash() - if err != nil { - log.Error("Failed to hash account trie", "error", err) - return common.Hash{} - } - return 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) - if err != nil { - return common.Hash{}, err - } - a.root = root - a.hasChanges = false // Avoid re-hashing until next update - } - 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. -func (a *AccountTrie) Commit(bool) (common.Hash, *trienode.NodeSet, error) { - // Get the hash of the trie. - 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 -} - -// UpdateContractCode implements state.Trie. -// Contract code is controlled by rawdb, so we don't need to do anything here. -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. -func (*AccountTrie) GetKey([]byte) []byte { - return nil -} - -// NodeIterator implements state.Trie. -// Firewood does not support iterating over internal nodes. -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. -func (*AccountTrie) Prove([]byte, ethdb.KeyValueWriter) error { - return errors.New("Prove not implemented for Firewood") -} - -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)), - } - - // Deep copy dirtyKeys map - for k, v := range a.dirtyKeys { - 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]...) - } - - return newTrie -} diff --git a/triedb/firewood/database.go b/triedb/firewood/database.go deleted file mode 100644 index f95104260f..0000000000 --- a/triedb/firewood/database.go +++ /dev/null @@ -1,601 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package firewood - -import ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "sync" - "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" - "github.com/ava-labs/libevm/triedb/database" -) - -const ( - // Directory where all Firewood state lives. - firewoodDir = "firewood" - firewoodFileName = "firewood.db" - firewoodRootStoreDir = "root_store" -) - -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) -) - -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) -} - -// 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 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 -} - -// 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, -} - -func (c Config) BackendConstructor(ethdb.Database) triedb.DBOverride { - db, err := New(c) - if err != nil { - log.Crit("firewood: error creating database", "error", err) - } - return db -} - -type Database 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) (*Database, error) { - firewoodDir := filepath.Join(config.ChainDataDir, firewoodDir) - filePath := filepath.Join(firewoodDir, firewoodFileName) - if err := validatePath(filePath); err != nil { - return nil, err - } - - var rootStoreDir string - if config.ArchiveMode { - rootStoreDir = filepath.Join(firewoodDir, firewoodRootStoreDir) - } - - fw, err := ffi.New(filePath, &ffi.Config{ - NodeCacheEntries: uint(config.CleanCacheSize) / 256, // TODO: estimate 256 bytes per node - FreeListCacheEntries: config.FreeListCacheEntries, - Revisions: config.Revisions, - ReadCacheStrategy: config.ReadCacheStrategy, - RootStoreDir: rootStoreDir, - }) - if err != nil { - return nil, err - } - - currentRoot, err := fw.Root() - if err != nil { - return nil, err - } - - return &Database{ - fwDisk: fw, - proposalMap: make(map[common.Hash][]*ProposalContext), - proposalTree: &ProposalContext{ - Root: common.Hash(currentRoot), - }, - }, nil -} - -func validatePath(path string) error { - if path == "" { - return errors.New("firewood database file path 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) - } - return nil - case err != nil: - return fmt.Errorf("error checking database directory: %w", err) - case !info.IsDir(): - return fmt.Errorf("database directory path is not a directory: %s", 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. -func (*Database) Scheme() string { - return rawdb.HashScheme -} - -// Initialized checks whether a non-empty genesis block has been written. -func (db *Database) Initialized(common.Hash) bool { - root, err := db.fwDisk.Root() - if err != nil { - log.Error("firewood: error getting 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. - 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 (db *Database) 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) - } - - // The rest of the operations except key-value arranging must occur with a lock - db.proposalLock.Lock() - defer db.proposalLock.Unlock() - - // 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 := db.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 - } - } - } - - keys, values := arrangeKeyValuePairs(nodes) // may return nil, nil if no changes - return db.propose(root, parentRoot, hash, parentHash, block, keys, values) -} - -// 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 (db *Database) 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 db.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, - } - - db.proposalMap[root] = append(db.proposalMap[root], pCtx) - parentProposal.Children = append(parentProposal.Children, pCtx) - 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 := db.proposalTree.Hashes[parentHash]; db.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 db.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(), db.proposalTree.Root.Hex(), root.Hex(), block) - } - - log.Debug("firewood: proposing from database root", "root", root.Hex(), "height", block) - p, err := createProposal(db.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: db.proposalTree, - } - db.proposalMap[root] = append(db.proposalMap[root], pCtx) - db.proposalTree.Children = append(db.proposalTree.Children, pCtx) - - return nil -} - -// 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. -// -// 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 (db *Database) Commit(root common.Hash, report bool) error { - // We need to lock the proposal tree to prevent concurrent writes. - db.proposalLock.Lock() - defer db.proposalLock.Unlock() - - // Find the proposal with the given root. - var pCtx *ProposalContext - for _, possible := range db.proposalMap[root] { - if possible.Parent.Root == db.proposalTree.Root && possible.Parent.Block == db.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()) - } - - start := time.Now() - // Commit the proposal to the database. - if err := pCtx.Proposal.Commit(); err != nil { - db.dereference(pCtx) // no longer committable - return fmt.Errorf("firewood: error 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 db.cleanupCommittedProposal(pCtx) - - // Assert that the root of the database matches the committed proposal root. - currentRoot, err := db.fwDisk.Root() - if err != nil { - return fmt.Errorf("firewood: error 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 report { - log.Info("Persisted proposal to firewood database", "root", root) - } else { - log.Debug("Persisted proposal to firewood database", "root", root) - } - 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 (*Database) Size() (common.StorageSize, common.StorageSize) { - return 0, 0 -} - -// Reference is a no-op. -func (*Database) Reference(common.Hash, common.Hash) {} - -// Dereference is a no-op since Firewood handles unused state roots internally. -func (*Database) Dereference(common.Hash) {} - -// Firewood does not support this. -func (*Database) Cap(common.StorageSize) error { - return nil -} - -func (db *Database) Close() error { - db.proposalLock.Lock() - defer db.proposalLock.Unlock() - - // before closing, we must deference any outstanding proposals to free the - // memory owned by firewood (outside of go's memory management) - for _, pCtx := range db.proposalTree.Children { - db.dereference(pCtx) - } - - db.proposalMap = nil - db.proposalTree.Children = nil - - // Close the database - // This may block momentarily while finalizers for Firewood objects run. - return db.fwDisk.Close(context.Background()) -} - -// 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)) - } - - start := time.Now() - p, err = layer.Propose(keys, values) - if err != nil { - return nil, fmt.Errorf("firewood: unable to create proposal for root %s: %w", 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) - } - - 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()) - } - - return p, nil -} - -// cleanupCommittedProposal dereferences the proposal and removes it from the proposal map. -// It also recursively dereferences all children of the proposal. -func (db *Database) cleanupCommittedProposal(pCtx *ProposalContext) { - start := time.Now() - oldChildren := db.proposalTree.Children - db.proposalTree = pCtx - db.proposalTree.Parent = nil - - db.removeProposalFromMap(pCtx) - - for _, childCtx := range oldChildren { - // Don't dereference the recently commit proposal. - if childCtx != pCtx { - db.dereference(childCtx) - } - } - 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 (db *Database) dereference(pCtx *ProposalContext) { - // Base case: if there are children, we need to dereference them as well. - for _, child := range pCtx.Children { - db.dereference(child) - } - pCtx.Children = nil - - // Remove the proposal from the map. - db.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) -} - -// removeProposalFromMap removes the proposal from the proposal map. -// The proposal lock must be held when calling this function. -func (db *Database) removeProposalFromMap(pCtx *ProposalContext) { - rootList := db.proposalMap[pCtx.Root] - for i, p := range rootList { - if p == pCtx { // pointer comparison - guaranteed to be unique - rootList[i] = rootList[len(rootList)-1] - rootList[len(rootList)-1] = nil - rootList = rootList[:len(rootList)-1] - break - } - } - if len(rootList) == 0 { - delete(db.proposalMap, pCtx.Root) - } else { - db.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 (db *Database) Reader(root common.Hash) (database.Reader, error) { - if _, err := db.fwDisk.GetFromRoot(ffi.Hash(root), []byte{}); err != nil { - return nil, fmt.Errorf("firewood: unable to retrieve from root %s: %w", root.Hex(), err) - } - return &reader{db: db, root: ffi.Hash(root)}, nil -} - -// reader is a state reader of Database which implements the Reader interface. -type reader struct { - db *Database - root ffi.Hash // The root of the state this reader is reading. -} - -// 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.db.fwDisk.GetFromRoot(reader.root, 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 (db *Database) getProposalHash(parentRoot common.Hash, keys, values [][]byte) (common.Hash, error) { - // This function only reads from existing tracked proposals, so we can use a read lock. - db.proposalLock.RLock() - defer db.proposalLock.RUnlock() - - var ( - p *ffi.Proposal - err error - ) - start := time.Now() - if db.proposalTree.Root == parentRoot { - // Propose from the database root. - p, err = db.fwDisk.Propose(keys, values) - if err != nil { - return common.Hash{}, fmt.Errorf("firewood: error 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 := db.proposalMap[parentRoot] - if !ok || len(proposals) == 0 { - return common.Hash{}, fmt.Errorf("firewood: no proposal found for parent root %s", parentRoot.Hex()) - } - rootProposal := proposals[0].Proposal - - p, err = rootProposal.Propose(keys, values) - if err != nil { - return common.Hash{}, fmt.Errorf("firewood: error proposing from parent proposal %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) - } - }() - - root, err := p.Root() - if err != nil { - return common.Hash{}, err - } - return common.Hash(root), nil -} - -func arrangeKeyValuePairs(nodes *trienode.MergedNodeSet) ([][]byte, [][]byte) { - if nodes == nil { - return nil, nil // No changes to propose - } - // Create key-value pairs for the nodes in bytes. - var ( - acctKeys [][]byte - acctValues [][]byte - storageKeys [][]byte - storageValues [][]byte - ) - - 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) - } - } - } - - // We need to do all storage operations first, so prefix-deletion works for accounts. - return append(storageKeys, acctKeys...), append(storageValues, acctValues...) -} diff --git a/triedb/firewood/storage_trie.go b/triedb/firewood/storage_trie.go deleted file mode 100644 index 9e6367183b..0000000000 --- a/triedb/firewood/storage_trie.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package firewood - -import ( - "github.com/ava-labs/libevm/common" - "github.com/ava-labs/libevm/trie/trienode" -) - -type StorageTrie struct { - *AccountTrie -} - -// `NewStorageTrie` returns a wrapper around an `AccountTrie` since Firewood -// does not require a separate storage trie. All changes are managed by the account trie. -func NewStorageTrie(accountTrie *AccountTrie) (*StorageTrie, error) { - return &StorageTrie{ - AccountTrie: accountTrie, - }, nil -} - -// 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. -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. -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. -func (*StorageTrie) Copy() *StorageTrie { - return nil -}