core/rawdb: fix WriteAncientHeaderChain and add tests

This commit is contained in:
Gary Rong 2026-05-15 13:44:20 +08:00
parent da877346c6
commit e67fa13e34
4 changed files with 183 additions and 6 deletions

View file

@ -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
})

View file

@ -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()

View file

@ -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,

View file

@ -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}})