diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 26899fbe73b..9ed0fc356b1 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -306,6 +306,13 @@ func (f *Freezer) TruncateHead(items uint64) (uint64, error) { return 0, err } } + // Truncating a table that holds nothing below the new head realigns its tail + // down as well, so pull the cached group tails along with it. + for _, cached := range f.tails { + if cached.Load() > items { + cached.Store(items) + } + } f.head.Store(items) return oitems, nil } diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index d18b75c2f71..1868d14c760 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -615,9 +615,11 @@ func (t *freezerTable) truncateHead(items uint64) error { hidden := t.itemHidden.Load() if items < hidden { - if existing == hidden { - // Empty table means that it is newly added. Its tail would be - // at the head, so we have to align the table down to the new head. + // A table whose tail sits exactly at its first stored item has never had + // anything pruned away: its tail is the alignment point assigned when the + // table was added to an existing store. Realigning it down to the new head + // discards nothing that was not already being discarded. + if t.itemOffset.Load() == hidden { return t.resetTo(items) } return errors.New("truncation below tail") diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go index 2197a297723..59897512d9a 100644 --- a/core/rawdb/freezer_test.go +++ b/core/rawdb/freezer_test.go @@ -496,6 +496,28 @@ func TestChainFreezerBALAlignment(t *testing.T) { if !bytes.Equal(got, balPayload) { t.Fatalf("BAL[%d]: got %x, want %x", items, got, balPayload) } + // Rewinding below the aligned tail must be permitted: the BAL table holds + // nothing down there, so dropping it is lossless. Rejecting it would abort + // the rewind, and because the other tables are truncated first, the freezer + // would be left inconsistent and fail to open again. + rewind := items / 2 + if _, err := f.TruncateHead(rewind); err != nil { + t.Fatalf("truncate head to %d: %v", rewind, err) + } + if tail, err := f.Tail(ChainFreezerBALGroup); err != nil || tail != rewind { + t.Fatalf("BAL tail after rewind: got %d (err %v), want %d", tail, err, rewind) + } + require.NoError(t, f.Close()) + + reopened, err := NewFreezer(dir, "", false, 2049, chainFreezerTableConfigs) + if err != nil { + t.Fatalf("can't re-open freezer after rewind: %v", err) + } + defer reopened.Close() + + if got, _ := reopened.Ancients(); got != rewind { + t.Fatalf("head after re-open: got %d, want %d", got, rewind) + } } func TestFreezerCloseSync(t *testing.T) {