Files
zeekayandHanzo Dev 854998852a consensus: a decision needs evidence; unbreak the build
Five defects. The first four let a decision be reached on evidence never
gathered; the fifth is that main does not build.

**wave.Tick rounded the vote threshold down.** `int(float64(K) * Alpha)`
truncates: K=20 Alpha=0.69 is 13.8, so 13 votes of 20 cleared a threshold that
config.DefaultParams declares as AlphaPreference 14 — 65.0% on a chain
configured for 69%. Mainnet gave 14 where 15 is declared, testnet 7 where 8 is.
config.AlphaForK and fpc.Selector.SelectThreshold both round up; only this path
did not. Now math.Ceil.

**wave.New validated nothing.** Beta = 0 makes `Count >= Beta` in Tick true
before a vote is counted; K = 0 or Alpha = 0 puts the threshold at 0, which
yesVotes = 0 already meets. All three are zero values, so a Config missing a
field decided immediately. New refuses each. Alpha is checked only when FPC is
disabled, since the selector derives the threshold from ThetaMin/ThetaMax
otherwise — an existing FPC test passes Alpha 0 legitimately and caught the
first, over-strict version of this check.

**ray and field discarded that error.** Both NewDriver functions defaulted on
`== 0`, so a negative PollSize or Beta, or an Alpha outside (0, 1], reached
wave.New's refusal — and because the error is dropped, the Driver kept a zero
Wave whose nil state map panics on the first Tick. Verified: with the old
defaulting the new test panics with "assignment to entry in nil map". Both now
normalise the whole invalid range.

**engine.NewLuxConsensus clamped a negative beta to 0.** The comment said
"ensure beta is non-negative for uint32 conversion" — that fixes the
representation and breaks the meaning. NewLuxConsensus(20, 15, -5) built a
driver whose wave decides with Count = 0, and a test asserted `beta == 0`. Beta
now clamps to 1. k < 1 clamps too: it was a division by zero in
`float64(alpha)/float64(k)`. An alpha ratio outside (0, 1] falls back to
ConsensusSuperMajority, since above 1 no vote count reaches it and nothing ever
decides.

The chain layer is unaffected in safety: engine/chain gates acceptance on its
own distinct-signer count (`block.acceptVotes >= c.alpha`) as well as the
driver's decision, so wave's verdict is a liveness trigger there, not the safety
boundary. Its partition matrix stays green.

**go.sum recorded a hash luxfi/pq v1.0.3 no longer resolves to**, so main did
not build at all: "checksum mismatch, downloaded pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=,
go.sum ksw1dmfTR0dqqNMRS7BjGcprCO2Fhc+3Iiq2/NMuONw=". Reconciled to the hash the
tag resolves to now.

Tests: protocol/wave/threshold_rounding_test.go pins each shipped profile's
threshold to its declared AlphaPreference and checks the boundary from both
sides — one vote below must not decide, exactly at must, on accept and reject —
plus every zero and out-of-range Config New must refuse.
protocol/{ray,field}/config_floor_test.go assert no malformed Config yields a
Driver that panics on Tick. engine/lux_consensus_test.go asserts beta >= 1, a
positive finite alpha ratio for k <= 0, and a clamped ratio for alpha > k.

go vet clean, full suite green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 16:35:11 -07:00

84 lines
2.8 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//
// NewDriver discards wave.New's error. That is only safe if its defaulting
// leaves no Config that wave.New refuses — otherwise the Driver holds a zero
// Wave whose nil state map panics on the first Tick.
//
// Defaulting used to test `== 0`, so a negative PollSize or Beta, or an Alpha
// outside (0, 1], reached wave.New's refusal and produced exactly that.
package field
import (
"context"
"testing"
"time"
"github.com/luxfi/consensus/core/types"
"github.com/luxfi/consensus/protocol/prism"
"github.com/luxfi/consensus/protocol/wave"
"github.com/stretchr/testify/require"
)
type floorTransport struct{}
func (floorTransport) RequestVotes(ctx context.Context, peers []types.NodeID, item string) <-chan wave.Photon[string] {
ch := make(chan wave.Photon[string])
close(ch)
return ch
}
func (floorTransport) MakeLocalPhoton(item string, prefer bool) wave.Photon[string] {
return wave.Photon[string]{Item: item, Prefer: prefer}
}
type floorCut struct{}
func (floorCut) Sample(k int) []types.NodeID { return nil }
func (floorCut) Luminance() prism.Luminance { return prism.Luminance{} }
type floorStore struct{}
func (floorStore) Head() []string { return []string{"a"} }
func (floorStore) Get(string) (BlockView[string], bool) { return nil, false }
func (floorStore) Children(string) []string { return nil }
type floorProposer struct{}
func (floorProposer) Propose(ctx context.Context, parents []string) (string, error) {
return "b", nil
}
type floorCommitter struct{}
func (floorCommitter) Commit(ctx context.Context, ordered []string) error { return nil }
// Every Config, however malformed, must yield a Driver whose Tick does not
// panic. Each negative case here left a zero Wave before the defaulting was
// widened past `== 0`.
func TestNewDriverSurvivesMalformedConfig(t *testing.T) {
cases := []struct {
name string
cfg Config
}{
{"zero", Config{}},
{"negative PollSize", Config{PollSize: -1, Alpha: 0.8, Beta: 15}},
{"zero Beta", Config{PollSize: 20, Alpha: 0.8, Beta: 0}},
{"negative Alpha", Config{PollSize: 20, Alpha: -0.5, Beta: 15}},
{"Alpha above one", Config{PollSize: 20, Alpha: 1.5, Beta: 15}},
{"Alpha exactly one is legal", Config{PollSize: 20, Alpha: 1.0, Beta: 15}},
{"every field invalid", Config{PollSize: -7, Alpha: -7, Beta: 0, RoundTO: -time.Second}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
d := NewDriver[string](c.cfg, floorCut{}, floorTransport{},
floorStore{}, floorProposer{}, floorCommitter{})
require.NotNil(t, d)
require.NotPanics(t, func() {
_ = d.Tick(context.Background())
}, "a Driver built from this Config must not panic on Tick")
})
}
}