Files
zeekay 3183dab89c Make the whole module build and test again after the port
go list ./... returned nothing, so no package here could be built or tested and
the failure was invisible: one directory held a Go test beside fifteen .cpp
files, which makes it a Go package the toolchain then refuses. The generator
moves to the golden/ subdirectory the other two C++ chains already use.

That silence was hiding a package that does not compile. host/ calls
EnsureIdentity and identity.go defines LoadIdentity — a rename that happened on
one side — and it has no importers, because the Go host is cmd/luxd now. Deleted
rather than repaired: the thing it was a stand-in for arrived.

The operator tools came across too (backup, blscheck, ceremony, config,
genesishash, keytoaddr, pqkeygen, repair-proposervm, wire) plus benchmarks and
examples, so the package set now differs from the retired repo by exactly one
name: main/ is cmd/luxd.

Two tests were right and are now satisfied rather than silenced. The purity test
looked for ./main; it looks where the daemon lives. The chain-address test found
conformance/dex spelling /v1/chain by hand — but that is its own module on
purpose, since it links an EVM ABI and a signer the node must not carry, so it
cannot call Chain. The rule is about this module, and the walk now says so.
2026-09-07 17:16:26 -07:00

353 lines
14 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command repair-proposervm is a fail-closed surgical tool to reconcile a
// proposervm chain-state DB onto a known-canonical outer block at a single height.
//
// The condition it repairs: a node locally accepted a sub-quorum proposervm
// envelope A while the network finalized the supermajority sibling B, which wraps
// the IDENTICAL inner EVM block. The two siblings differ ONLY in the proposervm
// outer envelope; the inner EVM state is byte-identical, so nothing about the EVM
// diverged. Such a node re-seeds finality from its persisted proposervm
// lastAccepted=A on restart and then fatals on B's cert as equivocation. This tool
// swaps the persisted record of `height` from A to the canonical B, leaving the
// inner EVM completely untouched (no EVM rollback).
//
// It is NOT a blind hex edit: it opens the exact same typed proposervm state
// (luxfi/node/vms/proposervm/state) over the exact same nested keyspace luxd uses
// (chainID -> "vm" -> "proposervm" -> versiondb -> chain/block/height), and writes
// via the state's own PutBlock / SetBlockIDAtHeight / SetLastAccepted so the on-disk
// bytes are identical to what luxd itself wrote for B on the canonical node.
//
// proposervm invariant honored: proLastAcceptedHeight must never be < the inner VM's
// last-accepted height (vm.repairAcceptedChainByHeight). The inner EVM is at `height`
// (it accepted the shared inner block under A), so the recovery target is the
// canonical block AT `height` (B), never height-1 — keeping outer==inner height.
//
// Modes:
//
// inspect : read-only. Print lastAccepted, height index at H and H-1, and the
// outer block currently recorded at H.
// export : read-only. Read the outer block recorded at H and write its raw
// stateless bytes to --block-file (run against a canonical node's DB).
// repair : read-write, fail-closed. Parse --block-file (canonical B), assert it is
// the expected block, assert the DB is in the expected bad state (lastAccepted
// and heightIndex[H] both == the expected sub-quorum block A, and B's parent
// == heightIndex[H-1]); then PutBlock(B), SetBlockIDAtHeight(H,B),
// SetLastAccepted(B), Commit. Idempotent: if already on B, it no-ops.
//
// The DB uses an exclusive LOCK; luxd MUST be stopped on the target before `repair`.
package main
import (
"errors"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/luxfi/database"
databasefactory "github.com/luxfi/database/factory"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/metric"
"github.com/luxfi/node/vms/proposervm/block"
"github.com/luxfi/node/vms/proposervm/state"
)
// The proposervm nests its state under these fixed prefixes inside the chain DB.
// chainDB = prefixdb(chainID[:], baseDB) [chains.ChainDBManager.GetDatabase]
// vmDB = prefixdb("vm", chainDB) [chains.ChainDBManager.GetVMDatabase / VMDBPrefix]
// ppvmDB = versiondb(prefixdb("proposervm", vmDB)) [proposervm.VM.Initialize dbPrefix]
// state.New(ppvmDB) -> "chain"/"block"/"height" sub-prefixes
var (
vmDBPrefix = []byte("vm")
proposervmDBPrefix = []byte("proposervm")
)
var (
dbPath string
dbType string
chainIDStr string
height uint64
blockFile string
expectBlockID string // canonical B (target)
expectCurID string // sub-quorum A (the bad state we expect to overwrite)
yes bool
)
func main() {
root := &cobra.Command{
Use: "repair-proposervm",
Short: "Surgical, fail-closed reconcile of a proposervm chain-state onto a canonical outer block at one height",
}
root.PersistentFlags().StringVar(&dbPath, "db-path", "", "zapdb root (e.g. /data/db/mainnet/db) (required)")
root.PersistentFlags().StringVar(&dbType, "db-type", "zapdb", "database type")
root.PersistentFlags().StringVar(&chainIDStr, "chain-id", "2wRdZGeca1qkxzNCq88NWDF5nJ5A9o623vRJKd3FsjRYvuVvvt", "blockchain ID (proposervm chain)")
root.PersistentFlags().Uint64Var(&height, "height", 1082814, "contested height")
root.MarkPersistentFlagRequired("db-path")
inspect := &cobra.Command{Use: "inspect", Short: "read-only: print proposervm finality state at the height", RunE: runInspect}
export := &cobra.Command{Use: "export", Short: "read-only: write the outer block recorded at the height to --block-file", RunE: runExport}
export.Flags().StringVar(&blockFile, "block-file", "", "output file for the canonical outer block bytes (required)")
export.MarkFlagRequired("block-file")
probe := &cobra.Command{Use: "probe", Short: "read-only: look up an arbitrary block by ID (is it present in the store?)", RunE: runProbe}
probe.Flags().StringVar(&expectBlockID, "block-id", "", "block ID to look up (required)")
probe.MarkFlagRequired("block-id")
dump := &cobra.Command{Use: "dump", Short: "read-only: write an arbitrary block (by ID) raw stateless bytes to --block-file", RunE: runDump}
dump.Flags().StringVar(&expectBlockID, "block-id", "wDMUyGyaKcC2Vng8i8ngU5f83XEtHZx5hqCSe5tMTwLAPagmo", "block ID to dump (canonical B)")
dump.Flags().StringVar(&blockFile, "block-file", "", "output file for the block bytes (required)")
dump.MarkFlagRequired("block-file")
repair := &cobra.Command{Use: "repair", Short: "fail-closed: swap height's outer block from A to canonical B", RunE: runRepair}
repair.Flags().StringVar(&blockFile, "block-file", "", "canonical outer block (B) bytes, from `export` (required)")
repair.Flags().StringVar(&expectBlockID, "expect-block", "wDMUyGyaKcC2Vng8i8ngU5f83XEtHZx5hqCSe5tMTwLAPagmo", "expected canonical block ID (B)")
repair.Flags().StringVar(&expectCurID, "expect-current", "2U2pR3DHCNEFDLnMq2uraNVkThRWgETDd468hR26yGHBQuAnNy", "expected current sub-quorum block ID (A) to be overwritten")
repair.Flags().BoolVar(&yes, "yes", false, "confirm the write")
repair.MarkFlagRequired("block-file")
root.AddCommand(inspect, export, probe, dump, repair)
if err := root.Execute(); err != nil {
fmt.Fprintln(os.Stderr, "ERROR:", err)
os.Exit(1)
}
}
// openState opens the proposervm typed state over the exact nested keyspace luxd uses.
// Returns the state, the proposervm versiondb (for Commit), and the base DB (for Close).
func openState(readOnly bool) (state.State, *versiondb.Database, database.Database, ids.ID, error) {
chainID, err := ids.FromString(chainIDStr)
if err != nil {
return nil, nil, nil, ids.Empty, fmt.Errorf("bad chain-id: %w", err)
}
logger := log.New("cmd", "repair-proposervm")
gatherer := metric.NewRegistry()
base, err := databasefactory.New(dbType, dbPath, readOnly, nil, gatherer, logger, "repair", "db")
if err != nil {
return nil, nil, nil, ids.Empty, fmt.Errorf("open db %q: %w", dbPath, err)
}
chainDB := prefixdb.New(chainID[:], base)
vmDB := prefixdb.New(vmDBPrefix, chainDB)
ppvmDB := versiondb.New(prefixdb.New(proposervmDBPrefix, vmDB))
return state.New(ppvmDB), ppvmDB, base, chainID, nil
}
func idAt(st state.State, h uint64) string {
id, err := st.GetBlockIDAtHeight(h)
if errors.Is(err, database.ErrNotFound) {
return "<none>"
}
if err != nil {
return "<err:" + err.Error() + ">"
}
return id.String()
}
func runInspect(_ *cobra.Command, _ []string) error {
st, _, base, _, err := openState(true)
if err != nil {
return err
}
defer base.Close()
la, err := st.GetLastAccepted()
if err != nil {
return fmt.Errorf("GetLastAccepted: %w", err)
}
fmt.Printf("proposervm lastAccepted = %s\n", la)
fmt.Printf("heightIndex[%d] = %s\n", height, idAt(st, height))
fmt.Printf("heightIndex[%d] = %s\n", height-1, idAt(st, height-1))
if id, err := st.GetBlockIDAtHeight(height); err == nil {
if blk, err := st.GetBlock(id); err == nil {
fmt.Printf("block@%d: id=%s parent=%s bytes=%d\n", height, blk.ID(), blk.ParentID(), len(blk.Bytes()))
}
}
return nil
}
func runProbe(_ *cobra.Command, _ []string) error {
want, err := ids.FromString(expectBlockID)
if err != nil {
return fmt.Errorf("bad --block-id: %w", err)
}
// RW so Badger replays its WAL: a verified/built-but-unflushed block may live
// only in the memtable. Disposable copy only.
st, _, base, _, err := openState(false)
if err != nil {
return err
}
defer base.Close()
blk, err := st.GetBlock(want)
if errors.Is(err, database.ErrNotFound) {
fmt.Printf("NOT-PRESENT: block %s is not in this store\n", want)
return nil
}
if err != nil {
return fmt.Errorf("GetBlock(%s): %w", want, err)
}
fmt.Printf("PRESENT: id=%s parent=%s bytes=%d\n", blk.ID(), blk.ParentID(), len(blk.Bytes()))
return nil
}
func runDump(_ *cobra.Command, _ []string) error {
want, err := ids.FromString(expectBlockID)
if err != nil {
return fmt.Errorf("bad --block-id: %w", err)
}
// RW so Badger replays its WAL: a verified-but-unflushed block may live only in
// the memtable. We never call a state WRITER here (read + write output file only),
// and this runs against an idle (luxd-stopped) pod DB; the contested sibling B was
// verified by this node when it saw the conflicting cert, so it is present by ID.
st, _, base, _, err := openState(false)
if err != nil {
return err
}
defer base.Close()
blk, err := st.GetBlock(want)
if errors.Is(err, database.ErrNotFound) {
return fmt.Errorf("block %s is NOT present in this store (cannot dump)", want)
}
if err != nil {
return fmt.Errorf("GetBlock(%s): %w", want, err)
}
if blk.ID() != want {
return fmt.Errorf("self-ID mismatch: requested=%s parsed=%s (refusing)", want, blk.ID())
}
if err := os.WriteFile(blockFile, blk.Bytes(), 0o644); err != nil {
return fmt.Errorf("write %s: %w", blockFile, err)
}
fmt.Printf("DUMPED id=%s parent=%s bytes=%d -> %s\n", blk.ID(), blk.ParentID(), len(blk.Bytes()), blockFile)
return nil
}
func runExport(_ *cobra.Command, _ []string) error {
// Open read-WRITE so Badger replays its value-log/WAL: the canonical block at
// the contested height was the LAST write before the chain went idle, so on a
// crash-consistent snapshot copy it may live only in the memtable/WAL, not yet
// in an SST. A read-only open skips recovery and could miss it. We never call a
// state writer here (we only read + write the output file), and this only ever
// runs against a DISPOSABLE snapshot copy of a canonical node — never the live node.
st, _, base, _, err := openState(false)
if err != nil {
return err
}
defer base.Close()
id, err := st.GetBlockIDAtHeight(height)
if err != nil {
return fmt.Errorf("GetBlockIDAtHeight(%d): %w", height, err)
}
blk, err := st.GetBlock(id)
if err != nil {
return fmt.Errorf("GetBlock(%s): %w", id, err)
}
if blk.ID() != id {
return fmt.Errorf("block id mismatch: index=%s block=%s", id, blk.ID())
}
if err := os.WriteFile(blockFile, blk.Bytes(), 0o644); err != nil {
return fmt.Errorf("write %s: %w", blockFile, err)
}
la, _ := st.GetLastAccepted()
fmt.Printf("EXPORTED height=%d id=%s parent=%s bytes=%d -> %s\n", height, blk.ID(), blk.ParentID(), len(blk.Bytes()), blockFile)
fmt.Printf(" (source lastAccepted=%s heightIndex[%d-1]=%s)\n", la, height, idAt(st, height-1))
return nil
}
func runRepair(_ *cobra.Command, _ []string) error {
wantB, err := ids.FromString(expectBlockID)
if err != nil {
return fmt.Errorf("bad --expect-block: %w", err)
}
wantA, err := ids.FromString(expectCurID)
if err != nil {
return fmt.Errorf("bad --expect-current: %w", err)
}
raw, err := os.ReadFile(blockFile)
if err != nil {
return fmt.Errorf("read %s: %w", blockFile, err)
}
blk, err := block.ParseWithoutVerification(raw)
if err != nil {
return fmt.Errorf("parse block file: %w", err)
}
// (1) the supplied block must be exactly the canonical target B.
if blk.ID() != wantB {
return fmt.Errorf("block-file id %s != --expect-block %s (refusing)", blk.ID(), wantB)
}
st, ppvmDB, base, _, err := openState(false)
if err != nil {
return err
}
defer base.Close()
la, err := st.GetLastAccepted()
if err != nil {
return fmt.Errorf("GetLastAccepted: %w", err)
}
curAtH, err := st.GetBlockIDAtHeight(height)
if err != nil {
return fmt.Errorf("GetBlockIDAtHeight(%d): %w", height, err)
}
// (2) idempotency: if already on B, do nothing.
if la == wantB && curAtH == wantB {
fmt.Printf("ALREADY-CANONICAL: lastAccepted and heightIndex[%d] already == B (%s); no-op\n", height, wantB)
return nil
}
// (3) fail-closed: only proceed from the exact expected bad state (A at H, lastAccepted A).
if la != wantA {
return fmt.Errorf("refusing: lastAccepted=%s is neither A(%s) nor B(%s) — unexpected state", la, wantA, wantB)
}
if curAtH != wantA {
return fmt.Errorf("refusing: heightIndex[%d]=%s != A(%s) — unexpected state", height, curAtH, wantA)
}
// (4) B must extend the SAME finalized prefix: B.parent == the block recorded at H-1.
parentAtH1, err := st.GetBlockIDAtHeight(height - 1)
if err != nil {
return fmt.Errorf("GetBlockIDAtHeight(%d): %w", height-1, err)
}
if blk.ParentID() != parentAtH1 {
return fmt.Errorf("refusing: B.parent=%s != heightIndex[%d]=%s (B does not extend the local finalized prefix)", blk.ParentID(), height-1, parentAtH1)
}
if _, err := st.GetBlock(parentAtH1); err != nil {
return fmt.Errorf("refusing: parent %s (height %d) not present in store: %w", parentAtH1, height-1, err)
}
fmt.Printf("PLAN: height=%d %s (A) -> %s (B)\n", height, wantA, wantB)
fmt.Printf(" current lastAccepted = %s\n", la)
fmt.Printf(" B.parent = %s == heightIndex[%d] OK\n", blk.ParentID(), height-1)
if !yes {
return errors.New("dry-run only: re-run with --yes to write")
}
// (5) apply via the state's own typed writers (identical on-disk bytes to luxd).
if err := st.PutBlock(blk); err != nil {
return fmt.Errorf("PutBlock(B): %w", err)
}
if err := st.SetBlockIDAtHeight(height, blk.ID()); err != nil {
return fmt.Errorf("SetBlockIDAtHeight(%d,B): %w", height, err)
}
if err := st.SetLastAccepted(blk.ID()); err != nil {
return fmt.Errorf("SetLastAccepted(B): %w", err)
}
if err := ppvmDB.Commit(); err != nil {
return fmt.Errorf("commit: %w", err)
}
// (6) re-read to confirm.
la2, _ := st.GetLastAccepted()
fmt.Printf("DONE: lastAccepted=%s heightIndex[%d]=%s heightIndex[%d]=%s\n", la2, height, idAt(st, height), height-1, idAt(st, height-1))
if la2 != wantB || idAt(st, height) != wantB.String() {
return fmt.Errorf("post-write verification FAILED")
}
fmt.Println("VERIFIED: proposervm now records canonical B at the contested height; inner EVM untouched.")
return nil
}