diff --git a/cmd/bal-scan/main.go b/cmd/bal-scan/main.go new file mode 100644 index 00000000000..1a9b173ca74 --- /dev/null +++ b/cmd/bal-scan/main.go @@ -0,0 +1,77 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// One-shot dev tool: scan the kv.BlockAccessList table and print one line per +// entry whose block number falls in [low, high]. Used to debug what BAL bytes +// are actually persisted (e.g. after the BAL downloader fetches from peers, +// to confirm the writes survived prune/compaction): +// +// bal-scan [low] [high] +// +// low/high default to 0 and ^uint64(0). Requires the MDBX to be unlocked +// (erigon stopped) for read access. + +package main + +import ( + "context" + "encoding/binary" + "fmt" + "os" + "strconv" + + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/kv/dbcfg" + "github.com/erigontech/erigon/db/kv/mdbx" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: bal-scan [low] [high]") + os.Exit(2) + } + path := os.Args[1] + var low uint64 = 0 + var high uint64 = ^uint64(0) + if len(os.Args) > 2 { + low, _ = strconv.ParseUint(os.Args[2], 10, 64) + } + if len(os.Args) > 3 { + high, _ = strconv.ParseUint(os.Args[3], 10, 64) + } + + logger := log.New() + db, err := mdbx.New(kv.Label(dbcfg.ChainDB), logger).Path(path).Readonly(true).Accede(true).Open(context.Background()) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + defer db.Close() + + count := 0 + matched := 0 + err = db.View(context.Background(), func(tx kv.Tx) error { + c, err := tx.Cursor(kv.BlockAccessList) + if err != nil { + return err + } + defer c.Close() + for k, v, err := c.First(); k != nil; k, v, err = c.Next() { + if err != nil { + return err + } + if len(k) < 8 { + continue + } + n := binary.BigEndian.Uint64(k[:8]) + if n >= low && n <= high { + fmt.Printf("block=%d hash=%x bal_len=%d\n", n, k[8:], len(v)) + matched++ + } + count++ + } + return nil + }) + fmt.Fprintf(os.Stderr, "total %d entries; matched %d in [%d,%d]; err=%v\n", count, matched, low, high, err) +} diff --git a/cmd/bal-test/main.go b/cmd/bal-test/main.go new file mode 100644 index 00000000000..78217b7123d --- /dev/null +++ b/cmd/bal-test/main.go @@ -0,0 +1,260 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// One-shot dev tool: dump / delete / compare BlockAccessList rawdb entries +// for a list of block numbers. Used to drive the eth/71 BAL downloader test: +// +// bal-test dump --datadir= --blocks=N1,N2,... > truth.json +// bal-test delete --datadir= --blocks=N1,N2,... +// # (restart erigon, wait for BALDownloader to refetch) +// bal-test compare --datadir= --blocks=N1,N2,... --truth=truth.json +// +// Requires the chaindata MDBX to be unlocked (erigon stopped) for delete. + +package main + +import ( + "context" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/kv/dbcfg" + "github.com/erigontech/erigon/db/kv/dbutils" + "github.com/erigontech/erigon/db/kv/mdbx" + "github.com/erigontech/erigon/db/rawdb" +) + +type entry struct { + Number uint64 `json:"number"` + Hash string `json:"hash"` + BAL string `json:"bal"` // hex-encoded +} + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: bal-test [flags]") + os.Exit(2) + } + cmd := os.Args[1] + fs := flag.NewFlagSet(cmd, flag.ExitOnError) + datadir := fs.String("datadir", "", "erigon datadir (must be unlocked for delete)") + blocksStr := fs.String("blocks", "", "comma-separated block numbers (e.g. 100,101,102)") + truth := fs.String("truth", "", "(compare only) path to truth.json from dump") + _ = fs.Parse(os.Args[2:]) + + if *datadir == "" || *blocksStr == "" { + fmt.Fprintln(os.Stderr, "--datadir and --blocks are required") + os.Exit(2) + } + blocks, err := parseBlocks(*blocksStr) + if err != nil { + fmt.Fprintln(os.Stderr, "bad --blocks:", err) + os.Exit(2) + } + + logger := log.New() + chainDB := *datadir + "/chaindata" + + switch cmd { + case "dump": + readOnly := true + entries, err := readEntries(chainDB, blocks, readOnly, logger) + if err != nil { + fmt.Fprintln(os.Stderr, "dump failed:", err) + os.Exit(1) + } + _ = json.NewEncoder(os.Stdout).Encode(entries) + case "delete": + if err := deleteEntries(chainDB, blocks, logger); err != nil { + fmt.Fprintln(os.Stderr, "delete failed:", err) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "deleted %d block(s)\n", len(blocks)) + case "compare": + if *truth == "" { + fmt.Fprintln(os.Stderr, "--truth required") + os.Exit(2) + } + if err := compare(chainDB, blocks, *truth, logger); err != nil { + fmt.Fprintln(os.Stderr, "compare failed:", err) + os.Exit(1) + } + default: + fmt.Fprintln(os.Stderr, "unknown subcommand:", cmd) + os.Exit(2) + } +} + +func parseBlocks(s string) ([]uint64, error) { + out := []uint64{} + for _, p := range strings.Split(s, ",") { + p = strings.TrimSpace(p) + if p == "" { + continue + } + n, err := strconv.ParseUint(p, 10, 64) + if err != nil { + return nil, fmt.Errorf("%q: %w", p, err) + } + out = append(out, n) + } + return out, nil +} + +func openDB(path string, readOnly bool, logger log.Logger) (kv.RwDB, error) { + b := mdbx.New(kv.Label(dbcfg.ChainDB), logger).Path(path) + if readOnly { + b = b.Readonly(true).Accede(true) + } else { + b = b.Accede(true) + } + return b.Open(context.Background()) +} + +func readEntries(path string, blocks []uint64, readOnly bool, logger log.Logger) ([]entry, error) { + db, err := openDB(path, readOnly, logger) + if err != nil { + return nil, fmt.Errorf("open db: %w", err) + } + defer db.Close() + + out := make([]entry, 0, len(blocks)) + err = db.View(context.Background(), func(tx kv.Tx) error { + for _, n := range blocks { + hash, err := canonicalHash(tx, n) + if err != nil { + return fmt.Errorf("block %d: %w", n, err) + } + if hash == (common.Hash{}) { + out = append(out, entry{Number: n}) + continue + } + bal, err := rawdb.ReadBlockAccessListBytes(tx, hash, n) + if err != nil { + return fmt.Errorf("read BAL block %d: %w", n, err) + } + out = append(out, entry{ + Number: n, + Hash: "0x" + hex.EncodeToString(hash[:]), + BAL: "0x" + hex.EncodeToString(bal), + }) + } + return nil + }) + return out, err +} + +func deleteEntries(path string, blocks []uint64, logger log.Logger) error { + db, err := openDB(path, false, logger) + if err != nil { + return fmt.Errorf("open db: %w", err) + } + defer db.Close() + + return db.Update(context.Background(), func(tx kv.RwTx) error { + for _, n := range blocks { + hash, err := canonicalHash(tx, n) + if err != nil { + return fmt.Errorf("block %d: %w", n, err) + } + if hash == (common.Hash{}) { + fmt.Fprintf(os.Stderr, "block %d: no canonical hash, skip\n", n) + continue + } + key := dbutils.BlockBodyKey(n, hash) + if err := tx.Delete(kv.BlockAccessList, key); err != nil { + return fmt.Errorf("delete block %d: %w", n, err) + } + fmt.Fprintf(os.Stderr, "deleted BAL for block %d hash %x\n", n, hash[:8]) + } + return nil + }) +} + +func compare(path string, blocks []uint64, truthPath string, logger log.Logger) error { + tdata, err := os.ReadFile(truthPath) + if err != nil { + return fmt.Errorf("read truth: %w", err) + } + var truthEntries []entry + if err := json.Unmarshal(tdata, &truthEntries); err != nil { + return fmt.Errorf("parse truth: %w", err) + } + truthBy := map[uint64]entry{} + for _, e := range truthEntries { + truthBy[e.Number] = e + } + + current, err := readEntries(path, blocks, true, logger) + if err != nil { + return err + } + + matched := 0 + missing := 0 + mismatch := 0 + for _, c := range current { + t, ok := truthBy[c.Number] + if !ok { + fmt.Printf("block %d: NOT IN TRUTH\n", c.Number) + continue + } + if c.BAL == "0x" { + fmt.Printf("block %d: still missing in current rawdb\n", c.Number) + missing++ + continue + } + if c.BAL == t.BAL && c.Hash == t.Hash { + fmt.Printf("block %d: MATCH (%d bytes)\n", c.Number, (len(c.BAL)-2)/2) + matched++ + continue + } + fmt.Printf("block %d: MISMATCH\n truth %d bytes (sha %s...)\n current %d bytes (sha %s...)\n", + c.Number, + (len(t.BAL)-2)/2, shortSha(t.BAL), + (len(c.BAL)-2)/2, shortSha(c.BAL)) + mismatch++ + } + fmt.Printf("\nresult: %d matched, %d still missing, %d mismatch\n", matched, missing, mismatch) + if mismatch > 0 { + return fmt.Errorf("byte mismatch on %d block(s)", mismatch) + } + return nil +} + +func shortSha(hexStr string) string { + if len(hexStr) < 18 { + return hexStr + } + return hexStr[:18] +} + +func canonicalHash(tx kv.Tx, number uint64) (common.Hash, error) { + v, err := tx.GetOne(kv.HeaderCanonical, encodeBE(number)) + if err != nil { + return common.Hash{}, err + } + if len(v) != 32 { + return common.Hash{}, nil + } + var h common.Hash + copy(h[:], v) + return h, nil +} + +func encodeBE(n uint64) []byte { + b := make([]byte, 8) + for i := 7; i >= 0; i-- { + b[i] = byte(n) + n >>= 8 + } + return b +} diff --git a/rpc/jsonrpc/debug_api.go b/rpc/jsonrpc/debug_api.go index 51a45538063..88545cde318 100644 --- a/rpc/jsonrpc/debug_api.go +++ b/rpc/jsonrpc/debug_api.go @@ -63,6 +63,7 @@ type PrivateDebugAPI interface { AccountAt(ctx context.Context, blockHash common.Hash, txIndex uint64, account common.Address) (*AccountResult, error) GetRawHeader(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) GetRawBlock(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) + GetRawBlockAccessList(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) GetRawReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]hexutil.Bytes, error) GetBadBlocks(ctx context.Context) ([]map[string]any, error) GetRawTransaction(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) @@ -667,6 +668,31 @@ func (api *DebugAPIImpl) GetRawBlock(ctx context.Context, blockNrOrHash rpc.Bloc return rlp.EncodeToBytes(block) } +// GetRawBlockAccessList implements debug_getRawBlockAccessList — returns the +// raw RLP-encoded BlockAccessList bytes (EIP-7928) that this node has stored +// for the given block. Returns nil if no BAL is recorded (pre-Amsterdam, or +// post-Amsterdam but not yet downloaded). The bytes returned are exactly what +// the server-side eth/71 GetBlockAccessLists handler would send to a peer. +func (api *DebugAPIImpl) GetRawBlockAccessList(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { + tx, err := api.db.BeginTemporalRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + n, h, _, err := rpchelper.GetBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters) + if err != nil { + if errors.As(err, &rpc.BlockNotFoundErr{}) { + return nil, nil + } + return nil, err + } + bal, err := rawdb.ReadBlockAccessListBytes(tx, h, n) + if err != nil { + return nil, fmt.Errorf("read block access list: %w", err) + } + return bal, nil +} + // GetRawReceipts implements debug_getRawReceipts - retrieves and returns an array of EIP-2718 binary-encoded receipts of a single block func (api *DebugAPIImpl) GetRawReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]hexutil.Bytes, error) { tx, err := api.db.BeginTemporalRo(ctx)