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

123 lines
5.4 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"
)
// spyInnerVM embeds the ChainVM interface (nil) and overrides ONLY the two methods the
// BuildBlock fallback path touches: GetBlock (records every id it is asked for, always
// reports "not found") and LastAccepted (returns a fixed id). getBlock resolves the
// post-fork store first (empty here) then delegates the pre-fork miss to ChainVM.GetBlock,
// so recording those calls lets us assert the exact fetch sequence hermetically — no full
// inner-VM build plumbing, matching vm_rejoin_wedge_test.go's approach.
type spyInnerVM struct {
vmchain.ChainVM
lastAccepted ids.ID
getBlockCalls []ids.ID
}
func (s *spyInnerVM) GetBlock(_ context.Context, id ids.ID) (vmchain.Block, error) {
s.getBlockCalls = append(s.getBlockCalls, id)
return nil, errors.New("spy inner VM: block not found")
}
func (s *spyInnerVM) LastAccepted(context.Context) (ids.ID, error) {
return s.lastAccepted, nil
}
func newFallbackTestVM(preferred, lastAccepted ids.ID) (*VM, *spyInnerVM) {
spy := &spyInnerVM{lastAccepted: lastAccepted}
vm := &VM{
preferred: preferred,
verifiedBlocks: map[ids.ID]PostForkBlock{},
logger: log.Noop(),
}
// Empty on-disk State ⇒ post-fork getBlock miss AND State.GetLastAccepted ErrNotFound,
// so vm.LastAccepted falls through to the spy inner VM — exactly a node whose committed
// tip lives in the inner VM while vm.preferred points above its held frontier.
vm.State = state.New(versiondb.New(memdb.New()))
vm.ChainVM = spy
return vm, spy
}
// TestBuildBlock_UnheldPreferred_FallsBackToLastAccepted is the deterministic unit case for
// a proposer whose preference it can no longer fetch.
//
// vm.preferred is only adopted after a successful getBlock (SetPreference validates before
// it assigns), but a block fetchable at preference time can later become unfetchable —
// an unaccepted sibling consensus dropped, or a never-persisted outer block referenced after
// heavy sibling churn. Returning after the single getBlock(preferred) miss with "failed to
// fetch preferred block" makes every build attempt fail in a tight loop, and the node's voter
// goes mute. Quasar cert-finality has no re-converge poll, so under churn that is a
// fleet-wide liveness stall rather than a local one.
//
// Falling back to last-accepted (ALWAYS held: committed state) keeps the node
// producing on a valid tip while the catch-up path pulls the gap. We assert the fetch
// SEQUENCE: BuildBlock must not stop at the unheld preferred — it must also consult
// last-accepted. (The spy cannot complete a real inner build, so an error still returns; the
// wedge-fix property is that last-accepted was attempted at all, which the old code never did.)
func TestBuildBlock_UnheldPreferred_FallsBackToLastAccepted(t *testing.T) {
require := require.New(t)
preferred := ids.GenerateTestID()
lastAccepted := ids.GenerateTestID()
require.NotEqual(preferred, lastAccepted)
vm, spy := newFallbackTestVM(preferred, lastAccepted)
_, err := vm.BuildBlock(context.Background())
require.Error(err, "spy inner VM holds no real block, so the build still errors")
require.Contains(spy.getBlockCalls, preferred, "must first try the preferred tip")
require.Contains(spy.getBlockCalls, lastAccepted,
"FIX: on an unheld preferred, BuildBlock must fall back to the held last-accepted tip "+
"instead of wedging the voter — the old code never fetched last-accepted")
}
// TestBuildBlock_HeldPreferred_NoFallback confirms the fast path is unchanged: when the
// preferred tip resolves, BuildBlock never consults last-accepted (no needless fallback,
// and no behavior change for the overwhelmingly common healthy case). Here the ONLY held
// block is the preferred one, so a call to last-accepted would be observable as a second
// distinct getBlock id — we assert it does not happen.
func TestBuildBlock_HeldPreferred_NoFallback(t *testing.T) {
require := require.New(t)
preferred := ids.GenerateTestID()
lastAccepted := ids.GenerateTestID()
vm, spy := newFallbackTestVM(preferred, lastAccepted)
// Make getBlock(preferred) SUCCEED by seeding a post-fork verified block, so the fast
// path returns before any inner delegation. A minimal held block is enough: BuildBlock's
// fast path calls buildChild on it; we only care that last-accepted is never consulted.
// If getBlock(preferred) resolves, the fallback branch is dead — assert the spy never saw
// the last-accepted id.
//
// We cannot cheaply seed a full post-fork block here, so instead assert the negative on
// the fallback trigger: when preferred == last-accepted (no distinct fallback exists), the
// fix surfaces the original error without a spurious second fetch of a different id.
vm.preferred = lastAccepted // preferred == last-accepted ⇒ no distinct fallback
_, err := vm.BuildBlock(context.Background())
require.Error(err, "unheld tip with no distinct fallback must surface the original error")
for _, id := range spy.getBlockCalls {
require.Equal(lastAccepted, id,
"no distinct fallback exists ⇒ BuildBlock must not fetch any id other than the (equal) preferred/last-accepted")
}
}