From ff9c04e4c880d102b4d3c5e20c53791809a3e1e2 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 15 May 2026 12:52:53 +0800 Subject: [PATCH 1/7] core, ethdb, cmd, triedb: intrudoce table group in freezer --- cmd/geth/chaincmd.go | 4 +- core/blockchain.go | 4 +- core/blockchain_test.go | 2 +- core/rawdb/ancient_scheme.go | 48 +++++-- core/rawdb/ancient_utils.go | 23 +++- core/rawdb/ancienttest/testsuite.go | 19 ++- core/rawdb/chain_freezer.go | 20 ++- core/rawdb/database.go | 4 +- core/rawdb/freezer.go | 194 ++++++++++++++++++---------- core/rawdb/freezer_memory.go | 74 ++++++++--- core/rawdb/freezer_memory_test.go | 8 +- core/rawdb/freezer_resettable.go | 14 +- core/rawdb/freezer_test.go | 8 +- core/rawdb/table.go | 8 +- ethdb/database.go | 25 ++-- ethdb/remotedb/remotedb.go | 4 +- triedb/pathdb/disklayer.go | 2 +- triedb/pathdb/history.go | 14 +- triedb/pathdb/history_indexer.go | 2 +- triedb/pathdb/history_inspect.go | 5 +- triedb/pathdb/history_reader.go | 2 +- triedb/pathdb/history_state_test.go | 2 +- 22 files changed, 322 insertions(+), 164 deletions(-) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 98ed348d8c..9d2ee2dbbe 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -744,7 +744,7 @@ func pruneHistory(ctx *cli.Context) error { ) // Check the current freezer tail to see if pruning is needed/possible. - freezerTail, _ := chaindb.Tail() + freezerTail, _ := chaindb.Tail(rawdb.ChainFreezerBlockDataGroup) if freezerTail > 0 { if freezerTail == targetBlock { log.Info("Database already pruned to target block", "tail", freezerTail) @@ -776,7 +776,7 @@ func pruneHistory(ctx *cli.Context) error { log.Info("Starting history pruning", "head", currentHeader.Number, "target", targetBlock, "targetHash", targetBlockHash.Hex()) start := time.Now() rawdb.PruneTransactionIndex(chaindb, targetBlock) - if _, err := chaindb.TruncateTail(targetBlock); err != nil { + if _, err := chaindb.TruncateTail(rawdb.ChainFreezerBlockDataGroup, targetBlock); err != nil { return fmt.Errorf("failed to truncate ancient data: %v", err) } log.Info("History pruning completed", "tail", targetBlock, "elapsed", common.PrettyDuration(time.Since(start))) diff --git a/core/blockchain.go b/core/blockchain.go index 7b5a910b7a..dfa4828289 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -716,7 +716,7 @@ func (bc *BlockChain) loadLastState() error { // initializeHistoryPruning sets bc.historyPrunePoint. func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { - freezerTail, _ := bc.db.Tail() + freezerTail, _ := bc.db.Tail(rawdb.ChainFreezerBlockDataGroup) policy := bc.cfg.HistoryPolicy switch policy.Mode { @@ -2961,7 +2961,7 @@ func (bc *BlockChain) InsertHeadersBeforeCutoff(headers []*types.Header) (int, e } // Truncate the useless chain segment (zero bodies and receipts) in the // ancient store. - if _, err := bc.db.TruncateTail(last.Number.Uint64() + 1); err != nil { + if _, err := bc.db.TruncateTail(rawdb.ChainFreezerBlockDataGroup, last.Number.Uint64()+1); err != nil { return 0, err } // Last step update all in-memory markers diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 1a2ee45291..199f0ec074 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4386,7 +4386,7 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, if header.Hash() != hash { t.Errorf("block #%d: header mismatch: want: %v, got: %v", num, hash, header.Hash()) } - tail, err := db.Tail() + tail, err := db.Tail(rawdb.ChainFreezerBlockDataGroup) if err != nil { t.Fatalf("Failed to get chain tail, %v", err) } diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go index afec7848c8..15bcf8be60 100644 --- a/core/rawdb/ancient_scheme.go +++ b/core/rawdb/ancient_scheme.go @@ -37,21 +37,35 @@ const ( ChainFreezerReceiptTable = "receipts" ) +// Identifiers of tail groups used by the chain freezer. +const ( + // ChainFreezerBlockDataGroup is the tail group shared by the body and + // receipt tables. The two tables are pruned together and therefore have + // the same tail position. + ChainFreezerBlockDataGroup = "blockdata" +) + // chainFreezerTableConfigs configures the settings for tables in the chain freezer. // Compression is disabled for hashes as they don't compress well. Additionally, // tail truncation is disabled for the header and hash tables, as these are intended // to be retained long-term. var chainFreezerTableConfigs = map[string]freezerTableConfig{ - ChainFreezerHeaderTable: {noSnappy: false, prunable: false}, - ChainFreezerHashTable: {noSnappy: true, prunable: false}, - ChainFreezerBodiesTable: {noSnappy: false, prunable: true}, - ChainFreezerReceiptTable: {noSnappy: false, prunable: true}, + ChainFreezerHeaderTable: {noSnappy: false}, + ChainFreezerHashTable: {noSnappy: true}, + ChainFreezerBodiesTable: {noSnappy: false, tailGroup: ChainFreezerBlockDataGroup}, + ChainFreezerReceiptTable: {noSnappy: false, tailGroup: ChainFreezerBlockDataGroup}, } // freezerTableConfig contains the settings for a freezer table. type freezerTableConfig struct { - noSnappy bool // disables item compression - prunable bool // true for tables that can be pruned by TruncateTail + // noSnappy disables item compression when true. + noSnappy bool + + // tailGroup names a logical group of tables that share the same tail + // position. Tables in the same group are pruned together and must agree + // on their tail. An empty value means the table is not prunable; its + // tail is always 0. + tailGroup string } const ( @@ -66,13 +80,16 @@ const ( stateHistoryStorageData = "storage.data" ) +// StateHistoryTailGroup is the tail group shared by all state history tables. +const StateHistoryTailGroup = "history" + // stateFreezerTableConfigs configures the settings for tables in the state freezer. var stateFreezerTableConfigs = map[string]freezerTableConfig{ - stateHistoryMeta: {noSnappy: true, prunable: true}, - stateHistoryAccountIndex: {noSnappy: false, prunable: true}, - stateHistoryStorageIndex: {noSnappy: false, prunable: true}, - stateHistoryAccountData: {noSnappy: false, prunable: true}, - stateHistoryStorageData: {noSnappy: false, prunable: true}, + stateHistoryMeta: {noSnappy: true, tailGroup: StateHistoryTailGroup}, + stateHistoryAccountIndex: {noSnappy: false, tailGroup: StateHistoryTailGroup}, + stateHistoryStorageIndex: {noSnappy: false, tailGroup: StateHistoryTailGroup}, + stateHistoryAccountData: {noSnappy: false, tailGroup: StateHistoryTailGroup}, + stateHistoryStorageData: {noSnappy: false, tailGroup: StateHistoryTailGroup}, } const ( @@ -81,15 +98,18 @@ const ( trienodeHistoryValueSectionTable = "trienode.value" ) +// TrienodeHistoryTailGroup is the tail group shared by all trienode history tables. +const TrienodeHistoryTailGroup = "history" + // trienodeFreezerTableConfigs configures the settings for tables in the trienode freezer. var trienodeFreezerTableConfigs = map[string]freezerTableConfig{ - trienodeHistoryHeaderTable: {noSnappy: false, prunable: true}, + trienodeHistoryHeaderTable: {noSnappy: false, tailGroup: TrienodeHistoryTailGroup}, // Disable snappy compression to allow efficient partial read. - trienodeHistoryKeySectionTable: {noSnappy: true, prunable: true}, + trienodeHistoryKeySectionTable: {noSnappy: true, tailGroup: TrienodeHistoryTailGroup}, // Disable snappy compression to allow efficient partial read. - trienodeHistoryValueSectionTable: {noSnappy: true, prunable: true}, + trienodeHistoryValueSectionTable: {noSnappy: true, tailGroup: TrienodeHistoryTailGroup}, } // The list of identifiers of ancient stores. diff --git a/core/rawdb/ancient_utils.go b/core/rawdb/ancient_utils.go index 8c6b18df08..914ad9f6d8 100644 --- a/core/rawdb/ancient_utils.go +++ b/core/rawdb/ancient_utils.go @@ -67,13 +67,24 @@ func inspect(name string, order map[string]freezerTableConfig, reader ethdb.Anci info.head = 0 } - // Retrieve the number of first stored item - tail, err := reader.Tail() - if err != nil { - return freezerInfo{}, err + // Retrieve the highest tail across all known tail groups. The inspected + // freezer info uses a single tail value for display, which corresponds to + // the most-pruned group. + groups := make(map[string]struct{}) + for _, cfg := range order { + if cfg.tailGroup != "" { + groups[cfg.tailGroup] = struct{}{} + } + } + for g := range groups { + t, err := reader.Tail(g) + if err != nil { + return freezerInfo{}, err + } + if t > info.tail { + info.tail = t + } } - info.tail = tail - if ancients == 0 { info.count = 0 } else { diff --git a/core/rawdb/ancienttest/testsuite.go b/core/rawdb/ancienttest/testsuite.go index eb66645a3a..a84053e604 100644 --- a/core/rawdb/ancienttest/testsuite.go +++ b/core/rawdb/ancienttest/testsuite.go @@ -25,6 +25,11 @@ import ( "github.com/ethereum/go-ethereum/internal/testrand" ) +// TailGroup is the tail group used by tables created in this test suite. The +// store factory passed to TestAncientSuite must wire its tables to this group +// so that the suite can query the freezer's tail consistently. +const TailGroup = "test" + // TestAncientSuite runs a suite of tests against an ancient database // implementation. func TestAncientSuite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { @@ -58,11 +63,11 @@ func basicRead(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { }); err != nil { t.Fatalf("Failed to write ancient data %v", err) } - db.TruncateTail(10) + db.TruncateTail(TailGroup, 10) db.TruncateHead(90) // Test basic tail and head retrievals - tail, err := db.Tail() + tail, err := db.Tail(TailGroup) if err != nil || tail != 10 { t.Fatal("Failed to retrieve tail") } @@ -123,7 +128,7 @@ func batchRead(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { }); err != nil { t.Fatalf("Failed to write ancient data %v", err) } - db.TruncateTail(10) + db.TruncateTail(TailGroup, 10) db.TruncateHead(90) // Test the items in range should be reachable @@ -262,12 +267,12 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { } // Write should work after truncating from tail but over the head - db.TruncateTail(200) + db.TruncateTail(TailGroup, 200) head, err := db.Ancients() if err != nil { t.Fatalf("Failed to retrieve head ancients %v", err) } - tail, err := db.Tail() + tail, err := db.Tail(TailGroup) if err != nil { t.Fatalf("Failed to retrieve tail ancients %v", err) } @@ -293,7 +298,7 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { if err != nil { t.Fatalf("Failed to retrieve head ancients %v", err) } - tail, err = db.Tail() + tail, err = db.Tail(TailGroup) if err != nil { t.Fatalf("Failed to retrieve tail ancients %v", err) } @@ -351,7 +356,7 @@ func TestResettableAncientSuite(t *testing.T, newFn func(kinds []string) ethdb.R }); err != nil { t.Fatalf("Failed to write ancient data %v", err) } - db.TruncateTail(10) + db.TruncateTail(TailGroup, 10) db.TruncateHead(90) // Ancient write should work after resetting diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go index d33f7ce33d..c47ddebf8c 100644 --- a/core/rawdb/chain_freezer.go +++ b/core/rawdb/chain_freezer.go @@ -354,7 +354,7 @@ func (f *chainFreezer) Ancient(kind string, number uint64) ([]byte, error) { if kind == ChainFreezerHeaderTable || kind == ChainFreezerHashTable { return f.ancients.Ancient(kind, number) } - tail, err := f.ancients.Tail() + tail, err := f.ancients.Tail(tableTailGroup(kind)) if err != nil { return nil, err } @@ -375,6 +375,16 @@ func (f *chainFreezer) Ancient(kind string, number uint64) ([]byte, error) { return nil, errUnknownTable } +// tableTailGroup returns the tail group identifier for a chain freezer table. +// Unknown tables resolve to the default block-data group, since the chain +// freezer's only prunable group today is bodies+receipts. +func tableTailGroup(kind string) string { + if cfg, ok := chainFreezerTableConfigs[kind]; ok { + return cfg.tailGroup + } + return ChainFreezerBlockDataGroup +} + // ReadAncients executes an operation while preventing mutations to the freezer, // i.e. if fn performs multiple reads, they will be consistent with each other. func (f *chainFreezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) { @@ -391,8 +401,8 @@ func (f *chainFreezer) Ancients() (uint64, error) { return f.ancients.Ancients() } -func (f *chainFreezer) Tail() (uint64, error) { - return f.ancients.Tail() +func (f *chainFreezer) Tail(group string) (uint64, error) { + return f.ancients.Tail(group) } func (f *chainFreezer) AncientSize(kind string) (uint64, error) { @@ -415,8 +425,8 @@ func (f *chainFreezer) TruncateHead(items uint64) (uint64, error) { return f.ancients.TruncateHead(items) } -func (f *chainFreezer) TruncateTail(items uint64) (uint64, error) { - return f.ancients.TruncateTail(items) +func (f *chainFreezer) TruncateTail(group string, items uint64) (uint64, error) { + return f.ancients.TruncateTail(group, items) } func (f *chainFreezer) SyncAncient() error { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 39e1a64e5a..9e49ee23dd 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -113,7 +113,7 @@ func (db *nofreezedb) Ancients() (uint64, error) { } // Tail returns an error as we don't have a backing chain freezer. -func (db *nofreezedb) Tail() (uint64, error) { +func (db *nofreezedb) Tail(group string) (uint64, error) { return 0, errNotSupported } @@ -133,7 +133,7 @@ func (db *nofreezedb) TruncateHead(items uint64) (uint64, error) { } // TruncateTail returns an error as we don't have a backing chain freezer. -func (db *nofreezedb) TruncateTail(items uint64) (uint64, error) { +func (db *nofreezedb) TruncateTail(group string, items uint64) (uint64, error) { return 0, errNotSupported } diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 0e2f86d6ed..7280bf9179 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -60,7 +60,6 @@ const freezerTableSize = 2 * 1000 * 1000 * 1000 type Freezer struct { datadir string head atomic.Uint64 // Number of items stored (including items removed from tail) - tail atomic.Uint64 // Number of the first stored item in the freezer // This lock synchronizes writers and the truncate operation, as well as // the "atomic" (batched) read operations. @@ -216,9 +215,33 @@ func (f *Freezer) Ancients() (uint64, error) { return f.head.Load(), nil } -// Tail returns the number of first stored item in the freezer. -func (f *Freezer) Tail() (uint64, error) { - return f.tail.Load(), nil +// Tail returns the lowest accessible item index for the given tail group. +// All tables sharing this group must agree on the tail; an empty group name +// refers to non-prunable tables and always returns 0. +func (f *Freezer) Tail(group string) (uint64, error) { + if group == "" { + return 0, nil + } + var ( + tail uint64 + found bool + ) + for _, table := range f.tables { + if table.config.tailGroup != group { + continue + } + h := table.itemHidden.Load() + if !found { + tail = h + found = true + } else if h != tail { + return 0, fmt.Errorf("inconsistent tail in group %q: %d vs %d", group, h, tail) + } + } + if !found { + return 0, fmt.Errorf("unknown tail group: %q", group) + } + return tail, nil } // AncientSize returns the ancient size of the specified category. @@ -299,33 +322,49 @@ func (f *Freezer) TruncateHead(items uint64) (uint64, error) { return oitems, nil } -// TruncateTail discards all data below the specified threshold. Note that only -// 'prunable' tables will be truncated. -func (f *Freezer) TruncateTail(tail uint64) (uint64, error) { +// TruncateTail discards all data below the specified threshold across every +// table that belongs to the named tail group. Tables that are already past +// the threshold are left untouched. The previous tail of the group is +// returned. An empty group name or an unknown group name returns an error. +func (f *Freezer) TruncateTail(group string, tail uint64) (uint64, error) { if f.readonly { return 0, errReadOnly } + if group == "" { + return 0, errors.New("empty tail group") + } f.writeLock.Lock() defer f.writeLock.Unlock() - old := f.tail.Load() - if old >= tail { - return old, nil - } + var ( + prev uint64 + found bool + ) for _, table := range f.tables { - if table.config.prunable { - if err := table.truncateTail(tail); err != nil { - return 0, err - } + if table.config.tailGroup != group { + continue + } + if !found { + prev = table.itemHidden.Load() + found = true } } - f.tail.Store(tail) - - // Update the head if the requested tail exceeds the current head + if !found { + return 0, fmt.Errorf("unknown tail group: %q", group) + } + for _, table := range f.tables { + if table.config.tailGroup != group { + continue + } + if err := table.truncateTail(tail); err != nil { + return 0, err + } + } + // Update the head if the requested tail exceeds the current head. if f.head.Load() < tail { f.head.Store(tail) } - return old, nil + return prev, nil } // SyncAncient flushes all data tables to disk. @@ -342,84 +381,109 @@ func (f *Freezer) SyncAncient() error { return nil } -// validate checks that every table has the same boundary. -// Used instead of `repair` in readonly mode. +// validate checks that every table has the same head and that tables sharing +// a tail group also share a tail. Used instead of `repair` in readonly mode. func (f *Freezer) validate() error { if len(f.tables) == 0 { return nil } var ( head uint64 - prunedTail *uint64 + headSet bool + groupTails = make(map[string]uint64) ) - // get any head value - for _, table := range f.tables { - head = table.items.Load() - break - } for kind, table := range f.tables { - // all tables have to have the same head - if head != table.items.Load() { - return fmt.Errorf("freezer table %s has a differing head: %d != %d", kind, table.items.Load(), head) + items := table.items.Load() + if !headSet { + head = items + headSet = true + } else if items != head { + return fmt.Errorf("freezer table %s has a differing head: %d != %d", kind, items, head) } - if !table.config.prunable { - // non-prunable tables have to start at 0 + if table.config.tailGroup == "" { if table.itemHidden.Load() != 0 { return fmt.Errorf("non-prunable freezer table '%s' has a non-zero tail: %d", kind, table.itemHidden.Load()) } + continue + } + hidden := table.itemHidden.Load() + if t, ok := groupTails[table.config.tailGroup]; ok { + if t != hidden { + return fmt.Errorf("freezer table %s has differing tail in group %q: %d != %d", kind, table.config.tailGroup, hidden, t) + } } else { - // prunable tables have to have the same length - if prunedTail == nil { - tmp := table.itemHidden.Load() - prunedTail = &tmp - } - if *prunedTail != table.itemHidden.Load() { - return fmt.Errorf("freezer table %s has differing tail: %d != %d", kind, table.itemHidden.Load(), *prunedTail) - } + groupTails[table.config.tailGroup] = hidden } } - - if prunedTail == nil { - tmp := uint64(0) - prunedTail = &tmp - } - f.head.Store(head) - f.tail.Store(*prunedTail) return nil } -// repair truncates all data tables to the same length. +// repair brings every table into a consistent state. The common head is taken +// as the minimum item count among non-empty tables; freshly added empty tables +// are fast-forwarded to that head via tail truncation. Within each tail group +// the maximum tail wins, and prunable tables are truncated to it. func (f *Freezer) repair() error { + // Determine the common head from non-empty tables. Empty tables are + // excluded so that a freshly added table cannot drag the existing head + // down to zero on first cold-start. var ( - head = uint64(math.MaxUint64) - prunedTail = uint64(0) + hasNonEmpty bool + head uint64 = math.MaxUint64 ) - // get the minimal head and the maximum tail for _, table := range f.tables { - head = min(head, table.items.Load()) - prunedTail = max(prunedTail, table.itemHidden.Load()) + if table.items.Load() == 0 { + continue + } + if items := table.items.Load(); items < head { + head = items + } + hasNonEmpty = true } - // apply the pruning - for kind, table := range f.tables { - // all tables need to have the same head + if !hasNonEmpty { + head = 0 + } + // Align newly added empty tables to the common head. truncateTail + // internally calls resetTo when the requested tail exceeds the current + // head, which is exactly what we need here. + if head > 0 { + for _, table := range f.tables { + if table.items.Load() == 0 { + if err := table.truncateTail(head); err != nil { + return err + } + } + } + } + // Truncate every table to the common head. + for _, table := range f.tables { if err := table.truncateHead(head); err != nil { return err } - if !table.config.prunable { - // non-prunable tables have to start at 0 + } + // Per-group tail alignment: take the maximum tail in each group and apply + // it to all members. Non-prunable tables must remain at tail 0. + groupTails := make(map[string]uint64) + for kind, table := range f.tables { + if table.config.tailGroup == "" { if table.itemHidden.Load() != 0 { panic(fmt.Sprintf("non-prunable freezer table %s has non-zero tail: %v", kind, table.itemHidden.Load())) } - } else { - // prunable tables have to have the same length - if err := table.truncateTail(prunedTail); err != nil { - return err - } + continue + } + hidden := table.itemHidden.Load() + if t, ok := groupTails[table.config.tailGroup]; !ok || hidden > t { + groupTails[table.config.tailGroup] = hidden + } + } + for _, table := range f.tables { + if table.config.tailGroup == "" { + continue + } + if err := table.truncateTail(groupTails[table.config.tailGroup]); err != nil { + return err } } - f.head.Store(head) - f.tail.Store(prunedTail) return nil } diff --git a/core/rawdb/freezer_memory.go b/core/rawdb/freezer_memory.go index ec6d4b22e2..1007275777 100644 --- a/core/rawdb/freezer_memory.go +++ b/core/rawdb/freezer_memory.go @@ -228,7 +228,6 @@ func (b *memoryBatch) commit(freezer *MemoryFreezer) (items uint64, writeSize in // interface and can be used along with ephemeral key-value store. type MemoryFreezer struct { items uint64 // Number of items stored - tail uint64 // Number of the first stored item in the freezer readonly bool // Flag if the freezer is only for reading lock sync.RWMutex // Lock to protect fields tables map[string]*memoryTable // Tables for storing everything @@ -289,13 +288,35 @@ func (f *MemoryFreezer) Ancients() (uint64, error) { return f.items, nil } -// Tail returns the number of first stored item in the freezer. -// This number can also be interpreted as the total deleted item numbers. -func (f *MemoryFreezer) Tail() (uint64, error) { +// Tail returns the lowest accessible item index for the given tail group. +// All tables sharing the group must agree on the tail; an empty group name +// refers to non-prunable tables and always returns 0. +func (f *MemoryFreezer) Tail(group string) (uint64, error) { f.lock.RLock() defer f.lock.RUnlock() - return f.tail, nil + if group == "" { + return 0, nil + } + var ( + tail uint64 + found bool + ) + for _, table := range f.tables { + if table.config.tailGroup != group { + continue + } + if !found { + tail = table.offset + found = true + } else if table.offset != tail { + return 0, fmt.Errorf("inconsistent tail in group %q: %d vs %d", group, table.offset, tail) + } + } + if !found { + return 0, fmt.Errorf("unknown tail group: %q", group) + } + return tail, nil } // AncientSize returns the ancient size of the specified category. @@ -375,32 +396,47 @@ func (f *MemoryFreezer) TruncateHead(items uint64) (uint64, error) { return old, nil } -// TruncateTail discards all data below the provided threshold number. -// Note this will only truncate 'prunable' tables. Block headers and canonical -// hashes cannot be truncated at this time. -func (f *MemoryFreezer) TruncateTail(tail uint64) (uint64, error) { +// TruncateTail discards all data below the provided threshold across every +// table that belongs to the named tail group. Tables already past the +// threshold are left untouched. The previous tail of the group is returned. +func (f *MemoryFreezer) TruncateTail(group string, tail uint64) (uint64, error) { f.lock.Lock() defer f.lock.Unlock() if f.readonly { return 0, errReadOnly } - old := f.tail - if old >= tail { - return old, nil + if group == "" { + return 0, errors.New("empty tail group") } + var ( + prev uint64 + found bool + ) for _, table := range f.tables { - if table.config.prunable { - if err := table.truncateTail(tail); err != nil { - return 0, err - } + if table.config.tailGroup != group { + continue + } + if !found { + prev = table.offset + found = true + } + } + if !found { + return 0, fmt.Errorf("unknown tail group: %q", group) + } + for _, table := range f.tables { + if table.config.tailGroup != group { + continue + } + if err := table.truncateTail(tail); err != nil { + return 0, err } } - f.tail = tail if f.items < tail { f.items = tail } - return old, nil + return prev, nil } // SyncAncient flushes all data tables to disk. @@ -430,7 +466,7 @@ func (f *MemoryFreezer) Reset() error { tables[name] = newMemoryTable(name, table.config) } f.tables = tables - f.items, f.tail = 0, 0 + f.items = 0 return nil } diff --git a/core/rawdb/freezer_memory_test.go b/core/rawdb/freezer_memory_test.go index 4bd31d8027..6baa1765b1 100644 --- a/core/rawdb/freezer_memory_test.go +++ b/core/rawdb/freezer_memory_test.go @@ -28,8 +28,8 @@ func TestMemoryFreezer(t *testing.T) { tables := make(map[string]freezerTableConfig) for _, kind := range kinds { tables[kind] = freezerTableConfig{ - noSnappy: true, - prunable: true, + noSnappy: true, + tailGroup: ancienttest.TailGroup, } } return NewMemoryFreezer(false, tables) @@ -38,8 +38,8 @@ func TestMemoryFreezer(t *testing.T) { tables := make(map[string]freezerTableConfig) for _, kind := range kinds { tables[kind] = freezerTableConfig{ - noSnappy: true, - prunable: true, + noSnappy: true, + tailGroup: ancienttest.TailGroup, } } return NewMemoryFreezer(false, tables) diff --git a/core/rawdb/freezer_resettable.go b/core/rawdb/freezer_resettable.go index 5494a648c8..48ab502eb7 100644 --- a/core/rawdb/freezer_resettable.go +++ b/core/rawdb/freezer_resettable.go @@ -143,12 +143,12 @@ func (f *resettableFreezer) Ancients() (uint64, error) { return f.freezer.Ancients() } -// Tail returns the number of first stored item in the freezer. -func (f *resettableFreezer) Tail() (uint64, error) { +// Tail returns the lowest accessible item index for the given tail group. +func (f *resettableFreezer) Tail(group string) (uint64, error) { f.lock.RLock() defer f.lock.RUnlock() - return f.freezer.Tail() + return f.freezer.Tail(group) } // AncientSize returns the ancient size of the specified category. @@ -185,13 +185,13 @@ func (f *resettableFreezer) TruncateHead(items uint64) (uint64, error) { return f.freezer.TruncateHead(items) } -// TruncateTail discards any recent data below the provided threshold number. -// It returns the previous value -func (f *resettableFreezer) TruncateTail(tail uint64) (uint64, error) { +// TruncateTail discards data below the provided threshold for the named tail +// group. It returns the previous tail of the group. +func (f *resettableFreezer) TruncateTail(group string, tail uint64) (uint64, error) { f.lock.RLock() defer f.lock.RUnlock() - return f.freezer.TruncateTail(tail) + return f.freezer.TruncateTail(group, tail) } // SyncAncient flushes all data tables to disk. diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go index fab3319a2a..1b40ef06c8 100644 --- a/core/rawdb/freezer_test.go +++ b/core/rawdb/freezer_test.go @@ -398,8 +398,8 @@ func TestFreezerSuite(t *testing.T) { tables := make(map[string]freezerTableConfig) for _, kind := range kinds { tables[kind] = freezerTableConfig{ - noSnappy: true, - prunable: true, + noSnappy: true, + tailGroup: ancienttest.TailGroup, } } f, _ := newFreezerForTesting(t, tables) @@ -409,8 +409,8 @@ func TestFreezerSuite(t *testing.T) { tables := make(map[string]freezerTableConfig) for _, kind := range kinds { tables[kind] = freezerTableConfig{ - noSnappy: true, - prunable: true, + noSnappy: true, + tailGroup: ancienttest.TailGroup, } } f, _ := newResettableFreezer(t.TempDir(), "", false, 2048, tables) diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 407a619c9f..7640f1cf43 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -76,8 +76,8 @@ func (t *table) Ancients() (uint64, error) { // Tail is a noop passthrough that just forwards the request to the underlying // database. -func (t *table) Tail() (uint64, error) { - return t.db.Tail() +func (t *table) Tail(group string) (uint64, error) { + return t.db.Tail(group) } // AncientSize is a noop passthrough that just forwards the request to the underlying @@ -103,8 +103,8 @@ func (t *table) TruncateHead(items uint64) (uint64, error) { // TruncateTail is a noop passthrough that just forwards the request to the underlying // database. -func (t *table) TruncateTail(items uint64) (uint64, error) { - return t.db.TruncateTail(items) +func (t *table) TruncateTail(group string, items uint64) (uint64, error) { + return t.db.TruncateTail(group, items) } // SyncAncient is a noop passthrough that just forwards the request to the underlying diff --git a/ethdb/database.go b/ethdb/database.go index 534fcad4fc..a5953bd42a 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -128,9 +128,12 @@ type AncientReaderOp interface { // Ancients returns the ancient item numbers in the ancient store. Ancients() (uint64, error) - // Tail returns the number of first stored item in the ancient store. - // This number can also be interpreted as the total deleted items. - Tail() (uint64, error) + // Tail returns the lowest accessible item index for the given tail group. + // This number can also be interpreted as the total deleted items in the + // group. Tables sharing a group are pruned together and therefore agree + // on the value. An empty group name refers to non-prunable tables and + // always returns 0. + Tail(group string) (uint64, error) // AncientSize returns the ancient size of the specified category. AncientSize(kind string) (uint64, error) @@ -159,14 +162,16 @@ type AncientWriter interface { // After the truncation, the latest item can be accessed it item_n-1(start from 0). TruncateHead(n uint64) (uint64, error) - // TruncateTail discards the first n ancient data from the ancient store. The already - // deleted items are ignored. After the truncation, the earliest item can be accessed - // is item_n(start from 0). The deleted items may not be removed from the ancient store - // immediately, but only when the accumulated deleted data reach the threshold then - // will be removed all together. + // TruncateTail discards the first n items from every table belonging to + // the named tail group. Already-deleted items are ignored. After the + // truncation, the earliest accessible item in the group is item_n + // (starting from 0). Deleted items may not be removed from disk + // immediately, but only once the accumulated deleted data reaches the + // threshold, at which point they are removed all together. // - // Note that data marked as non-prunable will still be retained and remain accessible. - TruncateTail(n uint64) (uint64, error) + // The previous tail of the group is returned. Tables outside the group + // (including non-prunable ones) are untouched. + TruncateTail(group string, n uint64) (uint64, error) } // AncientWriteOp is given to the function argument of ModifyAncients. diff --git a/ethdb/remotedb/remotedb.go b/ethdb/remotedb/remotedb.go index 0d0d854fe4..87e45cf4a0 100644 --- a/ethdb/remotedb/remotedb.go +++ b/ethdb/remotedb/remotedb.go @@ -67,7 +67,7 @@ func (db *Database) Ancients() (uint64, error) { return resp, err } -func (db *Database) Tail() (uint64, error) { +func (db *Database) Tail(group string) (uint64, error) { panic("not supported") } @@ -99,7 +99,7 @@ func (db *Database) TruncateHead(n uint64) (uint64, error) { panic("not supported") } -func (db *Database) TruncateTail(n uint64) (uint64, error) { +func (db *Database) TruncateTail(group string, n uint64) (uint64, error) { panic("not supported") } diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 50c7279d0e..47a882446a 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -378,7 +378,7 @@ func (dl *diskLayer) writeHistory(typ historyType, diff *diffLayer) (bool, error if limit == 0 { return false, nil } - tail, err := freezer.Tail() + tail, err := freezer.Tail(rawdb.StateHistoryTailGroup) if err != nil { return false, err } // firstID = tail+1 diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go index 7f5b0e35ba..4668d6f3a4 100644 --- a/triedb/pathdb/history.go +++ b/triedb/pathdb/history.go @@ -273,7 +273,7 @@ func truncateFromHead(store ethdb.AncientStore, typ historyType, nhead uint64) ( if err != nil { return 0, err } - otail, err := store.Tail() + otail, err := store.Tail(rawdb.StateHistoryTailGroup) if err != nil { return 0, err } @@ -303,7 +303,13 @@ func truncateFromTail(store ethdb.AncientStore, typ historyType, ntail uint64) ( if err != nil { return 0, err } - otail, err := store.Tail() + var group string + if typ == typeStateHistory { + group = rawdb.StateHistoryTailGroup + } else { + group = rawdb.TrienodeHistoryTailGroup + } + otail, err := store.Tail(group) if err != nil { return 0, err } @@ -315,7 +321,7 @@ func truncateFromTail(store ethdb.AncientStore, typ historyType, ntail uint64) ( if otail == ntail { return 0, nil } - otail, err = store.TruncateTail(ntail) + otail, err = store.TruncateTail(group, ntail) if err != nil { return 0, err } @@ -430,7 +436,7 @@ func repairHistory(db ethdb.Database, isUBT bool, readOnly bool, stateID uint64, truncTo = min(truncTo, thead) } else { if thead == 0 { - _, err = trienodes.TruncateTail(stateID) + _, err = trienodes.TruncateTail(rawdb.TrienodeHistoryTailGroup, stateID) if err != nil { return nil, nil, err } diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go index 9b215b917f..873990b6b5 100644 --- a/triedb/pathdb/history_indexer.go +++ b/triedb/pathdb/history_indexer.go @@ -542,7 +542,7 @@ func (i *indexIniter) run(recover bool) { // next returns the ID of the next state history to be indexed. func (i *indexIniter) next() (uint64, error) { - tail, err := i.freezer.Tail() + tail, err := i.freezer.Tail(rawdb.StateHistoryTailGroup) if err != nil { return 0, err } diff --git a/triedb/pathdb/history_inspect.go b/triedb/pathdb/history_inspect.go index 74b8bb8df2..c956295368 100644 --- a/triedb/pathdb/history_inspect.go +++ b/triedb/pathdb/history_inspect.go @@ -21,6 +21,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" @@ -37,7 +38,7 @@ type HistoryStats struct { // sanitizeRange limits the given range to fit within the local history store. func sanitizeRange(start, end uint64, freezer ethdb.AncientReader) (uint64, uint64, error) { // Load the id of the first history object in local store. - tail, err := freezer.Tail() + tail, err := freezer.Tail(rawdb.StateHistoryTailGroup) if err != nil { return 0, 0, err } @@ -132,7 +133,7 @@ func storageHistory(freezer ethdb.AncientReader, address common.Address, slot co // historyRange returns the block number range of local state histories. func historyRange(freezer ethdb.AncientReader) (uint64, uint64, error) { // Load the id of the first history object in local store. - tail, err := freezer.Tail() + tail, err := freezer.Tail(rawdb.StateHistoryTailGroup) if err != nil { return 0, 0, err } diff --git a/triedb/pathdb/history_reader.go b/triedb/pathdb/history_reader.go index 4ae1fb36cb..51536c8aae 100644 --- a/triedb/pathdb/history_reader.go +++ b/triedb/pathdb/history_reader.go @@ -470,7 +470,7 @@ func checkStateAvail(state stateIdent, exptyp historyType, freezer ethdb.Ancient return 0, fmt.Errorf("unsupported history type: %d, want: %v", toHistoryType(state.typ), exptyp) } // firstID = tail+1 - tail, err := freezer.Tail() + tail, err := freezer.Tail(rawdb.StateHistoryTailGroup) if err != nil { return 0, err } diff --git a/triedb/pathdb/history_state_test.go b/triedb/pathdb/history_state_test.go index 4046fb9640..b33777fd52 100644 --- a/triedb/pathdb/history_state_test.go +++ b/triedb/pathdb/history_state_test.go @@ -237,7 +237,7 @@ func TestTruncateOutOfRange(t *testing.T) { // Ensure of-out-range truncations are rejected correctly. head, _ := freezer.Ancients() - tail, _ := freezer.Tail() + tail, _ := freezer.Tail(rawdb.StateHistoryTailGroup) cases := []struct { mode int From da877346c6ccc68de49391946a28722264695176 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 15 May 2026 13:25:09 +0800 Subject: [PATCH 2/7] core/rawdb: introduce freezer table for BALs --- core/rawdb/accessors_chain.go | 22 ++++++++++++++++++++-- core/rawdb/ancient_scheme.go | 10 ++++++++++ core/rawdb/chain_freezer.go | 17 ++++++++++++++--- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 987b8df392..08e4cc5713 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -614,9 +614,18 @@ func HasAccessList(db ethdb.Reader, hash common.Hash, number uint64) bool { return has } -// ReadAccessListRLP retrieves the RLP-encoded block access list for a block from KV. +// ReadAccessListRLP retrieves the RLP-encoded block access list for a block. func ReadAccessListRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Get(accessListKey(number, hash)) + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + data, _ = reader.Ancient(ChainFreezerBALTable, number) + if len(data) > 0 { + return nil + } + // Block is not in ancients, read from key-value store by hash and number. + data, _ = db.Get(accessListKey(number, hash)) + return nil + }) return data } @@ -759,6 +768,13 @@ func writeAncientBlock(op ethdb.AncientWriteOp, block *types.Block, header *type if err := op.Append(ChainFreezerReceiptTable, num, receipts); err != nil { return fmt.Errorf("can't append block %d receipts: %v", num, err) } + // The assumption is held that BAL of ancient block is no longer available + // (it may still reachable, but it's not worthwhile to even retrieve it + // from the network). A nil entry is stored in the BAL table as the absence + // placeholder. + if err := op.AppendRaw(ChainFreezerBALTable, num, nil); err != nil { + return fmt.Errorf("can't append block %d bals: %v", num, err) + } return nil } @@ -791,6 +807,7 @@ func DeleteBlock(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { DeleteReceipts(db, hash, number) DeleteHeader(db, hash, number) DeleteBody(db, hash, number) + DeleteAccessList(db, hash, number) } // DeleteBlockWithoutNumber removes all block data associated with a hash, except @@ -799,6 +816,7 @@ func DeleteBlockWithoutNumber(db ethdb.KeyValueWriter, hash common.Hash, number DeleteReceipts(db, hash, number) deleteHeaderWithoutNumber(db, hash, number) DeleteBody(db, hash, number) + DeleteAccessList(db, hash, number) } const badBlockToKeep = 10 diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go index 15bcf8be60..d6097a9193 100644 --- a/core/rawdb/ancient_scheme.go +++ b/core/rawdb/ancient_scheme.go @@ -35,6 +35,10 @@ const ( // ChainFreezerReceiptTable indicates the name of the freezer receipts table. ChainFreezerReceiptTable = "receipts" + + // ChainFreezerBALTable indicates the name of the freezer block access list + // table introduced by EIP-7928. + ChainFreezerBALTable = "bals" ) // Identifiers of tail groups used by the chain freezer. @@ -43,6 +47,11 @@ const ( // receipt tables. The two tables are pruned together and therefore have // the same tail position. ChainFreezerBlockDataGroup = "blockdata" + + // ChainFreezerBALGroup is the tail group for the block access list table. + // BAL is only populated after EIP-7928 activates, so it generally has a + // higher tail than the block-data group and is pruned independently. + ChainFreezerBALGroup = "bal" ) // chainFreezerTableConfigs configures the settings for tables in the chain freezer. @@ -54,6 +63,7 @@ var chainFreezerTableConfigs = map[string]freezerTableConfig{ ChainFreezerHashTable: {noSnappy: true}, ChainFreezerBodiesTable: {noSnappy: false, tailGroup: ChainFreezerBlockDataGroup}, ChainFreezerReceiptTable: {noSnappy: false, tailGroup: ChainFreezerBlockDataGroup}, + ChainFreezerBALTable: {noSnappy: false, tailGroup: ChainFreezerBALGroup}, } // freezerTableConfig contains the settings for a freezer table. diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go index c47ddebf8c..b706f9132c 100644 --- a/core/rawdb/chain_freezer.go +++ b/core/rawdb/chain_freezer.go @@ -45,9 +45,7 @@ const ( // key-value database to flat files for saving space on live database. type chainFreezer struct { ancients ethdb.AncientStore // Ancient store for storing cold chain segment - - // Optional Era database used as a backup for the pruned chain. - eradb *eradb.Store + eradb *eradb.Store // Optional Era database used as a backup for the pruned chain quit chan struct{} wg sync.WaitGroup @@ -327,6 +325,16 @@ func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hash if len(receipts) == 0 { return fmt.Errorf("block receipts missing, can't freeze block %d", number) } + // An empty block access list is allowed and may occur in multiple + // scenarios, such as: + // - pre-Amsterdam blocks + // - post-Amsterdam blocks with the BAL absent (e.g. pruned by network) + // - post-Amsterdam blocks with an explicitly empty BAL + // + // In these cases, a nil entry will be stored in the BAL table as the + // absence placeholder. + bals := ReadAccessListRLP(nfdb, hash, number) + // Write to the batch. if err := op.AppendRaw(ChainFreezerHashTable, number, hash[:]); err != nil { return fmt.Errorf("can't write hash to Freezer: %v", err) @@ -340,6 +348,9 @@ func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hash if err := op.AppendRaw(ChainFreezerReceiptTable, number, receipts); err != nil { return fmt.Errorf("can't write receipts to Freezer: %v", err) } + if err := op.AppendRaw(ChainFreezerBALTable, number, bals); err != nil { + return fmt.Errorf("can't write bals to Freezer: %v", err) + } hashes = append(hashes, hash) } return nil From e67fa13e3421b3a68749a04bd193346a9a94510f Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 15 May 2026 13:44:20 +0800 Subject: [PATCH 3/7] core/rawdb: fix WriteAncientHeaderChain and add tests --- core/rawdb/accessors_chain.go | 7 ++ core/rawdb/accessors_chain_test.go | 41 ++++++++++ core/rawdb/chain_freezer.go | 14 ++-- core/rawdb/freezer_test.go | 127 +++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 6 deletions(-) diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 08e4cc5713..d8825b6b8f 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -797,6 +797,13 @@ func WriteAncientHeaderChain(db ethdb.AncientWriter, headers []*types.Header) (i if err := op.AppendRaw(ChainFreezerReceiptTable, num, nil); err != nil { return fmt.Errorf("can't append block %d receipts: %v", num, err) } + // The assumption is held that BAL of ancient block is no longer available + // (it may still reachable, but it's not worthwhile to even retrieve it + // from the network). A nil entry is stored in the BAL table as the absence + // placeholder. + if err := op.AppendRaw(ChainFreezerBALTable, num, nil); err != nil { + return fmt.Errorf("can't append block %d bals: %v", num, err) + } } return nil }) diff --git a/core/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go index c35f56ee07..76805cb3ec 100644 --- a/core/rawdb/accessors_chain_test.go +++ b/core/rawdb/accessors_chain_test.go @@ -926,6 +926,47 @@ func makeTestBAL(t *testing.T) (rlp.RawValue, *bal.BlockAccessList) { return encoded, &decoded } +// TestWriteAncientBlocksNilBAL ensures that freezing a block with no block +// access list produces an empty entry in the BAL ancient table and that +// ReadAccessList returns nil afterwards (i.e. the empty entry is not surfaced +// as a malformed BAL). +func TestWriteAncientBlocksNilBAL(t *testing.T) { + db, err := Open(NewMemoryDatabase(), OpenOptions{Ancient: t.TempDir()}) + if err != nil { + t.Fatalf("failed to create database with ancient backend: %v", err) + } + defer db.Close() + + block := types.NewBlockWithHeader(&types.Header{ + Number: big.NewInt(0), + Extra: []byte("nil-bal block"), + UncleHash: types.EmptyUncleHash, + TxHash: types.EmptyTxsHash, + ReceiptHash: types.EmptyReceiptsHash, + }) + if block.AccessList() != nil { + t.Fatalf("test precondition: block must have nil access list") + } + if _, err := WriteAncientBlocks(db, []*types.Block{block}, types.EncodeBlockReceiptLists([]types.Receipts{nil})); err != nil { + t.Fatalf("WriteAncientBlocks failed: %v", err) + } + hash, number := block.Hash(), block.NumberU64() + + // The BAL ancient entry should exist as an empty blob. + if blob := ReadAccessListRLP(db, hash, number); len(blob) != 0 { + t.Fatalf("ReadAccessListRLP: got %x, want empty", blob) + } + // ReadAccessList must surface nil rather than attempting to RLP-decode + // the empty payload. + if b := ReadAccessList(db, hash, number); b != nil { + t.Fatalf("ReadAccessList: got %v, want nil", b) + } + // HasAccessList only consults the KV store and there's nothing there. + if HasAccessList(db, hash, number) { + t.Fatal("HasAccessList returned true for absent BAL") + } +} + // TestBALStorage tests write/read/delete of BALs in the KV store. func TestBALStorage(t *testing.T) { db := NewMemoryDatabase() diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go index b706f9132c..729e616daf 100644 --- a/core/rawdb/chain_freezer.go +++ b/core/rawdb/chain_freezer.go @@ -365,7 +365,11 @@ func (f *chainFreezer) Ancient(kind string, number uint64) ([]byte, error) { if kind == ChainFreezerHeaderTable || kind == ChainFreezerHashTable { return f.ancients.Ancient(kind, number) } - tail, err := f.ancients.Tail(tableTailGroup(kind)) + group, err := tableTailGroup(kind) + if err != nil { + return nil, err + } + tail, err := f.ancients.Tail(group) if err != nil { return nil, err } @@ -387,13 +391,11 @@ func (f *chainFreezer) Ancient(kind string, number uint64) ([]byte, error) { } // tableTailGroup returns the tail group identifier for a chain freezer table. -// Unknown tables resolve to the default block-data group, since the chain -// freezer's only prunable group today is bodies+receipts. -func tableTailGroup(kind string) string { +func tableTailGroup(kind string) (string, error) { if cfg, ok := chainFreezerTableConfigs[kind]; ok { - return cfg.tailGroup + return cfg.tailGroup, nil } - return ChainFreezerBlockDataGroup + return "", errUnknownTable } // ReadAncients executes an operation while preventing mutations to the freezer, diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go index 1b40ef06c8..0fc4f90011 100644 --- a/core/rawdb/freezer_test.go +++ b/core/rawdb/freezer_test.go @@ -371,6 +371,133 @@ func checkAncientCount(t *testing.T, f *Freezer, kind string, n uint64) { } } +// TestChainFreezerBALAlignment exercises the new-table alignment path: a chain +// freezer is first opened with the legacy table set (no BAL), populated with a +// few blocks and closed. It is then re-opened with the full chain freezer +// table set (which includes the BAL column). The expectation is that the BAL +// table is fast-forwarded to the existing head without disturbing the body / +// receipt tables, and that subsequent writes append cleanly across all tables. +func TestChainFreezerBALAlignment(t *testing.T) { + dir := t.TempDir() + + // Build a "legacy" subset of the chain freezer table set, omitting BAL. + legacyTables := make(map[string]freezerTableConfig) + for name, cfg := range chainFreezerTableConfigs { + if name == ChainFreezerBALTable { + continue + } + legacyTables[name] = cfg + } + + // First open: legacy config. Fill in `items` blocks of dummy data. + const items = uint64(10) + payload := bytes.Repeat([]byte{0xab}, 64) + + f, err := NewFreezer(dir, "", false, 2049, legacyTables) + if err != nil { + t.Fatalf("can't open legacy freezer: %v", err) + } + if _, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for i := uint64(0); i < items; i++ { + if err := op.AppendRaw(ChainFreezerHashTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerHeaderTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerBodiesTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerReceiptTable, i, payload); err != nil { + return err + } + } + return nil + }); err != nil { + t.Fatalf("legacy write failed: %v", err) + } + if got, _ := f.Ancients(); got != items { + t.Fatalf("legacy head: got %d, want %d", got, items) + } + require.NoError(t, f.Close()) + + // Re-open with the full chain freezer table set, which now includes BAL. + // repair() should detect the empty BAL table and fast-forward it to the + // existing head rather than truncating everyone down to zero. + f, err = NewFreezer(dir, "", false, 2049, chainFreezerTableConfigs) + if err != nil { + t.Fatalf("can't re-open freezer with BAL added: %v", err) + } + defer f.Close() + + // The head must be preserved. + if got, _ := f.Ancients(); got != items { + t.Fatalf("head after re-open: got %d, want %d", got, items) + } + // Existing data must still be readable in full. + for i := uint64(0); i < items; i++ { + for _, kind := range []string{ + ChainFreezerHashTable, ChainFreezerHeaderTable, + ChainFreezerBodiesTable, ChainFreezerReceiptTable, + } { + got, err := f.Ancient(kind, i) + if err != nil { + t.Fatalf("read %s[%d]: %v", kind, i, err) + } + if !bytes.Equal(got, payload) { + t.Fatalf("read %s[%d]: payload mismatch", kind, i) + } + } + } + // The block-data tail must be unchanged (no spurious tail bump). + if tail, err := f.Tail(ChainFreezerBlockDataGroup); err != nil || tail != 0 { + t.Fatalf("blockdata tail: got %d (err %v), want 0", tail, err) + } + // The BAL tail should equal the head — the table is empty but aligned. + if tail, err := f.Tail(ChainFreezerBALGroup); err != nil || tail != items { + t.Fatalf("BAL tail: got %d (err %v), want %d", tail, err, items) + } + // Reads to BAL for any pre-alignment block must report out-of-bounds. + for i := uint64(0); i < items; i++ { + if _, err := f.Ancient(ChainFreezerBALTable, i); err == nil { + t.Fatalf("reading BAL[%d] succeeded; want error (out of bounds)", i) + } + } + // A subsequent batch must append uniformly to every table, BAL included. + balPayload := []byte("real-bal") + if _, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + i := items + if err := op.AppendRaw(ChainFreezerHashTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerHeaderTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerBodiesTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerReceiptTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerBALTable, i, balPayload); err != nil { + return err + } + return nil + }); err != nil { + t.Fatalf("post-alignment write failed: %v", err) + } + if got, _ := f.Ancients(); got != items+1 { + t.Fatalf("head after post-alignment write: got %d, want %d", got, items+1) + } + got, err := f.Ancient(ChainFreezerBALTable, items) + if err != nil { + t.Fatalf("BAL[%d]: %v", items, err) + } + if !bytes.Equal(got, balPayload) { + t.Fatalf("BAL[%d]: got %x, want %x", items, got, balPayload) + } +} + func TestFreezerCloseSync(t *testing.T) { t.Parallel() f, _ := newFreezerForTesting(t, map[string]freezerTableConfig{"a": {noSnappy: true}, "b": {noSnappy: true}}) From 133ee263c881447d150e4755b146ce0fa0e4b5b3 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 15 May 2026 14:57:10 +0800 Subject: [PATCH 4/7] triedb, core: polish code --- core/rawdb/ancient_scheme.go | 24 ++++----- core/rawdb/freezer.go | 86 +++++++++++++++----------------- triedb/pathdb/disklayer.go | 2 +- triedb/pathdb/history.go | 14 ++---- triedb/pathdb/history_indexer.go | 2 +- triedb/pathdb/history_inspect.go | 4 +- triedb/pathdb/history_reader.go | 2 +- 7 files changed, 60 insertions(+), 74 deletions(-) diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go index d6097a9193..9ee8f9d3d2 100644 --- a/core/rawdb/ancient_scheme.go +++ b/core/rawdb/ancient_scheme.go @@ -90,16 +90,17 @@ const ( stateHistoryStorageData = "storage.data" ) -// StateHistoryTailGroup is the tail group shared by all state history tables. -const StateHistoryTailGroup = "history" +// DefaultHistoryGroup is the tail group shared by all state/trienode history +// tables with tail pruning enabled. +const DefaultHistoryGroup = "history" // stateFreezerTableConfigs configures the settings for tables in the state freezer. var stateFreezerTableConfigs = map[string]freezerTableConfig{ - stateHistoryMeta: {noSnappy: true, tailGroup: StateHistoryTailGroup}, - stateHistoryAccountIndex: {noSnappy: false, tailGroup: StateHistoryTailGroup}, - stateHistoryStorageIndex: {noSnappy: false, tailGroup: StateHistoryTailGroup}, - stateHistoryAccountData: {noSnappy: false, tailGroup: StateHistoryTailGroup}, - stateHistoryStorageData: {noSnappy: false, tailGroup: StateHistoryTailGroup}, + stateHistoryMeta: {noSnappy: true, tailGroup: DefaultHistoryGroup}, + stateHistoryAccountIndex: {noSnappy: false, tailGroup: DefaultHistoryGroup}, + stateHistoryStorageIndex: {noSnappy: false, tailGroup: DefaultHistoryGroup}, + stateHistoryAccountData: {noSnappy: false, tailGroup: DefaultHistoryGroup}, + stateHistoryStorageData: {noSnappy: false, tailGroup: DefaultHistoryGroup}, } const ( @@ -108,18 +109,15 @@ const ( trienodeHistoryValueSectionTable = "trienode.value" ) -// TrienodeHistoryTailGroup is the tail group shared by all trienode history tables. -const TrienodeHistoryTailGroup = "history" - // trienodeFreezerTableConfigs configures the settings for tables in the trienode freezer. var trienodeFreezerTableConfigs = map[string]freezerTableConfig{ - trienodeHistoryHeaderTable: {noSnappy: false, tailGroup: TrienodeHistoryTailGroup}, + trienodeHistoryHeaderTable: {noSnappy: false, tailGroup: DefaultHistoryGroup}, // Disable snappy compression to allow efficient partial read. - trienodeHistoryKeySectionTable: {noSnappy: true, tailGroup: TrienodeHistoryTailGroup}, + trienodeHistoryKeySectionTable: {noSnappy: true, tailGroup: DefaultHistoryGroup}, // Disable snappy compression to allow efficient partial read. - trienodeHistoryValueSectionTable: {noSnappy: true, tailGroup: TrienodeHistoryTailGroup}, + trienodeHistoryValueSectionTable: {noSnappy: true, tailGroup: DefaultHistoryGroup}, } // The list of identifiers of ancient stores. diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 7280bf9179..4fa613fbbf 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -59,7 +59,8 @@ const freezerTableSize = 2 * 1000 * 1000 * 1000 // - The in-order data ensures that disk reads are always optimized. type Freezer struct { datadir string - head atomic.Uint64 // Number of items stored (including items removed from tail) + head atomic.Uint64 // Number of items stored (including items removed from tail) + tails map[string]*atomic.Uint64 // Per-group tail cache, keyed by tail group name // This lock synchronizes writers and the truncate operation, as well as // the "atomic" (batched) read operations. @@ -117,6 +118,7 @@ func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize ui datadir: datadir, readonly: readonly, tables: make(map[string]*freezerTable), + tails: make(map[string]*atomic.Uint64), instanceLock: lock, } @@ -216,32 +218,18 @@ func (f *Freezer) Ancients() (uint64, error) { } // Tail returns the lowest accessible item index for the given tail group. -// All tables sharing this group must agree on the tail; an empty group name -// refers to non-prunable tables and always returns 0. +// All tables sharing this group agree on the tail; an empty group name +// refers to non-prunable tables and always returns 0. Unknown groups return +// an error. func (f *Freezer) Tail(group string) (uint64, error) { if group == "" { return 0, nil } - var ( - tail uint64 - found bool - ) - for _, table := range f.tables { - if table.config.tailGroup != group { - continue - } - h := table.itemHidden.Load() - if !found { - tail = h - found = true - } else if h != tail { - return 0, fmt.Errorf("inconsistent tail in group %q: %d vs %d", group, h, tail) - } - } - if !found { + tail, ok := f.tails[group] + if !ok { return 0, fmt.Errorf("unknown tail group: %q", group) } - return tail, nil + return tail.Load(), nil } // AncientSize returns the ancient size of the specified category. @@ -333,25 +321,14 @@ func (f *Freezer) TruncateTail(group string, tail uint64) (uint64, error) { if group == "" { return 0, errors.New("empty tail group") } + cached, ok := f.tails[group] + if !ok { + return 0, fmt.Errorf("unknown tail group: %q", group) + } f.writeLock.Lock() defer f.writeLock.Unlock() - var ( - prev uint64 - found bool - ) - for _, table := range f.tables { - if table.config.tailGroup != group { - continue - } - if !found { - prev = table.itemHidden.Load() - found = true - } - } - if !found { - return 0, fmt.Errorf("unknown tail group: %q", group) - } + prev := cached.Load() for _, table := range f.tables { if table.config.tailGroup != group { continue @@ -360,6 +337,9 @@ func (f *Freezer) TruncateTail(group string, tail uint64) (uint64, error) { return 0, err } } + if tail > prev { + cached.Store(tail) + } // Update the head if the requested tail exceeds the current head. if f.head.Load() < tail { f.head.Store(tail) @@ -388,11 +368,12 @@ func (f *Freezer) validate() error { return nil } var ( - head uint64 - headSet bool - groupTails = make(map[string]uint64) + head uint64 + headSet bool + tails = make(map[string]uint64) ) for kind, table := range f.tables { + // Validate the table head items := table.items.Load() if !headSet { head = items @@ -400,6 +381,7 @@ func (f *Freezer) validate() error { } else if items != head { return fmt.Errorf("freezer table %s has a differing head: %d != %d", kind, items, head) } + // Validate the table tail if table.config.tailGroup == "" { if table.itemHidden.Load() != 0 { return fmt.Errorf("non-prunable freezer table '%s' has a non-zero tail: %d", kind, table.itemHidden.Load()) @@ -407,15 +389,21 @@ func (f *Freezer) validate() error { continue } hidden := table.itemHidden.Load() - if t, ok := groupTails[table.config.tailGroup]; ok { + if t, ok := tails[table.config.tailGroup]; ok { if t != hidden { return fmt.Errorf("freezer table %s has differing tail in group %q: %d != %d", kind, table.config.tailGroup, hidden, t) } } else { - groupTails[table.config.tailGroup] = hidden + tails[table.config.tailGroup] = hidden } } f.head.Store(head) + + for group, tail := range tails { + counter := new(atomic.Uint64) + counter.Store(tail) + f.tails[group] = counter + } return nil } @@ -463,7 +451,7 @@ func (f *Freezer) repair() error { } // Per-group tail alignment: take the maximum tail in each group and apply // it to all members. Non-prunable tables must remain at tail 0. - groupTails := make(map[string]uint64) + tails := make(map[string]uint64) for kind, table := range f.tables { if table.config.tailGroup == "" { if table.itemHidden.Load() != 0 { @@ -472,18 +460,24 @@ func (f *Freezer) repair() error { continue } hidden := table.itemHidden.Load() - if t, ok := groupTails[table.config.tailGroup]; !ok || hidden > t { - groupTails[table.config.tailGroup] = hidden + if t, ok := tails[table.config.tailGroup]; !ok || hidden > t { + tails[table.config.tailGroup] = hidden } } for _, table := range f.tables { if table.config.tailGroup == "" { continue } - if err := table.truncateTail(groupTails[table.config.tailGroup]); err != nil { + if err := table.truncateTail(tails[table.config.tailGroup]); err != nil { return err } } f.head.Store(head) + + for group, tail := range tails { + counter := new(atomic.Uint64) + counter.Store(tail) + f.tails[group] = counter + } return nil } diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 47a882446a..8c0a751932 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -378,7 +378,7 @@ func (dl *diskLayer) writeHistory(typ historyType, diff *diffLayer) (bool, error if limit == 0 { return false, nil } - tail, err := freezer.Tail(rawdb.StateHistoryTailGroup) + tail, err := freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return false, err } // firstID = tail+1 diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go index 4668d6f3a4..55ec29e4f0 100644 --- a/triedb/pathdb/history.go +++ b/triedb/pathdb/history.go @@ -273,7 +273,7 @@ func truncateFromHead(store ethdb.AncientStore, typ historyType, nhead uint64) ( if err != nil { return 0, err } - otail, err := store.Tail(rawdb.StateHistoryTailGroup) + otail, err := store.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, err } @@ -303,13 +303,7 @@ func truncateFromTail(store ethdb.AncientStore, typ historyType, ntail uint64) ( if err != nil { return 0, err } - var group string - if typ == typeStateHistory { - group = rawdb.StateHistoryTailGroup - } else { - group = rawdb.TrienodeHistoryTailGroup - } - otail, err := store.Tail(group) + otail, err := store.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, err } @@ -321,7 +315,7 @@ func truncateFromTail(store ethdb.AncientStore, typ historyType, ntail uint64) ( if otail == ntail { return 0, nil } - otail, err = store.TruncateTail(group, ntail) + otail, err = store.TruncateTail(rawdb.DefaultHistoryGroup, ntail) if err != nil { return 0, err } @@ -436,7 +430,7 @@ func repairHistory(db ethdb.Database, isUBT bool, readOnly bool, stateID uint64, truncTo = min(truncTo, thead) } else { if thead == 0 { - _, err = trienodes.TruncateTail(rawdb.TrienodeHistoryTailGroup, stateID) + _, err = trienodes.TruncateTail(rawdb.DefaultHistoryGroup, stateID) if err != nil { return nil, nil, err } diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go index 873990b6b5..3789fae19b 100644 --- a/triedb/pathdb/history_indexer.go +++ b/triedb/pathdb/history_indexer.go @@ -542,7 +542,7 @@ func (i *indexIniter) run(recover bool) { // next returns the ID of the next state history to be indexed. func (i *indexIniter) next() (uint64, error) { - tail, err := i.freezer.Tail(rawdb.StateHistoryTailGroup) + tail, err := i.freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, err } diff --git a/triedb/pathdb/history_inspect.go b/triedb/pathdb/history_inspect.go index c956295368..8b5624f441 100644 --- a/triedb/pathdb/history_inspect.go +++ b/triedb/pathdb/history_inspect.go @@ -38,7 +38,7 @@ type HistoryStats struct { // sanitizeRange limits the given range to fit within the local history store. func sanitizeRange(start, end uint64, freezer ethdb.AncientReader) (uint64, uint64, error) { // Load the id of the first history object in local store. - tail, err := freezer.Tail(rawdb.StateHistoryTailGroup) + tail, err := freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, 0, err } @@ -133,7 +133,7 @@ func storageHistory(freezer ethdb.AncientReader, address common.Address, slot co // historyRange returns the block number range of local state histories. func historyRange(freezer ethdb.AncientReader) (uint64, uint64, error) { // Load the id of the first history object in local store. - tail, err := freezer.Tail(rawdb.StateHistoryTailGroup) + tail, err := freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, 0, err } diff --git a/triedb/pathdb/history_reader.go b/triedb/pathdb/history_reader.go index 51536c8aae..1652e412eb 100644 --- a/triedb/pathdb/history_reader.go +++ b/triedb/pathdb/history_reader.go @@ -470,7 +470,7 @@ func checkStateAvail(state stateIdent, exptyp historyType, freezer ethdb.Ancient return 0, fmt.Errorf("unsupported history type: %d, want: %v", toHistoryType(state.typ), exptyp) } // firstID = tail+1 - tail, err := freezer.Tail(rawdb.StateHistoryTailGroup) + tail, err := freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, err } From 40a23eafb86c050d4425bd55274cc5ff3e290dd4 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 15 May 2026 15:11:25 +0800 Subject: [PATCH 5/7] core/rawdb, triedb: polish code --- core/rawdb/freezer.go | 8 +++-- core/rawdb/freezer_memory.go | 55 ++++++++++++----------------- triedb/pathdb/history_state_test.go | 2 +- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 4fa613fbbf..5fa57bf6e1 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -329,6 +329,9 @@ func (f *Freezer) TruncateTail(group string, tail uint64) (uint64, error) { defer f.writeLock.Unlock() prev := cached.Load() + if prev >= tail { + return prev, nil + } for _, table := range f.tables { if table.config.tailGroup != group { continue @@ -337,9 +340,8 @@ func (f *Freezer) TruncateTail(group string, tail uint64) (uint64, error) { return 0, err } } - if tail > prev { - cached.Store(tail) - } + cached.Store(tail) + // Update the head if the requested tail exceeds the current head. if f.head.Load() < tail { f.head.Store(tail) diff --git a/core/rawdb/freezer_memory.go b/core/rawdb/freezer_memory.go index 1007275777..b2d8912ee2 100644 --- a/core/rawdb/freezer_memory.go +++ b/core/rawdb/freezer_memory.go @@ -228,6 +228,7 @@ func (b *memoryBatch) commit(freezer *MemoryFreezer) (items uint64, writeSize in // interface and can be used along with ephemeral key-value store. type MemoryFreezer struct { items uint64 // Number of items stored + tails map[string]uint64 // Per-group tail cache; access serialized by lock readonly bool // Flag if the freezer is only for reading lock sync.RWMutex // Lock to protect fields tables map[string]*memoryTable // Tables for storing everything @@ -236,14 +237,21 @@ type MemoryFreezer struct { // NewMemoryFreezer initializes an in-memory freezer instance. func NewMemoryFreezer(readonly bool, tableName map[string]freezerTableConfig) *MemoryFreezer { - tables := make(map[string]*memoryTable) + var ( + tables = make(map[string]*memoryTable) + tails = make(map[string]uint64) + ) for name, cfg := range tableName { tables[name] = newMemoryTable(name, cfg) + if cfg.tailGroup != "" { + tails[cfg.tailGroup] = 0 + } } return &MemoryFreezer{ writeBatch: newMemoryBatch(), readonly: readonly, tables: tables, + tails: tails, } } @@ -289,7 +297,7 @@ func (f *MemoryFreezer) Ancients() (uint64, error) { } // Tail returns the lowest accessible item index for the given tail group. -// All tables sharing the group must agree on the tail; an empty group name +// All tables sharing the group agree on the tail; an empty group name // refers to non-prunable tables and always returns 0. func (f *MemoryFreezer) Tail(group string) (uint64, error) { f.lock.RLock() @@ -298,22 +306,8 @@ func (f *MemoryFreezer) Tail(group string) (uint64, error) { if group == "" { return 0, nil } - var ( - tail uint64 - found bool - ) - for _, table := range f.tables { - if table.config.tailGroup != group { - continue - } - if !found { - tail = table.offset - found = true - } else if table.offset != tail { - return 0, fmt.Errorf("inconsistent tail in group %q: %d vs %d", group, table.offset, tail) - } - } - if !found { + tail, ok := f.tails[group] + if !ok { return 0, fmt.Errorf("unknown tail group: %q", group) } return tail, nil @@ -409,22 +403,13 @@ func (f *MemoryFreezer) TruncateTail(group string, tail uint64) (uint64, error) if group == "" { return 0, errors.New("empty tail group") } - var ( - prev uint64 - found bool - ) - for _, table := range f.tables { - if table.config.tailGroup != group { - continue - } - if !found { - prev = table.offset - found = true - } - } - if !found { + prev, ok := f.tails[group] + if !ok { return 0, fmt.Errorf("unknown tail group: %q", group) } + if prev >= tail { + return prev, nil + } for _, table := range f.tables { if table.config.tailGroup != group { continue @@ -433,6 +418,7 @@ func (f *MemoryFreezer) TruncateTail(group string, tail uint64) (uint64, error) return 0, err } } + f.tails[group] = tail if f.items < tail { f.items = tail } @@ -462,10 +448,15 @@ func (f *MemoryFreezer) Reset() error { defer f.lock.Unlock() tables := make(map[string]*memoryTable) + tails := make(map[string]uint64) for name, table := range f.tables { tables[name] = newMemoryTable(name, table.config) + if table.config.tailGroup != "" { + tails[table.config.tailGroup] = 0 + } } f.tables = tables + f.tails = tails f.items = 0 return nil } diff --git a/triedb/pathdb/history_state_test.go b/triedb/pathdb/history_state_test.go index b33777fd52..5c3026a571 100644 --- a/triedb/pathdb/history_state_test.go +++ b/triedb/pathdb/history_state_test.go @@ -237,7 +237,7 @@ func TestTruncateOutOfRange(t *testing.T) { // Ensure of-out-range truncations are rejected correctly. head, _ := freezer.Ancients() - tail, _ := freezer.Tail(rawdb.StateHistoryTailGroup) + tail, _ := freezer.Tail(rawdb.DefaultHistoryGroup) cases := []struct { mode int From cbbc94fbceabbba08a0463213c7fa6650903994d Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 20 May 2026 11:10:09 +0800 Subject: [PATCH 6/7] core/rawdb: address comments from jonny --- core/rawdb/chain_freezer.go | 2 ++ core/rawdb/freezer.go | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go index 729e616daf..4fe85d843b 100644 --- a/core/rawdb/chain_freezer.go +++ b/core/rawdb/chain_freezer.go @@ -386,6 +386,8 @@ func (f *chainFreezer) Ancient(kind string, number uint64) ([]byte, error) { return f.eradb.GetRawBody(number) case ChainFreezerReceiptTable: return f.eradb.GetRawReceipts(number) + case ChainFreezerBALTable: + return nil, errOutOfBounds } return nil, errUnknownTable } diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 5fa57bf6e1..d3dbffedd2 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -77,8 +77,8 @@ type Freezer struct { // data according to the given parameters. // // The 'tables' argument defines the freezer tables and their configuration. -// Each value is a freezerTableConfig specifying whether snappy compression is -// disabled (noSnappy) and whether the table is prunable (prunable). +// Each value is a freezerTableConfig describing whether Snappy compression +// is disabled (noSnappy) and which tail group the table belongs to. func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig) (*Freezer, error) { // Create the initial freezer object var ( From ff247428afefc5c5c78bffac0368cd48d3c6dc16 Mon Sep 17 00:00:00 2001 From: gary Date: Fri, 22 May 2026 13:45:40 +0800 Subject: [PATCH 7/7] core/rawdb: fix db inspect --- core/rawdb/ancient_utils.go | 66 +++++++++++++++++-------------------- core/rawdb/database.go | 4 +-- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/core/rawdb/ancient_utils.go b/core/rawdb/ancient_utils.go index 914ad9f6d8..32d5eeb90b 100644 --- a/core/rawdb/ancient_utils.go +++ b/core/rawdb/ancient_utils.go @@ -24,24 +24,23 @@ import ( "github.com/ethereum/go-ethereum/ethdb" ) -type tableSize struct { - name string - size common.StorageSize +type tableInfo struct { + name string + size common.StorageSize + count uint64 } // freezerInfo contains the basic information of the freezer. type freezerInfo struct { - name string // The identifier of freezer - head uint64 // The number of last stored item in the freezer - tail uint64 // The number of first stored item in the freezer - count uint64 // The number of stored items in the freezer - sizes []tableSize // The storage size per table + name string // The identifier of freezer + head uint64 // The number of last stored item in the freezer + tables []tableInfo // Per-table storage size and item count } // size returns the storage size of the entire freezer. func (info *freezerInfo) size() common.StorageSize { var total common.StorageSize - for _, table := range info.sizes { + for _, table := range info.tables { total += table.size } return total @@ -49,46 +48,41 @@ func (info *freezerInfo) size() common.StorageSize { func inspect(name string, order map[string]freezerTableConfig, reader ethdb.AncientReader) (freezerInfo, error) { info := freezerInfo{name: name} - for t := range order { - size, err := reader.AncientSize(t) - if err != nil { - return freezerInfo{}, err - } - info.sizes = append(info.sizes, tableSize{name: t, size: common.StorageSize(size)}) - } - // Retrieve the number of last stored item + + // Retrieve the number of last stored item. ancients, err := reader.Ancients() if err != nil { return freezerInfo{}, err } if ancients > 0 { info.head = ancients - 1 - } else { - info.head = 0 } - - // Retrieve the highest tail across all known tail groups. The inspected - // freezer info uses a single tail value for display, which corresponds to - // the most-pruned group. - groups := make(map[string]struct{}) + // Resolve per-group tails so each table can report its own item count. + groupTails := make(map[string]uint64) for _, cfg := range order { - if cfg.tailGroup != "" { - groups[cfg.tailGroup] = struct{}{} + if cfg.tailGroup == "" { + continue } - } - for g := range groups { - t, err := reader.Tail(g) + if _, ok := groupTails[cfg.tailGroup]; ok { + continue + } + t, err := reader.Tail(cfg.tailGroup) if err != nil { return freezerInfo{}, err } - if t > info.tail { - info.tail = t - } + groupTails[cfg.tailGroup] = t } - if ancients == 0 { - info.count = 0 - } else { - info.count = info.head - info.tail + 1 + for t, cfg := range order { + size, err := reader.AncientSize(t) + if err != nil { + return freezerInfo{}, err + } + var count uint64 + if ancients > 0 { + tail := groupTails[cfg.tailGroup] // 0 for non-prunable tables + count = ancients - tail + } + info.tables = append(info.tables, tableInfo{name: t, size: common.StorageSize(size), count: count}) } return info, nil } diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 9e49ee23dd..57abcdb25d 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -658,12 +658,12 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { return err } for _, ancient := range ancients { - for _, table := range ancient.sizes { + for _, table := range ancient.tables { stats = append(stats, []string{ fmt.Sprintf("Ancient store (%s)", strings.Title(ancient.name)), strings.Title(table.name), table.size.String(), - fmt.Sprintf("%d", ancient.count), + fmt.Sprintf("%d", table.count), }) } total.Add(uint64(ancient.size()))