diff --git a/core/blockchain.go b/core/blockchain.go index 06f0baf102..b0c1b119fc 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -979,17 +979,16 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha // Ignore the error here since light client won't hit this path frozen, _ := bc.db.Ancients() if num+1 <= frozen { - // Truncate all relative data(header, total difficulty, body, receipt - // and canonical hash) from ancient store. - if _, err := bc.db.TruncateHead(num); err != nil { - log.Crit("Failed to truncate ancient data", "number", num, "err", err) - } - // Remove the hash <-> number mapping from the active store. - rawdb.DeleteHeaderNumber(db, hash) + // The chain segment, such as the block header, canonical hash, + // body, and receipt, will be removed from the ancient store + // in one go. + // + // The hash-to-number mapping in the key-value store will be + // removed by the hc.SetHead function. } else { - // Remove relative body and receipts from the active store. - // The header, total difficulty and canonical hash will be - // removed in the hc.SetHead function. + // Remove the associated body and receipts from the key-value store. + // The header, hash-to-number mapping, and canonical hash will be + // removed by the hc.SetHead function. rawdb.DeleteBody(db, hash, num) rawdb.DeleteReceipts(db, hash, num) } @@ -2627,6 +2626,7 @@ func (bc *BlockChain) InsertHeadersBeforeCutoff(headers []*types.Header) (int, e if err != nil { return 0, err } + // Sync the ancient store explicitly to ensure all data has been flushed to disk. if err := bc.db.SyncAncient(); err != nil { return 0, err } diff --git a/core/headerchain.go b/core/headerchain.go index f7acc49bef..29cb55a212 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -591,17 +591,45 @@ func (hc *HeaderChain) setHead(headBlock uint64, headTime uint64, updateFn Updat hashes = append(hashes, hdr.Hash()) } for _, hash := range hashes { + // Remove the associated block body and receipts if required. + // + // If the block is in the chain freezer, then this delete operation + // is actually ineffective. if delFn != nil { delFn(batch, hash, num) } + // Remove the hash->number mapping along with the header itself rawdb.DeleteHeader(batch, hash, num) } + // Remove the number->hash mapping rawdb.DeleteCanonicalHash(batch, num) } } // Flush all accumulated deletions. if err := batch.Write(); err != nil { - log.Crit("Failed to rewind block", "error", err) + log.Crit("Failed to commit batch in setHead", "err", err) + } + // Explicitly flush the pending writes in the key-value store to disk, ensuring + // data durability of the previous deletions. + if err := hc.chainDb.Sync(); err != nil { + log.Crit("Failed to sync the key-value store in setHead", "err", err) + } + // Truncate the excessive chain segments in the ancient store. + // These are actually deferred deletions from the loop above. + // + // This step must be performed after synchronizing the key-value store; + // otherwise, in the event of a panic, it's theoretically possible to + // lose recent key-value store writes while the ancient store deletions + // remain, leading to data inconsistency. + if delFn != nil { + // Ignore the error here since light client won't hit this path + frozen, _ := hc.chainDb.Ancients() + if headBlock+1 < frozen { + _, err := hc.chainDb.TruncateHead(headBlock + 1) + if err != nil { + log.Crit("Failed to truncate head block", "err", err) + } + } } // Clear out any stale content from the caches hc.headerCache.Purge() diff --git a/ethdb/leveldb/leveldb.go b/ethdb/leveldb/leveldb.go index f29a0f064d..e8f15753ec 100644 --- a/ethdb/leveldb/leveldb.go +++ b/ethdb/leveldb/leveldb.go @@ -327,6 +327,15 @@ func (db *Database) Path() string { // Sync flushes all pending writes in the write-ahead-log to disk, ensuring // data durability up to that point. func (db *Database) Sync() error { + // In theory, the WAL (Write-Ahead Log) can be explicitly synchronized using + // a write operation with SYNC=true. However, there is no dedicated key reserved + // for this purpose, and even a nil key (key=nil) is considered a valid database entry. + // + // In LevelDB, writes are blocked until the data is written to the WAL, meaning + // recent writes won't be lost unless a power failure or system crash occurs. + // Additionally, LevelDB is no longer the default database engine and is likely + // only used by hash-mode archive nodes. Given this, the durability guarantees + // without explicit sync are acceptable in the context of LevelDB. return nil } diff --git a/ethdb/pebble/pebble.go b/ethdb/pebble/pebble.go index 4055be30ce..dc6ab92db0 100644 --- a/ethdb/pebble/pebble.go +++ b/ethdb/pebble/pebble.go @@ -227,8 +227,16 @@ func New(file string, cache int, handles int, namespace string, readonly bool) ( WriteStallBegin: db.onWriteStallBegin, WriteStallEnd: db.onWriteStallEnd, }, + Logger: panicLogger{}, // TODO(karalabe): Delete when this is upstreamed in Pebble + + // Pebble is configured to use asynchronous write mode, meaning write operations + // return as soon as the data is cached in memory, without waiting for the WAL + // to be written. This mode offers better write performance but risks losing + // recent writes if the application crashes or a power failure/system crash occurs. + // + // By setting the WALBytesPerSync, the cached WAL writes will be periodically + // flushed at the background if the accumulated size exceeds this threshold. WALBytesPerSync: 5 * ethdb.IdealBatchSize, - Logger: panicLogger{}, // TODO(karalabe): Delete when this is upstreamed in Pebble } // Disable seek compaction explicitly. Check https://github.com/ethereum/go-ethereum/pull/20130 // for more details. @@ -419,6 +427,11 @@ func (d *Database) Path() string { // data durability up to that point. func (d *Database) Sync() error { b := d.db.NewBatch() + + // The entry (value=nil) is not written to the database; it is only + // added to the WAL. Writing this special log entry in sync mode + // automatically flushes all previous writes, ensuring database + // durability up to this point. b.LogData(nil, nil) return d.db.Apply(b, pebble.Sync) } diff --git a/ethdb/pebble/pebble_test.go b/ethdb/pebble/pebble_test.go index 3265491d4a..e703a8d0ce 100644 --- a/ethdb/pebble/pebble_test.go +++ b/ethdb/pebble/pebble_test.go @@ -17,6 +17,7 @@ package pebble import ( + "errors" "testing" "github.com/cockroachdb/pebble" @@ -54,3 +55,26 @@ func BenchmarkPebbleDB(b *testing.B) { } }) } + +func TestPebbleLogData(t *testing.T) { + db, err := pebble.Open("", &pebble.Options{ + FS: vfs.NewMem(), + }) + if err != nil { + t.Fatal(err) + } + + _, _, err = db.Get(nil) + if !errors.Is(err, pebble.ErrNotFound) { + t.Fatal("Unknown database entry") + } + + b := db.NewBatch() + b.LogData(nil, nil) + db.Apply(b, pebble.Sync) + + _, _, err = db.Get(nil) + if !errors.Is(err, pebble.ErrNotFound) { + t.Fatal("Unknown database entry") + } +} diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index d48850c102..85103063b5 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -454,6 +454,15 @@ func (db *Database) Recover(root common.Hash) error { db.tree.reset(dl) } rawdb.DeleteTrieJournal(db.diskdb) + + // Explicitly sync the key-value store to ensure all recent writes are + // flushed to disk. This step is crucial to prevent a scenario where + // recent key-value writes are lost due to an application panic, while + // the associated state histories have already been removed, resulting + // in the inability to perform a state rollback. + if err := db.diskdb.Sync(); err != nil { + return err + } _, err := truncateFromHead(db.diskdb, db.freezer, dl.stateID()) if err != nil { return err