mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
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.
123 lines
5.4 KiB
Go
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")
|
|
}
|
|
}
|