diff --git a/cmd/geth/main.go b/cmd/geth/main.go index e1f80f42c2..1e2f68b82a 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -79,6 +79,7 @@ var ( utils.BlobPoolDataDirFlag, utils.BlobPoolDataCapFlag, utils.BlobPoolPriceBumpFlag, + utils.TrieDBJournalFlag, utils.SyncModeFlag, utils.SyncTargetFlag, utils.ExitWhenSyncedFlag, @@ -97,7 +98,7 @@ var ( utils.CacheFlag, utils.CacheDatabaseFlag, utils.CacheTrieFlag, - utils.CacheTrieJournalFlag, + utils.CacheTrieJournalFlag, // deprecated utils.CacheTrieRejournalFlag, // deprecated utils.CacheGCFlag, utils.CacheSnapshotFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 3cda290b3f..7f30835dc7 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -446,6 +446,15 @@ var ( Value: ethconfig.Defaults.BlobPool.PriceBump, Category: flags.BlobPoolCategory, } + + // Trie database settings + TrieDBJournalFlag = &cli.StringFlag{ + Name: "triedb.journal", + Usage: "Path to the journal used for persisting trie data across node restarts", + Value: ethconfig.Defaults.TrieDBJournal, + Category: flags.TrieDatabaseCategory, + } + // Performance tuning settings CacheFlag = &cli.IntFlag{ Name: "cache", @@ -465,11 +474,6 @@ var ( Value: 15, Category: flags.PerfCategory, } - CacheTrieJournalFlag = &cli.StringFlag{ - Name: "cache.trie.journal", - Usage: "Disk journal directory for trie cache to survive node restarts (experimental, default is disabled)", - Category: flags.PerfCategory, - } CacheGCFlag = &cli.IntFlag{ Name: "cache.gc", Usage: "Percentage of cache memory allowance to use for trie pruning (default = 25% full mode, 0% archive mode)", @@ -988,6 +992,7 @@ var ( DataDirFlag, AncientFlag, EraFlag, + TrieDBJournalFlag, RemoteDBFlag, DBEngineFlag, StateSchemeFlag, @@ -1685,8 +1690,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(CacheFlag.Name) || ctx.IsSet(CacheSnapshotFlag.Name) { cfg.SnapshotCache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheSnapshotFlag.Name) / 100 } - if journal := ctx.String(CacheTrieJournalFlag.Name); journal != "" { - cfg.TrieJournal = stack.ResolvePath(journal) + if ctx.IsSet(TrieDBJournalFlag.Name) { + cfg.TrieDBJournal = stack.ResolvePath(ctx.String(TrieDBJournalFlag.Name)) } if ctx.IsSet(CacheLogSizeFlag.Name) { cfg.FilterLogCacheSize = ctx.Int(CacheLogSizeFlag.Name) @@ -2211,8 +2216,8 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh options.Preimages = true log.Info("Enabling recording of key preimages since archive mode is used") } - if journal := ctx.String(CacheTrieJournalFlag.Name); journal != "" { - options.TrieJournal = stack.ResolvePath(journal) + if ctx.IsSet(TrieDBJournalFlag.Name) { + options.TrieDBJournal = stack.ResolvePath(ctx.String(TrieDBJournalFlag.Name)) } if !ctx.Bool(SnapshotFlag.Name) { options.SnapshotLimit = 0 // Disabled diff --git a/cmd/utils/flags_legacy.go b/cmd/utils/flags_legacy.go index 02d114a487..dc41a75d11 100644 --- a/cmd/utils/flags_legacy.go +++ b/cmd/utils/flags_legacy.go @@ -35,6 +35,7 @@ var ShowDeprecated = &cli.Command{ var DeprecatedFlags = []cli.Flag{ NoUSBFlag, LegacyWhitelistFlag, + CacheTrieJournalFlag, CacheTrieRejournalFlag, LegacyDiscoveryV5Flag, TxLookupLimitFlag, @@ -59,6 +60,11 @@ var ( Category: flags.DeprecatedCategory, } // Deprecated July 2023 + CacheTrieJournalFlag = &cli.StringFlag{ + Name: "cache.trie.journal", + Usage: "Disk journal directory for trie cache to survive node restarts", + Category: flags.DeprecatedCategory, + } CacheTrieRejournalFlag = &cli.DurationFlag{ Name: "cache.trie.rejournal", Usage: "Time interval to regenerate the trie cache journal", diff --git a/core/blockchain.go b/core/blockchain.go index fab33915b0..3c16fc8b9b 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -166,7 +166,7 @@ type BlockChainConfig struct { TrieDirtyLimit int // Memory limit (MB) at which to start flushing dirty trie nodes to disk TrieTimeLimit time.Duration // Time limit after which to flush the current in-memory trie to disk TrieNoAsyncFlush bool // Whether the asynchronous buffer flushing is disallowed - TrieJournal string // Path used to store the trie pathdb journal + TrieDBJournal string // Path to the journal used for persisting trie data across node restarts Preimages bool // Whether to store preimage of trie key to the disk StateHistory uint64 // Number of blocks from head whose state histories are reserved. @@ -247,6 +247,7 @@ func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config { EnableStateIndexing: cfg.ArchiveMode, TrieCleanSize: cfg.TrieCleanLimit * 1024 * 1024, StateCleanSize: cfg.SnapshotLimit * 1024 * 1024, + JournalPath: cfg.TrieDBJournal, // TODO(rjl493456442): The write buffer represents the memory limit used // for flushing both trie data and state data to disk. The config name diff --git a/eth/backend.go b/eth/backend.go index eb0ac7e70e..d6441c8bff 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -227,7 +227,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { TrieDirtyLimit: config.TrieDirtyCache, ArchiveMode: config.NoPruning, TrieTimeLimit: config.TrieTimeout, - TrieJournal: config.TrieJournal, SnapshotLimit: config.SnapshotCache, Preimages: config.Preimages, StateHistory: config.StateHistory, @@ -239,6 +238,11 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { }, } ) + if config.TrieDBJournal != "" { + options.TrieDBJournal = stack.ResolvePath(config.TrieDBJournal) + } else { + log.Warn("Trie database journal is persisted within the database") + } if config.VMTrace != "" { traceConfig := json.RawMessage("{}") diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index b876efff35..519f22a964 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -60,6 +60,7 @@ var Defaults = Config{ TrieCleanCache: 154, TrieDirtyCache: 256, TrieTimeout: 60 * time.Minute, + TrieDBJournal: "triedb.journal", SnapshotCache: 102, FilterLogCacheSize: 32, Miner: miner.DefaultConfig, @@ -103,12 +104,6 @@ type Config struct { LogHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head where a log search index is maintained. LogNoHistory bool `toml:",omitempty"` // No log search index is maintained. LogExportCheckpoints string // export log index checkpoints to file - StateHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head whose state histories are reserved. - - // State scheme represents the scheme used to store ethereum states and trie - // nodes on top. It can be 'hash', 'path', or none which means use the scheme - // consistent with persistent state. - StateScheme string `toml:",omitempty"` // RequiredBlocks is a set of block number -> hash mappings which must be in the // canonical chain of all remote peers. Setting the option makes geth verify the @@ -122,12 +117,20 @@ type Config struct { DatabaseFreezer string DatabaseEra string + // Trie database options. TODO(rjl493456442) move all trie database options + // into a separate config structure. TrieCleanCache int TrieDirtyCache int TrieTimeout time.Duration SnapshotCache int Preimages bool - TrieJournal string + StateHistory uint64 `toml:",omitempty"` // The maximum number of blocks from head whose state histories are reserved. + TrieDBJournal string // Path to the journal used for persisting trie data across node restarts + + // State scheme represents the scheme used to store ethereum states and trie + // nodes on top. It can be 'hash', 'path', or none which means use the scheme + // consistent with persistent state. + StateScheme string `toml:",omitempty"` // This is the number of blocks for which logs will be cached in the filter system. FilterLogCacheSize int diff --git a/internal/flags/categories.go b/internal/flags/categories.go index d426add55b..f44d6c3cac 100644 --- a/internal/flags/categories.go +++ b/internal/flags/categories.go @@ -19,24 +19,25 @@ package flags import "github.com/urfave/cli/v2" const ( - EthCategory = "ETHEREUM" - BeaconCategory = "BEACON CHAIN" - DevCategory = "DEVELOPER CHAIN" - StateCategory = "STATE HISTORY MANAGEMENT" - TxPoolCategory = "TRANSACTION POOL (EVM)" - BlobPoolCategory = "TRANSACTION POOL (BLOB)" - PerfCategory = "PERFORMANCE TUNING" - AccountCategory = "ACCOUNT" - APICategory = "API AND CONSOLE" - NetworkingCategory = "NETWORKING" - MinerCategory = "MINER" - GasPriceCategory = "GAS PRICE ORACLE" - VMCategory = "VIRTUAL MACHINE" - LoggingCategory = "LOGGING AND DEBUGGING" - MetricsCategory = "METRICS AND STATS" - MiscCategory = "MISC" - TestingCategory = "TESTING" - DeprecatedCategory = "ALIASED (deprecated)" + EthCategory = "ETHEREUM" + BeaconCategory = "BEACON CHAIN" + DevCategory = "DEVELOPER CHAIN" + StateCategory = "STATE HISTORY MANAGEMENT" + TxPoolCategory = "TRANSACTION POOL (EVM)" + BlobPoolCategory = "TRANSACTION POOL (BLOB)" + PerfCategory = "PERFORMANCE TUNING" + AccountCategory = "ACCOUNT" + APICategory = "API AND CONSOLE" + NetworkingCategory = "NETWORKING" + MinerCategory = "MINER" + TrieDatabaseCategory = "TRIE DATABASE" + GasPriceCategory = "GAS PRICE ORACLE" + VMCategory = "VIRTUAL MACHINE" + LoggingCategory = "LOGGING AND DEBUGGING" + MetricsCategory = "METRICS AND STATS" + MiscCategory = "MISC" + TestingCategory = "TESTING" + DeprecatedCategory = "ALIASED (deprecated)" ) func init() { diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 7c8c327484..65095eebd3 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -120,6 +120,7 @@ type Config struct { StateCleanSize int // Maximum memory allowance (in bytes) for caching clean state data WriteBufferSize int // Maximum memory allowance (in bytes) for write buffer ReadOnly bool // Flag whether the database is opened in read only mode + JournalPath string // Absolute path of journal file (null means the journal data is persisted in key-value store) // Testing configurations SnapshotNoBuild bool // Flag Whether the state generation is allowed @@ -156,6 +157,9 @@ func (c *Config) fields() []interface{} { } else { list = append(list, "history", fmt.Sprintf("last %d blocks", c.StateHistory)) } + if c.JournalPath != "" { + list = append(list, "journal", c.JournalPath) + } return list } diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go index 2982202009..fb1bf57e9d 100644 --- a/triedb/pathdb/database_test.go +++ b/triedb/pathdb/database_test.go @@ -21,12 +21,15 @@ import ( "errors" "fmt" "math/rand" + "os" + "path/filepath" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/internal/testrand" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" @@ -121,7 +124,7 @@ type tester struct { snapStorages map[common.Hash]map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key } -func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, enableIndex bool) *tester { +func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, enableIndex bool, journal string) *tester { var ( disk, _ = rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{Ancient: t.TempDir()}) db = New(disk, &Config{ @@ -131,6 +134,7 @@ func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, ena StateCleanSize: 256 * 1024, WriteBufferSize: 256 * 1024, NoAsyncFlush: true, + JournalPath: journal, }, isVerkle) obj = &tester{ @@ -466,7 +470,7 @@ func TestDatabaseRollback(t *testing.T) { }() // Verify state histories - tester := newTester(t, 0, false, 32, false) + tester := newTester(t, 0, false, 32, false, "") defer tester.release() if err := tester.verifyHistory(); err != nil { @@ -500,7 +504,7 @@ func TestDatabaseRecoverable(t *testing.T) { }() var ( - tester = newTester(t, 0, false, 12, false) + tester = newTester(t, 0, false, 12, false, "") index = tester.bottomIndex() ) defer tester.release() @@ -544,7 +548,7 @@ func TestDisable(t *testing.T) { maxDiffLayers = 128 }() - tester := newTester(t, 0, false, 32, false) + tester := newTester(t, 0, false, 32, false, "") defer tester.release() stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil)) @@ -586,7 +590,7 @@ func TestCommit(t *testing.T) { maxDiffLayers = 128 }() - tester := newTester(t, 0, false, 12, false) + tester := newTester(t, 0, false, 12, false, "") defer tester.release() if err := tester.db.Commit(tester.lastHash(), false); err != nil { @@ -610,13 +614,18 @@ func TestCommit(t *testing.T) { } func TestJournal(t *testing.T) { + testJournal(t, "") + testJournal(t, t.TempDir()) +} + +func testJournal(t *testing.T, journal string) { // Redefine the diff layer depth allowance for faster testing. maxDiffLayers = 4 defer func() { maxDiffLayers = 128 }() - tester := newTester(t, 0, false, 12, false) + tester := newTester(t, 0, false, 12, false, journal) defer tester.release() if err := tester.db.Journal(tester.lastHash()); err != nil { @@ -640,13 +649,30 @@ func TestJournal(t *testing.T) { } func TestCorruptedJournal(t *testing.T) { + testCorruptedJournal(t, "", func(db ethdb.Database) { + // Mutate the journal in disk, it should be regarded as invalid + blob := rawdb.ReadTrieJournal(db) + blob[0] = 0xa + rawdb.WriteTrieJournal(db, blob) + }) + + path := filepath.Join(t.TempDir(), "journal") + testCorruptedJournal(t, path, func(_ ethdb.Database) { + f, _ := os.OpenFile(path, os.O_WRONLY, 0644) + f.WriteAt([]byte{0xa}, 0) + f.Sync() + f.Close() + }) +} + +func testCorruptedJournal(t *testing.T, journal string, modifyFn func(database ethdb.Database)) { // Redefine the diff layer depth allowance for faster testing. maxDiffLayers = 4 defer func() { maxDiffLayers = 128 }() - tester := newTester(t, 0, false, 12, false) + tester := newTester(t, 0, false, 12, false, journal) defer tester.release() if err := tester.db.Journal(tester.lastHash()); err != nil { @@ -655,13 +681,10 @@ func TestCorruptedJournal(t *testing.T) { tester.db.Close() root := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil)) - // Mutate the journal in disk, it should be regarded as invalid - blob := rawdb.ReadTrieJournal(tester.db.diskdb) - blob[0] = 0xa - rawdb.WriteTrieJournal(tester.db.diskdb, blob) + modifyFn(tester.db.diskdb) // Verify states, all not-yet-written states should be discarded - tester.db = New(tester.db.diskdb, nil, false) + tester.db = New(tester.db.diskdb, tester.db.config, false) for i := 0; i < len(tester.roots); i++ { if tester.roots[i] == root { if err := tester.verifyState(root); err != nil { @@ -694,7 +717,7 @@ func TestTailTruncateHistory(t *testing.T) { maxDiffLayers = 128 }() - tester := newTester(t, 10, false, 12, false) + tester := newTester(t, 10, false, 12, false, "") defer tester.release() tester.db.Close() diff --git a/triedb/pathdb/fileutils.go b/triedb/pathdb/fileutils.go new file mode 100644 index 0000000000..0247d336c6 --- /dev/null +++ b/triedb/pathdb/fileutils.go @@ -0,0 +1,54 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "errors" + "os" + "syscall" +) + +func isErrInvalid(err error) bool { + if errors.Is(err, os.ErrInvalid) { + return true + } + // Go >= 1.8 returns *os.PathError instead + if patherr, ok := err.(*os.PathError); ok && patherr.Err == syscall.EINVAL { + return true + } + return false +} + +func syncDir(name string) error { + // As per fsync manpage, Linux seems to expect fsync on directory, however + // some system don't support this, so we will ignore syscall.EINVAL. + // + // From fsync(2): + // Calling fsync() does not necessarily ensure that the entry in the + // directory containing the file has also reached disk. For that an + // explicit fsync() on a file descriptor for the directory is also needed. + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + + if err := f.Sync(); err != nil && !isErrInvalid(err) { + return err + } + return nil +} diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go index 9e050cd730..cbecef456f 100644 --- a/triedb/pathdb/journal.go +++ b/triedb/pathdb/journal.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "os" + "path/filepath" "time" "github.com/ethereum/go-ethereum/common" @@ -53,16 +54,17 @@ const journalVersion uint64 = 3 // loadJournal tries to parse the layer journal from the disk. func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) { var reader io.Reader - if db.config.Journal != "" && common.FileExist(db.config.Journal) { - log.Info("Load pathdb disklayer journal", "path", db.config.Journal) + if db.config.JournalPath != "" && common.FileExist(db.config.JournalPath) { // If a journal file is specified, read it from there - f, err := os.OpenFile(db.config.Journal, os.O_RDONLY, 0644) + log.Info("Load database journal from file", "path", db.config.JournalPath) + f, err := os.OpenFile(db.config.JournalPath, os.O_RDONLY, 0644) if err != nil { - return nil, fmt.Errorf("failed to read journal file %s: %w", db.config.Journal, err) + return nil, fmt.Errorf("failed to read journal file %s: %w", db.config.JournalPath, err) } defer f.Close() reader = f } else { + log.Info("Load database journal from database") journal := rawdb.ReadTrieJournal(db.diskdb) if len(journal) == 0 { return nil, errMissJournal @@ -331,21 +333,24 @@ func (db *Database) Journal(root common.Hash) error { return errDatabaseReadOnly } - var journal io.Writer - var file *os.File // Store the journal into the database and return - if db.config.Journal != "" { + var ( + file *os.File + journal io.Writer + tmpName = db.config.JournalPath + ".tmp" + ) + if db.config.JournalPath != "" { // Write into a temp file first var err error - file, err = os.OpenFile(db.config.Journal+".new", os.O_WRONLY|os.O_CREATE, 0644) + file, err = os.OpenFile(tmpName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { - return fmt.Errorf("failed to open journal file %s: %w", db.config.Journal, err) + return fmt.Errorf("failed to open journal file %s: %w", db.config.JournalPath, err) } defer func() { if file != nil { file.Close() - // Clean up temp file if we didn't successfully rename it - os.Remove(db.config.Journal + ".new") + os.Remove(tmpName) // Clean up temp file if we didn't successfully rename it + log.Warn("Removed leftover temporary journal file", "path", tmpName) } }() journal = file @@ -373,21 +378,28 @@ func (db *Database) Journal(root common.Hash) error { // Store the journal into the database and return var size int - if db.config.Journal == "" { + if db.config.JournalPath == "" { data := journal.(*bytes.Buffer) size = data.Len() rawdb.WriteTrieJournal(db.diskdb, data.Bytes()) } else { - // Close the temporary file and atomically rename it - if err := file.Close(); err != nil { - return fmt.Errorf("failed to close temporary journal file: %w", err) - } - // Replace the live journal with the newly generated one - if err := os.Rename(db.config.Journal+".new", db.config.Journal); err != nil { - return fmt.Errorf("failed to rename temporary journal file: %w", err) - } stat, _ := journal.(*os.File).Stat() size = int(stat.Size()) + + // Close the temporary file and atomically rename it + if err := file.Sync(); err != nil { + return fmt.Errorf("failed to fsync the journal, %v", err) + } + if err := file.Close(); err != nil { + return fmt.Errorf("failed to close the journal: %v", err) + } + // Replace the live journal with the newly generated one + if err := os.Rename(tmpName, db.config.JournalPath); err != nil { + return fmt.Errorf("failed to rename the journal: %v", err) + } + if err := syncDir(filepath.Dir(db.config.JournalPath)); err != nil { + return fmt.Errorf("failed to fsync the dir: %v", err) + } file = nil }