Files
consensus/engine/lux_consensus_test.go
zeekayandHanzo Dev e808906131 the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 12:58:35 -07:00

534 lines
14 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc All rights reserved.
// See the file LICENSE for licensing terms.
package engine
import (
"context"
"testing"
"github.com/luxfi/consensus/protocol/prism"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
// TestNewLuxConsensus tests the NewLuxConsensus constructor
func TestNewLuxConsensus(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(20, 15, 10)
require.NotNil(lc)
require.Equal(20, lc.k)
require.Equal(float64(15)/float64(20), lc.alpha)
require.Equal(uint32(10), lc.beta)
require.NotNil(lc.wave)
require.NotNil(lc.focus)
require.NotNil(lc.decided)
require.NotNil(lc.decisions)
require.NotNil(lc.consecutiveSuccesses)
}
// A negative beta must be clamped to 1, not 0.
//
// wave.Tick decides once Count >= Beta, so Beta = 0 finalises before a single
// vote is counted. The old clamp to 0 kept the uint32 conversion safe and made
// NewLuxConsensus(20, 15, -5) a consensus driver that accepts on no evidence.
func TestNewLuxConsensusNegativeBetaStillDemandsEvidence(t *testing.T) {
require := require.New(t)
for _, beta := range []int{-5, -1, 0} {
lc := NewLuxConsensus(20, 15, beta)
require.NotNil(lc)
require.GreaterOrEqual(lc.beta, uint32(1),
"beta %d must clamp to at least 1 so a decision needs a confirmation", beta)
}
}
// k = 0 divides by zero computing the alpha ratio and puts the vote threshold
// at zero.
func TestNewLuxConsensusZeroKStillDemandsEvidence(t *testing.T) {
require := require.New(t)
for _, k := range []int{0, -1} {
lc := NewLuxConsensus(k, 15, 5)
require.NotNil(lc)
require.GreaterOrEqual(lc.k, 1, "k %d must clamp to a positive sample size", k)
require.Greater(lc.alpha, 0.0, "alpha ratio must be positive, not NaN or Inf")
require.LessOrEqual(lc.alpha, 1.0, "alpha ratio must be reachable")
}
}
// alpha > k gives a ratio above 1, which no vote count can reach, so nothing
// ever decides.
func TestNewLuxConsensusUnreachableAlphaIsClamped(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(20, 40, 5)
require.NotNil(lc)
require.LessOrEqual(lc.alpha, 1.0)
require.Greater(lc.alpha, 0.0)
}
// TestLuxConsensusRecordVote tests the RecordVote method
func TestLuxConsensusRecordVote(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(5, 3, 5)
itemID := ids.GenerateTestID()
// Initial state
require.Equal(uint32(0), lc.consecutiveSuccesses[itemID])
// Record votes
lc.RecordVote(itemID)
require.Equal(uint32(1), lc.consecutiveSuccesses[itemID])
lc.RecordVote(itemID)
require.Equal(uint32(2), lc.consecutiveSuccesses[itemID])
}
// TestLuxConsensusRecordVoteAlreadyDecided tests voting on decided item
func TestLuxConsensusRecordVoteAlreadyDecided(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(5, 3, 5)
itemID := ids.GenerateTestID()
// Mark as decided
lc.mu.Lock()
lc.decided[itemID] = true
lc.consecutiveSuccesses[itemID] = 5
lc.mu.Unlock()
// Vote should be ignored
lc.RecordVote(itemID)
lc.mu.RLock()
count := lc.consecutiveSuccesses[itemID]
lc.mu.RUnlock()
require.Equal(uint32(5), count) // Should not change
}
// TestLuxConsensusPoll tests the Poll method
func TestLuxConsensusPoll(t *testing.T) {
lc := NewLuxConsensus(5, 3, 2)
itemID := ids.GenerateTestID()
responses := map[ids.ID]int{
itemID: 5,
}
// First poll
continuePolling := lc.Poll(responses)
// May or may not continue depending on confidence
_ = continuePolling
}
// TestLuxConsensusPollEmptyVotes tests poll with zero total votes
func TestLuxConsensusPollEmptyVotes(t *testing.T) {
lc := NewLuxConsensus(5, 3, 2)
itemID := ids.GenerateTestID()
responses := map[ids.ID]int{
itemID: 0,
}
continuePolling := lc.Poll(responses)
if !continuePolling {
t.Log("Poll returned false with empty votes")
}
}
// TestLuxConsensusPollAlreadyDecided tests poll with already decided item
func TestLuxConsensusPollAlreadyDecided(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(5, 3, 2)
itemID := ids.GenerateTestID()
// Mark as decided
lc.mu.Lock()
lc.decided[itemID] = true
lc.mu.Unlock()
responses := map[ids.ID]int{
itemID: 5,
}
continuePolling := lc.Poll(responses)
require.True(continuePolling)
}
// TestLuxConsensusPollMultipleItems tests poll with multiple items
func TestLuxConsensusPollMultipleItems(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(5, 4, 1) // High alpha ratio for quick decision
item1 := ids.GenerateTestID()
item2 := ids.GenerateTestID()
responses := map[ids.ID]int{
item1: 4, // 80% - above alpha
item2: 1, // 20% - below alpha
}
_ = lc.Poll(responses)
require.NotNil(lc)
}
// TestLuxConsensusDecided tests the Decided method
func TestLuxConsensusDecided(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(5, 3, 2)
// Initially not decided
require.False(lc.Decided())
// Mark something as decided
lc.mu.Lock()
lc.decided[ids.GenerateTestID()] = true
lc.mu.Unlock()
require.True(lc.Decided())
}
// TestLuxConsensusPreference tests the Preference method
func TestLuxConsensusPreference(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(5, 3, 2)
// Initially empty
require.Equal(ids.Empty, lc.Preference())
// Set preference
prefID := ids.GenerateTestID()
lc.mu.Lock()
lc.preference = prefID
lc.mu.Unlock()
require.Equal(prefID, lc.Preference())
}
// TestLuxConsensusDecision tests the Decision method
func TestLuxConsensusDecision(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(5, 3, 2)
itemID := ids.GenerateTestID()
// No decision yet
decision, exists := lc.Decision(itemID)
require.False(exists)
require.Equal(decision, decision) // just to use the variable
// Add a decision
lc.mu.Lock()
lc.decisions[itemID] = 1 // DecideAccept
lc.mu.Unlock()
decision, exists = lc.Decision(itemID)
require.True(exists)
}
// TestLuxConsensusParameters tests the Parameters method
func TestLuxConsensusParameters(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(20, 15, 10)
params := lc.Parameters()
require.Equal(20, params.K)
require.Equal(float64(15)/float64(20), params.Alpha)
require.Equal(uint32(10), params.Beta)
require.Equal(15, params.AlphaPreference)
require.Equal(15, params.AlphaConfidence)
}
// TestSimpleCutSample tests the SimpleCut.Sample method
func TestSimpleCutSample(t *testing.T) {
require := require.New(t)
cut := &SimpleCut{k: 5}
nodes := cut.Sample(3)
require.Len(nodes, 3)
for _, node := range nodes {
require.NotEqual(ids.EmptyNodeID, node)
}
}
// TestSimpleCutLuminance tests the SimpleCut.Luminance method
func TestSimpleCutLuminance(t *testing.T) {
require := require.New(t)
cut := &SimpleCut{k: 10}
luminance := cut.Luminance()
require.Equal(10, luminance.ActivePeers)
require.Equal(10, luminance.TotalPeers)
require.Equal(float64(10), luminance.Lx)
}
// TestSimpleTransportRequestVotesReturnsEmpty tests that SimpleTransport
// no longer fabricates votes -- it returns an empty closed channel.
func TestSimpleTransportRequestVotesReturnsEmpty(t *testing.T) {
require := require.New(t)
transport := &SimpleTransport{}
peers := make([]ids.NodeID, 3)
for i := 0; i < 3; i++ {
peers[i] = ids.GenerateTestNodeID()
}
itemID := ids.GenerateTestID()
ctx := context.Background()
ch := transport.RequestVotes(ctx, peers, itemID)
count := 0
for range ch {
count++
}
require.Equal(0, count, "SimpleTransport must not fabricate votes")
}
// TestSimpleTransportErr tests that SimpleTransport reports no-transport error.
func TestSimpleTransportErr(t *testing.T) {
transport := &SimpleTransport{}
require.ErrorIs(t, transport.Err(), ErrNoTransport)
}
// TestSimpleTransportMakeLocalPhoton tests the MakeLocalPhoton method
func TestSimpleTransportMakeLocalPhoton(t *testing.T) {
require := require.New(t)
transport := &SimpleTransport{}
itemID := ids.GenerateTestID()
// Test prefer=true
photon := transport.MakeLocalPhoton(itemID, true)
require.Equal(itemID, photon.Item)
require.True(photon.Prefer)
require.NotEqual(ids.EmptyNodeID, photon.Sender)
require.False(photon.Timestamp.IsZero())
// Test prefer=false
photon = transport.MakeLocalPhoton(itemID, false)
require.Equal(itemID, photon.Item)
require.False(photon.Prefer)
}
// TestLuxConsensusPollDecision tests poll leading to decision
func TestLuxConsensusPollDecision(t *testing.T) {
require := require.New(t)
// Use low beta for quick decision
lc := NewLuxConsensus(5, 4, 1)
itemID := ids.GenerateTestID()
// Poll multiple times to build confidence
for i := 0; i < 10; i++ {
responses := map[ids.ID]int{
itemID: 5, // High votes
}
lc.Poll(responses)
}
// Check if decided
lc.mu.RLock()
decided := lc.decided[itemID]
lc.mu.RUnlock()
// The decision may or may not have been made depending on internal state
_ = decided
require.NotNil(lc)
}
// TestLuxConsensusPollRejectDecision tests poll leading to reject decision
func TestLuxConsensusPollRejectDecision(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(10, 8, 1) // High alpha
item1 := ids.GenerateTestID()
item2 := ids.GenerateTestID()
// Poll with low votes for item1
for i := 0; i < 10; i++ {
responses := map[ids.ID]int{
item1: 1, // Low votes
item2: 9, // High votes
}
lc.Poll(responses)
}
require.NotNil(lc)
}
// TestLuxConsensusPollDecisionWithLowRatio tests Poll with a low vote ratio leading to reject
func TestLuxConsensusPollDecisionWithLowRatio(t *testing.T) {
require := require.New(t)
// Create with very low beta for immediate decision
lc := NewLuxConsensus(10, 9, 1)
item := ids.GenerateTestID()
// Set up Focus to immediately decide with low confidence
// We need the focus to reach decided state with low ratio
// Poll repeatedly with low votes to build confidence towards rejection
for i := 0; i < 50; i++ {
responses := map[ids.ID]int{
item: 1, // Very low votes - below alpha threshold
}
continuePolling := lc.Poll(responses)
if !continuePolling {
break
}
}
// Check if decision was made
lc.mu.RLock()
decision, exists := lc.decisions[item]
lc.mu.RUnlock()
if exists {
// If decided, verify the decision
t.Logf("Decision made: %v", decision)
}
require.NotNil(lc)
}
// TestNewLuxConsensusWithStakeWeightedCut tests constructing with a real StakeWeightedCut.
func TestNewLuxConsensusWithStakeWeightedCut(t *testing.T) {
require := require.New(t)
validators := make([]prism.Validator, 20)
for i := range validators {
validators[i] = prism.Validator{
ID: ids.GenerateTestNodeID(),
Weight: uint64(100 + i),
}
}
cut, err := prism.NewStakeWeightedCut(validators)
require.NoError(err)
lc := NewLuxConsensus(20, 15, 10, WithCut(cut))
require.NotNil(lc)
require.Equal(20, lc.k)
// Verify the cut is stake-weighted (not SimpleCut).
nodes := lc.prismCut.Sample(5)
require.Len(nodes, 5)
// All sampled nodes should be from our validator set.
validIDs := make(map[ids.NodeID]bool)
for _, v := range validators {
validIDs[v.ID] = true
}
for _, n := range nodes {
require.True(validIDs[n], "sampled node not in validator set")
}
}
// TestLuxConsensusPollFocusDecidedWithLowRatio tests the reject branch in Poll
// This test manipulates internal state to reach the DecideReject branch
func TestLuxConsensusPollFocusDecidedWithLowRatio(t *testing.T) {
require := require.New(t)
// Create consensus with threshold 1 (beta=1) so decisions happen quickly
lc := NewLuxConsensus(10, 8, 1) // K=10, alpha=8, beta=1
item := ids.GenerateTestID()
// First, poll with high ratio to build confidence
// This will make focus.State return decided=true
highRatioResponses := map[ids.ID]int{
item: 10, // 100% - above alpha
}
lc.Poll(highRatioResponses)
// Now check if decision was made
lc.mu.RLock()
decided := lc.decided[item]
decision := lc.decisions[item]
lc.mu.RUnlock()
// Since we used high ratio and beta=1, should be decided with Accept
if decided {
require.NotNil(decision)
}
}
// TestLuxConsensusPollWaveDecision tests the Wave protocol decision path
func TestLuxConsensusPollWaveDecision(t *testing.T) {
require := require.New(t)
lc := NewLuxConsensus(5, 4, 100) // High beta so focus won't decide quickly
item := ids.GenerateTestID()
// Poll multiple times to let Wave make a decision
for i := 0; i < 200; i++ {
responses := map[ids.ID]int{
item: 5, // High votes
}
continuePolling := lc.Poll(responses)
if !continuePolling {
break
}
}
require.NotNil(lc)
}
// TestLuxConsensusPollFocusDecideReject tests the reject branch in Poll
// This covers the case where focus.State returns decided=true but ratio < alpha
func TestLuxConsensusPollFocusDecideReject(t *testing.T) {
require := require.New(t)
// Create consensus with beta=1 (threshold=1) for immediate decision
// K=10, alpha=9 (90% threshold), beta=1
lc := NewLuxConsensus(10, 9, 1)
item := ids.GenerateTestID()
// First, build up confidence by polling with high ratio
// This makes focus.states[item] reach the threshold
highRatioResponses := map[ids.ID]int{
item: 10, // 100% - well above alpha (90%)
}
continuePolling := lc.Poll(highRatioResponses)
// Now the focus state for this item should be at threshold
// If decided with high ratio, we got Accept - that's fine
// We need to create a new scenario for the reject branch
// Create a fresh consensus
lc2 := NewLuxConsensus(10, 9, 1)
item2 := ids.GenerateTestID()
// Directly manipulate the focus state to be at threshold
// Then poll with low ratio to trigger the reject branch
lc2.focus.Update(item2, 0.95) // Above alpha, increments state to 1
// Now state is 1, threshold is 1, so decided=true
// Poll with low ratio to hit the reject branch
lowRatioResponses := map[ids.ID]int{
item2: 1, // Only 10% - below alpha (90%)
ids.GenerateTestID(): 9, // 90% for another item
}
continuePolling = lc2.Poll(lowRatioResponses)
// Check decision was made
lc2.mu.RLock()
decided := lc2.decided[item2]
decision, exists := lc2.decisions[item2]
lc2.mu.RUnlock()
// Should be decided with Reject since ratio < alpha when focus decided
if exists && decided {
t.Logf("Decision made for item2: %v", decision)
}
require.NotNil(lc)
_ = continuePolling
}