Skip to content
This repository was archived by the owner on Nov 25, 2025. It is now read-only.

Commit 7629fa1

Browse files
feat: enable archival mode for Firewood (#1416)
1 parent 84766a8 commit 7629fa1

File tree

12 files changed

+286
-79
lines changed

12 files changed

+286
-79
lines changed

RELEASES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
- Removes `avax.version` API
66
- Removes `customethclient` package in favor of `ethclient` package and temporary type registrations (`WithTempRegisteredLibEVMExtras`)
77
- Also removes blockHook extension in `ethclient` package.
8+
- Enables Firewood to run with pruning disabled.
9+
- This change modifies the filepath of Firewood and any nodes using Firewood will need to resync.
810

911
## [v0.16.0](https://github.com/ava-labs/coreth/releases/tag/v0.16.0)
1012

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"
@@ -177,8 +176,6 @@ const (
177176
// trieCleanCacheStatsNamespace is the namespace to surface stats from the trie
178177
// clean cache's underlying fastcache.
179178
trieCleanCacheStatsNamespace = "hashdb/memcache/clean/fastcache"
180-
181-
firewoodFileName = "firewood.db"
182179
)
183180

184181
// CacheConfig contains the configuration values for the trie database
@@ -231,12 +228,14 @@ func (c *CacheConfig) triedbConfig() *triedb.Config {
231228
if c.ChainDataDir == "" {
232229
log.Crit("Chain data directory must be specified for Firewood")
233230
}
231+
234232
config.DBOverride = firewood.Config{
235-
FilePath: filepath.Join(c.ChainDataDir, firewoodFileName),
233+
ChainDataDir: c.ChainDataDir,
236234
CleanCacheSize: c.TrieCleanLimit * 1024 * 1024,
237235
FreeListCacheEntries: firewood.Defaults.FreeListCacheEntries,
238236
Revisions: uint(c.StateHistory), // must be at least 2
239237
ReadCacheStrategy: ffi.CacheAllReads,
238+
ArchiveMode: !c.Pruning,
240239
}.BackendConstructor
241240
}
242241
return config

core/blockchain_ext_test.go

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

169-
// This copies all files from a flat directory [src] to a new temporary directory and returns
170-
// the path to the new directory.
171-
func copyFlatDir(t *testing.T, src string) string {
169+
// copyDir recursively copies all files and folders from a directory [src] to a
170+
// new temporary directory and returns the path to the new directory.
171+
func copyDir(t *testing.T, src string) string {
172172
t.Helper()
173+
173174
if src == "" {
174175
return ""
175176
}
176177

177178
dst := t.TempDir()
178-
ents, err := os.ReadDir(src)
179-
require.NoError(t, err)
179+
err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
180+
if err != nil {
181+
return err
182+
}
180183

181-
for _, e := range ents {
182-
require.False(t, e.IsDir(), "expected flat directory")
183-
name := e.Name()
184-
data, err := os.ReadFile(filepath.Join(src, name))
185-
require.NoError(t, err)
186-
info, err := e.Info()
187-
require.NoError(t, err)
188-
require.NoError(t, os.WriteFile(filepath.Join(dst, name), data, info.Mode().Perm()))
189-
}
184+
// Calculate the relative path from src
185+
relPath, err := filepath.Rel(src, path)
186+
if err != nil {
187+
return err
188+
}
189+
190+
// Skip the root directory itself
191+
if relPath == "." {
192+
return nil
193+
}
194+
195+
dstPath := filepath.Join(dst, relPath)
196+
197+
if info.IsDir() {
198+
return os.MkdirAll(dstPath, info.Mode().Perm())
199+
}
200+
201+
data, err := os.ReadFile(path)
202+
if err != nil {
203+
return err
204+
}
205+
206+
return os.WriteFile(dstPath, data, info.Mode().Perm())
207+
})
208+
209+
require.NoError(t, err)
190210
return dst
191211
}
192212

@@ -237,7 +257,7 @@ func checkBlockChainState(
237257
// Copy the database over to prevent any issues when re-using [originalDB] after this call.
238258
originalDB, err = copyMemDB(originalDB)
239259
require.NoError(err)
240-
newChainDataDir := copyFlatDir(t, oldChainDataDir)
260+
newChainDataDir := copyDir(t, oldChainDataDir)
241261
restartedChain, err := create(originalDB, gspec, lastAcceptedBlock.Hash(), newChainDataDir)
242262
require.NoError(err)
243263
defer restartedChain.Stop()
@@ -1682,7 +1702,7 @@ func ReexecCorruptedStateTest(t *testing.T, create ReexecTestFunc) {
16821702
blockchain.Stop()
16831703

16841704
// Restart blockchain with existing state
1685-
newDir := copyFlatDir(t, tempDir) // avoid file lock
1705+
newDir := copyDir(t, tempDir) // avoid file lock
16861706
restartedBlockchain, err := create(chainDB, gspec, chain[1].Hash(), newDir, 4096)
16871707
require.NoError(t, err)
16881708
defer restartedBlockchain.Stop()

core/blockchain_test.go

Lines changed: 183 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ var (
7272
AcceptorQueueLimit: 64,
7373
}
7474

75-
// Firewood should only be included for non-archive, snapshot disabled tests.
75+
// Firewood should only be included for snapshot disabled tests.
7676
schemes = []string{rawdb.HashScheme, customrawdb.FirewoodScheme}
7777
)
7878

@@ -111,22 +111,36 @@ func TestArchiveBlockChain(t *testing.T) {
111111
}
112112

113113
func TestArchiveBlockChainSnapsDisabled(t *testing.T) {
114-
create := func(db ethdb.Database, gspec *Genesis, lastAcceptedHash common.Hash, _ string) (*BlockChain, error) {
114+
for _, scheme := range schemes {
115+
t.Run(scheme, func(t *testing.T) {
116+
testArchiveBlockChainSnapsDisabled(t, scheme)
117+
})
118+
}
119+
}
120+
121+
func testArchiveBlockChainSnapsDisabled(t *testing.T, scheme string) {
122+
create := func(db ethdb.Database, gspec *Genesis, lastAcceptedHash common.Hash, dataPath string) (*BlockChain, error) {
123+
cacheConfig := &CacheConfig{
124+
TrieCleanLimit: 256,
125+
TrieDirtyLimit: 256,
126+
TrieDirtyCommitTarget: 20,
127+
TriePrefetcherParallelism: 4,
128+
Pruning: false, // Archive mode
129+
StateHistory: 32, // Required for Firewood's minimum Revision count
130+
SnapshotLimit: 0, // Disable snapshots
131+
AcceptorQueueLimit: 64,
132+
StateScheme: scheme,
133+
ChainDataDir: dataPath,
134+
}
135+
115136
return createBlockChain(
116137
db,
117-
&CacheConfig{
118-
TrieCleanLimit: 256,
119-
TrieDirtyLimit: 256,
120-
TrieDirtyCommitTarget: 20,
121-
TriePrefetcherParallelism: 4,
122-
Pruning: false, // Archive mode
123-
SnapshotLimit: 0, // Disable snapshots
124-
AcceptorQueueLimit: 64,
125-
},
138+
cacheConfig,
126139
gspec,
127140
lastAcceptedHash,
128141
)
129142
}
143+
130144
for _, tt := range tests {
131145
t.Run(tt.Name, func(t *testing.T) {
132146
tt.testFunc(t, create)
@@ -409,6 +423,164 @@ func TestBlockChainOfflinePruningUngracefulShutdown(t *testing.T) {
409423
}
410424
}
411425

426+
// TestPruningToNonPruning tests that opening a previously pruned database as a
427+
// non-pruned database is successful.
428+
func TestPruningToNonPruning(t *testing.T) {
429+
for _, scheme := range schemes {
430+
t.Run(scheme, func(t *testing.T) {
431+
testPruningToNonPruning(t, scheme)
432+
})
433+
}
434+
}
435+
436+
// testPruningToNonPruning tests that opening a previously pruned database as a
437+
// non-pruned database is successful.
438+
//
439+
// This test checks the following invariants:
440+
// 1. Verifies that a pruned node does not have the state for all blocks (except
441+
// the last accepted block) upon restart.
442+
// 2. Verify that a pruned => archival node has the state for all blocks
443+
// accepted during archival mode upon restart.
444+
func testPruningToNonPruning(t *testing.T, scheme string) {
445+
var (
446+
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
447+
key2, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
448+
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
449+
addr2 = crypto.PubkeyToAddress(key2.PublicKey)
450+
chainDB = rawdb.NewMemoryDatabase()
451+
numStates = uint64(5)
452+
)
453+
454+
gspec := &Genesis{
455+
Config: &params.ChainConfig{HomesteadBlock: new(big.Int)},
456+
Alloc: types.GenesisAlloc{addr1: {Balance: big.NewInt(1000000)}},
457+
}
458+
459+
chainDataDir := t.TempDir()
460+
pruningConfig := &CacheConfig{
461+
TrieCleanLimit: 256,
462+
TrieDirtyLimit: 256,
463+
TrieDirtyCommitTarget: 20,
464+
TriePrefetcherParallelism: 4,
465+
Pruning: true, // Enable pruning
466+
CommitInterval: 4096,
467+
StateHistory: numStates,
468+
AcceptorQueueLimit: 64,
469+
StateScheme: scheme,
470+
ChainDataDir: chainDataDir,
471+
}
472+
473+
// Create a node in pruning mode.
474+
blockchain, err := createBlockChain(chainDB, pruningConfig, gspec, common.Hash{})
475+
if err != nil {
476+
t.Fatal(err)
477+
}
478+
479+
// Generate 10 (2 * numStates) blocks.
480+
signer := types.HomesteadSigner{}
481+
_, blocks, _, err := GenerateChainWithGenesis(gspec, blockchain.engine, 2*int(numStates), 10, func(i int, gen *BlockGen) {
482+
tx, _ := types.SignTx(types.NewTransaction(gen.TxNonce(addr1), addr2, big.NewInt(10000), ethparams.TxGas, nil, nil), signer, key1)
483+
gen.AddTx(tx)
484+
})
485+
if err != nil {
486+
t.Fatal(err)
487+
}
488+
489+
prunedBlocks := blocks[:numStates]
490+
nonPrunedBlocks := blocks[numStates:]
491+
492+
// Insert the first five blocks.
493+
// The states of the first four blocks will be lost upon restart.
494+
if _, err := blockchain.InsertChain(prunedBlocks); err != nil {
495+
t.Fatal(err)
496+
}
497+
for _, block := range prunedBlocks {
498+
if err := blockchain.Accept(block); err != nil {
499+
t.Fatal(err)
500+
}
501+
}
502+
blockchain.DrainAcceptorQueue()
503+
504+
lastAcceptedHash := blockchain.LastConsensusAcceptedBlock().Hash()
505+
blockchain.Stop()
506+
507+
// Reopen the node.
508+
blockchain, err = createBlockChain(chainDB, pruningConfig, gspec, lastAcceptedHash)
509+
if err != nil {
510+
t.Fatal(err)
511+
}
512+
513+
// 1. Verify that a pruned node does not have the state for all blocks (except
514+
// the last accepted block) upon restart.
515+
for _, block := range prunedBlocks[:numStates-1] {
516+
if blockchain.HasState(block.Root()) {
517+
t.Fatalf("Expected blockchain to be missing state for intermediate block %d with pruning enabled", block.NumberU64())
518+
}
519+
}
520+
521+
blockchain.Stop()
522+
523+
archiveConfig := &CacheConfig{
524+
TrieCleanLimit: 256,
525+
TrieDirtyLimit: 256,
526+
TrieDirtyCommitTarget: 20,
527+
TriePrefetcherParallelism: 4,
528+
Pruning: false, // Archive mode
529+
AcceptorQueueLimit: 64,
530+
StateScheme: scheme,
531+
StateHistory: 32,
532+
ChainDataDir: chainDataDir,
533+
}
534+
535+
// Reopen the node, but switch from pruning to archival mode.
536+
blockchain, err = createBlockChain(
537+
chainDB,
538+
archiveConfig,
539+
gspec,
540+
lastAcceptedHash,
541+
)
542+
if err != nil {
543+
t.Fatal(err)
544+
}
545+
546+
// Insert the remaining five blocks.
547+
// The states of all these blocks will still be accessible on restart
548+
// since we're now in archival mode.
549+
if _, err := blockchain.InsertChain(nonPrunedBlocks); err != nil {
550+
t.Fatal(err)
551+
}
552+
553+
for _, block := range nonPrunedBlocks {
554+
if err := blockchain.Accept(block); err != nil {
555+
t.Fatal(err)
556+
}
557+
}
558+
blockchain.DrainAcceptorQueue()
559+
560+
lastAcceptedHash = blockchain.LastConsensusAcceptedBlock().Hash()
561+
blockchain.Stop()
562+
563+
// Reopen the archival node.
564+
blockchain, err = createBlockChain(
565+
chainDB,
566+
archiveConfig,
567+
gspec,
568+
lastAcceptedHash,
569+
)
570+
if err != nil {
571+
t.Fatal(err)
572+
}
573+
defer blockchain.Stop()
574+
575+
// 2. Verify that a pruned => archival node has the state for all blocks
576+
// accepted during archival mode upon restart.
577+
for _, block := range nonPrunedBlocks {
578+
if !blockchain.HasState(block.Root()) {
579+
t.Fatalf("Expected blockchain to have the state for block %d with pruning disabled", block.NumberU64())
580+
}
581+
}
582+
}
583+
412584
func testRepopulateMissingTriesParallel(t *testing.T, parallelism int) {
413585
var (
414586
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

@@ -307,7 +306,7 @@ func newDbConfig(t *testing.T, scheme string) *triedb.Config {
307306
case customrawdb.FirewoodScheme:
308307
fwCfg := firewood.Defaults
309308
// Create a unique temporary directory for each test
310-
fwCfg.FilePath = filepath.Join(t.TempDir(), "firewood_state") // matches blockchain.go
309+
fwCfg.ChainDataDir = t.TempDir()
311310
return &triedb.Config{DBOverride: fwCfg.BackendConstructor}
312311
default:
313312
t.Fatalf("unknown scheme %s", scheme)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ go 1.24.9
1616
require (
1717
github.com/VictoriaMetrics/fastcache v1.12.1
1818
github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761
19-
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.14
19+
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15
2020
github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2
2121
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
2222
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
@@ -28,8 +28,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
2828
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
2929
github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761 h1:FrsqYm5sms00jWnr8pV9Nj08v1ipYjp1x6p11NIpIBU=
3030
github.com/ava-labs/avalanchego v1.14.1-0.20251120155522-df4a8e531761/go.mod h1:Ntq3RBvDQzNjy14NU3RC2Jf1A9pzfM5RVQ30Gwx/6IM=
31-
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.14 h1:Be+LO61hwmo7XKNm57Yoqx7ld8SgBapjVBEPjUcgI8o=
32-
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.14/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE=
31+
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15 h1:NAVjEu508HwdgbxH/xQxMQoBUgYUn9RQf0VeCrhtYMY=
32+
github.com/ava-labs/firewood-go-ethhash/ffi v0.0.15/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE=
3333
github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2 h1:hQ15IJxY7WOKqeJqCXawsiXh0NZTzmoQOemkWHz7rr4=
3434
github.com/ava-labs/libevm v1.13.15-0.20251016142715-1bccf4f2ddb2/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU=
3535
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=

0 commit comments

Comments
 (0)