Files
node/vms/proposervm/vm_rejoin_wedge_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

100 lines
4.1 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package proposervm
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/versiondb"
"github.com/luxfi/ids"
"github.com/luxfi/log"
vmchain "github.com/luxfi/vm/chain"
"github.com/luxfi/node/vms/proposervm/state"
)
// stubInnerVM embeds the ChainVM interface (nil) and overrides ONLY GetBlock to report
// "not found", so getBlock (getPostForkBlock miss → getPreForkBlock → ChainVM.GetBlock)
// cleanly errors for an unheld id without a panic. No other inner method is invoked on
// the miss path under test — validate-before-assign returns before any inner delegation.
type stubInnerVM struct{ vmchain.ChainVM }
func (stubInnerVM) GetBlock(context.Context, ids.ID) (vmchain.Block, error) {
return nil, errors.New("stub inner VM: block not found")
}
// newWedgeTestVM builds the minimal proposervm needed to exercise SetPreference's
// getBlock path: an empty on-disk State (post-fork miss) + a stub inner VM (pre-fork
// miss) ⇒ getBlock cleanly errors for any id not pre-seeded, exactly as a validator
// that fell behind the frontier sees a build-tip above its own store.
func newWedgeTestVM(priorPreferred ids.ID) *VM {
vm := &VM{
preferred: priorPreferred,
verifiedBlocks: map[ids.ID]PostForkBlock{},
logger: log.Noop(),
}
vm.State = state.New(versiondb.New(memdb.New()))
vm.ChainVM = stubInnerVM{}
return vm
}
// TestSetPreference_UnheldBlock_DoesNotPoison is the deterministic unit repro of the
// REJOIN WEDGE.
//
// A validator that fell behind is steered by the consensus engine
// (consensus/engine/chain/integration.go) to a build tip ABOVE its own
// frontier: an id this proposervm does not hold. The OLD SetPreference assigned
// vm.preferred = <unheld id> BEFORE validating, so on the (inevitable) getBlock miss it
// returned a hard error but LEFT vm.preferred poisoned. BuildBlock then read the poisoned
// id (getBlock) and logged "unexpected build block failure / failed to fetch preferred
// block" on EVERY build attempt forever — Quasar cert-finality has no re-converge poll, so
// the node never recovered.
//
// The fix validates BEFORE assigning: on an unheld id, vm.preferred is UNCHANGED (kept
// prior-good) and SetPreference is a non-fatal no-op, so BuildBlock stays live on the last
// held tip while the catch-up path pulls the gap.
func TestSetPreference_UnheldBlock_DoesNotPoison(t *testing.T) {
require := require.New(t)
priorGood := ids.GenerateTestID()
vm := newWedgeTestVM(priorGood)
unheld := ids.GenerateTestID()
err := vm.SetPreference(context.Background(), unheld)
// An unheld build hint must be a non-fatal no-op — NEVER a hard error that also leaves
// the id poisoned, which wedges BuildBlock.
require.NoError(err, "unheld SetPreference must not be fatal")
// THE anti-wedge invariant: vm.preferred is NOT poisoned to the unheld id.
require.Equal(priorGood, vm.preferred,
"vm.preferred must stay the prior-good tip; poisoning it wedges BuildBlock forever")
require.NotEqual(unheld, vm.preferred)
}
// TestSetPreference_UnheldBlock_KeepsBuildLive asserts the DOWNSTREAM property the fix
// exists to protect: after an unheld SetPreference, the preference the VM will build on is
// still the prior-good (held) tip, not the poisoned id — i.e. BuildBlock would fetch a
// block it holds, not spam "failed to fetch preferred block". We assert on vm.preferred
// (what BuildBlock reads) rather than driving a full inner-VM build, keeping the repro
// hermetic.
func TestSetPreference_UnheldBlock_KeepsBuildLive(t *testing.T) {
require := require.New(t)
priorGood := ids.GenerateTestID()
vm := newWedgeTestVM(priorGood)
// Repeated unheld steers (the engine re-steers on every gossip receipt) must never
// accumulate poison: the build preference stays pinned to the held tip.
for i := 0; i < 5; i++ {
require.NoError(vm.SetPreference(context.Background(), ids.GenerateTestID()))
require.Equal(priorGood, vm.preferred, "build tip must remain the held prior-good after steer %d", i)
}
}