mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
consensus: write the finality standard down as data
The C++ implementation signs 70 bytes under "LUX/chain/vote/v1\0"; this one signs 226 under v2. Neither side could ever verify the other, and nothing in the repository said so. The rule was only ever expressed as Go, so the only way to check another language against it was to read it and retype it — which is how the two drifted. conformance/corpus.json is the standard as data: the signed message, the certificate wire form, both stake floors across the full u64 range, the ladder's authorizations and the committee parameters, all read from the live definitions rather than restated beside them. An implementation in another language is conformant when it reproduces that file. A golden file alone would be weak — it can be re-blessed with -update and the change vanishes. So the load-bearing facts are also stated outright, transcribed from the spec and sharing no code with the encoder: the message is 226 bytes, the tag is the one the network uses, the two-thirds floor recomputed in arbitrary precision, and every signed axis moved one at a time to prove it is bound. Re-blessing the golden does not get past those. CanonicalRejectMessage is exported alongside it. The engine has always signed reject votes, but only the accept half could be reached from outside the package, so only half the standard could be written down. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -9,6 +9,36 @@ See [LP-105 §Claims and evidence](https://github.com/luxfi/lps/blob/main/LP-105
|
||||
[](https://github.com/luxfi/consensus/actions)
|
||||
[](https://go.dev)
|
||||
|
||||
## What decides finality
|
||||
|
||||
`engine/chain` is the engine `luxd` builds — `chains/manager.go` calls
|
||||
`consensuschain.NewRuntime` and injects a BLS `VoteVerifier`. It decides by
|
||||
metastable sampling to β-confidence and records the decision on the ladder in
|
||||
[`engine/chain/finality.go`](engine/chain/finality.go):
|
||||
|
||||
Photon → Wave → Nova → Quasar → Horizon
|
||||
|
||||
Nova is a strict majority of stake and authorizes LOCAL execution; it is
|
||||
reorgable. Quasar is a strict two-thirds of stake and is the export rung —
|
||||
bridges, DEX settlement, cross-chain, validator-set transitions. Both floors have
|
||||
one definition, `config.TwoThirdsStakeFloor` and `config.HalfStakeFloor`, which
|
||||
the certificate verifier enforces and the parameter sizer derives α from, so the
|
||||
count threshold cannot drift from the stake predicate.
|
||||
|
||||
The certificate ([`engine/chain/cert.go`](engine/chain/cert.go)) is the portable
|
||||
witness of a decision, not a second decider: α distinct validators each sign
|
||||
ACCEPT over one position, nothing is aggregated, and any node holding the votes
|
||||
assembles byte-identical bytes.
|
||||
|
||||
**The standard is written down.** The signed message, the certificate wire form,
|
||||
both stake floors, the ladder's authorizations and the committee parameters are
|
||||
emitted from those definitions into
|
||||
[`conformance/corpus.json`](conformance/corpus.json). An implementation in
|
||||
another language is conformant when it reproduces that file exactly. Regenerate
|
||||
with `go test ./conformance -update`; a diff there is a protocol change.
|
||||
|
||||
Of the packages below, `engine/chain` uses `protocol/quasar` and no other.
|
||||
|
||||
## Quasar Family of Consensus
|
||||
|
||||
Quasar is the consensus engine for the Lux network. It provides unified
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package conformance writes the finality standard down as data.
|
||||
//
|
||||
// Go is the network. The Rust and C++ implementations of Lux consensus are
|
||||
// correct exactly insofar as they reproduce what this package emits, byte for
|
||||
// byte. Everything here is READ from the live definitions — CanonicalVoteMessage,
|
||||
// QuorumCert.MarshalBinary, config.TwoThirdsStakeFloor, config.HalfStakeFloor,
|
||||
// chain.NovaSignerFloor, config.FeasibleParams, the Finality ladder — never
|
||||
// restated. There is one definition of each rule and this is its projection, so
|
||||
// an edit to a rule moves the corpus and the golden test fails loudly at the
|
||||
// source rather than at a foreign node six months later.
|
||||
//
|
||||
// The failure this exists to prevent already happened: the C++ implementation
|
||||
// signs a 70-byte message under tag "LUX/chain/vote/v1\0" while Go signs 226
|
||||
// bytes under "LUX/chain/vote/v2\0". Neither side could ever verify the other,
|
||||
// and nothing said so. A corpus captured from the Go definitions says so on the
|
||||
// first run.
|
||||
//
|
||||
// Determinism is total: no clock, no randomness, no I/O, no map iteration in the
|
||||
// output. Every uint64 is written as a decimal STRING — a stake total near 2^64
|
||||
// loses precision in a JSON parser that keeps numbers as doubles, and a
|
||||
// conformance comparison must never be lost to formatting.
|
||||
package conformance
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/luxfi/consensus/config"
|
||||
"github.com/luxfi/consensus/engine/chain"
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// Corpus is the whole standard: what a validator signs, what a certificate looks
|
||||
// like on the wire, the thresholds that decide finality, the ladder of
|
||||
// authorizations, and the committee parameters a node sizes itself to.
|
||||
type Corpus struct {
|
||||
Standard string `json:"standard"`
|
||||
Source string `json:"source"`
|
||||
Version uint16 `json:"version"`
|
||||
Vote Vote `json:"vote"`
|
||||
Cert Cert `json:"cert"`
|
||||
Ladder []Rung `json:"ladder"`
|
||||
Threshold Threshold `json:"threshold"`
|
||||
Committee []Committee `json:"committee"`
|
||||
}
|
||||
|
||||
// Vote is the signed message: the statement every quorum argument is about.
|
||||
type Vote struct {
|
||||
Tag string `json:"tag"`
|
||||
QCType uint8 `json:"qcType"`
|
||||
Length int `json:"length"`
|
||||
Layout []string `json:"layout"`
|
||||
Cases []VoteCase `json:"cases"`
|
||||
}
|
||||
|
||||
// VoteCase is one position and the exact bytes a validator's key signs over it.
|
||||
type VoteCase struct {
|
||||
Name string `json:"name"`
|
||||
Note string `json:"note"`
|
||||
ChainID string `json:"chainID"`
|
||||
Height string `json:"height"`
|
||||
Round uint32 `json:"round"`
|
||||
BlockID string `json:"blockID"`
|
||||
ParentID string `json:"parentID"`
|
||||
CanonicalID string `json:"canonicalID"`
|
||||
ParentCanonicalID string `json:"parentCanonicalID"`
|
||||
ExecutionStateRoot string `json:"executionStateRoot"`
|
||||
PayloadRoot string `json:"payloadRoot"`
|
||||
ValidatorSetRoot string `json:"validatorSetRoot"`
|
||||
Accept bool `json:"accept"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Cert is the gossiped finality witness: the wire form of a QuorumCert.
|
||||
type Cert struct {
|
||||
Layout []string `json:"layout"`
|
||||
Cases []CertCase `json:"cases"`
|
||||
}
|
||||
|
||||
// CertCase is one assembled certificate and its wire bytes. Votes are recorded
|
||||
// in the order the encoder emits them, which is the order AssembleQuorumCert
|
||||
// produced — strictly increasing by node id, whatever order they arrived in.
|
||||
type CertCase struct {
|
||||
Name string `json:"name"`
|
||||
Note string `json:"note"`
|
||||
Tier string `json:"tier"`
|
||||
Threshold uint32 `json:"threshold"`
|
||||
Position string `json:"position"`
|
||||
Votes []CertVote `json:"votes"`
|
||||
Wire string `json:"wire"`
|
||||
Length int `json:"length"`
|
||||
}
|
||||
|
||||
// CertVote is one signed record inside a certificate.
|
||||
type CertVote struct {
|
||||
NodeID string `json:"nodeID"`
|
||||
Accept bool `json:"accept"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// Rung is one step of the finality ladder and what it authorizes. The three
|
||||
// booleans are the whole safety boundary: a block below Quasar may drive local
|
||||
// execution and may never be exported.
|
||||
type Rung struct {
|
||||
Name string `json:"name"`
|
||||
Value uint8 `json:"value"`
|
||||
AuthorizesLocalExecution bool `json:"authorizesLocalExecution"`
|
||||
AuthorizesExport bool `json:"authorizesExport"`
|
||||
AuthorizesIrreversibleSettlement bool `json:"authorizesIrreversibleSettlement"`
|
||||
}
|
||||
|
||||
// Threshold is every finality predicate as a table of inputs and outputs.
|
||||
type Threshold struct {
|
||||
Stake []StakeFloor `json:"stake"`
|
||||
Count []CountFloor `json:"count"`
|
||||
Weighted []Weighted `json:"weighted"`
|
||||
StakeNote string `json:"stakeNote"`
|
||||
CountNote string `json:"countNote"`
|
||||
}
|
||||
|
||||
// StakeFloor pairs a total stake with the two floors read off it. A quorum must
|
||||
// STRICTLY EXCEED the floor for its rung; equality is not a quorum.
|
||||
type StakeFloor struct {
|
||||
Total string `json:"total"`
|
||||
TwoThirds string `json:"twoThirds"`
|
||||
Half string `json:"half"`
|
||||
QuasarNeed string `json:"quasarNeed"`
|
||||
NovaNeed string `json:"novaNeed"`
|
||||
}
|
||||
|
||||
// CountFloor pairs a live validator count with the count-side thresholds.
|
||||
type CountFloor struct {
|
||||
N int `json:"n"`
|
||||
NovaQuorum int `json:"novaQuorum"`
|
||||
NovaSignerFloor int `json:"novaSignerFloor"`
|
||||
NovaBeta int `json:"novaBeta"`
|
||||
CrashTolerance int `json:"crashTolerance"`
|
||||
EqualStakeQuasar int `json:"equalStakeQuasar"`
|
||||
}
|
||||
|
||||
// Weighted is the minimum vote count that CAN reach the ⅔-by-stake predicate for
|
||||
// a given weight vector — the count the parameter sizer derives from the same
|
||||
// floor the certificate verifier enforces.
|
||||
type Weighted struct {
|
||||
Note string `json:"note"`
|
||||
Weights []string `json:"weights"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// Committee is the parameter set a node sizes itself to for a live validator
|
||||
// count on a network. K and alpha are derived purely from the live set; only
|
||||
// timing varies by network.
|
||||
type Committee struct {
|
||||
Network string `json:"network"`
|
||||
NetworkID uint32 `json:"networkID"`
|
||||
N int `json:"n"`
|
||||
K int `json:"k"`
|
||||
AlphaPreference int `json:"alphaPreference"`
|
||||
AlphaConfidence int `json:"alphaConfidence"`
|
||||
Beta uint32 `json:"beta"`
|
||||
BetaVirtuous int `json:"betaVirtuous"`
|
||||
BetaRogue int `json:"betaRogue"`
|
||||
BlockTimeMS int64 `json:"blockTimeMS"`
|
||||
RoundTimeoutMS int64 `json:"roundTimeoutMS"`
|
||||
}
|
||||
|
||||
// Build reads the live definitions and returns the standard. Deterministic: the
|
||||
// same binary always returns the same value.
|
||||
func Build() Corpus {
|
||||
return Corpus{
|
||||
Standard: "lux consensus finality",
|
||||
Source: "github.com/luxfi/consensus",
|
||||
Version: chain.QuorumCertVersion,
|
||||
Vote: Vote{
|
||||
Tag: "LUX/chain/vote/v2\x00",
|
||||
QCType: uint8(chain.QCFinality),
|
||||
Length: len(chain.CanonicalVoteMessage(chain.VotePosition{})),
|
||||
Layout: []string{
|
||||
"tag:18", "version:2", "qcType:1",
|
||||
"chainID:32", "height:8", "round:4",
|
||||
"canonicalID:32", "parentCanonicalID:32",
|
||||
"executionStateRoot:32", "payloadRoot:32", "validatorSetRoot:32",
|
||||
"accept:1",
|
||||
},
|
||||
Cases: voteCases(),
|
||||
},
|
||||
Cert: Cert{
|
||||
Layout: []string{
|
||||
"version:2", "type:1", "tier:1",
|
||||
"chainID:32", "height:8", "round:4",
|
||||
"blockID:32", "parentID:32",
|
||||
"canonicalID:32", "parentCanonicalID:32",
|
||||
"executionStateRoot:32", "payloadRoot:32", "validatorSetRoot:32",
|
||||
"threshold:4", "voteCount:4",
|
||||
"per vote: nodeID:20 accept:1 sigLen:4 sig:sigLen",
|
||||
},
|
||||
Cases: certCases(),
|
||||
},
|
||||
Ladder: ladder(),
|
||||
Threshold: thresholds(),
|
||||
Committee: committees(),
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal renders the corpus as the canonical JSON document the corpus repo
|
||||
// carries. Indented two spaces, newline-terminated, no HTML escaping — so the
|
||||
// bytes are readable and a diff is a real diff.
|
||||
func Marshal(c Corpus) ([]byte, error) {
|
||||
b, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(b, '\n'), nil
|
||||
}
|
||||
|
||||
// fill returns the id whose every byte is b — a value recognisable on sight in a
|
||||
// hexdump, so a field emitted in the wrong slot reads as wrong rather than
|
||||
// merely unequal.
|
||||
func fill(b byte) ids.ID {
|
||||
var id ids.ID
|
||||
for i := range id {
|
||||
id[i] = b
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func fillNode(b byte) ids.NodeID {
|
||||
var n ids.NodeID
|
||||
for i := range n {
|
||||
n[i] = b
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func u64(v uint64) string { return strconv.FormatUint(v, 10) }
|
||||
|
||||
// voteCase records a position and the message the live builder produces for it.
|
||||
func voteCase(name, note string, pos chain.VotePosition, accept bool) VoteCase {
|
||||
var msg []byte
|
||||
if accept {
|
||||
msg = chain.CanonicalVoteMessage(pos)
|
||||
} else {
|
||||
msg = chain.CanonicalRejectMessage(pos)
|
||||
}
|
||||
return VoteCase{
|
||||
Name: name,
|
||||
Note: note,
|
||||
ChainID: pos.ChainID.Hex(),
|
||||
Height: u64(pos.Height),
|
||||
Round: pos.Round,
|
||||
BlockID: pos.BlockID.Hex(),
|
||||
ParentID: pos.ParentID.Hex(),
|
||||
CanonicalID: pos.CanonicalID.Hex(),
|
||||
ParentCanonicalID: pos.ParentCanonicalID.Hex(),
|
||||
ExecutionStateRoot: pos.ExecutionStateRoot.Hex(),
|
||||
PayloadRoot: pos.PayloadRoot.Hex(),
|
||||
ValidatorSetRoot: pos.ValidatorSetRoot.Hex(),
|
||||
Accept: accept,
|
||||
Message: hex.EncodeToString(msg),
|
||||
}
|
||||
}
|
||||
|
||||
// spec is the reference position: every axis a distinct recognisable value, a
|
||||
// height and round that exercise every byte of their widths.
|
||||
func spec() chain.VotePosition {
|
||||
return chain.VotePosition{
|
||||
ChainID: fill(0x11),
|
||||
Height: 0x0102030405060708,
|
||||
Round: 0x0A0B0C0D,
|
||||
BlockID: fill(0x22),
|
||||
ParentID: fill(0x33),
|
||||
CanonicalID: fill(0x44),
|
||||
ParentCanonicalID: fill(0x55),
|
||||
ExecutionStateRoot: fill(0x66),
|
||||
PayloadRoot: fill(0x77),
|
||||
ValidatorSetRoot: fill(0x88),
|
||||
}
|
||||
}
|
||||
|
||||
func voteCases() []VoteCase {
|
||||
// The outer envelope moved, everything signed held fixed. The message must
|
||||
// not move with it — that is the whole reason the transport ids are absent.
|
||||
outerMoved := spec()
|
||||
outerMoved.BlockID = fill(0xE1)
|
||||
outerMoved.ParentID = fill(0xE2)
|
||||
|
||||
// A block with no inner/outer split: the canonical slots are unset and the
|
||||
// builder binds the outer ids under them. The pair below states the degrade
|
||||
// twice — implicitly and explicitly — and both must produce one message.
|
||||
degrade := chain.VotePosition{
|
||||
ChainID: fill(0x11),
|
||||
Height: 9,
|
||||
Round: 1,
|
||||
BlockID: fill(0x22),
|
||||
ParentID: fill(0x33),
|
||||
}
|
||||
degradeExplicit := degrade
|
||||
degradeExplicit.CanonicalID = fill(0x22)
|
||||
degradeExplicit.ParentCanonicalID = fill(0x33)
|
||||
|
||||
// Every width at its maximum: a builder that truncates a field or writes it
|
||||
// little-endian cannot survive this one.
|
||||
max := chain.VotePosition{
|
||||
ChainID: fill(0xFF),
|
||||
Height: ^uint64(0),
|
||||
Round: ^uint32(0),
|
||||
BlockID: fill(0xFF),
|
||||
ParentID: fill(0xFF),
|
||||
CanonicalID: fill(0xFF),
|
||||
ParentCanonicalID: fill(0xFF),
|
||||
ExecutionStateRoot: fill(0xFF),
|
||||
PayloadRoot: fill(0xFF),
|
||||
ValidatorSetRoot: fill(0xFF),
|
||||
}
|
||||
|
||||
return []VoteCase{
|
||||
voteCase("zero", "every axis empty, accept — the minimal signed message",
|
||||
chain.VotePosition{}, true),
|
||||
voteCase("zero_reject", "every axis empty, reject — differs from accept in the final byte alone",
|
||||
chain.VotePosition{}, false),
|
||||
voteCase("spec", "the reference position; the transport ids (blockID, parentID) do not appear in the message",
|
||||
spec(), true),
|
||||
voteCase("spec_reject", "the reference position, reject",
|
||||
spec(), false),
|
||||
voteCase("outer_moved", "the reference position with both transport ids changed — the message is unchanged",
|
||||
outerMoved, true),
|
||||
voteCase("degrade", "canonical slots unset: the transport ids are bound under them",
|
||||
degrade, true),
|
||||
voteCase("degrade_explicit", "the same position with canonical == outer stated outright — identical bytes",
|
||||
degradeExplicit, true),
|
||||
voteCase("max", "every field at its maximum width — catches a truncated or byte-swapped integer",
|
||||
max, true),
|
||||
}
|
||||
}
|
||||
|
||||
// certCases assembles certificates through the live AssembleQuorumCert and
|
||||
// records the bytes MarshalBinary produces. Signatures are recognisable fills:
|
||||
// the codec does not verify them, and a corpus that needed real keys would need
|
||||
// a key ceremony to be readable.
|
||||
func certCases() []CertCase {
|
||||
pos := spec()
|
||||
|
||||
sig := func(b byte, n int) []byte {
|
||||
s := make([]byte, n)
|
||||
for i := range s {
|
||||
s[i] = b
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// One vote, the local-execution rung.
|
||||
nova := []chain.SignedVote{
|
||||
{NodeID: fillNode(0xA1), Accept: true, Signature: sig(0x01, 8)},
|
||||
}
|
||||
// Three votes handed in DESCENDING order. The encoder must emit them
|
||||
// ascending — assembly order is part of the standard, not a detail of the
|
||||
// caller, or two nodes holding one quorum would gossip two different certs.
|
||||
quasar := []chain.SignedVote{
|
||||
{NodeID: fillNode(0xC3), Accept: true, Signature: sig(0x33, 4)},
|
||||
{NodeID: fillNode(0xA1), Accept: true, Signature: sig(0x11, 96)},
|
||||
{NodeID: fillNode(0xB2), Accept: true, Signature: sig(0x22, 48)},
|
||||
}
|
||||
// A zero-length signature exercises the length prefix on its own.
|
||||
empty := []chain.SignedVote{
|
||||
{NodeID: fillNode(0x01), Accept: true, Signature: nil},
|
||||
}
|
||||
|
||||
build := func(name, note string, tier chain.Finality, threshold uint32, votes []chain.SignedVote) CertCase {
|
||||
cert, err := chain.AssembleQuorumCert(pos, tier, threshold, votes)
|
||||
if err != nil {
|
||||
panic("conformance: " + name + ": " + err.Error())
|
||||
}
|
||||
wire, err := cert.MarshalBinary()
|
||||
if err != nil {
|
||||
panic("conformance: " + name + ": " + err.Error())
|
||||
}
|
||||
recorded := make([]CertVote, 0, len(cert.Votes))
|
||||
for i := range cert.Votes {
|
||||
v := &cert.Votes[i]
|
||||
recorded = append(recorded, CertVote{
|
||||
// Hex, like every other identifier here. NodeID.String() is
|
||||
// base58 with a checksum, and a harness in another language
|
||||
// should not have to carry that codec to compare an id.
|
||||
NodeID: hex.EncodeToString(v.NodeID[:]),
|
||||
Accept: v.Accept,
|
||||
Signature: hex.EncodeToString(v.Signature),
|
||||
})
|
||||
}
|
||||
return CertCase{
|
||||
Name: name,
|
||||
Note: note,
|
||||
Tier: cert.Tier.String(),
|
||||
Threshold: cert.Threshold,
|
||||
Position: "spec",
|
||||
Votes: recorded,
|
||||
Wire: hex.EncodeToString(wire),
|
||||
Length: len(wire),
|
||||
}
|
||||
}
|
||||
|
||||
return []CertCase{
|
||||
build("nova_single", "one signer at the local-execution rung — the smallest well-formed certificate",
|
||||
chain.Nova, 1, nova),
|
||||
build("quasar_sorted", "votes handed in descending order; the wire carries them ascending by node id",
|
||||
chain.Quasar, 3, quasar),
|
||||
build("empty_signature", "a zero-length signature — the length prefix stands alone",
|
||||
chain.Nova, 1, empty),
|
||||
}
|
||||
}
|
||||
|
||||
func ladder() []Rung {
|
||||
rungs := []chain.Finality{chain.Photon, chain.Wave, chain.Nova, chain.Quasar, chain.Horizon}
|
||||
out := make([]Rung, 0, len(rungs))
|
||||
for _, f := range rungs {
|
||||
out = append(out, Rung{
|
||||
Name: f.String(),
|
||||
Value: uint8(f),
|
||||
AuthorizesLocalExecution: f.AuthorizesLocalExecution(),
|
||||
AuthorizesExport: f.AuthorizesExport(),
|
||||
AuthorizesIrreversibleSettlement: f.AuthorizesIrreversibleSettlement(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func thresholds() Threshold {
|
||||
// Totals chosen for the boundaries: the three residues mod 3, the powers,
|
||||
// and the top of the range where a naive 2·total overflows.
|
||||
totals := []uint64{
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 99, 100, 101, 102,
|
||||
1 << 20, 1<<32 - 1, 1 << 32,
|
||||
4611686018427387904, // 2^62
|
||||
6148914691236517205, // (2^64-1)/3
|
||||
18446744073709551614, // 2^64-2
|
||||
18446744073709551615, // 2^64-1 — 2·total overflows, floor(2·total/3) must not
|
||||
}
|
||||
stake := make([]StakeFloor, 0, len(totals))
|
||||
for _, t := range totals {
|
||||
two := config.TwoThirdsStakeFloor(t)
|
||||
half := config.HalfStakeFloor(t)
|
||||
stake = append(stake, StakeFloor{
|
||||
Total: u64(t),
|
||||
TwoThirds: u64(two),
|
||||
Half: u64(half),
|
||||
QuasarNeed: u64(two + 1),
|
||||
NovaNeed: u64(half + 1),
|
||||
})
|
||||
}
|
||||
|
||||
ns := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 20, 21, 22, 100, 1000}
|
||||
count := make([]CountFloor, 0, len(ns))
|
||||
for _, n := range ns {
|
||||
count = append(count, CountFloor{
|
||||
N: n,
|
||||
NovaQuorum: chain.NovaQuorum(n),
|
||||
NovaSignerFloor: chain.NovaSignerFloor(n),
|
||||
NovaBeta: chain.NovaBeta(n),
|
||||
CrashTolerance: chain.CrashTolerance(n),
|
||||
EqualStakeQuasar: config.EqualStakeSupermajorityThreshold(n),
|
||||
})
|
||||
}
|
||||
|
||||
vectors := []struct {
|
||||
note string
|
||||
weights []uint64
|
||||
}{
|
||||
{"equal stake, five validators — collapses to ⌊2n/3⌋+1", []uint64{1, 1, 1, 1, 1}},
|
||||
{"equal stake, twenty-one validators — 15, not 14: 14/21 does not strictly exceed ⅔", []uint64{
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}},
|
||||
{"one validator holds most of the stake", []uint64{100, 1, 1, 1, 1}},
|
||||
{"two heavy validators cannot reach ⅔ alone", []uint64{5, 5, 4, 3, 3}},
|
||||
{"a dust registration must not move the gate", []uint64{1000, 1000, 1000, 1}},
|
||||
{"empty set — no stake model, fail closed", nil},
|
||||
{"all weights zero — no stake model, fail closed", []uint64{0, 0, 0}},
|
||||
}
|
||||
weighted := make([]Weighted, 0, len(vectors))
|
||||
for _, v := range vectors {
|
||||
ws := make([]string, 0, len(v.weights))
|
||||
for _, w := range v.weights {
|
||||
ws = append(ws, u64(w))
|
||||
}
|
||||
weighted = append(weighted, Weighted{
|
||||
Note: v.note,
|
||||
Weights: ws,
|
||||
Count: config.WeightedSupermajorityThreshold(v.weights),
|
||||
})
|
||||
}
|
||||
|
||||
return Threshold{
|
||||
Stake: stake,
|
||||
Count: count,
|
||||
Weighted: weighted,
|
||||
StakeNote: "a quorum must STRICTLY EXCEED its floor. Quasar (export) needs voted > twoThirds; " +
|
||||
"Nova (local execution) needs voted > half. Equality is not a quorum.",
|
||||
CountNote: "novaSignerFloor is the count guard the stake predicate cannot give — a lone holder " +
|
||||
"of a stake majority must not self-ignite. equalStakeQuasar is the ⅔ count for a uniform set.",
|
||||
}
|
||||
}
|
||||
|
||||
func committees() []Committee {
|
||||
nets := []struct {
|
||||
name string
|
||||
id uint32
|
||||
}{
|
||||
{"mainnet", constants.MainnetID},
|
||||
{"testnet", constants.TestnetID},
|
||||
{"local", constants.LocalID},
|
||||
}
|
||||
ns := []int{1, 4, 5, 11, 21, 100}
|
||||
out := make([]Committee, 0, len(nets)*len(ns))
|
||||
for _, net := range nets {
|
||||
for _, n := range ns {
|
||||
p := config.FeasibleParams(net.id, n)
|
||||
out = append(out, Committee{
|
||||
Network: net.name,
|
||||
NetworkID: net.id,
|
||||
N: n,
|
||||
K: p.K,
|
||||
AlphaPreference: p.AlphaPreference,
|
||||
AlphaConfidence: p.AlphaConfidence,
|
||||
Beta: p.Beta,
|
||||
BetaVirtuous: p.BetaVirtuous,
|
||||
BetaRogue: p.BetaRogue,
|
||||
BlockTimeMS: p.BlockTime.Milliseconds(),
|
||||
RoundTimeoutMS: p.RoundTO.Milliseconds(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
{
|
||||
"standard": "lux consensus finality",
|
||||
"source": "github.com/luxfi/consensus",
|
||||
"version": 3,
|
||||
"vote": {
|
||||
"tag": "LUX/chain/vote/v2\u0000",
|
||||
"qcType": 1,
|
||||
"length": 226,
|
||||
"layout": [
|
||||
"tag:18",
|
||||
"version:2",
|
||||
"qcType:1",
|
||||
"chainID:32",
|
||||
"height:8",
|
||||
"round:4",
|
||||
"canonicalID:32",
|
||||
"parentCanonicalID:32",
|
||||
"executionStateRoot:32",
|
||||
"payloadRoot:32",
|
||||
"validatorSetRoot:32",
|
||||
"accept:1"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"name": "zero",
|
||||
"note": "every axis empty, accept — the minimal signed message",
|
||||
"chainID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"height": "0",
|
||||
"round": 0,
|
||||
"blockID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"canonicalID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentCanonicalID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"executionStateRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"payloadRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"validatorSetRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"accept": true,
|
||||
"message": "4c55582f636861696e2f766f74652f76320000030100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
|
||||
},
|
||||
{
|
||||
"name": "zero_reject",
|
||||
"note": "every axis empty, reject — differs from accept in the final byte alone",
|
||||
"chainID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"height": "0",
|
||||
"round": 0,
|
||||
"blockID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"canonicalID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentCanonicalID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"executionStateRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"payloadRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"validatorSetRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"accept": false,
|
||||
"message": "4c55582f636861696e2f766f74652f76320000030100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
{
|
||||
"name": "spec",
|
||||
"note": "the reference position; the transport ids (blockID, parentID) do not appear in the message",
|
||||
"chainID": "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"height": "72623859790382856",
|
||||
"round": 168496141,
|
||||
"blockID": "2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"parentID": "3333333333333333333333333333333333333333333333333333333333333333",
|
||||
"canonicalID": "4444444444444444444444444444444444444444444444444444444444444444",
|
||||
"parentCanonicalID": "5555555555555555555555555555555555555555555555555555555555555555",
|
||||
"executionStateRoot": "6666666666666666666666666666666666666666666666666666666666666666",
|
||||
"payloadRoot": "7777777777777777777777777777777777777777777777777777777777777777",
|
||||
"validatorSetRoot": "8888888888888888888888888888888888888888888888888888888888888888",
|
||||
"accept": true,
|
||||
"message": "4c55582f636861696e2f766f74652f763200000301111111111111111111111111111111111111111111111111111111111111111101020304050607080a0b0c0d4444444444444444444444444444444444444444444444444444444444444444555555555555555555555555555555555555555555555555555555555555555566666666666666666666666666666666666666666666666666666666666666667777777777777777777777777777777777777777777777777777777777777777888888888888888888888888888888888888888888888888888888888888888801"
|
||||
},
|
||||
{
|
||||
"name": "spec_reject",
|
||||
"note": "the reference position, reject",
|
||||
"chainID": "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"height": "72623859790382856",
|
||||
"round": 168496141,
|
||||
"blockID": "2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"parentID": "3333333333333333333333333333333333333333333333333333333333333333",
|
||||
"canonicalID": "4444444444444444444444444444444444444444444444444444444444444444",
|
||||
"parentCanonicalID": "5555555555555555555555555555555555555555555555555555555555555555",
|
||||
"executionStateRoot": "6666666666666666666666666666666666666666666666666666666666666666",
|
||||
"payloadRoot": "7777777777777777777777777777777777777777777777777777777777777777",
|
||||
"validatorSetRoot": "8888888888888888888888888888888888888888888888888888888888888888",
|
||||
"accept": false,
|
||||
"message": "4c55582f636861696e2f766f74652f763200000301111111111111111111111111111111111111111111111111111111111111111101020304050607080a0b0c0d4444444444444444444444444444444444444444444444444444444444444444555555555555555555555555555555555555555555555555555555555555555566666666666666666666666666666666666666666666666666666666666666667777777777777777777777777777777777777777777777777777777777777777888888888888888888888888888888888888888888888888888888888888888800"
|
||||
},
|
||||
{
|
||||
"name": "outer_moved",
|
||||
"note": "the reference position with both transport ids changed — the message is unchanged",
|
||||
"chainID": "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"height": "72623859790382856",
|
||||
"round": 168496141,
|
||||
"blockID": "e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1",
|
||||
"parentID": "e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2e2",
|
||||
"canonicalID": "4444444444444444444444444444444444444444444444444444444444444444",
|
||||
"parentCanonicalID": "5555555555555555555555555555555555555555555555555555555555555555",
|
||||
"executionStateRoot": "6666666666666666666666666666666666666666666666666666666666666666",
|
||||
"payloadRoot": "7777777777777777777777777777777777777777777777777777777777777777",
|
||||
"validatorSetRoot": "8888888888888888888888888888888888888888888888888888888888888888",
|
||||
"accept": true,
|
||||
"message": "4c55582f636861696e2f766f74652f763200000301111111111111111111111111111111111111111111111111111111111111111101020304050607080a0b0c0d4444444444444444444444444444444444444444444444444444444444444444555555555555555555555555555555555555555555555555555555555555555566666666666666666666666666666666666666666666666666666666666666667777777777777777777777777777777777777777777777777777777777777777888888888888888888888888888888888888888888888888888888888888888801"
|
||||
},
|
||||
{
|
||||
"name": "degrade",
|
||||
"note": "canonical slots unset: the transport ids are bound under them",
|
||||
"chainID": "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"height": "9",
|
||||
"round": 1,
|
||||
"blockID": "2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"parentID": "3333333333333333333333333333333333333333333333333333333333333333",
|
||||
"canonicalID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"parentCanonicalID": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"executionStateRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"payloadRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"validatorSetRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"accept": true,
|
||||
"message": "4c55582f636861696e2f766f74652f76320000030111111111111111111111111111111111111111111111111111111111111111110000000000000009000000012222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
|
||||
},
|
||||
{
|
||||
"name": "degrade_explicit",
|
||||
"note": "the same position with canonical == outer stated outright — identical bytes",
|
||||
"chainID": "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"height": "9",
|
||||
"round": 1,
|
||||
"blockID": "2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"parentID": "3333333333333333333333333333333333333333333333333333333333333333",
|
||||
"canonicalID": "2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"parentCanonicalID": "3333333333333333333333333333333333333333333333333333333333333333",
|
||||
"executionStateRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"payloadRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"validatorSetRoot": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"accept": true,
|
||||
"message": "4c55582f636861696e2f766f74652f76320000030111111111111111111111111111111111111111111111111111111111111111110000000000000009000000012222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"
|
||||
},
|
||||
{
|
||||
"name": "max",
|
||||
"note": "every field at its maximum width — catches a truncated or byte-swapped integer",
|
||||
"chainID": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"height": "18446744073709551615",
|
||||
"round": 4294967295,
|
||||
"blockID": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"parentID": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"canonicalID": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"parentCanonicalID": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"executionStateRoot": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"payloadRoot": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"validatorSetRoot": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"accept": true,
|
||||
"message": "4c55582f636861696e2f766f74652f763200000301ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cert": {
|
||||
"layout": [
|
||||
"version:2",
|
||||
"type:1",
|
||||
"tier:1",
|
||||
"chainID:32",
|
||||
"height:8",
|
||||
"round:4",
|
||||
"blockID:32",
|
||||
"parentID:32",
|
||||
"canonicalID:32",
|
||||
"parentCanonicalID:32",
|
||||
"executionStateRoot:32",
|
||||
"payloadRoot:32",
|
||||
"validatorSetRoot:32",
|
||||
"threshold:4",
|
||||
"voteCount:4",
|
||||
"per vote: nodeID:20 accept:1 sigLen:4 sig:sigLen"
|
||||
],
|
||||
"cases": [
|
||||
{
|
||||
"name": "nova_single",
|
||||
"note": "one signer at the local-execution rung — the smallest well-formed certificate",
|
||||
"tier": "nova",
|
||||
"threshold": 1,
|
||||
"position": "spec",
|
||||
"votes": [
|
||||
{
|
||||
"nodeID": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
"accept": true,
|
||||
"signature": "0101010101010101"
|
||||
}
|
||||
],
|
||||
"wire": "00030102111111111111111111111111111111111111111111111111111111111111111101020304050607080a0b0c0d22222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444455555555555555555555555555555555555555555555555555555555555555556666666666666666666666666666666666666666666666666666666666666666777777777777777777777777777777777777777777777777777777777777777788888888888888888888888888888888888888888888888888888888888888880000000100000001a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a101000000080101010101010101",
|
||||
"length": 313
|
||||
},
|
||||
{
|
||||
"name": "quasar_sorted",
|
||||
"note": "votes handed in descending order; the wire carries them ascending by node id",
|
||||
"tier": "quasar",
|
||||
"threshold": 3,
|
||||
"position": "spec",
|
||||
"votes": [
|
||||
{
|
||||
"nodeID": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
||||
"accept": true,
|
||||
"signature": "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
|
||||
},
|
||||
{
|
||||
"nodeID": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
||||
"accept": true,
|
||||
"signature": "222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222"
|
||||
},
|
||||
{
|
||||
"nodeID": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
|
||||
"accept": true,
|
||||
"signature": "33333333"
|
||||
}
|
||||
],
|
||||
"wire": "00030103111111111111111111111111111111111111111111111111111111111111111101020304050607080a0b0c0d22222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444455555555555555555555555555555555555555555555555555555555555555556666666666666666666666666666666666666666666666666666666666666666777777777777777777777777777777777777777777777777777777777777777788888888888888888888888888888888888888888888888888888888888888880000000300000003a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a10100000060111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b20100000030222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3010000000433333333",
|
||||
"length": 503
|
||||
},
|
||||
{
|
||||
"name": "empty_signature",
|
||||
"note": "a zero-length signature — the length prefix stands alone",
|
||||
"tier": "nova",
|
||||
"threshold": 1,
|
||||
"position": "spec",
|
||||
"votes": [
|
||||
{
|
||||
"nodeID": "0101010101010101010101010101010101010101",
|
||||
"accept": true,
|
||||
"signature": ""
|
||||
}
|
||||
],
|
||||
"wire": "00030102111111111111111111111111111111111111111111111111111111111111111101020304050607080a0b0c0d2222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333344444444444444444444444444444444444444444444444444444444444444445555555555555555555555555555555555555555555555555555555555555555666666666666666666666666666666666666666666666666666666666666666677777777777777777777777777777777777777777777777777777777777777778888888888888888888888888888888888888888888888888888888888888888000000010000000101010101010101010101010101010101010101010100000000",
|
||||
"length": 305
|
||||
}
|
||||
]
|
||||
},
|
||||
"ladder": [
|
||||
{
|
||||
"name": "photon",
|
||||
"value": 0,
|
||||
"authorizesLocalExecution": false,
|
||||
"authorizesExport": false,
|
||||
"authorizesIrreversibleSettlement": false
|
||||
},
|
||||
{
|
||||
"name": "wave",
|
||||
"value": 1,
|
||||
"authorizesLocalExecution": false,
|
||||
"authorizesExport": false,
|
||||
"authorizesIrreversibleSettlement": false
|
||||
},
|
||||
{
|
||||
"name": "nova",
|
||||
"value": 2,
|
||||
"authorizesLocalExecution": true,
|
||||
"authorizesExport": false,
|
||||
"authorizesIrreversibleSettlement": false
|
||||
},
|
||||
{
|
||||
"name": "quasar",
|
||||
"value": 3,
|
||||
"authorizesLocalExecution": true,
|
||||
"authorizesExport": true,
|
||||
"authorizesIrreversibleSettlement": false
|
||||
},
|
||||
{
|
||||
"name": "horizon",
|
||||
"value": 4,
|
||||
"authorizesLocalExecution": true,
|
||||
"authorizesExport": true,
|
||||
"authorizesIrreversibleSettlement": true
|
||||
}
|
||||
],
|
||||
"threshold": {
|
||||
"stake": [
|
||||
{
|
||||
"total": "0",
|
||||
"twoThirds": "0",
|
||||
"half": "0",
|
||||
"quasarNeed": "1",
|
||||
"novaNeed": "1"
|
||||
},
|
||||
{
|
||||
"total": "1",
|
||||
"twoThirds": "0",
|
||||
"half": "0",
|
||||
"quasarNeed": "1",
|
||||
"novaNeed": "1"
|
||||
},
|
||||
{
|
||||
"total": "2",
|
||||
"twoThirds": "1",
|
||||
"half": "1",
|
||||
"quasarNeed": "2",
|
||||
"novaNeed": "2"
|
||||
},
|
||||
{
|
||||
"total": "3",
|
||||
"twoThirds": "2",
|
||||
"half": "1",
|
||||
"quasarNeed": "3",
|
||||
"novaNeed": "2"
|
||||
},
|
||||
{
|
||||
"total": "4",
|
||||
"twoThirds": "2",
|
||||
"half": "2",
|
||||
"quasarNeed": "3",
|
||||
"novaNeed": "3"
|
||||
},
|
||||
{
|
||||
"total": "5",
|
||||
"twoThirds": "3",
|
||||
"half": "2",
|
||||
"quasarNeed": "4",
|
||||
"novaNeed": "3"
|
||||
},
|
||||
{
|
||||
"total": "6",
|
||||
"twoThirds": "4",
|
||||
"half": "3",
|
||||
"quasarNeed": "5",
|
||||
"novaNeed": "4"
|
||||
},
|
||||
{
|
||||
"total": "7",
|
||||
"twoThirds": "4",
|
||||
"half": "3",
|
||||
"quasarNeed": "5",
|
||||
"novaNeed": "4"
|
||||
},
|
||||
{
|
||||
"total": "99",
|
||||
"twoThirds": "66",
|
||||
"half": "49",
|
||||
"quasarNeed": "67",
|
||||
"novaNeed": "50"
|
||||
},
|
||||
{
|
||||
"total": "100",
|
||||
"twoThirds": "66",
|
||||
"half": "50",
|
||||
"quasarNeed": "67",
|
||||
"novaNeed": "51"
|
||||
},
|
||||
{
|
||||
"total": "101",
|
||||
"twoThirds": "67",
|
||||
"half": "50",
|
||||
"quasarNeed": "68",
|
||||
"novaNeed": "51"
|
||||
},
|
||||
{
|
||||
"total": "102",
|
||||
"twoThirds": "68",
|
||||
"half": "51",
|
||||
"quasarNeed": "69",
|
||||
"novaNeed": "52"
|
||||
},
|
||||
{
|
||||
"total": "1048576",
|
||||
"twoThirds": "699050",
|
||||
"half": "524288",
|
||||
"quasarNeed": "699051",
|
||||
"novaNeed": "524289"
|
||||
},
|
||||
{
|
||||
"total": "4294967295",
|
||||
"twoThirds": "2863311530",
|
||||
"half": "2147483647",
|
||||
"quasarNeed": "2863311531",
|
||||
"novaNeed": "2147483648"
|
||||
},
|
||||
{
|
||||
"total": "4294967296",
|
||||
"twoThirds": "2863311530",
|
||||
"half": "2147483648",
|
||||
"quasarNeed": "2863311531",
|
||||
"novaNeed": "2147483649"
|
||||
},
|
||||
{
|
||||
"total": "4611686018427387904",
|
||||
"twoThirds": "3074457345618258602",
|
||||
"half": "2305843009213693952",
|
||||
"quasarNeed": "3074457345618258603",
|
||||
"novaNeed": "2305843009213693953"
|
||||
},
|
||||
{
|
||||
"total": "6148914691236517205",
|
||||
"twoThirds": "4099276460824344803",
|
||||
"half": "3074457345618258602",
|
||||
"quasarNeed": "4099276460824344804",
|
||||
"novaNeed": "3074457345618258603"
|
||||
},
|
||||
{
|
||||
"total": "18446744073709551614",
|
||||
"twoThirds": "12297829382473034409",
|
||||
"half": "9223372036854775807",
|
||||
"quasarNeed": "12297829382473034410",
|
||||
"novaNeed": "9223372036854775808"
|
||||
},
|
||||
{
|
||||
"total": "18446744073709551615",
|
||||
"twoThirds": "12297829382473034410",
|
||||
"half": "9223372036854775807",
|
||||
"quasarNeed": "12297829382473034411",
|
||||
"novaNeed": "9223372036854775808"
|
||||
}
|
||||
],
|
||||
"count": [
|
||||
{
|
||||
"n": 0,
|
||||
"novaQuorum": 1,
|
||||
"novaSignerFloor": 1,
|
||||
"novaBeta": 1,
|
||||
"crashTolerance": 0,
|
||||
"equalStakeQuasar": 1
|
||||
},
|
||||
{
|
||||
"n": 1,
|
||||
"novaQuorum": 1,
|
||||
"novaSignerFloor": 1,
|
||||
"novaBeta": 1,
|
||||
"crashTolerance": 0,
|
||||
"equalStakeQuasar": 1
|
||||
},
|
||||
{
|
||||
"n": 2,
|
||||
"novaQuorum": 2,
|
||||
"novaSignerFloor": 2,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 0,
|
||||
"equalStakeQuasar": 2
|
||||
},
|
||||
{
|
||||
"n": 3,
|
||||
"novaQuorum": 2,
|
||||
"novaSignerFloor": 2,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 1,
|
||||
"equalStakeQuasar": 3
|
||||
},
|
||||
{
|
||||
"n": 4,
|
||||
"novaQuorum": 3,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 1,
|
||||
"equalStakeQuasar": 3
|
||||
},
|
||||
{
|
||||
"n": 5,
|
||||
"novaQuorum": 3,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 2,
|
||||
"equalStakeQuasar": 4
|
||||
},
|
||||
{
|
||||
"n": 6,
|
||||
"novaQuorum": 4,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 2,
|
||||
"equalStakeQuasar": 5
|
||||
},
|
||||
{
|
||||
"n": 7,
|
||||
"novaQuorum": 4,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 3,
|
||||
"equalStakeQuasar": 5
|
||||
},
|
||||
{
|
||||
"n": 8,
|
||||
"novaQuorum": 5,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 3,
|
||||
"equalStakeQuasar": 6
|
||||
},
|
||||
{
|
||||
"n": 11,
|
||||
"novaQuorum": 6,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 5,
|
||||
"equalStakeQuasar": 8
|
||||
},
|
||||
{
|
||||
"n": 20,
|
||||
"novaQuorum": 11,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 9,
|
||||
"equalStakeQuasar": 14
|
||||
},
|
||||
{
|
||||
"n": 21,
|
||||
"novaQuorum": 11,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 10,
|
||||
"equalStakeQuasar": 15
|
||||
},
|
||||
{
|
||||
"n": 22,
|
||||
"novaQuorum": 12,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 10,
|
||||
"equalStakeQuasar": 15
|
||||
},
|
||||
{
|
||||
"n": 100,
|
||||
"novaQuorum": 51,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 49,
|
||||
"equalStakeQuasar": 67
|
||||
},
|
||||
{
|
||||
"n": 1000,
|
||||
"novaQuorum": 501,
|
||||
"novaSignerFloor": 3,
|
||||
"novaBeta": 2,
|
||||
"crashTolerance": 499,
|
||||
"equalStakeQuasar": 667
|
||||
}
|
||||
],
|
||||
"weighted": [
|
||||
{
|
||||
"note": "equal stake, five validators — collapses to ⌊2n/3⌋+1",
|
||||
"weights": [
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1"
|
||||
],
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"note": "equal stake, twenty-one validators — 15, not 14: 14/21 does not strictly exceed ⅔",
|
||||
"weights": [
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1"
|
||||
],
|
||||
"count": 15
|
||||
},
|
||||
{
|
||||
"note": "one validator holds most of the stake",
|
||||
"weights": [
|
||||
"100",
|
||||
"1",
|
||||
"1",
|
||||
"1",
|
||||
"1"
|
||||
],
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"note": "two heavy validators cannot reach ⅔ alone",
|
||||
"weights": [
|
||||
"5",
|
||||
"5",
|
||||
"4",
|
||||
"3",
|
||||
"3"
|
||||
],
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"note": "a dust registration must not move the gate",
|
||||
"weights": [
|
||||
"1000",
|
||||
"1000",
|
||||
"1000",
|
||||
"1"
|
||||
],
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"note": "empty set — no stake model, fail closed",
|
||||
"weights": [],
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"note": "all weights zero — no stake model, fail closed",
|
||||
"weights": [
|
||||
"0",
|
||||
"0",
|
||||
"0"
|
||||
],
|
||||
"count": 0
|
||||
}
|
||||
],
|
||||
"stakeNote": "a quorum must STRICTLY EXCEED its floor. Quasar (export) needs voted \u003e twoThirds; Nova (local execution) needs voted \u003e half. Equality is not a quorum.",
|
||||
"countNote": "novaSignerFloor is the count guard the stake predicate cannot give — a lone holder of a stake majority must not self-ignite. equalStakeQuasar is the ⅔ count for a uniform set."
|
||||
},
|
||||
"committee": [
|
||||
{
|
||||
"network": "mainnet",
|
||||
"networkID": 1,
|
||||
"n": 1,
|
||||
"k": 4,
|
||||
"alphaPreference": 3,
|
||||
"alphaConfidence": 3,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 200,
|
||||
"roundTimeoutMS": 400
|
||||
},
|
||||
{
|
||||
"network": "mainnet",
|
||||
"networkID": 1,
|
||||
"n": 4,
|
||||
"k": 4,
|
||||
"alphaPreference": 3,
|
||||
"alphaConfidence": 3,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 200,
|
||||
"roundTimeoutMS": 400
|
||||
},
|
||||
{
|
||||
"network": "mainnet",
|
||||
"networkID": 1,
|
||||
"n": 5,
|
||||
"k": 5,
|
||||
"alphaPreference": 4,
|
||||
"alphaConfidence": 4,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 200,
|
||||
"roundTimeoutMS": 400
|
||||
},
|
||||
{
|
||||
"network": "mainnet",
|
||||
"networkID": 1,
|
||||
"n": 11,
|
||||
"k": 11,
|
||||
"alphaPreference": 8,
|
||||
"alphaConfidence": 8,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 200,
|
||||
"roundTimeoutMS": 400
|
||||
},
|
||||
{
|
||||
"network": "mainnet",
|
||||
"networkID": 1,
|
||||
"n": 21,
|
||||
"k": 21,
|
||||
"alphaPreference": 15,
|
||||
"alphaConfidence": 15,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 21,
|
||||
"blockTimeMS": 200,
|
||||
"roundTimeoutMS": 400
|
||||
},
|
||||
{
|
||||
"network": "mainnet",
|
||||
"networkID": 1,
|
||||
"n": 100,
|
||||
"k": 100,
|
||||
"alphaPreference": 67,
|
||||
"alphaConfidence": 67,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 100,
|
||||
"blockTimeMS": 200,
|
||||
"roundTimeoutMS": 400
|
||||
},
|
||||
{
|
||||
"network": "testnet",
|
||||
"networkID": 2,
|
||||
"n": 1,
|
||||
"k": 4,
|
||||
"alphaPreference": 3,
|
||||
"alphaConfidence": 3,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 100,
|
||||
"roundTimeoutMS": 225
|
||||
},
|
||||
{
|
||||
"network": "testnet",
|
||||
"networkID": 2,
|
||||
"n": 4,
|
||||
"k": 4,
|
||||
"alphaPreference": 3,
|
||||
"alphaConfidence": 3,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 100,
|
||||
"roundTimeoutMS": 225
|
||||
},
|
||||
{
|
||||
"network": "testnet",
|
||||
"networkID": 2,
|
||||
"n": 5,
|
||||
"k": 5,
|
||||
"alphaPreference": 4,
|
||||
"alphaConfidence": 4,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 100,
|
||||
"roundTimeoutMS": 225
|
||||
},
|
||||
{
|
||||
"network": "testnet",
|
||||
"networkID": 2,
|
||||
"n": 11,
|
||||
"k": 11,
|
||||
"alphaPreference": 8,
|
||||
"alphaConfidence": 8,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 100,
|
||||
"roundTimeoutMS": 225
|
||||
},
|
||||
{
|
||||
"network": "testnet",
|
||||
"networkID": 2,
|
||||
"n": 21,
|
||||
"k": 21,
|
||||
"alphaPreference": 15,
|
||||
"alphaConfidence": 15,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 21,
|
||||
"blockTimeMS": 100,
|
||||
"roundTimeoutMS": 225
|
||||
},
|
||||
{
|
||||
"network": "testnet",
|
||||
"networkID": 2,
|
||||
"n": 100,
|
||||
"k": 100,
|
||||
"alphaPreference": 67,
|
||||
"alphaConfidence": 67,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 100,
|
||||
"blockTimeMS": 100,
|
||||
"roundTimeoutMS": 225
|
||||
},
|
||||
{
|
||||
"network": "local",
|
||||
"networkID": 1337,
|
||||
"n": 1,
|
||||
"k": 4,
|
||||
"alphaPreference": 3,
|
||||
"alphaConfidence": 3,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 1,
|
||||
"roundTimeoutMS": 5
|
||||
},
|
||||
{
|
||||
"network": "local",
|
||||
"networkID": 1337,
|
||||
"n": 4,
|
||||
"k": 4,
|
||||
"alphaPreference": 3,
|
||||
"alphaConfidence": 3,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 1,
|
||||
"roundTimeoutMS": 5
|
||||
},
|
||||
{
|
||||
"network": "local",
|
||||
"networkID": 1337,
|
||||
"n": 5,
|
||||
"k": 5,
|
||||
"alphaPreference": 4,
|
||||
"alphaConfidence": 4,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 1,
|
||||
"roundTimeoutMS": 5
|
||||
},
|
||||
{
|
||||
"network": "local",
|
||||
"networkID": 1337,
|
||||
"n": 11,
|
||||
"k": 11,
|
||||
"alphaPreference": 8,
|
||||
"alphaConfidence": 8,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 20,
|
||||
"blockTimeMS": 1,
|
||||
"roundTimeoutMS": 5
|
||||
},
|
||||
{
|
||||
"network": "local",
|
||||
"networkID": 1337,
|
||||
"n": 21,
|
||||
"k": 21,
|
||||
"alphaPreference": 15,
|
||||
"alphaConfidence": 15,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 21,
|
||||
"blockTimeMS": 1,
|
||||
"roundTimeoutMS": 5
|
||||
},
|
||||
{
|
||||
"network": "local",
|
||||
"networkID": 1337,
|
||||
"n": 100,
|
||||
"k": 100,
|
||||
"alphaPreference": 67,
|
||||
"alphaConfidence": 67,
|
||||
"beta": 2,
|
||||
"betaVirtuous": 2,
|
||||
"betaRogue": 100,
|
||||
"blockTimeMS": 1,
|
||||
"roundTimeoutMS": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// corpus_test.go — two independent guards on the standard.
|
||||
//
|
||||
// The first is the golden file: the corpus this build produces must equal
|
||||
// corpus.json byte for byte, so any edit to a finality rule shows up as a diff
|
||||
// in the standard rather than as a foreign node that cannot verify our
|
||||
// signatures.
|
||||
//
|
||||
// A golden file alone is weak — it can be re-blessed with -update and the
|
||||
// change disappears. So the second guard states the load-bearing facts OUTRIGHT,
|
||||
// transcribed from the spec rather than read from the encoder: the message is
|
||||
// 226 bytes, the tag is "LUX/chain/vote/v2\0", the two-thirds floor is
|
||||
// floor(2·total/3) computed in arbitrary precision, and the transport ids are
|
||||
// absent from the signed bytes. Re-blessing the golden does not get past these.
|
||||
package conformance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"math/big"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/consensus/config"
|
||||
"github.com/luxfi/consensus/engine/chain"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "rewrite corpus.json from the live definitions")
|
||||
|
||||
const goldenPath = "corpus.json"
|
||||
|
||||
func TestCorpusMatchesGolden(t *testing.T) {
|
||||
got, err := Marshal(Build())
|
||||
if err != nil {
|
||||
t.Fatalf("marshal corpus: %v", err)
|
||||
}
|
||||
|
||||
if *update {
|
||||
if err := os.WriteFile(goldenPath, got, 0o644); err != nil {
|
||||
t.Fatalf("write golden: %v", err)
|
||||
}
|
||||
t.Logf("wrote %s (%d bytes)", goldenPath, len(got))
|
||||
return
|
||||
}
|
||||
|
||||
want, err := os.ReadFile(goldenPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read golden (run `go test ./conformance -update` to create it): %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("the corpus moved: a finality rule changed.\n"+
|
||||
"Every other implementation of Lux consensus is now wrong against this build.\n"+
|
||||
"If the change is intended, run `go test ./conformance -update`, review the diff as a\n"+
|
||||
"protocol change, and re-capture luxfi/conformance from it.\ngot %d bytes, want %d",
|
||||
len(got), len(want))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCorpusIsDeterministic — two builds in one process must agree. A map
|
||||
// iterated into the output, a clock, or a random id would show up here.
|
||||
func TestCorpusIsDeterministic(t *testing.T) {
|
||||
a, err := Marshal(Build())
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
b, err := Marshal(Build())
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if !bytes.Equal(a, b) {
|
||||
t.Fatal("two builds of the corpus differ; something in it is not deterministic")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoldenIsValidJSON — the committed file has to parse in the languages that
|
||||
// read it, not only in the one that wrote it.
|
||||
func TestGoldenIsValidJSON(t *testing.T) {
|
||||
raw, err := os.ReadFile(goldenPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read golden: %v", err)
|
||||
}
|
||||
var c Corpus
|
||||
if err := json.Unmarshal(raw, &c); err != nil {
|
||||
t.Fatalf("golden does not parse: %v", err)
|
||||
}
|
||||
if c.Version != chain.QuorumCertVersion {
|
||||
t.Errorf("golden version %d, live QuorumCertVersion %d", c.Version, chain.QuorumCertVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignedBytesStatedOutright transcribes the message layout from the spec and
|
||||
// rebuilds the reference message by hand. It shares no code with the encoder, so
|
||||
// a reordered field, a changed width, an edited tag or a dropped binding fails
|
||||
// here even if the golden was re-blessed in the same commit.
|
||||
func TestSignedBytesStatedOutright(t *testing.T) {
|
||||
const tag = "LUX/chain/vote/v2\x00"
|
||||
|
||||
var want []byte
|
||||
want = append(want, tag...)
|
||||
want = append(want, 0x00, 0x03) // version 3, big-endian
|
||||
want = append(want, 0x01) // qcType = finality
|
||||
want = append(want, bytes.Repeat([]byte{0x11}, 32)...)
|
||||
want = append(want, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08) // height
|
||||
want = append(want, 0x0A, 0x0B, 0x0C, 0x0D) // round
|
||||
want = append(want, bytes.Repeat([]byte{0x44}, 32)...) // canonical
|
||||
want = append(want, bytes.Repeat([]byte{0x55}, 32)...) // parent canonical
|
||||
want = append(want, bytes.Repeat([]byte{0x66}, 32)...) // execution state root
|
||||
want = append(want, bytes.Repeat([]byte{0x77}, 32)...) // payload root
|
||||
want = append(want, bytes.Repeat([]byte{0x88}, 32)...) // validator set root
|
||||
want = append(want, 0x01) // accept
|
||||
|
||||
if len(want) != 226 {
|
||||
t.Fatalf("the transcribed layout is %d bytes, the spec says 226", len(want))
|
||||
}
|
||||
|
||||
got := chain.CanonicalVoteMessage(spec())
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("the signed message is not what the spec says.\n got %x\nwant %x", got, want)
|
||||
}
|
||||
|
||||
// The corpus must carry exactly these bytes under "spec".
|
||||
if hexOf(t, "spec") != hex.EncodeToString(want) {
|
||||
t.Error("the corpus's spec case does not carry the transcribed message")
|
||||
}
|
||||
|
||||
// Reject differs in the last byte alone — this is what stops an accept
|
||||
// signature from being presented as a reject.
|
||||
rej := chain.CanonicalRejectMessage(spec())
|
||||
if len(rej) != len(got) {
|
||||
t.Fatalf("reject message is %d bytes, accept is %d", len(rej), len(got))
|
||||
}
|
||||
if !bytes.Equal(rej[:len(rej)-1], got[:len(got)-1]) {
|
||||
t.Error("accept and reject differ before the final byte")
|
||||
}
|
||||
if rej[len(rej)-1] != 0x00 || got[len(got)-1] != 0x01 {
|
||||
t.Errorf("decision byte is reject=%#x accept=%#x, want 0x00 and 0x01", rej[len(rej)-1], got[len(got)-1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransportIDsAreNotSigned holds the property the whole canonical/transport
|
||||
// split exists for: two nodes that executed one inner block sign identical
|
||||
// bytes however it was wrapped.
|
||||
func TestTransportIDsAreNotSigned(t *testing.T) {
|
||||
base := spec()
|
||||
moved := base
|
||||
moved.BlockID = fill(0xE1)
|
||||
moved.ParentID = fill(0xE2)
|
||||
|
||||
if !bytes.Equal(chain.CanonicalVoteMessage(base), chain.CanonicalVoteMessage(moved)) {
|
||||
t.Fatal("moving the transport ids moved the signed message; they are being signed")
|
||||
}
|
||||
if hexOf(t, "spec") != hexOf(t, "outer_moved") {
|
||||
t.Error("the corpus records different messages for the same execution identity")
|
||||
}
|
||||
|
||||
// Every signed axis, moved one at a time, must move the message. A field
|
||||
// that is silently dropped from the encoder is caught here and nowhere else.
|
||||
for _, m := range []struct {
|
||||
name string
|
||||
edit func(*chain.VotePosition)
|
||||
}{
|
||||
{"chainID", func(p *chain.VotePosition) { p.ChainID = fill(0xEE) }},
|
||||
{"height", func(p *chain.VotePosition) { p.Height++ }},
|
||||
{"round", func(p *chain.VotePosition) { p.Round++ }},
|
||||
{"canonicalID", func(p *chain.VotePosition) { p.CanonicalID = fill(0xEE) }},
|
||||
{"parentCanonicalID", func(p *chain.VotePosition) { p.ParentCanonicalID = fill(0xEE) }},
|
||||
{"executionStateRoot", func(p *chain.VotePosition) { p.ExecutionStateRoot = fill(0xEE) }},
|
||||
{"payloadRoot", func(p *chain.VotePosition) { p.PayloadRoot = fill(0xEE) }},
|
||||
{"validatorSetRoot", func(p *chain.VotePosition) { p.ValidatorSetRoot = fill(0xEE) }},
|
||||
} {
|
||||
p := spec()
|
||||
m.edit(&p)
|
||||
if bytes.Equal(chain.CanonicalVoteMessage(p), chain.CanonicalVoteMessage(spec())) {
|
||||
t.Errorf("%s is not bound into the signed message", m.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStakeFloorsStatedOutright recomputes both floors in arbitrary precision.
|
||||
// The production functions are written to survive a total near 2^64 where 2·total
|
||||
// overflows; big.Int has no such problem and therefore no shared mistake.
|
||||
func TestStakeFloorsStatedOutright(t *testing.T) {
|
||||
three := big.NewInt(3)
|
||||
two := big.NewInt(2)
|
||||
|
||||
for _, row := range Build().Threshold.Stake {
|
||||
total, err := strconv.ParseUint(row.Total, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("total %q: %v", row.Total, err)
|
||||
}
|
||||
|
||||
bt := new(big.Int).SetUint64(total)
|
||||
wantTwoThirds := new(big.Int).Div(new(big.Int).Mul(bt, two), three).String()
|
||||
wantHalf := new(big.Int).Div(bt, two).String()
|
||||
|
||||
if row.TwoThirds != wantTwoThirds {
|
||||
t.Errorf("total=%s: twoThirds=%s, floor(2·total/3)=%s", row.Total, row.TwoThirds, wantTwoThirds)
|
||||
}
|
||||
if row.Half != wantHalf {
|
||||
t.Errorf("total=%s: half=%s, floor(total/2)=%s", row.Total, row.Half, wantHalf)
|
||||
}
|
||||
|
||||
// The recorded need is the floor plus one — the predicate is STRICTLY
|
||||
// greater than the floor, and "≥ floor" is the classic off-by-one that
|
||||
// finalizes on exactly two thirds.
|
||||
if row.QuasarNeed != new(big.Int).Add(new(big.Int).SetUint64(config.TwoThirdsStakeFloor(total)), big.NewInt(1)).String() {
|
||||
t.Errorf("total=%s: quasarNeed=%s is not twoThirds+1", row.Total, row.QuasarNeed)
|
||||
}
|
||||
if row.NovaNeed != new(big.Int).Add(new(big.Int).SetUint64(config.HalfStakeFloor(total)), big.NewInt(1)).String() {
|
||||
t.Errorf("total=%s: novaNeed=%s is not half+1", row.Total, row.NovaNeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLadderBoundary states the invariant in words: nothing below Quasar may be
|
||||
// exported, and Nova and brighter may drive local execution.
|
||||
func TestLadderBoundary(t *testing.T) {
|
||||
want := map[string][3]bool{
|
||||
// local export irreversible
|
||||
"photon": {false, false, false},
|
||||
"wave": {false, false, false},
|
||||
"nova": {true, false, false},
|
||||
"quasar": {true, true, false},
|
||||
"horizon": {true, true, true},
|
||||
}
|
||||
rungs := Build().Ladder
|
||||
if len(rungs) != len(want) {
|
||||
t.Fatalf("ladder has %d rungs, want %d", len(rungs), len(want))
|
||||
}
|
||||
for _, r := range rungs {
|
||||
w, ok := want[r.Name]
|
||||
if !ok {
|
||||
t.Errorf("unknown rung %q on the ladder", r.Name)
|
||||
continue
|
||||
}
|
||||
got := [3]bool{r.AuthorizesLocalExecution, r.AuthorizesExport, r.AuthorizesIrreversibleSettlement}
|
||||
if got != w {
|
||||
t.Errorf("%s authorizes %v, want %v", r.Name, got, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNovaSitsBelowQuasar — the two rungs are distinct authorizations, so on any
|
||||
// set where they can differ the local-execution gate must be the lower one. If
|
||||
// they ever coincide, Nova has stopped being a bare majority and export has
|
||||
// stopped being ⅔.
|
||||
func TestNovaSitsBelowQuasar(t *testing.T) {
|
||||
for _, row := range Build().Threshold.Stake {
|
||||
total, err := strconv.ParseUint(row.Total, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("total %q: %v", row.Total, err)
|
||||
}
|
||||
half := config.HalfStakeFloor(total)
|
||||
twoThirds := config.TwoThirdsStakeFloor(total)
|
||||
if half > twoThirds {
|
||||
t.Errorf("total=%d: the majority floor %d is above the supermajority floor %d", total, half, twoThirds)
|
||||
}
|
||||
if total >= 6 && half >= twoThirds {
|
||||
t.Errorf("total=%d: majority floor %d does not sit below supermajority floor %d", total, half, twoThirds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCertWireIsSortedByNodeID — assembly order is part of the standard. Two
|
||||
// nodes holding one quorum must gossip one certificate, so the encoder emits
|
||||
// votes ascending by node id whatever order they were collected in.
|
||||
func TestCertWireIsSortedByNodeID(t *testing.T) {
|
||||
for _, c := range Build().Cert.Cases {
|
||||
var prev string
|
||||
for i, v := range c.Votes {
|
||||
if i > 0 && v.NodeID <= prev {
|
||||
t.Errorf("%s: vote %d node id %s does not follow %s", c.Name, i, v.NodeID, prev)
|
||||
}
|
||||
prev = v.NodeID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hexOf returns the recorded message for a named vote case.
|
||||
func hexOf(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
for _, c := range Build().Vote.Cases {
|
||||
if c.Name == name {
|
||||
return c.Message
|
||||
}
|
||||
}
|
||||
t.Fatalf("no vote case named %q in the corpus", name)
|
||||
return ""
|
||||
}
|
||||
|
||||
// TestEveryCaseIsFullWidth — a message that came out short means a field was
|
||||
// dropped; every case must be the one fixed length the layout implies.
|
||||
func TestEveryCaseIsFullWidth(t *testing.T) {
|
||||
c := Build()
|
||||
for _, vc := range c.Vote.Cases {
|
||||
raw, err := hex.DecodeString(vc.Message)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", vc.Name, err)
|
||||
}
|
||||
if len(raw) != c.Vote.Length {
|
||||
t.Errorf("%s: message is %d bytes, the standard is %d", vc.Name, len(raw), c.Vote.Length)
|
||||
}
|
||||
if !strings.HasPrefix(vc.Message, hex.EncodeToString([]byte(c.Vote.Tag))) {
|
||||
t.Errorf("%s: message does not open with the domain tag", vc.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,17 @@ func CanonicalVoteMessage(pos VotePosition) []byte {
|
||||
return canonicalVoteMessageFor(pos, true)
|
||||
}
|
||||
|
||||
// CanonicalRejectMessage is the exact byte string a validator signs to vote
|
||||
// REJECT on a position — the twin of CanonicalVoteMessage, and the other half of
|
||||
// the signed-bytes standard. It differs from the accept message in the final
|
||||
// byte alone, which is what makes an accept signature unliftable to a reject and
|
||||
// the reverse. The engine verifies reject votes against it; it is exported so an
|
||||
// implementation in another language can be checked against both halves rather
|
||||
// than only the one a certificate happens to carry.
|
||||
func CanonicalRejectMessage(pos VotePosition) []byte {
|
||||
return canonicalVoteMessageFor(pos, false)
|
||||
}
|
||||
|
||||
// canonicalVoteMessageFor builds the domain-separated vote message for a
|
||||
// position AND a decision. The accept byte is bound, so an ACCEPT signature
|
||||
// (accept=1, what a cert carries) and a REJECT signature (accept=0) over the
|
||||
|
||||
Reference in New Issue
Block a user