Files
node/chains/golive_executed_test.go
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -07:00

166 lines
6.8 KiB
Go

// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// golive_executed_test.go — a node may go live only where it has actually executed.
//
// Gating normal operation on reaching the frontier exists so a validator never
// serves and votes at a stale height. The gate consults the acceptance oracle,
// which answers from the consensus ledger — what a quorum DECIDED. The VM answers
// what this node RAN. Those diverge, and when they do the ledger is the higher of
// the two, so the gate can be told the node is at the frontier while its VM sits
// far below it. Then the node goes live and nothing is trying to converge it.
package chains
import (
"context"
"testing"
chainbootstrap "github.com/luxfi/consensus/engine/chain/bootstrap"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/stretchr/testify/require"
)
// TestExecutedToRefusesAHeadTheVMHasNotRun is the failure this file exists for.
//
// The gate asks whether this node RAN the chain to the head it is about to go live
// at. The head comes from the consensus ledger — what a quorum decided — and the
// ledger runs ahead of execution, so the head can be well above anything this VM
// has executed. Answering yes there is a validator serving and voting at a stale
// height with nothing left trying to converge it.
func TestExecutedToRefusesAHeadTheVMHasNotRun(t *testing.T) {
const N = 5
chain, _ := buildBSChain(N, -1)
bh := &blockHandler{logger: log.NewNoOpLogger(), vm: newBSVM(chain)}
// The ledger's head is far above what this VM has executed — the live condition.
if bh.executedTo(context.Background(), uint64(N)+200) {
t.Fatalf("go-live was permitted at a head %d blocks above anything this VM ran", 200)
}
}
// TestExecutedToAllowsAHeadTheVMHasRun: the same rule must not refuse a node that
// has genuinely executed, or every healthy restart hangs in bootstrap.
func TestExecutedToAllowsAHeadTheVMHasRun(t *testing.T) {
const N = 5
chain, _ := buildBSChain(N, -1)
bh := &blockHandler{logger: log.NewNoOpLogger(), vm: newBSVM(chain)}
_, ran, err := bh.vmLastAccepted(context.Background())
if err != nil {
t.Fatalf("harness VM unreadable: %v", err)
}
if !bh.executedTo(context.Background(), ran) {
t.Fatalf("a VM that executed to %d was refused go-live at %d", ran, ran)
}
}
// TestExecutedToRefusesAnUnreadableVM: an absent measurement is not a passing one.
func TestExecutedToRefusesAnUnreadableVM(t *testing.T) {
bh := &blockHandler{logger: log.NewNoOpLogger()} // no VM at all
if bh.executedTo(context.Background(), 1) {
t.Fatal("go-live was permitted without being able to read the VM")
}
}
// TestGoLiveAllowedWhenTheVMHasExecuted is the other half, and it is what keeps
// the gate from becoming a hang: a node that genuinely ran to its head must still
// go live, or every healthy restart sits in bootstrap forever.
func TestGoLiveAllowedWhenTheVMHasExecuted(t *testing.T) {
const N = 5
chain, byID := buildBSChain(N, -1)
vm := newBSVM(chain) // executed to its own last-accepted
bh, chainID, beacons := newBSHandlerAndEngine(t, vm, 2)
bh.selfNodeID = beacons[0]
bh.msgCreator = bsMsgBuilder{}
bh.net = &bsBeaconNet{bh: bh, chainID: chainID, connected: beacons[1:], byID: byID, tip: chain[0]}
bh.bsActive.Store(true)
tip, status := bh.FrontierTip(context.Background())
bh.bsActive.Store(false)
require.Equal(t, chainbootstrap.FrontierCaughtUp, status,
"a node that executed to its head must go live — refusing here hangs every healthy restart")
require.Equal(t, chain[0].id, tip)
}
// wrappedVM models a wrapped chain: LastAccepted answers with the OUTER head,
// which leads the inner one, and InnerLastAccepted answers with what actually ran.
type wrappedVM struct {
*bsTestVM
outer *bsTestBlock
inner *bsTestBlock
}
func (v *wrappedVM) LastAccepted(context.Context) (ids.ID, error) { return v.outer.id, nil }
func (v *wrappedVM) InnerLastAccepted(context.Context) (ids.ID, error) {
return v.inner.id, nil
}
// The inner HEIGHT, resolved on the inner side. The id alone cannot be turned
// into one from outside — the wrapper's store is keyed by outer ids, so asking it
// to resolve an inner id misses and the miss reads as height zero.
func (v *wrappedVM) InnerLastAcceptedHeight(context.Context) (uint64, error) {
return v.inner.Height(), nil
}
// TestExecutedToAsksTheVMThatExecutes is the production divergence exactly: the
// wrapper's head is at the frontier and the executing VM is far below it. Asking
// the wrapper returns the frontier and lets the node go live having run nothing;
// asking the VM that executes returns the truth.
func TestExecutedToAsksTheVMThatExecutes(t *testing.T) {
const N = 6
chain, _ := buildBSChain(N, -1)
// Every block is resolvable, so the ONLY variable is which head is asked for.
vm := newBSVMAt(chain, N-1)
bh := &blockHandler{logger: log.NewNoOpLogger()}
bh.vm = &wrappedVM{bsTestVM: vm, outer: chain[N-1], inner: chain[0]}
if bh.executedTo(context.Background(), uint64(N-1)) {
t.Fatalf("the wrapper's head (%d) was accepted as proof of execution while the "+
"executing VM had only run to %d — the node goes live having run nothing",
N-1, 0)
}
}
// TestExecutedToTrustsTheInnerWhenItHasCaughtUp: once the executing VM has run to
// the head, the same node must be allowed live.
func TestExecutedToTrustsTheInnerWhenItHasCaughtUp(t *testing.T) {
const N = 6
chain, _ := buildBSChain(N, -1)
vm := newBSVMAt(chain, N-1) // this node HAS executed to the head
bh := &blockHandler{logger: log.NewNoOpLogger()}
bh.vm = &wrappedVM{bsTestVM: vm, outer: chain[N-1], inner: chain[N-1]}
if !bh.executedTo(context.Background(), uint64(N-1)) {
t.Fatal("the executing VM was at the head and go-live was still refused")
}
}
// The head every caller gets must be the one the node RAN, not the one its
// wrapper committed. The recovery loop lowers its position onto this number and
// reports whether a batch moved the node; read off the wrapper, it believes it
// stands past blocks it never executed, skips exactly the band it needs, and
// reports batch after batch landing while the chain it serves stays put.
func TestAppliedHeadIsTheInnerHead(t *testing.T) {
const N = 10
chain, _ := buildBSChain(N, -1)
bh := &blockHandler{logger: log.NewNoOpLogger()}
bh.vm = &wrappedVM{bsTestVM: newBSVMAt(chain, N), outer: chain[N], inner: chain[3]}
applied, err := bh.appliedHeight(context.Background())
require.NoError(t, err)
require.Equal(t, chain[3].Height(), applied,
"the applied height must be the inner one; the wrapper is at %d", chain[N].Height())
// And the head keeps naming the chain, which is a different question: a caller
// identifying the tip still wants the wrapper's id.
head, _, err := bh.vmLastAccepted(context.Background())
require.NoError(t, err)
require.Equal(t, chain[N].id, head, "the head must still name the chain's tip")
}