Skip to main content

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}