lux_consensus/finality.rs
1// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
2// See the file LICENSE for licensing terms.
3
4//! The finality standard, in Rust.
5//!
6//! Go is the network. This module is correct exactly insofar as it reproduces
7//! what `github.com/luxfi/consensus/engine/chain` produces, and `tests/
8//! conformance.rs` holds it to that against `conformance/corpus.json` — the
9//! corpus generated from the Go definitions themselves.
10//!
11//! Two rungs, never collapsed. `Nova` is a strict majority of stake and
12//! authorizes local execution; it is reorgable. `Quasar` is a strict two thirds
13//! of stake and is the only rung a bridge, a settlement or a cross-chain message
14//! may read. An implementation with one rung cannot express the accept that
15//! every Lux chain actually runs on.
16
17/// A 32-byte identifier. Empty means unset.
18pub type Id = [u8; 32];
19
20/// The empty identifier — the value that triggers the canonical degrade.
21pub const EMPTY: Id = [0u8; 32];
22
23/// The domain tag, NUL-terminated. Its own version rides in the tag so a
24/// signature over the canonical commitment can never be read as a signature
25/// over the outer envelope id.
26pub const VOTE_TAG: &[u8] = b"LUX/chain/vote/v2\0";
27
28/// The certificate version folded into every signed message.
29pub const QUORUM_CERT_VERSION: u16 = 3;
30
31/// The certificate role. A finality certificate witnesses acceptance, so this is
32/// the only role a chain vote carries.
33pub const QC_FINALITY: u8 = 1;
34
35/// The length of a signed vote message. Every field is fixed width, so the
36/// message is length-free and always exactly this long.
37pub const VOTE_MESSAGE_LEN: usize = 226;
38
39/// The consensus position a vote binds to.
40///
41/// It carries two identities. The canonical execution identity — `canonical_id`,
42/// `parent_canonical_id`, `execution_state_root`, `payload_root` — is the
43/// primary consensus object and is signed. The transport identity — `block_id`,
44/// `parent_id` — is the outer envelope, a cache key for block lookup, and is NOT
45/// signed. Two nodes that executed the same inner block therefore sign identical
46/// bytes however it was wrapped, and their votes interoperate.
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct Position {
49 pub chain_id: Id,
50 pub height: u64,
51 pub round: u32,
52 pub block_id: Id,
53 pub parent_id: Id,
54 pub canonical_id: Id,
55 pub parent_canonical_id: Id,
56 pub execution_state_root: Id,
57 pub payload_root: Id,
58 pub validator_set_root: Id,
59}
60
61impl Position {
62 /// The identity a finality index must key on: the value the signature
63 /// actually commits to. It is `canonical_id`, or `block_id` only in the
64 /// degrade where no canonical id is set — exactly the byte
65 /// [`canonical_vote_message`] folds in as the canonical. The transport
66 /// `block_id` is otherwise unsigned, so keying finality on it lets a
67 /// verified certificate be relabelled to a block it never attested; keying
68 /// on this value cannot be, and two positions that sign to the same bytes
69 /// resolve to the same finality entry rather than two.
70 pub fn signed_identity(&self) -> Id {
71 if self.canonical_id == EMPTY {
72 self.block_id
73 } else {
74 self.canonical_id
75 }
76 }
77}
78
79/// The exact bytes a validator signs.
80///
81/// Layout, big-endian and fixed width throughout:
82///
83/// ```text
84/// "LUX/chain/vote/v2\0" 18
85/// version 2
86/// qc_type 1
87/// chain_id 32
88/// height 8
89/// round 4
90/// canonical_id 32
91/// parent_canonical_id 32
92/// execution_state_root 32
93/// payload_root 32
94/// validator_set_root 32
95/// accept 1
96/// ```
97///
98/// `accept` is bound, so an accept signature and a reject signature over one
99/// position are distinct messages and neither can be presented as the other.
100///
101/// The degrade is resolved here and only here: a position whose canonical slots
102/// are unset — a block with no inner/outer split — binds its transport ids under
103/// them, so every producer of a position signs the same bytes for the same
104/// block.
105pub fn canonical_vote_message(pos: &Position, accept: bool) -> Vec<u8> {
106 let mut buf = Vec::with_capacity(VOTE_MESSAGE_LEN);
107 buf.extend_from_slice(VOTE_TAG);
108 buf.extend_from_slice(&QUORUM_CERT_VERSION.to_be_bytes());
109 buf.push(QC_FINALITY);
110 buf.extend_from_slice(&pos.chain_id);
111 buf.extend_from_slice(&pos.height.to_be_bytes());
112 buf.extend_from_slice(&pos.round.to_be_bytes());
113
114 // The same degrade rule the finality index keys on — one home, so the bytes
115 // signed and the bytes finalized cannot drift apart. See
116 // `Position::signed_identity`.
117 let canonical = pos.signed_identity();
118 let parent = if pos.parent_canonical_id == EMPTY { &pos.parent_id } else { &pos.parent_canonical_id };
119 buf.extend_from_slice(&canonical);
120 buf.extend_from_slice(parent);
121
122 buf.extend_from_slice(&pos.execution_state_root);
123 buf.extend_from_slice(&pos.payload_root);
124 buf.extend_from_slice(&pos.validator_set_root);
125 buf.push(if accept { 0x01 } else { 0x00 });
126 buf
127}
128
129/// `floor(2·total/3)` — the threshold an export quorum must STRICTLY exceed.
130///
131/// Computed from `total` alone because `2·total` overflows near 2^64:
132/// `floor(2·total/3) = 2·(total/3) + floor(2·(total mod 3)/3)`, and
133/// `floor(2r/3)` for r in {0,1,2} is {0,0,1}.
134pub fn two_thirds_stake_floor(total: u64) -> u64 {
135 let (q, r) = (total / 3, total % 3);
136 let mut floor = 2 * q;
137 if r == 2 {
138 floor += 1;
139 }
140 floor
141}
142
143/// `floor(total/2)` — the threshold a local-execution quorum must STRICTLY
144/// exceed. One rung below the export floor, and deliberately so.
145pub fn half_stake_floor(total: u64) -> u64 {
146 total / 2
147}
148
149/// The majority the sampler needs to ignite a block to Nova. `n < 1` yields 1: a
150/// lone node self-ignites, and never 0, which would let a transiently empty view
151/// self-accept.
152pub fn nova_quorum(n: i64) -> i64 {
153 if n < 1 {
154 return 1;
155 }
156 n / 2 + 1
157}
158
159/// The smallest Byzantine-fault-tolerant committee: the least n whose fault budget
160/// f = ⌊(n−1)/3⌋ reaches one. Below it a two-thirds supermajority tolerates no
161/// Byzantine fault at all.
162///
163/// Two consumers, one constant: [`nova_signer_floor`] saturates its count here so a
164/// lone node can never ignite, and [`crate::cert::QuorumCert::verify_weighted`]
165/// refuses an EXPORT certificate over a signing set smaller than this. Go's
166/// `engine/chain.minBFTCommittee`, C++'s `kMinBFTCommittee`.
167pub(crate) const MIN_BFT_COMMITTEE: i64 = 4;
168
169/// The minimum distinct signers a Nova certificate needs whatever the stake
170/// distribution. The Nova gate proper is a stake majority; this count is the
171/// guard the stake predicate cannot give — a single holder of a stake majority
172/// would otherwise self-ignite.
173pub fn nova_signer_floor(n: i64) -> i64 {
174 let q = nova_quorum(n);
175 let m = nova_quorum(MIN_BFT_COMMITTEE);
176 if q < m {
177 q
178 } else {
179 m
180 }
181}
182
183/// The confidence depth: consecutive majority rounds required to ignite Nova.
184pub fn nova_beta(n: i64) -> i64 {
185 if n <= 1 {
186 1
187 } else {
188 2
189 }
190}
191
192/// Simultaneous crash faults Nova ignition survives.
193pub fn crash_tolerance(n: i64) -> i64 {
194 if n < 2 {
195 return 0;
196 }
197 n - nova_quorum(n)
198}
199
200/// The two-thirds SUPERMAJORITY COUNT of n — the smallest number of seats that is
201/// strictly more than two thirds of them, `floor(2n/3) + 1`. For n = 21 this is
202/// 15, not 14: 14/21 does not strictly exceed two thirds. Derived from
203/// [`two_thirds_stake_floor`] over n unit weights rather than restated, so the
204/// count and the stake predicate cannot drift.
205///
206/// Two consumers, one rule seen from two sides. It is the count a live
207/// equal-stake network sizes alpha to, and it is the floor on DISTINCT signers
208/// that [`crate::cert::QuorumCert::verify_weighted`] demands of an export
209/// certificate whatever the stake distribution — the guard the stake predicate
210/// cannot give, because two thirds of the stake is one signature wherever two
211/// thirds of the stake is one validator. Go's `config.TwoThirdsCount`.
212pub fn two_thirds_count(n: i64) -> i64 {
213 if n <= 0 {
214 return 1;
215 }
216 two_thirds_stake_floor(n as u64) as i64 + 1
217}
218
219/// The DISTINCT signers a certificate must carry to attest `tier` over a set of
220/// `n` signers — the whole of a certificate's authority in seats, and a function
221/// of the set and the rung, never of the certificate.
222///
223/// One definition, read in three places: the assembler picks its alpha from it,
224/// the weighted predicate enforces it, and the derived-threshold clause compares
225/// the certificate's own declaration against it. A second spelling anywhere is
226/// how a certificate acquires a quorum of its own choosing.
227///
228/// * Nova — [`nova_signer_floor`], which saturates at three: local execution has
229/// to stay reachable on a small chain, and it is reorgable.
230/// * Quasar — [`two_thirds_count`], the export supermajority read in seats, the
231/// same supermajority the stake clause reads in weight.
232///
233/// A rung that is not an accept tier has no floor and gets none: 0, which every
234/// caller reads as a refusal. Go's `chain.SignerFloor`.
235pub fn signer_floor(tier: Finality, n: i64) -> i64 {
236 match tier {
237 Finality::Nova => nova_signer_floor(n),
238 Finality::Quasar => two_thirds_count(n),
239 _ => 0,
240 }
241}
242
243/// The minimum vote count that CAN reach the two-thirds-by-stake predicate for a
244/// weight vector: order heaviest first and count until the running stake first
245/// exceeds the floor. Returns 0 for an empty set, a zero total, or a vector with
246/// no representable total — no stake model, fail closed.
247///
248/// This is a SIZER, not a floor. It answers "below how many votes is two thirds
249/// of this particular stake distribution unreachable", which is what a parameter
250/// sizer needs and is Go's `config.WeightedSupermajorityThreshold`. The floor a
251/// certificate is held to is [`two_thirds_count`] over the set SIZE, and it is
252/// deliberately the larger of the two on a skewed set: the whole point of the
253/// floor is that concentrated stake must not shrink the number of parties whose
254/// agreement export finality reports.
255pub fn weighted_quasar(weights: &[u64]) -> usize {
256 // Checked, and out the same door as a zero total. A vector summing past
257 // `u64::MAX` has no total, so it has no two thirds of one to size a count
258 // against. A plain sum would wrap to a SMALL total and hand back a count
259 // below the real stake quorum — a count gate sized under the predicate it
260 // exists to anticipate, which is the fail-open direction. No admitted set
261 // can be such a vector: `ValidatorSet::insert` refuses the sum at the door.
262 let total = match weights.iter().try_fold(0u64, |acc, &w| acc.checked_add(w)) {
263 Some(total) => total,
264 None => return 0,
265 };
266 if total == 0 || weights.is_empty() {
267 return 0;
268 }
269 let floor = two_thirds_stake_floor(total);
270 let mut sorted = weights.to_vec();
271 sorted.sort_unstable_by(|a, b| b.cmp(a));
272 let mut cum: u64 = 0;
273 let mut count = 0usize;
274 for w in sorted {
275 count += 1;
276 // Bounded by `total`, which is representable, so the running sum is too.
277 cum += w;
278 if cum > floor {
279 break;
280 }
281 }
282 count
283}
284
285/// A block's rung: the single highest authority it has reached.
286#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
287#[repr(u8)]
288pub enum Finality {
289 /// In flight, being sampled. Authorizes nothing.
290 Photon = 0,
291 /// The forming preference. A preference is not an acceptance.
292 Wave = 1,
293 /// Ignition on a stake majority. Authorizes LOCAL execution, reorgable.
294 Nova = 2,
295 /// A two-thirds-by-stake certificate. Authorizes export.
296 Quasar = 3,
297 /// Post-quantum sealed. Authorizes irreversible settlement.
298 Horizon = 4,
299}
300
301impl Finality {
302 /// Whether this rung may drive local execution — Nova or brighter. A caller
303 /// acting here must be prepared to reorg until Quasar.
304 pub fn authorizes_local_execution(self) -> bool {
305 self >= Finality::Nova
306 }
307
308 /// Whether this block may leave the chain as final. THE INVARIANT: nothing
309 /// below Quasar may reach a bridge, another chain, or a settlement.
310 pub fn authorizes_export(self) -> bool {
311 self >= Finality::Quasar
312 }
313
314 /// Whether this block is irreversible against a quantum adversary.
315 pub fn authorizes_irreversible_settlement(self) -> bool {
316 self >= Finality::Horizon
317 }
318
319 /// The lowercase ontology name, used verbatim in status and metrics.
320 pub fn name(self) -> &'static str {
321 match self {
322 Finality::Photon => "photon",
323 Finality::Wave => "wave",
324 Finality::Nova => "nova",
325 Finality::Quasar => "quasar",
326 Finality::Horizon => "horizon",
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::weighted_quasar;
334
335 /// A weight vector with no representable total takes the fail-closed exit,
336 /// not the wrapped one. Two validators of 2^63 sum to 2^64: a plain sum
337 /// wraps that to 0 in release and panics in debug, and a wrapped total of 0
338 /// would hand `two_thirds_stake_floor` a floor the first voter clears — a
339 /// count gate of 1 for a set no count can finalize. One less than that pair
340 /// is a real total, and still counted exactly.
341 #[test]
342 fn a_weight_vector_with_no_total_is_fail_closed() {
343 assert_eq!(weighted_quasar(&[1 << 63, 1 << 63]), 0);
344 assert_eq!(weighted_quasar(&[u64::MAX, 1]), 0);
345 assert_eq!(weighted_quasar(&[u64::MAX / 2, u64::MAX / 2]), 2);
346 assert_eq!(weighted_quasar(&[u64::MAX]), 1);
347 }
348
349 /// The exits that were already there stay where they were.
350 #[test]
351 fn no_stake_model_is_zero() {
352 assert_eq!(weighted_quasar(&[]), 0);
353 assert_eq!(weighted_quasar(&[0, 0, 0]), 0);
354 }
355}