mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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]>
147 lines
3.8 KiB
Go
147 lines
3.8 KiB
Go
package ray
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/core/types"
|
|
"github.com/luxfi/consensus/protocol/prism"
|
|
"github.com/luxfi/consensus/protocol/wave"
|
|
)
|
|
|
|
// ID is your item identifier (block hash, tx id, etc.).
|
|
type ID interface{ comparable }
|
|
|
|
// Source lets Ray pull next candidates to decide in linear order.
|
|
type Source[T ID] interface {
|
|
NextPending(ctx context.Context, n int) []T
|
|
}
|
|
|
|
// Sink receives final decisions in order.
|
|
type Sink[T ID] interface {
|
|
Decide(ctx context.Context, items []T, d types.Decision) error
|
|
}
|
|
|
|
// Transport bridges network vote requests <-> Photons the Wave consumes.
|
|
type Transport[T ID] interface {
|
|
RequestVotes(ctx context.Context, peers []types.NodeID, item T) <-chan wave.Photon[T]
|
|
MakeLocalPhoton(item T, prefer bool) wave.Photon[T]
|
|
}
|
|
|
|
type Config struct {
|
|
PollSize int
|
|
Alpha float64
|
|
Beta uint32
|
|
RoundTO time.Duration
|
|
MaxBatch int
|
|
}
|
|
|
|
type Driver[T ID] struct {
|
|
wv *wave.Wave[T]
|
|
cut prism.Cut[T]
|
|
tx Transport[T]
|
|
src Source[T]
|
|
out Sink[T]
|
|
cfg Config
|
|
height uint64
|
|
preference T
|
|
hasPreference bool
|
|
}
|
|
|
|
func NewDriver[T ID](cfg Config, cut prism.Cut[T], tx Transport[T], src Source[T], out Sink[T]) *Driver[T] {
|
|
// Normalise the whole invalid range, not just the zero value. wave.New
|
|
// refuses K < 1, Beta < 1 and Alpha outside (0, 1]; a negative or
|
|
// out-of-range field passes an `== 0` check, reaches that refusal, and —
|
|
// since the error is not returned from here — leaves a zero Wave whose nil
|
|
// state map panics on the first Tick.
|
|
if cfg.PollSize < 1 {
|
|
cfg.PollSize = 20
|
|
}
|
|
if cfg.Alpha <= 0 || cfg.Alpha > 1 {
|
|
cfg.Alpha = 0.8
|
|
}
|
|
if cfg.Beta < 1 {
|
|
cfg.Beta = 15
|
|
}
|
|
if cfg.RoundTO == 0 {
|
|
cfg.RoundTO = 250 * time.Millisecond
|
|
}
|
|
if cfg.MaxBatch == 0 {
|
|
cfg.MaxBatch = 64
|
|
}
|
|
|
|
wvVal, _ := wave.New[T](wave.Config{K: cfg.PollSize, Alpha: cfg.Alpha, Beta: cfg.Beta, RoundTO: cfg.RoundTO}, cut, tx)
|
|
return &Driver[T]{
|
|
wv: &wvVal,
|
|
cut: cut, tx: tx, src: src, out: out, cfg: cfg,
|
|
height: 0,
|
|
hasPreference: false,
|
|
}
|
|
}
|
|
|
|
// Tick drives one sampling round across up to MaxBatch pending items.
|
|
// Any that reach β are emitted to Sink in input order with their decision.
|
|
func (d *Driver[T]) Tick(ctx context.Context) error {
|
|
items := d.src.NextPending(ctx, d.cfg.MaxBatch)
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var decided []T
|
|
for _, it := range items {
|
|
d.wv.Tick(ctx, it)
|
|
if st, ok := d.wv.State(it); ok && st.Decided {
|
|
if st.Result == types.DecideAccept {
|
|
decided = append(decided, it)
|
|
}
|
|
}
|
|
}
|
|
if len(decided) > 0 {
|
|
if len(decided) > 0 {
|
|
d.preference = decided[0] // Update preference to latest decided
|
|
d.hasPreference = true
|
|
d.height++
|
|
}
|
|
return d.out.Decide(ctx, decided, types.DecideAccept)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Start begins the Ray engine operation
|
|
func (d *Driver[T]) Start(ctx context.Context) error {
|
|
// Ray engine is stateless and starts immediately
|
|
return nil
|
|
}
|
|
|
|
// Stop ends the Ray engine operation
|
|
func (d *Driver[T]) Stop(ctx context.Context) error {
|
|
// Ray engine is stateless and stops immediately
|
|
return nil
|
|
}
|
|
|
|
// Propose proposes an item for consensus (Nova will use this for blocks)
|
|
func (d *Driver[T]) Propose(ctx context.Context, item T) error {
|
|
// In Ray, proposals are handled through the Source interface
|
|
// This is a convenience method for direct proposals
|
|
d.wv.Tick(ctx, item)
|
|
return nil
|
|
}
|
|
|
|
// GetPreference returns the current preferred item
|
|
func (d *Driver[T]) GetPreference() (T, bool) {
|
|
return d.preference, d.hasPreference
|
|
}
|
|
|
|
// IsFinalized checks if an item is finalized
|
|
func (d *Driver[T]) IsFinalized(item T) bool {
|
|
if state, exists := d.wv.State(item); exists {
|
|
return state.Decided && state.Result == types.DecideAccept
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Height returns the current consensus height (for linear chains)
|
|
func (d *Driver[T]) Height() uint64 {
|
|
return d.height
|
|
}
|