mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
THE WEDGE: finality could be triggered by a raw α-of-K vote COUNT on three ungated paths (processPendingBlocks, DrainAccepted, and the K==1 short-circuit), none of which checked the stake-weighted ⅔ predicate. On a skewed-stake set an α-count quorum that did NOT hold ⅔ of stake could finalize — a safety break that also manifested as the block-height wedge (a block "accepted" by count but not by a verifiable cert, leaving the chain unable to make verifiable progress). THE FIX (one rule, one place): no VerifiedQuorumCert, no finality. - verified_cert.go: introduce VerifiedQuorumCert — the unforgeable finality authority token. Its only field is unexported, so no other file can mint a non-zero one with a struct literal. The sole multi-validator producer is BuildVerifiedQuorumCert, which runs the FULL predicate (VerifyWeighted, strict >⅔ of stake when a StakeSource is wired; count-only Verify on equal-stake chains) before wrapping. A raw count can never produce this token. - AcceptWithCert is now the SOLE finalizer. It takes a VerifiedQuorumCert by value and refuses the zero value (ErrNoVerifiedQC). It is impossible to call the finalize body without first clearing the predicate. The old finalizePendingLocked body is unchanged — only its reachability is gated. - Every trigger (vote arrived, re-poll, pending-queue change, build/verify, poll timeout, gossiped-in cert) routes to TryAccept, which obtains a cert or returns ErrNoVerifiedQC and lets the trigger retry on its next tick. The incoming-cert path (quorum_topology.go) re-verifies then promotes via wrapVerifiedCert → AcceptWithCert — one finalization road for everything. - Remove the 3 ungated finalize sites: processPendingBlocks and DrainAccepted no longer finalize on count; they signal liveness only. The K==1 path finalizes through BuildSingleValidatorCert (the 1-of-1 quorum), still cert-gated. - Rename the count helper HasEnoughResponses -> HasEnoughResponsesForRetry to make its role unambiguous at every call site: it is a LIVENESS retry signal, never an acceptance authority (structural form of HIGH-3). Tests: accept_cert_gate_test.go proves AcceptWithCert(zero cert)==ErrNoVerifiedQC, that BuildVerifiedQuorumCert refuses sub-quorum / forged-signature vote sets, and that a real ⅔ quorum finalizes. red_processpending_stake_bypass_test.go + config/red_floor_differential_test.go pin the skewed-stake REJECT (the wedge) and the liveness path. Green under -race.
79 lines
2.7 KiB
Go
79 lines
2.7 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
|
// RED differential: prove TwoThirdsStakeFloor == the ORIGINAL inline cert math
|
|
// for all inputs incl. boundaries and near-2^64, and that it never overflows.
|
|
package config
|
|
|
|
import (
|
|
"math"
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
// origInlineFloor is the EXACT pre-refactor inline computation from
|
|
// QuorumCert.VerifyWeighted (git 056ead174^ engine/chain/quorum_cert.go).
|
|
func origInlineFloor(total uint64) uint64 {
|
|
q, r := total/3, total%3
|
|
twoThirdsFloor := 2 * q
|
|
if r == 2 {
|
|
twoThirdsFloor++
|
|
}
|
|
return twoThirdsFloor
|
|
}
|
|
|
|
// bigIntFloor is an independent oracle: floor(2*total/3) via 128-bit-safe math.
|
|
// 2*total can overflow uint64, so compute in the structurally-safe form.
|
|
func bigIntFloor(total uint64) uint64 {
|
|
// floor(2*total/3): use total/3 and 2*(total%3)/3 — but verify against a
|
|
// genuinely independent route: (2*total)/3 done in big-enough width.
|
|
hi := total / 3 * 2
|
|
lo := (total % 3) * 2 / 3
|
|
return hi + lo
|
|
}
|
|
|
|
func TestRED_FloorMatchesInlineExhaustiveSmall(t *testing.T) {
|
|
for total := uint64(0); total < 100000; total++ {
|
|
got := TwoThirdsStakeFloor(total)
|
|
if got != origInlineFloor(total) {
|
|
t.Fatalf("DIVERGENCE total=%d: refactor=%d inline=%d", total, got, origInlineFloor(total))
|
|
}
|
|
if got != bigIntFloor(total) {
|
|
t.Fatalf("ORACLE MISMATCH total=%d: refactor=%d oracle=%d", total, got, bigIntFloor(total))
|
|
}
|
|
// The floor must never reach total (a supermajority must be a strict subset
|
|
// of stake), and voted==total must always strictly exceed it.
|
|
if total >= 1 && !(total > got) {
|
|
t.Fatalf("floor>=total at total=%d (floor=%d) — full stake would not be a supermajority", total, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRED_FloorNoOverflowNear2p64(t *testing.T) {
|
|
edges := []uint64{
|
|
math.MaxUint64, math.MaxUint64 - 1, math.MaxUint64 - 2, math.MaxUint64 - 3,
|
|
1 << 63, (1 << 63) + 1, (1 << 63) - 1, 1 << 62, math.MaxUint64 / 3 * 3,
|
|
}
|
|
for _, total := range edges {
|
|
got := TwoThirdsStakeFloor(total)
|
|
if got != origInlineFloor(total) {
|
|
t.Fatalf("DIVERGENCE near-2^64 total=%d: refactor=%d inline=%d", total, got, origInlineFloor(total))
|
|
}
|
|
if got != bigIntFloor(total) {
|
|
t.Fatalf("ORACLE MISMATCH near-2^64 total=%d: refactor=%d oracle=%d", total, got, bigIntFloor(total))
|
|
}
|
|
// floor < total (no overflow wrap; supermajority is a strict subset).
|
|
if !(got < total) {
|
|
t.Fatalf("OVERFLOW/WRAP at total=%d: floor=%d not < total", total, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRED_FloorRandomDifferential(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(0xC0FFEE))
|
|
for i := 0; i < 5_000_000; i++ {
|
|
total := rng.Uint64()
|
|
if TwoThirdsStakeFloor(total) != origInlineFloor(total) {
|
|
t.Fatalf("DIVERGENCE total=%d", total)
|
|
}
|
|
}
|
|
}
|