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