Skip to content

Commit 9114d48

Browse files
feat: enable archival mode for Firewood (#1873)
Co-authored-by: rodrigo <[email protected]>
1 parent cab4a49 commit 9114d48

File tree

12 files changed

+289
-83
lines changed

12 files changed

+289
-83
lines changed

RELEASES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## [v0.8.1](https://github.com/ava-labs/subnet-evm/releases/tag/v0.8.1)
44

5-
- Add pending releases here
5+
- Enables Firewood to run with pruning disabled.
6+
- This change modifies the filepath of Firewood and any nodes using Firewood will need to resync.
67

78
## [v0.8.0](https://github.com/ava-labs/subnet-evm/releases/tag/v0.8.0)
89

core/blockchain.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434
"fmt"
3535
"io"
3636
"math/big"
37-
"path/filepath"
3837
"runtime"
3938
"strings"
4039
"sync"
@@ -174,8 +173,6 @@ const (
174173
// trieCleanCacheStatsNamespace is the namespace to surface stats from the trie
175174
// clean cache's underlying fastcache.
176175
trieCleanCacheStatsNamespace = "hashdb/memcache/clean/fastcache"
177-
178-
firewoodFileName = "firewood.db"
179176
)
180177

181178
// cacheableFeeConfig encapsulates fee configuration itself and the block number that it has changed at,
@@ -242,12 +239,14 @@ func (c *CacheConfig) triedbConfig() *triedb.Config {
242239
if c.ChainDataDir == "" {
243240
log.Crit("Chain data directory must be specified for Firewood")
244241
}
242+
245243
config.DBOverride = firewood.Config{
246-
FilePath: filepath.Join(c.ChainDataDir, firewoodFileName),
244+
ChainDataDir: c.ChainDataDir,
247245
CleanCacheSize: c.TrieCleanLimit * 1024 * 1024,
248246
FreeListCacheEntries: firewood.Defaults.FreeListCacheEntries,
249247
Revisions: uint(c.StateHistory), // must be at least 2
250248
ReadCacheStrategy: ffi.CacheAllReads,
249+
ArchiveMode: !c.Pruning,
251250
}.BackendConstructor
252251
}
253252
return config

core/blockchain_ext_test.go

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -141,27 +141,47 @@ func copyMemDB(db ethdb.Database) (ethdb.Database, error) {
141141
return newDB, nil
142142
}
143143

144-
// This copies all files from a flat directory [src] to a new temporary directory and returns
145-
// the path to the new directory.
146-
func copyFlatDir(t *testing.T, src string) string {
144+
// copyDir recursively copies all files and folders from a directory [src] to a
145+
// new temporary directory and returns the path to the new directory.
146+
func copyDir(t *testing.T, src string) string {
147147
t.Helper()
148+
148149
if src == "" {
149150
return ""
150151
}
151152

152153
dst := t.TempDir()
153-
ents, err := os.ReadDir(src)
154-
require.NoError(t, err)
154+
err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
155+
if err != nil {
156+
return err
157+
}
155158

156-
for _, e := range ents {
157-
require.False(t, e.IsDir(), "expected flat directory")
158-
name := e.Name()
159-
data, err := os.ReadFile(filepath.Join(src, name))
160-
require.NoError(t, err)
161-
info, err := e.Info()
162-
require.NoError(t, err)
163-
require.NoError(t, os.WriteFile(filepath.Join(dst, name), data, info.Mode().Perm()))
164-
}
159+
// Calculate the relative path from src
160+
relPath, err := filepath.Rel(src, path)
161+
if err != nil {
162+
return err
163+
}
164+
165+
// Skip the root directory itself
166+
if relPath == "." {
167+
return nil
168+
}
169+
170+
dstPath := filepath.Join(dst, relPath)
171+
172+
if info.IsDir() {
173+
return os.MkdirAll(dstPath, info.Mode().Perm())
174+
}
175+
176+
data, err := os.ReadFile(path)
177+
if err != nil {
178+
return err
179+
}
180+
181+
return os.WriteFile(dstPath, data, info.Mode().Perm())
182+
})
183+
184+
require.NoError(t, err)
165185
return dst
166186
}
167187

@@ -212,7 +232,7 @@ func checkBlockChainState(
212232
// Copy the database over to prevent any issues when re-using [originalDB] after this call.
213233
originalDB, err = copyMemDB(originalDB)
214234
require.NoError(err)
215-
newChainDataDir := copyFlatDir(t, oldChainDataDir)
235+
newChainDataDir := copyDir(t, oldChainDataDir)
216236
restartedChain, err := create(originalDB, gspec, lastAcceptedBlock.Hash(), newChainDataDir)
217237
require.NoError(err)
218238
defer restartedChain.Stop()
@@ -1818,7 +1838,7 @@ func ReexecCorruptedStateTest(t *testing.T, create ReexecTestFunc) {
18181838
blockchain.Stop()
18191839

18201840
// Restart blockchain with existing state
1821-
newDir := copyFlatDir(t, tempDir) // avoid file lock
1841+
newDir := copyDir(t, tempDir) // avoid file lock
18221842
restartedBlockchain, err := create(chainDB, gspec, chain[1].Hash(), newDir, 4096)
18231843
require.NoError(t, err)
18241844
defer restartedBlockchain.Stop()

core/blockchain_test.go

Lines changed: 183 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ var (
7575
AcceptorQueueLimit: 64,
7676
}
7777

78-
// Firewood should only be included for non-archive, snapshot disabled tests.
78+
// Firewood should only be included for snapshot disabled tests.
7979
schemes = []string{rawdb.HashScheme, customrawdb.FirewoodScheme}
8080
)
8181

@@ -114,22 +114,36 @@ func TestArchiveBlockChain(t *testing.T) {
114114
}
115115

116116
func TestArchiveBlockChainSnapsDisabled(t *testing.T) {
117-
create := func(db ethdb.Database, gspec *Genesis, lastAcceptedHash common.Hash, _ string) (*BlockChain, error) {
117+
for _, scheme := range schemes {
118+
t.Run(scheme, func(t *testing.T) {
119+
testArchiveBlockChainSnapsDisabled(t, scheme)
120+
})
121+
}
122+
}
123+
124+
func testArchiveBlockChainSnapsDisabled(t *testing.T, scheme string) {
125+
create := func(db ethdb.Database, gspec *Genesis, lastAcceptedHash common.Hash, dataPath string) (*BlockChain, error) {
126+
cacheConfig := &CacheConfig{
127+
TrieCleanLimit: 256,
128+
TrieDirtyLimit: 256,
129+
TrieDirtyCommitTarget: 20,
130+
TriePrefetcherParallelism: 4,
131+
Pruning: false, // Archive mode
132+
StateHistory: 32, // Required for Firewood's minimum Revision count
133+
SnapshotLimit: 0, // Disable snapshots
134+
AcceptorQueueLimit: 64,
135+
StateScheme: scheme,
136+
ChainDataDir: dataPath,
137+
}
138+
118139
return createBlockChain(
119140
db,
120-
&CacheConfig{
121-
TrieCleanLimit: 256,
122-
TrieDirtyLimit: 256,
123-
TrieDirtyCommitTarget: 20,
124-
TriePrefetcherParallelism: 4,
125-
Pruning: false, // Archive mode
126-
SnapshotLimit: 0, // Disable snapshots
127-
AcceptorQueueLimit: 64,
128-
},
141+
cacheConfig,
129142
gspec,
130143
lastAcceptedHash,
131144
)
132145
}
146+
133147
for _, tt := range tests {
134148
t.Run(tt.Name, func(t *testing.T) {
135149
tt.testFunc(t, create)
@@ -357,6 +371,164 @@ func TestBlockChainOfflinePruningUngracefulShutdown(t *testing.T) {
357371
}
358372
}
359373

374+
// TestPruningToNonPruning tests that opening a previously pruned database as a
375+
// non-pruned database is successful.
376+
func TestPruningToNonPruning(t *testing.T) {
377+
for _, scheme := range schemes {
378+
t.Run(scheme, func(t *testing.T) {
379+
testPruningToNonPruning(t, scheme)
380+
})
381+
}
382+
}
383+
384+
// testPruningToNonPruning tests that opening a previously pruned database as a
385+
// non-pruned database is successful.
386+
//
387+
// This test checks the following invariants:
388+
// 1. Verifies that a pruned node does not have the state for all blocks (except
389+
// the last accepted block) upon restart.
390+
// 2. Verify that a pruned => archival node has the state for all blocks
391+
// accepted during archival mode upon restart.
392+
func testPruningToNonPruning(t *testing.T, scheme string) {
393+
var (
394+
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
395+
key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
396+
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
397+
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
398+
chainDB = rawdb.NewMemoryDatabase()
399+
numStates = uint64(5)
400+
)
401+
402+
gspec := &Genesis{
403+
Config: &params.ChainConfig{HomesteadBlock: new(big.Int)},
404+
Alloc: types.GenesisAlloc{addr1: {Balance: big.NewInt(1000000)}},
405+
}
406+
407+
chainDataDir := t.TempDir()
408+
pruningConfig := &CacheConfig{
409+
TrieCleanLimit: 256,
410+
TrieDirtyLimit: 256,
411+
TrieDirtyCommitTarget: 20,
412+
TriePrefetcherParallelism: 4,
413+
Pruning: true, // Enable pruning
414+
CommitInterval: 4096,
415+
StateHistory: numStates,
416+
AcceptorQueueLimit: 64,
417+
StateScheme: scheme,
418+
ChainDataDir: chainDataDir,
419+
}
420+
421+
// Create a node in pruning mode.
422+
blockchain, err := createBlockChain(chainDB, pruningConfig, gspec, common.Hash{})
423+
if err != nil {
424+
t.Fatal(err)
425+
}
426+
427+
// Generate 10 (2 * numStates) blocks.
428+
signer := types.HomesteadSigner{}
429+
_, blocks, _, err := GenerateChainWithGenesis(gspec, blockchain.engine, 2*int(numStates), 10, func(i int, gen *BlockGen) {
430+
tx, _ := types.SignTx(types.NewTransaction(gen.TxNonce(addr1), addr2, big.NewInt(10000), ethparams.TxGas, nil, nil), signer, key1)
431+
gen.AddTx(tx)
432+
})
433+
if err != nil {
434+
t.Fatal(err)
435+
}
436+
437+
prunedBlocks := blocks[:numStates]
438+
nonPrunedBlocks := blocks[numStates:]
439+
440+
// Insert the first five blocks.
441+
// The states of the first four blocks will be lost upon restart.
442+
if _, err := blockchain.InsertChain(prunedBlocks); err != nil {
443+
t.Fatal(err)
444+
}
445+
for _, block := range prunedBlocks {
446+
if err := blockchain.Accept(block); err != nil {
447+
t.Fatal(err)
448+
}
449+
}
450+
blockchain.DrainAcceptorQueue()
451+
452+
lastAcceptedHash := blockchain.LastConsensusAcceptedBlock().Hash()
453+
blockchain.Stop()
454+
455+
// Reopen the node.
456+
blockchain, err = createBlockChain(chainDB, pruningConfig, gspec, lastAcceptedHash)
457+
if err != nil {
458+
t.Fatal(err)
459+
}
460+
461+
// 1. Verify that a pruned node does not have the state for all blocks (except
462+
// the last accepted block) upon restart.
463+
for _, block := range prunedBlocks[:numStates-1] {
464+
if blockchain.HasState(block.Root()) {
465+
t.Fatalf("Expected blockchain to be missing state for intermediate block %d with pruning enabled", block.NumberU64())
466+
}
467+
}
468+
469+
blockchain.Stop()
470+
471+
archiveConfig := &CacheConfig{
472+
TrieCleanLimit: 256,
473+
TrieDirtyLimit: 256,
474+
TrieDirtyCommitTarget: 20,
475+
TriePrefetcherParallelism: 4,
476+
Pruning: false, // Archive mode
477+
AcceptorQueueLimit: 64,
478+
StateScheme: scheme,
479+
StateHistory: 32,
480+
ChainDataDir: chainDataDir,
481+
}
482+
483+
// Reopen the node, but switch from pruning to archival mode.
484+
blockchain, err = createBlockChain(
485+
chainDB,
486+
archiveConfig,
487+
gspec,
488+
lastAcceptedHash,
489+
)
490+
if err != nil {
491+
t.Fatal(err)
492+
}
493+
494+
// Insert the remaining five blocks.
495+
// The states of all these blocks will still be accessible on restart
496+
// since we're now in archival mode.
497+
if _, err := blockchain.InsertChain(nonPrunedBlocks); err != nil {
498+
t.Fatal(err)
499+
}
500+
501+
for _, block := range nonPrunedBlocks {
502+
if err := blockchain.Accept(block); err != nil {
503+
t.Fatal(err)
504+
}
505+
}
506+
blockchain.DrainAcceptorQueue()
507+
508+
lastAcceptedHash = blockchain.LastConsensusAcceptedBlock().Hash()
509+
blockchain.Stop()
510+
511+
// Reopen the archival node.
512+
blockchain, err = createBlockChain(
513+
chainDB,
514+
archiveConfig,
515+
gspec,
516+
lastAcceptedHash,
517+
)
518+
if err != nil {
519+
t.Fatal(err)
520+
}
521+
defer blockchain.Stop()
522+
523+
// 2. Verify that a pruned => archival node has the state for all blocks
524+
// accepted during archival mode upon restart.
525+
for _, block := range nonPrunedBlocks {
526+
if !blockchain.HasState(block.Root()) {
527+
t.Fatalf("Expected blockchain to have the state for block %d with pruning disabled", block.NumberU64())
528+
}
529+
}
530+
}
531+
360532
func testRepopulateMissingTriesParallel(t *testing.T, parallelism int) {
361533
var (
362534
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")

core/extstate/database_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package extstate
66
import (
77
"encoding/binary"
88
"math/rand"
9-
"path/filepath"
109
"slices"
1110
"testing"
1211

@@ -82,8 +81,8 @@ func newFuzzState(t *testing.T) *fuzzState {
8281
})
8382

8483
firewoodMemdb := rawdb.NewMemoryDatabase()
85-
fwCfg := firewood.Defaults // copy the defaults
86-
fwCfg.FilePath = filepath.Join(t.TempDir(), "firewood") // Use a temporary directory for the Firewood
84+
fwCfg := firewood.Defaults // copy the defaults
85+
fwCfg.ChainDataDir = t.TempDir() // Use a temporary directory for the Firewood
8786
firewoodState := NewDatabaseWithConfig(
8887
firewoodMemdb,
8988
&triedb.Config{

core/genesis_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import (
3232
_ "embed"
3333
"fmt"
3434
"math/big"
35-
"path/filepath"
3635
"reflect"
3736
"testing"
3837

@@ -373,7 +372,7 @@ func newDbConfig(t *testing.T, scheme string) *triedb.Config {
373372
case customrawdb.FirewoodScheme:
374373
fwCfg := firewood.Defaults
375374
// Create a unique temporary directory for each test
376-
fwCfg.FilePath = filepath.Join(t.TempDir(), "firewood_state") // matches blockchain.go
375+
fwCfg.ChainDataDir = t.TempDir()
377376
return &triedb.Config{DBOverride: fwCfg.BackendConstructor}
378377
default:
379378
t.Fatalf("unknown scheme %s", scheme)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ require (
1717
github.com/VictoriaMetrics/fastcache v1.12.1
1818
github.com/antithesishq/antithesis-sdk-go v0.3.8
1919
github.com/ava-labs/avalanchego v1.14.1-0.20251111165133-29b4e6bc541b
20-
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.14
20+
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15
2121
github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2
2222
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
2323
github.com/deckarep/golang-set/v2 v2.1.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ github.com/ava-labs/avalanchego v1.14.1-0.20251111165133-29b4e6bc541b h1:0bvt1wO
3232
github.com/ava-labs/avalanchego v1.14.1-0.20251111165133-29b4e6bc541b/go.mod h1:Q5f/x4BxM8r4vQE/aZPkQyT8l/HS12/oNrfxjRd8uZU=
3333
github.com/ava-labs/coreth v0.16.0-rc.0 h1:nPvkDbxaH8N9f/wQe7B+IGMhPISMuW5CU0cDYuU8iCw=
3434
github.com/ava-labs/coreth v0.16.0-rc.0/go.mod h1:uGr1C7BP0+dWhvsIouhuH0yCyI8YDgS6sfEFIExs0iI=
35-
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.14 h1:Be+LO61hwmo7XKNm57Yoqx7ld8SgBapjVBEPjUcgI8o=
36-
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.14/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE=
35+
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15 h1:NAVjEu508HwdgbxH/xQxMQoBUgYUn9RQf0VeCrhtYMY=
36+
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE=
3737
github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 h1:hQ15IJxY7WOKqeJqCXawsiXh0NZTzmoQOemkWHz7rr4=
3838
github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU=
3939
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=

0 commit comments

Comments
 (0)