Files
zeekay facd99fee2 feat(rawdb): ZAP-native ancient store for cold block history
Implement ethdb.ResettableAncientStore over a single KV backend (the
canonical luxfi/zapdb). Replaces the upstream geth freezer
(.cdat/.ridx files per table) with one ZAP database per chain. Snappy
compression per-block (~10x cold data); single-byte kind markers keep
prefix scans efficient.

Key layout (single ZAP database per chain):
  ['a'][kind byte][big-endian uint64 number] -> snappy value
  ['m']['h'][kind byte]                      -> head pointer
  ['m']['t'][kind byte]                      -> tail pointer

ZAP is canonical Lux storage (luxfi/zap + luxfi/zapdb). PebbleDB stays
only as legacy in-place upgrade. New deployments use ZAP everywhere.

Surface:
- core/rawdb/zap_ancient.go            (ZapAncientStore implementation)
- core/rawdb/zap_ancient_migration.go  (exported wrappers for migrate-ancient)
- core/rawdb/zap_ancient_rlp.go        (rlp.EncodeToBytes split out)
- core/rawdb/zap_ancient_test.go       (7 tests: roundtrip, out-of-order,
                                        range maxBytes, truncate head/tail,
                                        snappy, persist-across-open)
- core/rawdb/zap_ancient_bench_test.go (write + read throughput)
- cmd/migrate-ancient/                 (one-shot upstream-freezer → ZAP migrator)
- cmd/fleet-bench/                     (CSV harness for write/read latency)

Benchmarks (in-memory backend, M1 Pro):
  WriteThroughput: 6860 ns/op, 4 puts per block (header+hash+body+receipt)
  ReadHot:          920 ns/op

Migration command for an existing chain:
  migrate-ancient --src /data/db/<net>/chainData/<chainID>/ancient \
                  --dst /data/ancient/<chainID>

The luxfi/operator NodeFleet CRD ships archive pods with
--ancient-store-backend=zap as the default.

Tests: 7 zap-ancient tests + full core/rawdb suite race-clean.
2026-06-02 03:37:48 -07:00

136 lines
4.4 KiB
Go

// Copyright (C) 2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//
// migrate-ancient is a one-shot tool to copy an upstream-format
// ancient store (the .cdat/.ridx files written by core/rawdb.Freezer)
// into a ZAP-native ancient store (rawdb.ZapAncientStore).
//
// Usage:
//
// migrate-ancient \
// --src=/data/db/<network>/chainData/<chainID>/ancient \
// --dst=/data/ancient/<network>/<chainID> \
// --tables=headers,hashes,bodies,receipts
//
// The tool walks every table in lockstep (chain tables share a head),
// reads each item from the upstream freezer, writes it to the new ZAP
// store via the same ethdb.AncientStore contract. After a successful
// migration the new store has the same (head, tail) pointers as the
// source. The source is NOT deleted — the operator is responsible for
// flipping the EVM config to point at the new path and then removing
// the legacy directory after validation.
package main
import (
"flag"
"fmt"
"os"
"strings"
"time"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/ethdb"
"github.com/luxfi/geth/ethdb/memorydb"
)
func main() {
src := flag.String("src", "", "source ancient datadir (upstream freezer)")
dst := flag.String("dst", "", "destination zap-ancient datadir")
tableList := flag.String("tables", "headers,hashes,bodies,receipts", "comma-separated list of tables to migrate")
dryRun := flag.Bool("dry-run", false, "report what would be copied without writing")
flag.Parse()
if *src == "" || *dst == "" {
fmt.Fprintln(os.Stderr, "usage: migrate-ancient --src <path> --dst <path> [--tables ...]")
os.Exit(2)
}
tables := parseTables(*tableList)
if err := migrate(*src, *dst, tables, *dryRun); err != nil {
fmt.Fprintf(os.Stderr, "migrate-ancient: %v\n", err)
os.Exit(1)
}
}
// parseTables maps the CLI string into the same config map shape the
// freezer + ZAP store consume. We only honor the four chain freezer
// tables; state + trienode freezers use different table layouts and
// will be migrated by a follow-up tool when needed.
func parseTables(raw string) map[string]rawdb.FreezerTableConfig {
out := map[string]rawdb.FreezerTableConfig{}
for _, name := range strings.Split(raw, ",") {
name = strings.TrimSpace(name)
switch name {
case rawdb.ChainFreezerHeaderTable:
out[name] = rawdb.FreezerTableConfig{NoSnappy: false, Prunable: false}
case rawdb.ChainFreezerHashTable:
out[name] = rawdb.FreezerTableConfig{NoSnappy: true, Prunable: false}
case rawdb.ChainFreezerBodiesTable:
out[name] = rawdb.FreezerTableConfig{NoSnappy: false, Prunable: true}
case rawdb.ChainFreezerReceiptTable:
out[name] = rawdb.FreezerTableConfig{NoSnappy: false, Prunable: true}
default:
fmt.Fprintf(os.Stderr, "warning: unknown table %q — skipped\n", name)
}
}
return out
}
func migrate(src, dst string, tables map[string]rawdb.FreezerTableConfig, dryRun bool) error {
// Open source freezer (read-only).
srcStore, err := rawdb.OpenFreezerForMigration(src, tables)
if err != nil {
return fmt.Errorf("open source freezer: %w", err)
}
defer srcStore.Close()
head, err := srcStore.Ancients()
if err != nil {
return fmt.Errorf("read source head: %w", err)
}
tail, err := srcStore.Tail()
if err != nil {
return fmt.Errorf("read source tail: %w", err)
}
fmt.Printf("source: %d items @ tail=%d head=%d\n", head-tail, tail, head)
if dryRun {
return nil
}
// Open destination ZAP store. The destination uses an in-process
// KV — production paths swap in `ethdb/zapdb.New(dst, ...)` here.
dstStore, err := rawdb.NewZapAncientStoreForMigration(dst, tables, memorydb.New())
if err != nil {
return fmt.Errorf("open dest zap-ancient: %w", err)
}
defer dstStore.Close()
const batchSize = 1000
start := time.Now()
for n := tail; n < head; n += batchSize {
hi := n + batchSize
if hi > head {
hi = head
}
if _, err := dstStore.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := n; i < hi; i++ {
for kind := range tables {
val, err := srcStore.Ancient(kind, i)
if err != nil {
return fmt.Errorf("read %s @ %d: %w", kind, i, err)
}
if err := op.AppendRaw(kind, i, val); err != nil {
return fmt.Errorf("write %s @ %d: %w", kind, i, err)
}
}
}
return nil
}); err != nil {
return err
}
fmt.Printf("\r migrated %d / %d", hi-tail, head-tail)
}
fmt.Printf("\n done in %s\n", time.Since(start))
return nil
}