Skip to main content

commonware_consensus/simplex/
mod.rs

1//! Simple and fast BFT agreement inspired by Simplex Consensus.
2//!
3//! Inspired by [Simplex Consensus](https://eprint.iacr.org/2023/463), `simplex` provides simple and fast BFT
4//! agreement with network-speed view (i.e. block time) latency and optimal finalization latency in a
5//! partially synchronous setting.
6//!
7//! # Features
8//!
9//! * Wicked Fast Block Times (2 Network Hops)
10//! * Optimal Finalization Latency (3 Network Hops)
11//! * Externalized Uptime and Fault Proofs
12//! * Require Certification Before Finalization
13//! * Decoupled Block Broadcast and Sync
14//! * Lazy Message Verification
15//! * Application-Defined Block Format
16//! * Pluggable Hashing and Cryptography
17//! * Embedded VRF (via [scheme::bls12381_threshold::vrf])
18//!
19//! # Design
20//!
21//! ## Protocol Description
22//!
23//! ### Genesis
24//!
25//! Genesis (view 0) is implicitly finalized. There is no finalization certificate for genesis;
26//! [`Config::floor`](config::Config::floor) supplies the initial finalized state. Voting begins
27//! at view 1, with the first proposal referencing genesis as its parent.
28//!
29//! ### Specification for View `v`
30//!
31//! Upon entering view `v`:
32//! * Determine leader `l` for view `v`
33//! * Set timer for leader proposal `t_l = 2Δ` and advance `t_a = 3Δ`
34//!     * If leader `l` has not been active for the configured skip timeout while a quorum of
35//!       participants has been, set both `t_l` and `t_a` to 0.
36//! * If leader `l`, broadcast `notarize(c,v)`
37//!   * If can't propose container in view `v` because missing notarization/nullification for a
38//!     previous view `v_m`, request `v_m`
39//!
40//! Upon receiving first `notarize(c,v)` from `l`:
41//! * Cancel `t_l`
42//! * If the container's parent `c_parent` is finalized (or both notarized and certified) at `v_parent`
43//!   and we have required nullifications covering the skipped views between `v_parent` and `v`
44//!   (a nullification covers the rest of its term; when `v` is not a term start, `v_parent` must
45//!   be exactly `v-1`), verify `c` and broadcast `notarize(c,v)`
46//!     * If verification of `c` fails, immediately broadcast `nullify(v)`
47//!
48//! Upon receiving `2f+1` `notarize(c,v)`:
49//! * Mark `c` as notarized
50//! * Broadcast `notarization(c,v)` (even if we have not verified `c`)
51//! * Attempt to certify `c` (see [Certification](#certification)), leaving `t_a` armed so a
52//!   stalled certification still times out the view
53//!     * On success: enter `v+1` and broadcast `finalize(c,v)` (skipped if we have broadcast
54//!       `nullify(v)` or observed `l` equivocate in `v`)
55//!     * On failure: treat as immediate timeout expiry and broadcast `nullify(v)`
56//!
57//! Upon receiving `2f+1` `nullify(v)`:
58//! * Broadcast `nullification(v)`
59//! * Enter `next_term_start(v)` (equivalent to `v+1` when `term_length = 1`)
60//!
61//! Upon receiving `2f+1` `finalize(c,v)`:
62//! * Mark `c` as finalized (and recursively finalize its parents)
63//! * Broadcast `finalization(c,v)` (even if we have not verified `c`)
64//!
65//! Upon `t_l` or `t_a` firing:
66//! * Broadcast `nullify(v)`
67//! * Every retry interval `t_r` after `nullify(v)` broadcast that we are still in view `v`:
68//!    * Rebroadcast `nullify(v)` alongside the best certificate for entering view `v` we hold at
69//!      that time: the finalization of `v-1` if we have one, otherwise the highest nullification
70//!      we hold for the previous term (checked only when `v` starts a term, as every view does
71//!      when `term_length` is 1), otherwise the notarization of `v-1`. If we hold none (as in
72//!      view 1), rebroadcast `nullify(v)` alone.
73//!
74//! _When `2f+1` votes of a given type (`notarize(c,v)`, `nullify(v)`, or `finalize(c,v)`) have been collected
75//! from unique participants, a certificate (`notarization(c,v)`, `nullification(v)`, or `finalization(c,v)`) can be assembled.
76//! These certificates serve as a standalone proof of consensus progress that downstream systems can ingest without executing
77//! the protocol._
78//!
79//! ### Joining Consensus
80//!
81//! As soon as `2f+1` nullifies or finalizes are observed for some view `v`, the `Voter` will
82//! enter the corresponding successor view (`next_term_start(v)` for nullification, `v+1` for
83//! finalization). Notarizations advance the view if-and-only-if the application certifies them.
84//! This means that a new participant joining consensus will immediately jump ahead on the previous
85//! view's nullification or finalization and begin participating in consensus at the current view.
86//!
87//! ### Certification
88//!
89//! After a payload is notarized, the application can optionally delay or prevent finalization via the
90//! [`CertifiableAutomaton::certify`](crate::CertifiableAutomaton::certify) method. By default, `certify`
91//! returns `true` for all payloads, meaning finalization proceeds immediately after notarization.
92//!
93//! Customizing `certify` is useful for systems that employ erasure coding, where participants may want
94//! to wait until they have received enough shards to reconstruct and validate the full block before
95//! voting to finalize.
96//!
97//! If `certify` returns `true`, the participant broadcasts a `finalize` vote for the payload (unless it has
98//! broadcast `nullify` or observed the leader equivocate) and enters the next view. If `certify` returns `false`, the participant broadcasts
99//! `nullify` for the view instead (treating it as an immediate timeout), and will refuse to build upon the
100//! proposal or notarize proposals that build upon it.
101//! Thus, a payload can only be finalized if a quorum of participants certify it.
102//!
103//! Certification of some notarization should only be abandoned once a finalization at the same or higher view is observed.
104//! Until then (say a nullification certificate for a view arrives before certification completes), the application should continue
105//! attempting to complete certification. This increases the likelihood that we can vote on the next honest proposer's block (which
106//! may build on our in-flight certification or the nullification). If we did not do this, it is possible that different parts of
107//! the network (neither with quorum) would refuse to vote on each other's blocks (halting consensus).
108//!
109//! _The decision returned by `certify` must be deterministic and consistent across all honest participants to ensure
110//! liveness._
111//!
112//! ### Deviations from Simplex Consensus
113//!
114//! * Fetch missing notarizations/nullifications as needed rather than assuming each proposal contains
115//!   a set of all notarizations/nullifications for all historical blocks.
116//! * Introduce distinct messages for `notarize` and `nullify` rather than referring to both as a `vote` for
117//!   either a "block" or a "dummy block", respectively.
118//! * Introduce a "leader timeout" to trigger early view transitions for unresponsive leaders.
119//! * Skip "leader timeout" and "certification timeout" if a designated leader has not participated
120//!   for the configured skip timeout while a quorum of participants has (again to trigger early
121//!   view transition for an unresponsive leader).
122//! * Introduce message rebroadcast to continue making progress if messages from a given view are dropped (only way
123//!   to ensure messages are reliably delivered is with a heavyweight reliable broadcast protocol).
124//! * Treat local proposal failure as immediate timeout expiry and broadcast `nullify(v)`.
125//! * Treat local verification failure as immediate timeout expiry and broadcast `nullify(v)`.
126//! * Consider the current leader's `nullify(v)` as immediate timeout expiry and broadcast `nullify(v)`.
127//! * Upon seeing `notarization(c,v)`, instead of moving to the view `v+1` immediately, request certification from
128//!   the application (see [Certification](#certification)). Only move to view `v+1` and broadcast `finalize(c,v)`
129//!   if certification succeeds, otherwise broadcast `nullify(v)` and refuse to build upon `c`.
130//! * With stable leaders (`term_length > 1`), a prior same-term `nullify` vote blocks later `finalize` votes until
131//!   a covering finalization is observed; notarize votes are never withheld (see
132//!   [Same-Term Vote Safety](#same-term-vote-safety)).
133//! * With stable leaders, optionally verify proposals and broadcast `notarize` votes up to
134//!   `optimistic_views` views ahead of certified ancestry within a term (configured alongside the
135//!   term length, see [`elector::Terms::stable`]); certification and `finalize` votes always wait
136//!   for explicit parent certification (see [Optimistic Validation](#optimistic-validation)).
137//! * If an entered view remains unfinalized for the stall timeout (configured alongside the term
138//!   length, see [`elector::Terms`]) and we are still in the same term, we locally time out the
139//!   current view and vote `nullify`. In practice, this tracks the oldest unfinalized view we have
140//!   entered in the current term.
141//! * Votes are tracked down to `view_retention` views below the highest finalized view: late
142//!   votes in that window are still reported, even though they are no longer verified or used
143//!   for certificate construction. By default, certification releases full vote evidence, making
144//!   later conflict reporting and peer blocking best effort.
145//!   [`Config::track_historical_votes`] instead retains each recorded vote until its round
146//!   is pruned. Votes below the window are ignored on arrival, so downstream systems consuming
147//!   per-vote activity (rewards, slashing) never observe them.
148//!
149//! ## Protocol Properties
150//!
151//! ### Forced Inclusion (Tail-Forking Resistance)
152//!
153//! A notarized payload in view `v` must appear in the canonical chain if no nullification
154//! certificate covers `v`. With stable leaders, a nullification covers the view it was created for
155//! and the rest of that term. This follows directly from the protocol rules:
156//!
157//! 1. To propose in view `v+k`, the leader must reference a certified parent in some view `v_p`
158//!    and possess required nullifications covering the skipped views from `v_p` to `v+k`.
159//! 2. A nullification certificate requires `2f+1` `nullify` votes for the covered view or an
160//!    earlier view in the same term.
161//! 3. An honest participant only broadcasts `nullify` on a nullify trigger: an expired timeout
162//!    (`t_l`, `t_a`, or the stall timeout) or an event treated as immediate timeout expiry
163//!    (proposal build failure, verification failure, certification failure, the leader's own
164//!    `nullify`, or leader inactivity, as listed in
165//!    [Deviations](#deviations-from-simplex-consensus)).
166//!
167//! Therefore, a nullification covering `v` can only form if at least `f+1` honest participants
168//! broadcast `nullify` at a single view `u` in `[term_start(v), v]`. If every view in that term
169//! prefix completes without any nullify trigger (just view `v` itself when
170//! `term_length` is 1), no honest participant has broadcast a covering `nullify`: at most `f`
171//! covering votes exist at any single view, which is insufficient to form a nullification
172//! certificate. Without that certificate, no future leader can skip view `v`, and the notarized
173//! payload must be included as an ancestor in all subsequent proposals. Note that a clean view
174//! `v` alone is not enough when `term_length > 1`: an honest `nullify` broadcast at an earlier
175//! view of the term (say, after a transient timeout at a view that later notarized) covers `v`
176//! even though no trigger fired at `v` itself.
177//!
178//! ### Same-Term Vote Safety
179//!
180//! With stable leaders, a nullification covers the view it was created for and the rest of that
181//! term: a later proposal may use it to skip all of those views at once. This is only safe if no
182//! covered view is finalized, so the protocol must maintain the invariant that a finalization at
183//! view `v` rules out a nullification at any view `u <= v` in the same term (otherwise a proposal
184//! could fork around a finalized view).
185//!
186//! The finalize gate maintains this invariant: a participant that voted `nullify(u)` withholds
187//! `finalize` votes for later views in that term until it observes a same-term finalization at or
188//! above its highest `nullify` vote. To see why the invariant holds, suppose both a nullification
189//! at `u` and a finalization at `v >= u` form in the same term. Their quorums intersect in at
190//! least one honest participant that voted both `nullify(u)` and `finalize(c,v)`. If `u = v`,
191//! this is impossible outright: no honest participant votes both `nullify` and `finalize` in a
192//! single view. If `u < v`, the gate means that participant first observed a same-term
193//! finalization at some `v*` with `u <= v* < v`: at or above `u` because the gate requires
194//! covering its highest `nullify` vote, and below `v` because participants only vote `finalize`
195//! for views above their highest observed finalization. Applying this same argument to the
196//! finalization at `v*` shows, by induction, that no nullification can form at or below `v*`,
197//! contradicting the nullification at `u <= v*`.
198//!
199//! The gate also recovers from a `nullify` vote that never became a nullification (e.g., a
200//! transient timeout on an otherwise healthy network): by the invariant, an observed same-term
201//! finalization at or above the vote proves the nullification can never form, so the vote is
202//! inert and the gate reopens ("heals"). This prevents one transient timeout from degrading the
203//! rest of the term. In a healthy network this takes one view: peers broadcast `finalize(v)` when
204//! they certify `v`, so the finalization for `v` typically arrives shortly after entering `v+1`.
205//!
206//! Healing does not proactively revisit earlier views: a `finalize` vote for a view certified
207//! while the gate was blocked is only emitted if a later message (such as a redelivered
208//! notarization) touches that view again. If more than `f` participants were blocked, that view may never
209//! gather its own finalization certificate, and neither may any later view in the term (healing
210//! itself requires a same-term finalization, which cannot assemble while more than `f`
211//! participants withhold `finalize`). Such a view is either finalized transitively by the
212//! finalization of a descendant in a later term or skipped entirely by a covering nullification:
213//! the timeouts that blocked the gate also mean forced inclusion does not apply to it.
214//!
215//! ### Optimistic Validation
216//!
217//! With stable leaders, a leader can propose for view `v+1` as soon as its proposal for view `v`
218//! is notarized, but participants that wait for `v`'s certification before verifying the new
219//! proposal add a round of certification latency to every view. When a nonzero `optimistic_views`
220//! lookahead is configured (see [`elector::Terms::stable`]), a participant instead verifies a
221//! proposal and broadcasts its `notarize` vote before the parent is certified, if all of the
222//! following hold:
223//!
224//! * The proposal's view is in the same term as its parent (optimism never crosses a term
225//!   boundary; a term start always requires explicitly certified ancestry).
226//! * At most `optimistic_views` views lie between the proposal's view and the last *directly
227//!   notarized* view (a view with an observed notarization or finalization certificate; a view is
228//!   *indirectly notarized* when only a descendant's certificate implies it), bounding
229//!   how far local votes run ahead of certified ancestry. This is the *issuance* window.
230//! * There is local evidence for the immediate parent: our own broadcast `notarize` vote, an
231//!   observed notarization certificate (unless our own certification rejected it), or (once the
232//!   proposal's view is current) the parent's explicit certification.
233//!
234//! Certification requests and `finalize` votes never run ahead: both require the parent's
235//! explicit certification first. If an optimistic ancestor fails to notarize or certify, the
236//! usual timeout and nullification path skips it, and any optimistic votes above it are inert.
237//!
238//! Peers admit and buffer votes up to `optimistic_views` views beyond their own current
239//! view (the *admission* window), so optimistic votes are not dropped by participants that have
240//! not yet observed the sender's ancestry. The setting is local: mismatched values across
241//! participants only degrade the optimization (votes beyond a peer's window are dropped until it
242//! catches up), never safety.
243//!
244//! ### Optimistic Finality
245//!
246//! The forced inclusion property provides a weaker but faster form of finality: a payload
247//! notarized at view `v` can be treated as speculatively final, because no future sequence of
248//! proposals can exclude it from the canonical chain if every view in `[term_start(v), v]`
249//! completes without any nullify trigger (just view `v` itself when `term_length` is 1).
250//!
251//! This "speculative finality" is available after just 2 network hops (proposal + notarization),
252//! compared to the 3 hops required for full finalization (proposal + notarization + finalization).
253//! Observing the notarization does not by itself rule out exclusion: honest participants may
254//! still be inside a view of the term prefix, where a trigger can still fire (say, a
255//! certification that outlives `t_a`). Exclusion requires `f+1` or more honest participants to
256//! broadcast `nullify` at a single view of that prefix. Because certification is deterministic,
257//! it either fails for all honest participants or none, so a certification failure always
258//! produces a nullification. In the common case (no faults, no timeouts), exclusion cannot
259//! happen.
260//!
261//! A Byzantine leader, however, can exclude even its own valid, certifiable, and timely proposal:
262//! honest participants treat the leader's `nullify(v)` as an immediate timeout, so a leader can
263//! single-handedly revoke its own notarized proposal. This is no new power. A leader can achieve
264//! the same exclusion by delivering its proposal so late that honest participants notarize it but
265//! time out before certifying. Speculative finality therefore assumes the term's leader wants its
266//! proposal to survive.
267//!
268//! ### Unchained Finalization
269//!
270//! Finalization does not require consecutive honest views. When a participant certifies
271//! `notarization(c,v)`, it broadcasts `finalize(c,v)` and immediately enters `v+1`,
272//! regardless of what happens in subsequent views. These `finalize(c,v)` votes accumulate
273//! independently of the current view: even if views `v+1` through `v+k` all time out
274//! (producing nullifications), the `finalize(c,v)` votes still count toward the `2f+1`
275//! threshold needed to form `finalization(c,v)`.
276//!
277//! This means a payload notarized in view `v` can be finalized while the network is
278//! in view `v+k` for any `k >= 1`. There is no requirement that a particular view
279//! after `v` succeeds or that any subsequent leader cooperates. As long as `2f+1`
280//! participants eventually certify and broadcast `finalize(c,v)`, the finalization
281//! certificate will form.
282//!
283//! ## Architecture
284//!
285//! All logic is split into four components: the `Batcher`, the `Voter`, the `Resolver`, and the `Application` (provided by the user).
286//! The `Batcher` is responsible for collecting messages from peers and lazily verifying them when a quorum is met. The `Voter`
287//! is responsible for directing participation in the current view. The `Resolver` is responsible for
288//! fetching artifacts from previous views required to verify proposed blocks in the latest view. Lastly, the `Application`
289//! is responsible for proposing new blocks and indicating whether some block is valid.
290//!
291//! To drive great performance, all interactions between `Batcher`, `Voter`, `Resolver`, and `Application` are
292//! non-blocking. This means that, for example, the `Voter` can continue processing messages while the
293//! `Application` verifies a proposed block or the `Resolver` fetches a notarization.
294//!
295//! ```txt
296//!                            +------------+          +++++++++++++++
297//!                            |            +--------->+             +
298//!                            |  Batcher   |          +    Peers    +
299//!                            |            |<---------+             +
300//!                            +-------+----+          +++++++++++++++
301//!                                |   ^
302//!                                |   |
303//!                                |   |
304//!                                |   |
305//!                                v   |
306//! +---------------+           +---------+            +++++++++++++++
307//! |               |<----------+         +----------->+             +
308//! |  Application  |           |  Voter  |            +    Peers    +
309//! |               +---------->|         |<-----------+             +
310//! +---------------+           +--+------+            +++++++++++++++
311//!                                |   ^
312//!                                |   |
313//!                                |   |
314//!                                |   |
315//!                                v   |
316//!                            +-------+----+          +++++++++++++++
317//!                            |            +--------->+             +
318//!                            |  Resolver  |          +    Peers    +
319//!                            |            |<---------+             +
320//!                            +------------+          +++++++++++++++
321//! ```
322//!
323//! ### Batched Verification
324//!
325//! Unlike other consensus constructions that verify all incoming messages received from peers, for schemes
326//! where [`Verifier::is_batchable()`](commonware_cryptography::certificate::Verifier::is_batchable) returns `true`
327//! (such as [scheme::ed25519], [scheme::bls12381_multisig] and [scheme::bls12381_threshold]), `simplex` lazily
328//! verifies messages (only when a quorum is met), enabling efficient batch verification. For schemes where
329//! `is_batchable()` returns `false` (such as [scheme::secp256r1]), signatures are verified eagerly as they
330//! arrive since there is no batching benefit.
331//!
332//! If an invalid signature is detected, the `Batcher` will perform repeated bisections over collected
333//! messages to find the offending message (and block the peer(s) that sent it via [commonware_p2p::Blocker]).
334//!
335//! _If using a p2p implementation that is not authenticated, it is not safe to employ this optimization
336//! as any attacking peer could simply reconnect from a different address. We recommend [commonware_p2p::authenticated]._
337//!
338//! ### Fetching Missing Certificates
339//!
340//! Background repair fetches nullifications above the local certified or finalized floor. If honest
341//! participants complete a view with different certificate types, both sides can still consider it
342//! complete while rejecting the other's proposal ancestry.
343//!
344//! Proposal verification repairs this split by requesting the first missing nullification or named
345//! parent from the proposal's elected leader, even below the certified floor. The voter rechecks the
346//! full ancestry after each delivery and votes only once it is valid. The voter does not request
347//! an uncertified parent inside the optimistic issuance window: its certificate is still forming
348//! from live votes (see [Optimistic Validation](#optimistic-validation)).
349//!
350//! The same split can block certification. A notarized view certifies only after its parent
351//! certifies, and certifying the parent requires its exact-view notarization. When the voter holds
352//! a view's notarization but not its parent's, the parent's votes have stopped circulating, and
353//! peers broadcast a certificate only once. A leader that withheld the certificate may never
354//! answer a fetch, so the voter requests the parent's notarization from any peer.
355//!
356//! A resolver key identifies a view, not a certificate. A notarization and a covering nullification
357//! for one view answer opposite questions, so a peer can return valid evidence that does not settle
358//! the request. The requester records that evidence and retries without faulting the peer. A
359//! delivered notarization completes its fetch on arrival, because certification judges evidence
360//! already in hand. Matching evidence or finalization retires pending work.
361//!
362//! ## Pluggable Hashing and Cryptography
363//!
364//! Hashing is abstracted via the [commonware_cryptography::Hasher] trait and cryptography is abstracted via
365//! the [commonware_cryptography::certificate::Scheme] trait, allowing deployments to employ approaches that best match their
366//! requirements (or to provide their own without modifying any consensus logic). The following schemes
367//! are supported out-of-the-box:
368//!
369//! ### [scheme::ed25519]
370//!
371//! [commonware_cryptography::ed25519] signatures are ["High-speed high-security signatures"](https://eprint.iacr.org/2011/368)
372//! with 32 byte public keys and 64 byte signatures. While they are well-supported by commercial HSMs and offer efficient batch
373//! verification, the signatures are not aggregatable (and certificates grow linearly with the quorum size).
374//!
375//! ### [scheme::bls12381_multisig]
376//!
377//! [commonware_cryptography::bls12381] is a ["digital signature scheme with aggregation properties"](https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-05.txt).
378//! Unlike [commonware_cryptography::ed25519], signatures from multiple participants (say the signers in a certificate) can be aggregated
379//! into a single signature (reducing bandwidth usage per broadcast). That being said, [commonware_cryptography::bls12381] is much slower
380//! to verify than [commonware_cryptography::ed25519] and isn't supported by most HSMs (a standardization effort expired in 2022).
381//!
382//! ### [scheme::secp256r1]
383//!
384//! [commonware_cryptography::secp256r1] signatures use the NIST P-256 elliptic curve (also known as prime256v1), which is widely
385//! supported by commercial HSMs and hardware security modules. Unlike [commonware_cryptography::ed25519], Secp256r1 does not
386//! benefit from batch verification, so signatures are verified individually. Certificates grow linearly with quorum size
387//! (similar to ed25519).
388//!
389//! ### [scheme::bls12381_threshold]
390//!
391//! [scheme::bls12381_threshold] employs threshold cryptography (BLS12-381 threshold signatures with a `2f+1` of `3f+1` quorum)
392//! to generate succinct consensus certificates (verifiable with just the static public key). This scheme requires instantiating
393//! the shared secret via [commonware_cryptography::bls12381::dkg] and resharing whenever participants change.
394//!
395//! Two (non-attributable) variants are provided:
396//!
397//! - [scheme::bls12381_threshold::standard]: Certificates contain only a vote signature.
398//!
399//! - [scheme::bls12381_threshold::vrf]: Certificates contain a vote signature and a view signature (i.e. a seed that can be used
400//!   as a VRF). This variant can be configured for random leader election (via [elector::Random]) and/or incorporate this randomness
401//!   into execution.
402//!
403//! #### Embedded VRF ([scheme::bls12381_threshold::vrf])
404//!
405//! Every `notarize(c,v)`, `nullify(v)`, or `finalize(c,v)` message includes an `attestation(v)` (a partial signature over the view `v`).
406//! After `2f+1` attestations are collected from unique participants, `seed(v)` can be recovered. Because `attestation(v)` is only over the
407//! view `v`, the seed derived for a given view `v` is the same regardless of which block (if any) is notarized in view `v`. The `2f+1`
408//! attestations can come from mutually incompatible messages (`notarize` for different blocks, `nullify`, `finalize`), so `seed(v)` is
409//! recoverable even when no certificate forms for view `v`.
410//!
411//! The value of `seed(v)` cannot be known prior to message broadcast by any participant (including the leader) in view `v` and cannot be
412//! manipulated by any participant (deterministic for any `2f+1` signers at a given view `v`), so it is a sound beacon for leader election
413//! (where `seed(v)` determines the leader for `v+1`). It is **not** safe as a source of randomness for execution within view `v` itself: a
414//! coalition of `f` Byzantine participants recovers `seed(v)` after only `f+1` honest attestations, before the round resolves, letting a
415//! malicious leader front-run the outcome. Consume `seed(v)` only in a later view or epoch (see [scheme::bls12381_threshold::vrf] for
416//! extended discussion of the attack and the commit-then-reveal mitigation).
417//!
418//! #### Succinct Certificates
419//!
420//! All broadcast consensus messages (`notarize(c,v)`, `nullify(v)`, `finalize(c,v)`) contain attestations (partial signatures) for a static
421//! public key (derived from a group polynomial that can be recomputed during reconfiguration using [dkg](commonware_cryptography::bls12381::dkg)).
422//! As soon as `2f+1` messages are collected, a threshold signature over `notarization(c,v)`, `nullification(v)`, and `finalization(c,v)`
423//! can be recovered, respectively. Because the public key is static, any of these certificates can be verified by an external
424//! process without following the consensus instance and/or tracking the current set of participants (as is typically required
425//! to operate a lite client).
426//!
427//! These threshold signatures over `notarization(c,v)`, `nullification(v)`, and `finalization(c,v)` (i.e. the consensus certificates)
428//! can be used to secure interoperability between different consensus instances and user interactions with an infrastructure provider
429//! (where any data served can be proven to derive from some finalized block of some consensus instance with a known static public key).
430//!
431//! ## Persistence
432//!
433//! The `Voter` caches all data required to participate in consensus to avoid any disk reads on
434//! on the critical path. To enable recovery, the `Voter` writes valid messages it receives from
435//! consensus and messages it generates to a write-ahead log (WAL) implemented by [commonware_storage::journal::segmented::variable::Journal].
436//! Before sending a message, any pending `Journal` appends are synced to prevent inadvertent Byzantine
437//! behavior on restart (especially in the case of unclean shutdown). All appends made in the same event
438//! loop iteration are coalesced into a single sync that runs after messages are constructed and before
439//! any are broadcast (even if there is nothing to broadcast). The proposal payload relay is not a
440//! consensus message and is not gated on this sync: to lower view latency, it is requested as soon
441//! as the automaton returns a payload, which is safe because extra payload bytes (unlike votes)
442//! cannot form a conflicting certificate (see [`Plan::Propose`]).
443//!
444//! ## Automaton Failure Semantics
445//!
446//! If a validator is the leader for a view but cannot build a valid payload yet (for example because
447//! it is still syncing), it should decline the [`Automaton::propose`](crate::Automaton::propose)
448//! request by dropping the response channel. Simplex treats this as a missing proposal, broadcasts
449//! `nullify(v)`, and other validators can use the leader-nullify fast path to skip the view.
450//!
451//! Once `propose` returns a payload, the local proposer is committed to that payload for verification
452//! and certification. [`Automaton::verify`](crate::Automaton::verify) and
453//! [`CertifiableAutomaton::certify`](crate::CertifiableAutomaton::certify) are stable verdict APIs,
454//! not backpressure or syncing signals. While missing data may still arrive (and/or a validator cannot
455//! immediately determine if a payload is valid), implementations should keep these requests pending rather
456//! than returning `false` or closing the channel.
457//!
458//! Returning `false` from `verify` means the proposal is permanently invalid and causes a local
459//! nullify. Returning `false` from `certify` means the notarized payload is permanently
460//! uncertifiable for that round and also causes a local nullify. Closing `certify` does not cause
461//! `nullify(v)` to be broadcast before the normal round deadline and can halt progress because
462//! certification requests are not retried during the same run. The safe way to stop working on
463//! certification is to keep the request pending until Simplex drops it after finalizing the block
464//! or a descendant.
465
466pub mod elector;
467pub mod scheme;
468pub mod types;
469
470use crate::types::{TermLength, View, ViewDelta};
471
472/// Defines term boundaries and optimistic lookahead.
473///
474/// The admission window limits future votes relative to the current view
475/// ([`Self::in_admission_window`]). The issuance window limits how far a
476/// directly notarized anchor can authorize uncertified descendants
477/// ([`Self::issuance_floor`]). See the [module docs] for the full rules.
478///
479/// `current` arguments must be locally derived views (they feed
480/// panicking arithmetic in [`View::term_end`]); candidate arguments
481/// (`pending`, `view`) may be adversarial.
482///
483/// [module docs]: crate::simplex#optimistic-validation
484#[derive(Clone, Copy)]
485#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
486pub(crate) struct Lookahead {
487    /// Number of views in each leader term.
488    pub term_length: TermLength,
489    /// Depth of the admission and issuance windows; zero disables
490    /// optimistic validation.
491    pub optimistic_views: ViewDelta,
492}
493
494#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
495impl Lookahead {
496    /// Builds the term geometry from an elector's [`elector::Terms`].
497    pub(crate) const fn new(terms: &elector::Terms) -> Self {
498        Self {
499            term_length: terms.length(),
500            optimistic_views: terms.optimistic_views(),
501        }
502    }
503
504    /// Returns true when `pending` is inside the optimistic admission
505    /// window of `current`: a same-term future view at most
506    /// `optimistic_views` ahead.
507    pub fn in_admission_window(&self, current: View, pending: View) -> bool {
508        current < pending && pending <= self.admission_limit(current)
509    }
510
511    /// Returns whether `pending` is admissible relative to `current`
512    /// (extending [`View::admits`] with the optimistic admission
513    /// window).
514    ///
515    /// Views at or below `current` are always admitted. Admitted futures are:
516    /// - `current + 1`
517    /// - `next_term_start(current)`
518    /// - views in the optimistic admission window
519    pub fn admits(&self, current: View, pending: View) -> bool {
520        current.admits(pending, self.term_length) || self.in_admission_window(current, pending)
521    }
522
523    /// Returns the highest view in the admission window of `current`
524    /// (`current` itself when the window is empty).
525    pub fn admission_limit(&self, current: View) -> View {
526        current
527            .term_end(self.term_length)
528            .min(current.saturating_add(self.optimistic_views))
529    }
530
531    /// Returns the lowest view whose direct notarization can anchor
532    /// `view` inside the optimistic *issuance* window, or `None` when
533    /// `view` can never be issued optimistically, either because
534    /// optimism is disabled or because `view` starts a term and so
535    /// requires explicitly certified ancestry.
536    ///
537    /// An anchor below the floor fails the hop bound exactly like no
538    /// anchor at all, so a caller decides membership by asking whether
539    /// any directly-notarized view sits in `floor..view`. A floor of
540    /// genesis means the window is open until the first notarization
541    /// lands. Compare [`Self::in_admission_window`], which anchors at
542    /// the current view instead.
543    pub const fn issuance_floor(&self, view: View) -> Option<View> {
544        if self.optimistic_views.is_zero() || view.is_term_start(self.term_length) {
545            return None;
546        }
547        Some(
548            view.saturating_sub(self.optimistic_views)
549                .saturating_sub(ViewDelta::new(1)),
550        )
551    }
552}
553
554cfg_if::cfg_if! {
555    if #[cfg(not(target_arch = "wasm32"))] {
556        use crate::types::Round;
557        use commonware_cryptography::PublicKey;
558        use commonware_p2p::Recipients;
559
560        mod actors;
561        pub mod config;
562        pub use config::{Config, Floor, ForwardPolicy, SkipBudget, SkipPolicy};
563        mod engine;
564        pub use engine::Engine;
565        mod metrics;
566
567        /// The window of views an actor tracks, bounded below by retention
568        /// and above by admission policy.
569        #[derive(Clone, Copy)]
570        pub(crate) struct Viewport {
571            /// Highest finalized view observed.
572            pub finalized: View,
573            /// View currently being driven.
574            pub current: View,
575            /// Views retained below `finalized` (for reporting and backfill).
576            pub view_retention: ViewDelta,
577            /// Term geometry bounding admitted future views.
578            pub lookahead: Lookahead,
579        }
580
581        impl Viewport {
582            /// Returns the lowest view retained (genesis is never tracked).
583            pub const fn floor(&self) -> View {
584                self.finalized.saturating_sub(self.view_retention)
585            }
586
587            /// Returns whether `view` is retained: at or above the activity
588            /// floor and not genesis. Views up to `view_retention` below
589            /// `finalized` are kept so late votes are still reported (even
590            /// when no longer needed for progress).
591            pub const fn retains(&self, view: View) -> bool {
592                !view.is_zero() && view.get() >= self.floor().get()
593            }
594
595            /// Returns whether a vote at `pending` is tracked: retained and no
596            /// further ahead than the next view, the first view of the next
597            /// term, or a bounded same-term optimistic lookahead view,
598            /// bounding memory committed to unverified votes (see
599            /// [`View::admits`]).
600            pub fn admits_vote(&self, pending: View) -> bool {
601                self.retains(pending) && self.lookahead.admits(self.current, pending)
602            }
603
604            /// Returns whether a certificate at `pending` is tracked:
605            /// certificates are self-certifying and may arrive from arbitrarily
606            /// far ahead (letting a lagging participant fast-forward), so only
607            /// retention bounds them.
608            pub const fn admits_certificate(&self, pending: View) -> bool {
609                self.retains(pending)
610            }
611        }
612
613        /// Describes how a payload should be broadcast to the network.
614        pub enum Plan<P: PublicKey> {
615            /// Initial broadcast of a newly proposed block to all participants.
616            ///
617            /// Requested before the proposer's notarize vote is durable: a
618            /// proposer that crashes and restarts may emit this plan again
619            /// with a different payload for the same round. Consumers must
620            /// tolerate multiple candidates per round (at most one is ever
621            /// referenced by the proposer's signed votes).
622            Propose {
623                /// The round in which the block was proposed.
624                round: Round,
625            },
626            /// Forward a block to a specific set of peers.
627            ///
628            /// Requested only for a proposal already backed by a certificate.
629            /// Forwarding is best-effort help for lagging peers and advertises
630            /// nothing about the sender's own state, so it needs no durability
631            /// ordering.
632            Forward {
633                /// The round in which the forwarded block was proposed.
634                round: Round,
635                /// The recipients to forward the block to.
636                recipients: Recipients<P>,
637            },
638        }
639    }
640}
641
642#[cfg(any(test, feature = "mocks"))]
643pub mod mocks;
644
645/// Convenience alias for [`N3f1::quorum`].
646#[cfg(test)]
647pub(crate) fn quorum(n: u32) -> u32 {
648    use commonware_utils::{Faults, N3f1};
649
650    N3f1::quorum(n)
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656    use crate::{
657        Monitor, Viewable,
658        simplex::{
659            elector::{self, Config as _, Elector as _, Random, RandomVersion, RoundRobin},
660            mocks::{
661                scheme as scheme_mocks,
662                twins::{self, Elector as TwinsElector},
663                wrapped,
664            },
665            scheme::{
666                Scheme, bls12381_multisig,
667                bls12381_threshold::{
668                    standard as bls12381_threshold_std,
669                    vrf::{self as bls12381_threshold_vrf, Seedable},
670                },
671                ed25519, secp256r1,
672            },
673            types::{
674                Certificate, Finalization as TFinalization, Finalize as TFinalize,
675                Notarization as TNotarization, Notarize as TNotarize,
676                Nullification as TNullification, Nullify as TNullify, Proposal, Vote,
677            },
678        },
679        types::{Epoch, Participant, Round, TermLength, View, ViewDelta},
680    };
681    use commonware_codec::{Decode, DecodeExt, Encode};
682    use commonware_cryptography::{
683        Hasher as _, Sha256, Signer as _,
684        bls12381::primitives::variant::{MinPk, MinSig, Variant},
685        certificate::mocks::Fixture,
686        ed25519::{PrivateKey, PublicKey},
687        sha256::{Digest as Sha256Digest, Digest as D},
688    };
689    use commonware_macros::{select, test_group, test_traced};
690    use commonware_p2p::{
691        Manager as _, Recipients, Sender as _, TrackedPeers,
692        simulated::{Config, Link, Network, Oracle, Receiver, Sender, SplitOrigin},
693        utils::mocks::inert_channel,
694    };
695    use commonware_parallel::{Sequential, Strategy};
696    use commonware_runtime::{
697        Clock, IoBuf, Metrics as _, Quota, Runner, Spawner, Strategizer as _, Supervisor as _,
698        buffer::paged::CacheRef, deterministic, telemetry::metrics::count_running_tasks,
699    };
700    use commonware_utils::{
701        Faults, N3f1, NZU16, NZU32, NZUsize, TestRng, non_empty, ordered::Set, probability,
702        sync::Mutex, test_rng,
703    };
704    use engine::Engine;
705    use futures::future::join_all;
706    use rand::{RngExt as _, SeedableRng, rngs::StdRng};
707    use rand_core::CryptoRng;
708    use std::{
709        collections::{BTreeMap, HashMap, HashSet},
710        num::{NonZeroU16, NonZeroU32, NonZeroUsize},
711        sync::Arc,
712        time::Duration,
713    };
714    use tracing::{debug, info, warn};
715    use types::Activity;
716
717    // Invoke `$cb!($($args)*, $suffix, $elector, $fixture, $elector_config)`
718    // once per canonical (elector, scheme) fixture.
719    macro_rules! for_each_fixture {
720        ($cb:ident!($($args:tt)*)) => {
721            $cb!($($args)*, bls12381_threshold_vrf_min_pk, Random, bls12381_threshold_vrf::fixture::<MinPk, _>, Random::new(RandomVersion::V1));
722            $cb!($($args)*, bls12381_threshold_vrf_min_sig, Random, bls12381_threshold_vrf::fixture::<MinSig, _>, Random::new(RandomVersion::V1));
723            $cb!($($args)*, bls12381_threshold_std_min_pk, RoundRobin, bls12381_threshold_std::fixture::<MinPk, _>, RoundRobin::default());
724            $cb!($($args)*, bls12381_threshold_std_min_sig, RoundRobin, bls12381_threshold_std::fixture::<MinSig, _>, RoundRobin::default());
725            $cb!($($args)*, bls12381_multisig_min_pk, RoundRobin, bls12381_multisig::fixture::<MinPk, _>, RoundRobin::default());
726            $cb!($($args)*, bls12381_multisig_min_sig, RoundRobin, bls12381_multisig::fixture::<MinSig, _>, RoundRobin::default());
727            $cb!($($args)*, ed25519, RoundRobin, ed25519::fixture, RoundRobin::default());
728            $cb!($($args)*, secp256r1, RoundRobin, secp256r1::fixture, RoundRobin::default());
729        };
730    }
731
732    // Generate one `#[test_group("slow")] #[test_traced]` test per canonical
733    // (elector, scheme) fixture, named `test_<callee>_<suffix>`. The helper takes
734    // the elector config type as its third generic parameter and the concrete
735    // config after the fixture argument.
736    //
737    // Supported forms:
738    //   test_for_all_fixtures!(callee);                  // callee::<_, _, Elector>(fixture, config)
739    //   test_for_all_fixtures!(callee, arg);             // callee::<_, _, Elector, _>(fixture, config, arg)
740    //   test_for_all_fixtures!(callee, arg, level = "INFO"); // arg with a trace-level override
741    //   test_for_all_fixtures!(callee, seeds = N);       // loops callee::<_, _, Elector>(seed, fixture, config)
742    //   test_for_all_fixtures!(callee, level = "INFO");  // overrides the trace level
743    macro_rules! test_for_all_fixtures {
744        ($callee:ident) => {
745            for_each_fixture!(test_for_all_fixtures!(@emit [test_traced] $callee [] []));
746        };
747        ($callee:ident, level = $level:literal) => {
748            for_each_fixture!(test_for_all_fixtures!(@emit [test_traced($level)] $callee [] []));
749        };
750        ($callee:ident, seeds = $n:expr) => {
751            for_each_fixture!(test_for_all_fixtures!(@seeded $n, $callee));
752        };
753        ($callee:ident, $arg:expr, level = $level:literal) => {
754            for_each_fixture!(test_for_all_fixtures!(@emit [test_traced($level)] $callee [, _] [, $arg]));
755        };
756        ($callee:ident, $arg:expr) => {
757            for_each_fixture!(test_for_all_fixtures!(@emit [test_traced] $callee [, _] [, $arg]));
758        };
759        (@emit [$traced:meta] $callee:ident [$($generics:tt)*] [$($args:tt)*], $suffix:ident, $elector:ty, $fixture:expr, $elector_config:expr) => {
760            paste::paste! {
761                #[test_group("slow")]
762                #[$traced]
763                fn [<test_ $callee _ $suffix>]() {
764                    $callee::<_, _, $elector $($generics)*>($fixture, $elector_config $($args)*);
765                }
766            }
767        };
768        (@seeded $n:expr, $callee:ident, $suffix:ident, $elector:ty, $fixture:expr, $elector_config:expr) => {
769            paste::paste! {
770                #[test_group("slow")]
771                #[test_traced]
772                fn [<test_ $callee _ $suffix>]() {
773                    for seed in 0..$n {
774                        $callee::<_, _, $elector>(seed, $fixture, $elector_config);
775                    }
776                }
777            }
778        };
779    }
780
781    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
782    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
783    const TEST_QUOTA: Quota = Quota::per_second(NonZeroU32::MAX);
784
785    type TestChannel = (
786        Sender<PublicKey, deterministic::Context>,
787        Receiver<PublicKey>,
788    );
789    type TestRegistration = (TestChannel, TestChannel, TestChannel);
790    type TestRegistrations = HashMap<PublicKey, TestRegistration>;
791
792    /// Builds a [Lookahead] with a term length of 5.
793    fn test_lookahead(optimistic_views: u64) -> Lookahead {
794        Lookahead {
795            term_length: TermLength::new(NZU32!(5)),
796            optimistic_views: ViewDelta::new(optimistic_views),
797        }
798    }
799
800    #[test]
801    fn test_lookahead_admits() {
802        let current = View::new(6);
803
804        // Always allow immediate successor.
805        assert!(test_lookahead(0).admits(current, current.next()));
806
807        // Always allow next-term start.
808        let next_term_start = current.next_term_start(test_lookahead(0).term_length);
809        assert!(test_lookahead(0).admits(current, next_term_start));
810
811        // Bounded same-term optimistic lookahead.
812        assert!(test_lookahead(2).admits(current, View::new(8)));
813        assert!(!test_lookahead(2).admits(current, View::new(9)));
814
815        // Never allow arbitrarily far future when outside all accepted lanes.
816        assert!(!test_lookahead(10).admits(current, View::new(100)));
817    }
818
819    #[test]
820    fn test_optimistic_future_does_not_bleed_into_next_term() {
821        let lookahead = test_lookahead(100);
822        let current = View::new(9);
823        let next_term_start = current.next_term_start(lookahead.term_length);
824
825        // Large configuration still caps same-term optimism at term end (view 10).
826        assert!(lookahead.admits(current, View::new(10)));
827
828        // Next-term start is always accepted as a special transition.
829        assert!(lookahead.admits(current, next_term_start));
830
831        // But optimistic lookahead must not bleed into later views of the next term.
832        assert!(!lookahead.admits(current, next_term_start.next()));
833    }
834
835    #[test]
836    fn test_admission_limit() {
837        // Term of view 6 spans views 6..=10.
838        let current = View::new(6);
839
840        // No optimism: the window is empty and the limit is `current` itself.
841        assert_eq!(test_lookahead(0).admission_limit(current), current);
842
843        // Bounded by the optimistic lookahead when it fits within the term.
844        assert_eq!(test_lookahead(3).admission_limit(current), View::new(9));
845
846        // Clamped at term end when the lookahead would cross it.
847        assert_eq!(test_lookahead(100).admission_limit(current), View::new(10));
848
849        // At the last view of a term, the window is always empty.
850        assert_eq!(
851            test_lookahead(3).admission_limit(View::new(10)),
852            View::new(10)
853        );
854    }
855
856    #[test]
857    fn test_issuance_floor() {
858        // No optimism: nothing is issuable.
859        assert!(test_lookahead(0).issuance_floor(View::new(7)).is_none());
860
861        // Term starts are never issued optimistically (terms start at 1, 6, 11).
862        assert!(test_lookahead(100).issuance_floor(View::new(11)).is_none());
863
864        // The floor sits `optimistic_views + 1` views below: view 9 is anchored
865        // by a notarization at view 6, but not by one at view 5.
866        assert_eq!(
867            test_lookahead(2).issuance_floor(View::new(9)),
868            Some(View::new(6))
869        );
870
871        // Genesis floors the window until the first notarization lands.
872        assert_eq!(
873            test_lookahead(2).issuance_floor(View::new(2)),
874            Some(View::zero())
875        );
876    }
877
878    /// Register a validator with the oracle.
879    async fn register_validator(
880        oracle: &mut Oracle<PublicKey, deterministic::Context>,
881        validator: PublicKey,
882    ) -> TestRegistration {
883        let control = oracle.control(validator.clone());
884        let (vote_sender, vote_receiver) = control.register(0, TEST_QUOTA).await.unwrap();
885        let (certificate_sender, certificate_receiver) =
886            control.register(1, TEST_QUOTA).await.unwrap();
887        let (resolver_sender, resolver_receiver) = control.register(2, TEST_QUOTA).await.unwrap();
888        (
889            (vote_sender, vote_receiver),
890            (certificate_sender, certificate_receiver),
891            (resolver_sender, resolver_receiver),
892        )
893    }
894
895    /// Registers all validators using the oracle.
896    async fn register_validators(
897        oracle: &mut Oracle<PublicKey, deterministic::Context>,
898        validators: &[PublicKey],
899    ) -> TestRegistrations {
900        let mut registrations = HashMap::new();
901        for validator in validators.iter() {
902            let registration = register_validator(oracle, validator.clone()).await;
903            registrations.insert(validator.clone(), registration);
904        }
905        registrations
906    }
907
908    async fn start_test_network_with_peers<I>(
909        context: deterministic::Context,
910        peers: I,
911        disconnect_on_block: bool,
912    ) -> Oracle<PublicKey, deterministic::Context>
913    where
914        I: IntoIterator<Item = PublicKey>,
915    {
916        let peers: Vec<_> = peers.into_iter().collect();
917        let (network, oracle) = Network::new_with_peers(
918            context.child("network"),
919            Config {
920                max_size: 1024 * 1024,
921                // Some tests replace the initial set with the committee plus one injector.
922                max_peers_per_set: NZUsize!(peers.len() + 1),
923                disconnect_on_block,
924                tracked_peer_sets: NZUsize!(1),
925            },
926            peers,
927        )
928        .await;
929        network.start();
930        oracle
931    }
932
933    async fn start_test_network_with_split_peers<I, J>(
934        context: deterministic::Context,
935        primary: I,
936        secondary: J,
937        disconnect_on_block: bool,
938    ) -> Oracle<PublicKey, deterministic::Context>
939    where
940        I: IntoIterator<Item = PublicKey>,
941        J: IntoIterator<Item = PublicKey>,
942    {
943        let primary: Vec<_> = primary.into_iter().collect();
944        let secondary: Vec<_> = secondary.into_iter().collect();
945        let (network, oracle) = Network::new_with_split_peers(
946            context.child("network"),
947            Config {
948                max_size: 1024 * 1024,
949                max_peers_per_set: NZUsize!(primary.len() + secondary.len()),
950                disconnect_on_block,
951                tracked_peer_sets: NZUsize!(1),
952            },
953            primary,
954            secondary,
955        )
956        .await;
957        network.start();
958        oracle
959    }
960
961    /// Enum to describe the action to take when linking validators.
962    enum Action {
963        Link(Link),
964        Update(Link), // Unlink and then link
965        Unlink,
966    }
967
968    /// Links (or unlinks) validators using the oracle.
969    ///
970    /// The `action` parameter determines the action (e.g. link, unlink) to take.
971    /// The `restrict_to` function can be used to restrict the linking to certain connections,
972    /// otherwise all validators will be linked to all other validators.
973    async fn link_validators(
974        oracle: &mut Oracle<PublicKey, deterministic::Context>,
975        validators: &[PublicKey],
976        action: Action,
977        restrict_to: Option<fn(usize, usize, usize) -> bool>,
978    ) {
979        for (i1, v1) in validators.iter().enumerate() {
980            for (i2, v2) in validators.iter().enumerate() {
981                // Ignore self
982                if v2 == v1 {
983                    continue;
984                }
985
986                // Restrict to certain connections
987                if let Some(f) = restrict_to
988                    && !f(validators.len(), i1, i2)
989                {
990                    continue;
991                }
992
993                // Do any unlinking first
994                match action {
995                    Action::Update(_) | Action::Unlink => {
996                        oracle.remove_link(v1.clone(), v2.clone()).await.unwrap();
997                    }
998                    _ => {}
999                }
1000
1001                // Do any linking after
1002                match action {
1003                    Action::Link(ref link) | Action::Update(ref link) => {
1004                        oracle
1005                            .add_link(v1.clone(), v2.clone(), link.clone())
1006                            .await
1007                            .unwrap();
1008                    }
1009                    _ => {}
1010                }
1011            }
1012        }
1013    }
1014
1015    /// Counts lines where all patterns match and the trailing value is non-zero.
1016    fn count_nonzero_metric_lines(encoded: &str, patterns: &[&str]) -> u32 {
1017        encoded
1018            .lines()
1019            .filter(|line| patterns.iter().all(|p| line.contains(p)))
1020            .filter(|line| {
1021                line.split_whitespace()
1022                    .last()
1023                    .and_then(|s| s.parse::<u64>().ok())
1024                    .is_some_and(|n| n > 0)
1025            })
1026            .count() as u32
1027    }
1028
1029    fn all_online<S, F, L, T>(
1030        mut fixture: F,
1031        elector: L,
1032        strategy: impl FnOnce(&mut deterministic::Context) -> T + Send + 'static,
1033    ) where
1034        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
1035        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
1036        L: elector::Config<S>,
1037        T: Strategy,
1038    {
1039        // Create context
1040        let n = 5;
1041        let quorum = quorum(n) as usize;
1042        let required_containers = View::new(100);
1043        let view_retention = ViewDelta::new(10);
1044        let skip_timeout = Duration::from_secs(12);
1045        let namespace = b"consensus".to_vec();
1046        let executor = deterministic::Runner::timed(Duration::from_secs(300));
1047        executor.start(|mut context| async move {
1048            // Register participants
1049            let Fixture {
1050                participants,
1051                schemes,
1052                ..
1053            } = fixture(&mut context, &namespace, n);
1054            let strategy = strategy(&mut context);
1055            let mut oracle =
1056                start_test_network_with_peers(context.child("network"), participants.clone(), true)
1057                    .await;
1058            let mut registrations = register_validators(&mut oracle, &participants).await;
1059
1060            // Link all validators. The 200ms latency is deliberate: high
1061            // enough (relative to the 2s/3s timeouts below) that stable-leader
1062            // variants only stay nullification-free by pipelining views
1063            // optimistically, while still leaving the non-optimistic variants
1064            // comfortable margin.
1065            let link = Link {
1066                latency: Duration::from_millis(200),
1067                jitter: Duration::from_millis(1),
1068                success_rate: probability!(1.0),
1069            };
1070            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
1071
1072            // Create engines
1073            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
1074            let mut reporters = Vec::new();
1075            let mut engine_handlers = Vec::new();
1076            for (idx, validator) in participants.iter().enumerate() {
1077                // Create scheme context
1078                let context = context
1079                    .child("validator")
1080                    .with_attribute("public_key", validator);
1081
1082                // Configure engine
1083                let reporter_config = mocks::reporter::Config {
1084                    participants: participants.clone().try_into().unwrap(),
1085                    scheme: schemes[idx].clone(),
1086                    elector: elector.clone(),
1087                };
1088                let reporter =
1089                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
1090                reporters.push(reporter.clone());
1091                let application_cfg = mocks::application::Config::<Sha256, _> {
1092                    relay: relay.clone(),
1093                    me: validator.clone(),
1094                    propose_latency: (10.0, 5.0),
1095                    verify_latency: (10.0, 5.0),
1096                    certify_latency: (10.0, 5.0),
1097                    should_certify: mocks::application::Certifier::Always,
1098                };
1099                let (actor, application) = mocks::application::Application::new(
1100                    context.child("application"),
1101                    application_cfg,
1102                );
1103                actor.start();
1104                let blocker = oracle.control(validator.clone());
1105                let cfg = config::Config {
1106                    scheme: schemes[idx].clone(),
1107                    elector: elector.clone(),
1108                    blocker,
1109                    automaton: application.clone(),
1110                    relay: application.clone(),
1111                    reporter: reporter.clone(),
1112                    strategy: strategy.clone(),
1113                    partition: validator.to_string(),
1114                    mailbox_size: NZUsize!(1024),
1115                    epoch: Epoch::new(333),
1116                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
1117                        Epoch::new(333),
1118                    )),
1119                    leader_timeout: Duration::from_secs(2),
1120                    certification_timeout: Duration::from_secs(3),
1121                    timeout_retry: Duration::from_secs(10),
1122                    fetch_timeout: Duration::from_secs(1),
1123                    view_retention,
1124                    skip: SkipPolicy::Enabled {
1125                        timeout: skip_timeout,
1126                        budget: SkipBudget::Participants,
1127                    },
1128                    replay_buffer: NZUsize!(1024 * 1024),
1129                    write_buffer: NZUsize!(1024 * 1024),
1130                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1131                    forward: ForwardPolicy::Disabled,
1132                    track_historical_votes: false,
1133                };
1134                let engine = Engine::new(context.child("engine"), cfg);
1135
1136                // Start engine
1137                let (pending, recovered, resolver) = registrations
1138                    .remove(validator)
1139                    .expect("validator should be registered");
1140                engine_handlers.push(engine.start(pending, recovered, resolver));
1141            }
1142
1143            // Wait for all engines to finish
1144            let mut finalizers = Vec::new();
1145            for reporter in reporters.iter_mut() {
1146                let (mut latest, mut monitor) = reporter.subscribe().await;
1147                finalizers.push(context.child("finalizer").spawn(move |_| async move {
1148                    while latest < required_containers {
1149                        latest = monitor.recv().await.expect("event missing");
1150                    }
1151                }));
1152            }
1153            join_all(finalizers).await;
1154
1155            // Check reporters for correct activity
1156            let latest_complete = required_containers.saturating_sub(view_retention);
1157            for reporter in reporters.iter() {
1158                // Ensure no faults
1159                reporter.assert_no_faults();
1160
1161                // Ensure no invalid signatures
1162                reporter.assert_no_invalid();
1163
1164                // Ensure certificates for all views
1165                {
1166                    let certified = reporter.certified.lock();
1167                    for view in View::range(View::new(1), latest_complete) {
1168                        // Ensure certificate for every view
1169                        if !certified.contains(&view) {
1170                            panic!("view: {view}");
1171                        }
1172                    }
1173                }
1174
1175                // Ensure no forks
1176                let mut notarized = HashMap::new();
1177                let mut finalized = HashMap::new();
1178                {
1179                    let notarizes = reporter.notarizes.lock();
1180                    for view in View::range(View::new(1), latest_complete) {
1181                        // Ensure only one payload proposed per view
1182                        let Some(payloads) = notarizes.get(&view) else {
1183                            continue;
1184                        };
1185                        if payloads.len() > 1 {
1186                            panic!("view: {view}");
1187                        }
1188                        let (digest, notarizers) = payloads.iter().next().unwrap();
1189                        notarized.insert(view, *digest);
1190
1191                        if notarizers.len() < quorum {
1192                            // We can't verify that everyone participated at every view because some nodes may
1193                            // have started later.
1194                            panic!("view: {view}");
1195                        }
1196                    }
1197                }
1198                {
1199                    let notarizations = reporter.notarizations.lock();
1200                    for view in View::range(View::new(1), latest_complete) {
1201                        // Ensure notarization matches digest from notarizes
1202                        let Some(notarization) = notarizations.get(&view) else {
1203                            continue;
1204                        };
1205                        let Some(digest) = notarized.get(&view) else {
1206                            continue;
1207                        };
1208                        assert_eq!(&notarization.proposal.payload, digest);
1209                    }
1210                }
1211                {
1212                    let finalizes = reporter.finalizes.lock();
1213                    for view in View::range(View::new(1), latest_complete) {
1214                        // Ensure only one payload proposed per view
1215                        let Some(payloads) = finalizes.get(&view) else {
1216                            continue;
1217                        };
1218                        if payloads.len() > 1 {
1219                            panic!("view: {view}");
1220                        }
1221                        let (digest, finalizers) = payloads.iter().next().unwrap();
1222                        finalized.insert(view, *digest);
1223
1224                        // Only check at views below timeout
1225                        if view > latest_complete {
1226                            continue;
1227                        }
1228
1229                        // Ensure everyone participating
1230                        if finalizers.len() < quorum {
1231                            // We can't verify that everyone participated at every view because some nodes may
1232                            // have started later.
1233                            panic!("view: {view}");
1234                        }
1235
1236                        // Ensure no nullifies for any finalizers
1237                        let nullifies = reporter.nullifies.lock();
1238                        let Some(nullifies) = nullifies.get(&view) else {
1239                            continue;
1240                        };
1241                        for finalizers in payloads.values() {
1242                            for finalizer in finalizers.iter() {
1243                                if nullifies.contains(finalizer) {
1244                                    panic!("should not nullify and finalize at same view");
1245                                }
1246                            }
1247                        }
1248                    }
1249                }
1250                {
1251                    let finalizations = reporter.finalizations.lock();
1252                    for view in View::range(View::new(1), latest_complete) {
1253                        // Ensure finalization matches digest from finalizes
1254                        let Some(finalization) = finalizations.get(&view) else {
1255                            continue;
1256                        };
1257                        let Some(digest) = finalized.get(&view) else {
1258                            continue;
1259                        };
1260                        assert_eq!(&finalization.proposal.payload, digest);
1261                    }
1262                }
1263            }
1264
1265            // Ensure no blocked connections
1266            let blocked = oracle.blocked().await.unwrap();
1267            assert!(blocked.is_empty());
1268        });
1269    }
1270
1271    test_for_all_fixtures!(all_online, |_| Sequential);
1272
1273    #[test_group("slow")]
1274    #[test_traced]
1275    fn test_all_online_rayon_bls12381_threshold_vrf_min_pk() {
1276        all_online::<_, _, Random, _>(
1277            bls12381_threshold_vrf::fixture::<MinPk, _>,
1278            Random::new(RandomVersion::V1),
1279            |context| context.strategy(NZUsize!(2)),
1280        );
1281    }
1282
1283    fn non_genesis_floor_joiner_catches_tip<S, F, L>(fixture: F, elector: L)
1284    where
1285        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
1286        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
1287        L: elector::Config<S>,
1288    {
1289        non_genesis_floor_joiner_catches_tip_with_term::<S, F, L>(elector, fixture);
1290    }
1291
1292    fn non_genesis_floor_joiner_catches_tip_with_term<S, F, L>(elector: L, mut fixture: F)
1293    where
1294        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
1295        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
1296        L: elector::Config<S>,
1297    {
1298        // First let a quorum finalize beyond genesis so the joiner has a real
1299        // floor certificate and existing tip to catch.
1300        let n = 5;
1301        let active_count = quorum(n) as usize;
1302        let initial_tip_target = View::new(15);
1303        let view_retention = ViewDelta::new(10);
1304        let skip_timeout = Duration::from_secs(5);
1305        let timeout_retry = Duration::from_secs(1);
1306        let namespace = b"consensus".to_vec();
1307        let executor = deterministic::Runner::timed(Duration::from_secs(300));
1308        executor.start(|mut context| async move {
1309            let Fixture {
1310                participants,
1311                schemes,
1312                ..
1313            } = fixture(&mut context, &namespace, n);
1314            let mut oracle =
1315                start_test_network_with_peers(context.child("network"), participants.clone(), true)
1316                    .await;
1317
1318            let active = &participants[..active_count];
1319            let joiner_idx = active_count;
1320            let joiner = participants[joiner_idx].clone();
1321
1322            let link = Link {
1323                latency: Duration::from_millis(10),
1324                jitter: Duration::from_millis(1),
1325                success_rate: probability!(1.0),
1326            };
1327            link_validators(&mut oracle, active, Action::Link(link.clone()), None).await;
1328
1329            let term_length = elector
1330                .clone()
1331                .build(schemes[0].participants())
1332                .terms()
1333                .length();
1334            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
1335            let mut reporters = Vec::new();
1336            let mut engine_handlers = Vec::new();
1337
1338            for (idx, validator) in active.iter().enumerate() {
1339                let validator_context = context
1340                    .child("validator")
1341                    .with_attribute("public_key", validator);
1342
1343                let reporter_config = mocks::reporter::Config {
1344                    participants: participants.clone().try_into().unwrap(),
1345                    scheme: schemes[idx].clone(),
1346                    elector: elector.clone(),
1347                };
1348                let reporter = mocks::reporter::Reporter::new(
1349                    validator_context.child("reporter"),
1350                    reporter_config,
1351                );
1352                reporters.push(reporter.clone());
1353
1354                let application_cfg = mocks::application::Config::<Sha256, _> {
1355                    relay: relay.clone(),
1356                    me: validator.clone(),
1357                    propose_latency: (10.0, 5.0),
1358                    verify_latency: (10.0, 5.0),
1359                    certify_latency: (10.0, 5.0),
1360                    should_certify: mocks::application::Certifier::Always,
1361                };
1362                let (actor, application) = mocks::application::Application::new(
1363                    validator_context.child("application"),
1364                    application_cfg,
1365                );
1366                actor.start();
1367
1368                let cfg = config::Config {
1369                    scheme: schemes[idx].clone(),
1370                    elector: elector.clone(),
1371                    blocker: oracle.control(validator.clone()),
1372                    automaton: application.clone(),
1373                    relay: application.clone(),
1374                    reporter: reporter.clone(),
1375                    strategy: Sequential,
1376                    partition: format!("joiner_catches_tip_{validator}"),
1377                    mailbox_size: NZUsize!(1024),
1378                    epoch: Epoch::new(333),
1379                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
1380                        Epoch::new(333),
1381                    )),
1382                    leader_timeout: Duration::from_secs(1),
1383                    certification_timeout: Duration::from_secs(2),
1384                    timeout_retry,
1385                    fetch_timeout: Duration::from_secs(1),
1386                    view_retention,
1387                    skip: SkipPolicy::Enabled {
1388                        timeout: skip_timeout,
1389                        budget: SkipBudget::Participants,
1390                    },
1391                    replay_buffer: NZUsize!(1024 * 1024),
1392                    write_buffer: NZUsize!(1024 * 1024),
1393                    page_cache: CacheRef::from_pooler(
1394                        &validator_context,
1395                        PAGE_SIZE,
1396                        PAGE_CACHE_SIZE,
1397                    ),
1398                    forward: ForwardPolicy::Disabled,
1399                    track_historical_votes: false,
1400                };
1401                let engine = Engine::new(validator_context.child("engine"), cfg);
1402                let (pending, recovered, resolver) =
1403                    register_validator(&mut oracle, validator.clone()).await;
1404                engine_handlers.push(engine.start(pending, recovered, resolver));
1405            }
1406
1407            let mut finalizers = Vec::new();
1408            for reporter in reporters.iter_mut() {
1409                let (mut latest, mut monitor) = reporter.subscribe().await;
1410                finalizers.push(
1411                    context
1412                        .child("initial_finalizer")
1413                        .spawn(move |_| async move {
1414                            while latest < initial_tip_target {
1415                                latest = monitor.recv().await.expect("event missing");
1416                            }
1417                            latest
1418                        }),
1419                );
1420            }
1421            let tip_at_join = join_all(finalizers)
1422                .await
1423                .into_iter()
1424                .map(|result| result.expect("initial finalizer failed"))
1425                .min()
1426                .expect("initial validators missing");
1427
1428            // Prefer a mid-term floor to exercise startup term arithmetic; at
1429            // term_length 1 every view is a term start, so fall back to the
1430            // minimum eligible view.
1431            let (floor_view, floor_finalization) = {
1432                let finalizations = reporters[0].finalizations.lock();
1433                let mut eligible: Vec<_> = finalizations
1434                    .iter()
1435                    .filter(|(view, _)| **view > View::zero() && **view < tip_at_join)
1436                    .collect();
1437                eligible.sort_by_key(|(view, _)| view.get());
1438                eligible
1439                    .iter()
1440                    .find(|(view, _)| !view.is_term_start(term_length))
1441                    .or_else(|| eligible.first())
1442                    .map(|(view, finalization)| (**view, (*finalization).clone()))
1443                    .expect("non-genesis floor finalization missing")
1444            };
1445            assert!(floor_view > View::zero());
1446            assert!(floor_view < tip_at_join);
1447            if term_length.get() > 1 {
1448                assert!(
1449                    !floor_view.is_term_start(term_length),
1450                    "expected a mid-term floor at view {floor_view}"
1451                );
1452            }
1453
1454            // Start the extra validator from the non-genesis floor and require
1455            // it to catch both the existing tip and later cluster progress.
1456            for validator in active.iter() {
1457                oracle
1458                    .add_link(joiner.clone(), validator.clone(), link.clone())
1459                    .await
1460                    .unwrap();
1461                oracle
1462                    .add_link(validator.clone(), joiner.clone(), link.clone())
1463                    .await
1464                    .unwrap();
1465            }
1466
1467            let joiner_context = context
1468                .child("validator")
1469                .with_attribute("public_key", &joiner);
1470            let reporter_config = mocks::reporter::Config {
1471                participants: participants.clone().try_into().unwrap(),
1472                scheme: schemes[joiner_idx].clone(),
1473                elector: elector.clone(),
1474            };
1475            let mut joiner_reporter =
1476                mocks::reporter::Reporter::new(joiner_context.child("reporter"), reporter_config);
1477            reporters.push(joiner_reporter.clone());
1478
1479            let application_cfg = mocks::application::Config::<Sha256, _> {
1480                relay: relay.clone(),
1481                me: joiner.clone(),
1482                propose_latency: (10.0, 5.0),
1483                verify_latency: (10.0, 5.0),
1484                certify_latency: (10.0, 5.0),
1485                should_certify: mocks::application::Certifier::Always,
1486            };
1487            let (actor, application) = mocks::application::Application::new(
1488                joiner_context.child("application"),
1489                application_cfg,
1490            );
1491            actor.start();
1492
1493            let cfg = config::Config {
1494                scheme: schemes[joiner_idx].clone(),
1495                elector,
1496                blocker: oracle.control(joiner.clone()),
1497                automaton: application.clone(),
1498                relay: application.clone(),
1499                reporter: joiner_reporter.clone(),
1500                strategy: Sequential,
1501                partition: format!("joiner_catches_tip_{joiner}"),
1502                mailbox_size: NZUsize!(1024),
1503                epoch: Epoch::new(333),
1504                floor: config::Floor::Finalized(floor_finalization),
1505                leader_timeout: Duration::from_secs(1),
1506                certification_timeout: Duration::from_secs(2),
1507                timeout_retry,
1508                fetch_timeout: Duration::from_secs(1),
1509                view_retention,
1510                skip: SkipPolicy::Enabled {
1511                    timeout: skip_timeout,
1512                    budget: SkipBudget::Participants,
1513                },
1514                replay_buffer: NZUsize!(1024 * 1024),
1515                write_buffer: NZUsize!(1024 * 1024),
1516                page_cache: CacheRef::from_pooler(&joiner_context, PAGE_SIZE, PAGE_CACHE_SIZE),
1517                forward: ForwardPolicy::Disabled,
1518                track_historical_votes: false,
1519            };
1520            let engine = Engine::new(joiner_context.child("engine"), cfg);
1521            let (pending, recovered, resolver) = register_validator(&mut oracle, joiner).await;
1522            engine_handlers.push(engine.start(pending, recovered, resolver));
1523
1524            let (mut joiner_latest, mut joiner_monitor) = joiner_reporter.subscribe().await;
1525            while joiner_latest < tip_at_join {
1526                joiner_latest = joiner_monitor.recv().await.expect("event missing");
1527            }
1528
1529            let post_join_target = tip_at_join.saturating_add(ViewDelta::new(5));
1530            while joiner_latest < post_join_target {
1531                joiner_latest = joiner_monitor.recv().await.expect("event missing");
1532            }
1533
1534            for reporter in reporters.iter() {
1535                reporter.assert_no_faults();
1536                reporter.assert_no_invalid();
1537            }
1538
1539            let blocked = oracle.blocked().await.unwrap();
1540            assert!(blocked.is_empty());
1541        });
1542    }
1543
1544    test_for_all_fixtures!(non_genesis_floor_joiner_catches_tip);
1545
1546    #[test_group("slow")]
1547    #[test_traced]
1548    fn test_non_genesis_floor_joiner_catches_tip_stable_leader() {
1549        non_genesis_floor_joiner_catches_tip_with_term::<_, _, RoundRobin>(
1550            RoundRobin::default().with_term(
1551                TermLength::new(NZU32!(3)),
1552                Duration::from_secs(12),
1553                ViewDelta::new(0),
1554            ),
1555            scheme_mocks::fixture,
1556        );
1557    }
1558
1559    /// A dishonest leader (validator 0) proposes payloads that all honest peers
1560    /// refuse to certify.
1561    ///
1562    /// All n validators use the honest Application, but every peer's certifier
1563    /// rejects proposals from views where validator 0 is the elected leader.
1564    /// When validator 0 IS the leader, it short-circuits certification locally
1565    /// (it built the proposal) and votes finalize, but every other peer
1566    /// rejects via the Custom predicate and nullifies. The lone finalize vote
1567    /// cannot form a certificate (quorum=4). The nullification cert (4 honest
1568    /// peers) advances everyone.
1569    ///
1570    /// When an honest validator leads, all peers (including validator 0)
1571    /// certify normally and finalize. The cluster makes progress on honest
1572    /// leader views and nullifies dishonest leader views.
1573    fn dishonest_leader_certification_rejected<S, F>(mut fixture: F)
1574    where
1575        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
1576        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
1577        RoundRobin: elector::Config<S>,
1578    {
1579        let n = 5;
1580        let required_containers = View::new(50);
1581        let view_retention = ViewDelta::new(10);
1582        let skip_timeout = Duration::from_secs(12);
1583        let namespace = b"consensus".to_vec();
1584        let executor = deterministic::Runner::timed(Duration::from_secs(300));
1585        executor.start(|mut context| async move {
1586            let Fixture {
1587                participants,
1588                schemes,
1589                ..
1590            } = fixture(&mut context, &namespace, n);
1591            let mut oracle =
1592                start_test_network_with_peers(context.child("network"), participants.clone(), true)
1593                    .await;
1594            let mut registrations = register_validators(&mut oracle, &participants).await;
1595
1596            let link = Link {
1597                latency: Duration::from_millis(10),
1598                jitter: Duration::from_millis(1),
1599                success_rate: probability!(1.0),
1600            };
1601            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
1602
1603            let elector = RoundRobin::default();
1604            let participants_set: Set<S::PublicKey> = participants.clone().try_into().unwrap();
1605            let built_elector = elector.clone().build(&participants_set);
1606            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
1607            let mut reporters = Vec::new();
1608            let mut engine_handlers = Vec::new();
1609            let dishonest = Participant::new(0);
1610            for (idx, validator) in participants.iter().enumerate() {
1611                let context = context
1612                    .child("validator")
1613                    .with_attribute("public_key", validator);
1614                let reporter_config = mocks::reporter::Config {
1615                    participants: participants.clone().try_into().unwrap(),
1616                    scheme: schemes[idx].clone(),
1617                    elector: elector.clone(),
1618                };
1619                let reporter =
1620                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
1621                reporters.push(reporter.clone());
1622
1623                let application_cfg = mocks::application::Config::<Sha256, _> {
1624                    relay: relay.clone(),
1625                    me: validator.clone(),
1626                    propose_latency: (10.0, 5.0),
1627                    verify_latency: (10.0, 5.0),
1628                    certify_latency: (10.0, 5.0),
1629                    should_certify: mocks::application::Certifier::Custom(Box::new({
1630                        let built_elector_clone = built_elector.clone();
1631                        move |round, _| built_elector_clone.elect(round, None) != dishonest
1632                    })),
1633                };
1634                let (actor, application) = mocks::application::Application::new(
1635                    context.child("application"),
1636                    application_cfg,
1637                );
1638                actor.start();
1639
1640                let blocker = oracle.control(validator.clone());
1641                let cfg = config::Config {
1642                    scheme: schemes[idx].clone(),
1643                    elector: elector.clone(),
1644                    blocker,
1645                    automaton: application.clone(),
1646                    relay: application.clone(),
1647                    reporter: reporter.clone(),
1648                    strategy: Sequential,
1649                    partition: validator.to_string(),
1650                    mailbox_size: NZUsize!(1024),
1651                    epoch: Epoch::new(333),
1652                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
1653                        Epoch::new(333),
1654                    )),
1655                    leader_timeout: Duration::from_secs(1),
1656                    certification_timeout: Duration::from_secs(2),
1657                    timeout_retry: Duration::from_secs(10),
1658                    fetch_timeout: Duration::from_secs(1),
1659                    view_retention,
1660                    skip: SkipPolicy::Enabled {
1661                        timeout: skip_timeout,
1662                        budget: SkipBudget::Participants,
1663                    },
1664                    replay_buffer: NZUsize!(1024 * 1024),
1665                    write_buffer: NZUsize!(1024 * 1024),
1666                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1667                    forward: ForwardPolicy::Disabled,
1668                    track_historical_votes: false,
1669                };
1670                let engine = Engine::new(context.child("engine"), cfg);
1671                let (pending, recovered, resolver) = registrations
1672                    .remove(validator)
1673                    .expect("validator should be registered");
1674                engine_handlers.push(engine.start(pending, recovered, resolver));
1675            }
1676
1677            let mut finalizers = Vec::new();
1678            for reporter in reporters.iter_mut() {
1679                let (mut latest, mut monitor) = reporter.subscribe().await;
1680                finalizers.push(context.child("finalizer").spawn(move |_| async move {
1681                    while latest < required_containers {
1682                        latest = monitor.recv().await.expect("event missing");
1683                    }
1684                }));
1685            }
1686            join_all(finalizers).await;
1687
1688            for reporter in reporters.iter() {
1689                reporter.assert_no_faults();
1690                reporter.assert_no_invalid();
1691            }
1692        });
1693    }
1694
1695    #[test_group("slow")]
1696    #[test_traced]
1697    fn test_dishonest_leader_certification_rejected() {
1698        dishonest_leader_certification_rejected::<_, _>(
1699            bls12381_threshold_vrf::fixture::<MinPk, _>,
1700        );
1701        dishonest_leader_certification_rejected::<_, _>(
1702            bls12381_threshold_vrf::fixture::<MinSig, _>,
1703        );
1704        dishonest_leader_certification_rejected::<_, _>(
1705            bls12381_threshold_std::fixture::<MinPk, _>,
1706        );
1707        dishonest_leader_certification_rejected::<_, _>(
1708            bls12381_threshold_std::fixture::<MinSig, _>,
1709        );
1710        dishonest_leader_certification_rejected::<_, _>(bls12381_multisig::fixture::<MinPk, _>);
1711        dishonest_leader_certification_rejected::<_, _>(bls12381_multisig::fixture::<MinSig, _>);
1712        dishonest_leader_certification_rejected::<_, _>(ed25519::fixture);
1713        dishonest_leader_certification_rejected::<_, _>(secp256r1::fixture);
1714    }
1715
1716    /// Reporter used by the stable-leader end-to-end tests.
1717    type StableLeaderReporter = mocks::reporter::Reporter<
1718        deterministic::Context,
1719        ed25519::Scheme,
1720        RoundRobin<Sha256>,
1721        Sha256Digest,
1722    >;
1723
1724    /// Spins up the fully-linked five-validator ed25519 cluster shared by the
1725    /// stable-leader end-to-end tests, parameterized by the knobs that differ
1726    /// between them. Returns the per-validator reporters, the index of the
1727    /// leader elected for view 1 (stable for the whole term), and the network
1728    /// oracle.
1729    ///
1730    /// The 1.5s leader and 3.5s certification timeouts are tuned to the
1731    /// callers' link latencies: with latency near or above
1732    /// half the leader timeout, a view that waits for its parent's
1733    /// certification (two or more network trips) times out, so runs stay
1734    /// nullification-free only when views pipeline optimistically.
1735    async fn setup_stable_leader_cluster(
1736        context: &mut deterministic::Context,
1737        namespace: &[u8],
1738        link: Link,
1739        term_length: TermLength,
1740        optimistic_views: ViewDelta,
1741        propose_latency: (f64, f64),
1742        stall_timeout: Duration,
1743    ) -> (
1744        Vec<StableLeaderReporter>,
1745        usize,
1746        Oracle<PublicKey, deterministic::Context>,
1747    ) {
1748        let epoch = Epoch::new(333);
1749        let Fixture {
1750            participants,
1751            schemes,
1752            ..
1753        } = ed25519::fixture(context, namespace, 5);
1754        let mut oracle =
1755            start_test_network_with_peers(context.child("network"), participants.clone(), true)
1756                .await;
1757        let mut registrations = register_validators(&mut oracle, &participants).await;
1758        link_validators(&mut oracle, &participants, Action::Link(link), None).await;
1759
1760        let elector =
1761            RoundRobin::<Sha256>::default().with_term(term_length, stall_timeout, optimistic_views);
1762        let relay = Arc::new(mocks::relay::Relay::new());
1763        let mut reporters = Vec::new();
1764
1765        for (idx, validator) in participants.iter().enumerate() {
1766            let context = context
1767                .child("validator")
1768                .with_attribute("public_key", validator);
1769            let reporter_config = mocks::reporter::Config {
1770                participants: participants.clone().try_into().unwrap(),
1771                scheme: schemes[idx].clone(),
1772                elector: elector.clone(),
1773            };
1774            let reporter =
1775                mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
1776            reporters.push(reporter.clone());
1777
1778            let application_cfg = mocks::application::Config::<Sha256, _> {
1779                relay: relay.clone(),
1780                me: validator.clone(),
1781                propose_latency,
1782                verify_latency: (1.0, 0.0),
1783                certify_latency: (1.0, 0.0),
1784                should_certify: mocks::application::Certifier::Always,
1785            };
1786            let (actor, application) =
1787                mocks::application::Application::new(context.child("application"), application_cfg);
1788            actor.start();
1789
1790            let blocker = oracle.control(validator.clone());
1791            let cfg = config::Config {
1792                scheme: schemes[idx].clone(),
1793                elector: elector.clone(),
1794                blocker,
1795                automaton: application.clone(),
1796                relay: application.clone(),
1797                reporter: reporter.clone(),
1798                strategy: Sequential,
1799                partition: validator.to_string(),
1800                mailbox_size: NZUsize!(1024),
1801                epoch,
1802                floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(epoch)),
1803                leader_timeout: Duration::from_millis(1_500),
1804                certification_timeout: Duration::from_millis(3_500),
1805                timeout_retry: Duration::from_secs(10),
1806                fetch_timeout: Duration::from_secs(1),
1807                view_retention: ViewDelta::new(10),
1808                skip: SkipPolicy::Enabled {
1809                    timeout: Duration::from_secs(12),
1810                    budget: SkipBudget::Participants,
1811                },
1812                replay_buffer: NZUsize!(1024 * 1024),
1813                write_buffer: NZUsize!(1024 * 1024),
1814                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
1815                forward: ForwardPolicy::Disabled,
1816                track_historical_votes: false,
1817            };
1818            let engine = Engine::new(context.child("engine"), cfg);
1819            let (pending, recovered, resolver) = registrations
1820                .remove(validator)
1821                .expect("validator should be registered");
1822            engine.start(pending, recovered, resolver);
1823        }
1824
1825        let participants_set = participants.clone().try_into().unwrap();
1826        let built_elector: elector::RoundRobinElector<ed25519::Scheme> =
1827            elector.build(&participants_set);
1828        let leader_idx = usize::from(built_elector.elect(Round::new(epoch, View::new(1)), None));
1829
1830        (reporters, leader_idx, oracle)
1831    }
1832
1833    #[test_traced]
1834    fn test_stable_leader_optimistic_blocks_faster_than_network_latency() {
1835        let required_containers = View::new(100);
1836        let link_latency = Duration::from_millis(100);
1837        let executor = deterministic::Runner::timed(Duration::from_secs(30));
1838        executor.start(|mut context| async move {
1839            let (reporters, leader_idx, _oracle) = setup_stable_leader_cluster(
1840                &mut context,
1841                b"consensus_stable_leader_high_latency",
1842                Link {
1843                    latency: link_latency,
1844                    jitter: Duration::from_millis(0),
1845                    success_rate: probability!(1.0),
1846                },
1847                TermLength::new(NZU32!(128)),
1848                ViewDelta::new(128),
1849                /* propose_latency */ (10.0, 0.0),
1850                /* stall_timeout */ Duration::from_secs(20),
1851            )
1852            .await;
1853
1854            let leader_reporter = reporters[leader_idx].clone();
1855            let start = context.current();
1856            while !leader_reporter
1857                .notarizes
1858                .lock()
1859                .contains_key(&required_containers)
1860            {
1861                context.sleep(Duration::from_millis(10)).await;
1862            }
1863            let elapsed = context.current().duration_since(start).unwrap_or_default();
1864            let average_block_time_s = elapsed.as_secs_f64() / required_containers.get() as f64;
1865            let network_latency_s = link_latency.as_secs_f64();
1866            assert!(
1867                average_block_time_s < network_latency_s,
1868                "expected average optimistic block time ({:.3}ms) to be below network latency ({:.3}ms); elapsed {:?} for {} views",
1869                average_block_time_s * 1000.0,
1870                network_latency_s * 1000.0,
1871                elapsed,
1872                required_containers
1873            );
1874
1875            for reporter in reporters.iter() {
1876                reporter.assert_no_invalid();
1877            }
1878        });
1879    }
1880
1881    #[test_group("slow")]
1882    #[test]
1883    fn test_stable_leader_finalizes_full_term_without_nullification() {
1884        let required_view = View::new(1000);
1885        let executor = deterministic::Runner::timed(Duration::from_secs(40));
1886        executor.start(|mut context| async move {
1887            let (reporters, leader_idx, oracle) = setup_stable_leader_cluster(
1888                &mut context,
1889                b"consensus_stable_leader_full_term_no_nullify",
1890                // 1s latency shrinks the 1.5s leader timeout below a
1891                // certification round-trip: staying nullification-free (the
1892                // assertion below) is only possible via optimistic pipelining.
1893                Link {
1894                    latency: Duration::from_millis(1_000),
1895                    jitter: Duration::from_millis(1),
1896                    success_rate: probability!(1.0),
1897                },
1898                TermLength::new(NZU32!(1000)),
1899                ViewDelta::new(100),
1900                /* propose_latency */ (1.0, 0.0),
1901                /* stall_timeout */ Duration::from_secs(6),
1902            )
1903            .await;
1904
1905            let start = context.current();
1906            let deadline = start + Duration::from_secs(25);
1907            while !reporters
1908                .iter()
1909                .all(|reporter| reporter.finalizations.lock().contains_key(&required_view))
1910            {
1911                if context.current() >= deadline {
1912                    let progress: Vec<_> = reporters
1913                        .iter()
1914                        .map(|reporter| {
1915                            let finalized = reporter
1916                                .finalizations
1917                                .lock()
1918                                .keys()
1919                                .copied()
1920                                .max()
1921                                .unwrap_or(View::zero());
1922                            (finalized, reporter.nullifications.lock().len())
1923                        })
1924                        .collect();
1925                    panic!(
1926                        "expected all validators to finalize view {required_view} before {deadline:?}; (max finalized, nullifications) per reporter: {progress:?}",
1927                    );
1928                }
1929                context.sleep(Duration::from_millis(10)).await;
1930            }
1931
1932            let leader_reporter = &reporters[leader_idx];
1933            assert!(
1934                leader_reporter.notarizes.lock().contains_key(&required_view),
1935                "stable leader must notarize through full term ending at view {}",
1936                required_view
1937            );
1938            let leader_tip_notarization = leader_reporter
1939                .notarizations
1940                .lock()
1941                .get(&required_view)
1942                .cloned()
1943                .expect("leader reporter missing tip notarization");
1944            assert_eq!(
1945                leader_tip_notarization.proposal.parent,
1946                required_view.previous().unwrap_or(View::zero()),
1947                "unexpected parent for leader tip notarization"
1948            );
1949
1950            for (idx, reporter) in reporters.iter().enumerate() {
1951                reporter.assert_no_invalid();
1952                reporter.assert_no_faults();
1953
1954                assert!(
1955                    reporter.nullifies.lock().is_empty(),
1956                    "reporter {} observed unexpected nullify votes",
1957                    idx
1958                );
1959                assert!(
1960                    reporter.nullifications.lock().is_empty(),
1961                    "reporter {} observed unexpected nullification certificates",
1962                    idx
1963                );
1964
1965                let finalization = reporter
1966                    .finalizations
1967                    .lock()
1968                    .get(&required_view)
1969                    .cloned()
1970                    .unwrap_or_else(|| panic!("reporter {idx} missing tip finalization"));
1971                assert_eq!(
1972                    finalization.proposal.round.view(),
1973                    required_view,
1974                    "reporter {idx} has mismatched tip finalization round"
1975                );
1976                assert_eq!(
1977                    finalization.proposal.parent,
1978                    required_view.previous().unwrap_or(View::zero()),
1979                    "reporter {idx} has non-chain tip finalization parent"
1980                );
1981            }
1982
1983            let blocked = oracle.blocked().await.unwrap();
1984            assert!(blocked.is_empty());
1985        });
1986    }
1987
1988    fn observer<S, F, L>(mut fixture: F, elector: L)
1989    where
1990        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
1991        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
1992        L: elector::Config<S>,
1993    {
1994        // Create context
1995        let n_active = 5;
1996        let required_containers = View::new(100);
1997        let view_retention = ViewDelta::new(10);
1998        let skip_timeout = Duration::from_secs(12);
1999        let namespace = b"consensus".to_vec();
2000        let executor = deterministic::Runner::timed(Duration::from_secs(300));
2001        executor.start(|mut context| async move {
2002            // Register participants (active)
2003            let Fixture {
2004                participants,
2005                schemes,
2006                verifier,
2007                ..
2008            } = fixture(&mut context, &namespace, n_active);
2009
2010            // Add observer (no share)
2011            let private_key_observer = PrivateKey::from_seed(n_active as u64);
2012            let public_key_observer = private_key_observer.public_key();
2013
2014            let mut oracle = start_test_network_with_split_peers(
2015                context.child("network"),
2016                participants.clone(),
2017                [public_key_observer.clone()],
2018                true,
2019            )
2020            .await;
2021
2022            // Register all (including observer) with the network
2023            let mut all_validators = participants.clone();
2024            all_validators.push(public_key_observer.clone());
2025            all_validators.sort();
2026            let mut registrations = register_validators(&mut oracle, &all_validators).await;
2027
2028            // Link all peers (including observer)
2029            let link = Link {
2030                latency: Duration::from_millis(10),
2031                jitter: Duration::from_millis(1),
2032                success_rate: probability!(1.0),
2033            };
2034            link_validators(&mut oracle, &all_validators, Action::Link(link), None).await;
2035
2036            // Create engines
2037            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
2038            let mut reporters = Vec::new();
2039
2040            for (idx, validator) in participants.iter().enumerate() {
2041                let is_observer = *validator == public_key_observer;
2042
2043                // Create scheme context
2044                let context = context
2045                    .child("validator")
2046                    .with_attribute("public_key", validator);
2047
2048                // Configure engine
2049                let signing = if is_observer {
2050                    verifier.clone()
2051                } else {
2052                    schemes[idx].clone()
2053                };
2054                let reporter_config = mocks::reporter::Config {
2055                    participants: participants.clone().try_into().unwrap(),
2056                    scheme: signing.clone(),
2057                    elector: elector.clone(),
2058                };
2059                let reporter =
2060                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
2061                reporters.push(reporter.clone());
2062                let application_cfg = mocks::application::Config::<Sha256, _> {
2063                    relay: relay.clone(),
2064                    me: validator.clone(),
2065                    propose_latency: (10.0, 5.0),
2066                    verify_latency: (10.0, 5.0),
2067                    certify_latency: (10.0, 5.0),
2068                    should_certify: mocks::application::Certifier::Always,
2069                };
2070                let (actor, application) = mocks::application::Application::new(
2071                    context.child("application"),
2072                    application_cfg,
2073                );
2074                actor.start();
2075                let blocker = oracle.control(validator.clone());
2076                let cfg = config::Config {
2077                    scheme: signing.clone(),
2078                    elector: elector.clone(),
2079                    blocker,
2080                    automaton: application.clone(),
2081                    relay: application.clone(),
2082                    reporter: reporter.clone(),
2083                    strategy: Sequential,
2084                    partition: validator.to_string(),
2085                    mailbox_size: NZUsize!(1024),
2086                    epoch: Epoch::new(333),
2087                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
2088                        Epoch::new(333),
2089                    )),
2090                    leader_timeout: Duration::from_secs(1),
2091                    certification_timeout: Duration::from_secs(2),
2092                    timeout_retry: Duration::from_secs(10),
2093                    fetch_timeout: Duration::from_secs(1),
2094                    view_retention,
2095                    skip: SkipPolicy::Enabled {
2096                        timeout: skip_timeout,
2097                        budget: SkipBudget::Participants,
2098                    },
2099                    replay_buffer: NZUsize!(1024 * 1024),
2100                    write_buffer: NZUsize!(1024 * 1024),
2101                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2102                    forward: ForwardPolicy::Disabled,
2103                    track_historical_votes: false,
2104                };
2105                let engine = Engine::new(context.child("engine"), cfg);
2106
2107                // Start engine
2108                let (pending, recovered, resolver) = registrations
2109                    .remove(validator)
2110                    .expect("validator should be registered");
2111                engine.start(pending, recovered, resolver);
2112            }
2113
2114            // Wait for all  engines to finish
2115            let mut finalizers = Vec::new();
2116            for reporter in reporters.iter_mut() {
2117                let (mut latest, mut monitor) = reporter.subscribe().await;
2118                finalizers.push(context.child("finalizer").spawn(move |_| async move {
2119                    while latest < required_containers {
2120                        latest = monitor.recv().await.expect("event missing");
2121                    }
2122                }));
2123            }
2124            join_all(finalizers).await;
2125
2126            // Sanity check. The standalone secondary observer should still
2127            // process the chain to the same progress threshold as validators.
2128            for reporter in reporters.iter() {
2129                // Ensure no faults or invalid signatures
2130                reporter.assert_no_faults();
2131                reporter.assert_no_invalid();
2132
2133                // Ensure no blocked connections
2134                let blocked = oracle.blocked().await.unwrap();
2135                assert!(blocked.is_empty());
2136            }
2137        });
2138    }
2139
2140    test_for_all_fixtures!(observer);
2141
2142    fn unclean_shutdown<S, F, L>(fixture: F, elector: L)
2143    where
2144        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
2145        F: FnMut(&mut TestRng, &[u8], u32) -> Fixture<S>,
2146        L: elector::Config<S>,
2147    {
2148        unclean_shutdown_with_term::<S, F, L>(elector, fixture);
2149    }
2150
2151    fn unclean_shutdown_with_term<S, F, L>(elector: L, mut fixture: F)
2152    where
2153        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
2154        F: FnMut(&mut TestRng, &[u8], u32) -> Fixture<S>,
2155        L: elector::Config<S>,
2156    {
2157        // Create context
2158        let n = 5;
2159        let required_containers = View::new(100);
2160        let view_retention = ViewDelta::new(10);
2161        let skip_timeout = Duration::from_secs(12);
2162        let namespace = b"consensus".to_vec();
2163
2164        // Random restarts every x seconds
2165        let shutdowns: Arc<Mutex<u64>> = Arc::new(Mutex::new(0));
2166        let supervised = Arc::new(Mutex::new(Vec::new()));
2167        let mut prev_checkpoint = None;
2168
2169        // Create validator keys
2170        let mut rng = test_rng();
2171        let Fixture {
2172            participants,
2173            schemes,
2174            ..
2175        } = fixture(&mut rng, &namespace, n);
2176        let reporter_seed: [u8; 32] = rng.random();
2177
2178        // Create block relay, shared across restarts.
2179        let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, S::PublicKey>::new());
2180
2181        loop {
2182            let participants = participants.clone();
2183            let schemes = schemes.clone();
2184            let shutdowns = shutdowns.clone();
2185            let supervised = supervised.clone();
2186            let relay = relay.clone();
2187            let elector = elector.clone();
2188            relay.deregister_all(); // Clear all recipients from previous restart.
2189
2190            let f = |mut context: deterministic::Context| async move {
2191                // Register participants
2192                let mut oracle = start_test_network_with_peers(
2193                    context.child("network"),
2194                    participants.clone(),
2195                    true,
2196                )
2197                .await;
2198                let mut registrations = register_validators(&mut oracle, &participants).await;
2199
2200                // Link all validators
2201                let link = Link {
2202                    latency: Duration::from_millis(50),
2203                    jitter: Duration::from_millis(50),
2204                    success_rate: probability!(1.0),
2205                };
2206                link_validators(&mut oracle, &participants, Action::Link(link), None).await;
2207
2208                // Create engines
2209                let elector = elector.clone();
2210                let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
2211                let mut reporters = HashMap::new();
2212                let mut engine_handlers = Vec::new();
2213                for (idx, validator) in participants.iter().enumerate() {
2214                    // Create scheme context
2215                    let context = context
2216                        .child("validator")
2217                        .with_attribute("public_key", validator);
2218
2219                    // Configure engine
2220                    let reporter_config = mocks::reporter::Config {
2221                        participants: participants.clone().try_into().unwrap(),
2222                        scheme: schemes[idx].clone(),
2223                        elector: elector.clone(),
2224                    };
2225                    let reporter_rng = StdRng::from_seed(reporter_seed);
2226                    let reporter = mocks::reporter::Reporter::new(reporter_rng, reporter_config);
2227                    reporters.insert(validator.clone(), reporter.clone());
2228                    let application_cfg = mocks::application::Config::<Sha256, _> {
2229                        relay: relay.clone(),
2230                        me: validator.clone(),
2231                        propose_latency: (10.0, 5.0),
2232                        verify_latency: (10.0, 5.0),
2233                        certify_latency: (10.0, 5.0),
2234                        should_certify: mocks::application::Certifier::Always,
2235                    };
2236                    let (actor, application) = mocks::application::Application::new(
2237                        context.child("application"),
2238                        application_cfg,
2239                    );
2240                    actor.start();
2241                    let blocker = oracle.control(validator.clone());
2242                    let cfg = config::Config {
2243                        scheme: schemes[idx].clone(),
2244                        elector: elector.clone(),
2245                        blocker,
2246                        automaton: application.clone(),
2247                        relay: application.clone(),
2248                        reporter: reporter.clone(),
2249                        strategy: Sequential,
2250                        partition: validator.to_string(),
2251                        mailbox_size: NZUsize!(1024),
2252                        epoch: Epoch::new(333),
2253                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
2254                            Epoch::new(333),
2255                        )),
2256                        // Keep the progress timeouts and the timeout retry short to allow for quick
2257                        // timeouts upon restart.
2258                        leader_timeout: Duration::from_millis(500),
2259                        certification_timeout: Duration::from_secs(1),
2260                        timeout_retry: Duration::from_millis(500),
2261                        fetch_timeout: Duration::from_secs(1),
2262                        view_retention,
2263                        skip: SkipPolicy::Enabled {
2264                            timeout: skip_timeout,
2265                            budget: SkipBudget::Participants,
2266                        },
2267                        replay_buffer: NZUsize!(1024 * 1024),
2268                        write_buffer: NZUsize!(1024 * 1024),
2269                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2270                        forward: ForwardPolicy::Disabled,
2271                        track_historical_votes: false,
2272                    };
2273                    let engine = Engine::new(context.child("engine"), cfg);
2274
2275                    // Start engine
2276                    let (pending, recovered, resolver) = registrations
2277                        .remove(validator)
2278                        .expect("validator should be registered");
2279                    engine_handlers.push(engine.start(pending, recovered, resolver));
2280                }
2281
2282                // Store all finalizer handles
2283                let mut finalizers = Vec::new();
2284                for reporter in reporters.values_mut() {
2285                    let (mut latest, mut monitor) = reporter.subscribe().await;
2286                    finalizers.push(context.child("finalizer").spawn(move |_| async move {
2287                        while latest < required_containers {
2288                            latest = monitor.recv().await.expect("event missing");
2289                        }
2290                    }));
2291                }
2292
2293                // Exit at random points for unclean shutdown of entire set
2294                let wait =
2295                    context.random_range(Duration::from_millis(100)..Duration::from_millis(2_000));
2296                let result = select! {
2297                    _ = context.sleep(wait) => {
2298                        // Collect reporters to check faults
2299                        {
2300                            let mut shutdowns = shutdowns.lock();
2301                            debug!(shutdowns = *shutdowns, elapsed = ?wait, "restarting");
2302                            *shutdowns += 1;
2303                        }
2304                        supervised.lock().push(reporters);
2305                        false
2306                    },
2307                    _ = join_all(finalizers) => {
2308                        // Check reporters for faults activity
2309                        let supervised = supervised.lock();
2310                        for reporters in supervised.iter() {
2311                            for reporter in reporters.values() {
2312                                reporter.assert_no_faults();
2313                            }
2314                        }
2315                        true
2316                    },
2317                };
2318
2319                // Ensure no blocked connections
2320                let blocked = oracle.blocked().await.unwrap();
2321                assert!(blocked.is_empty());
2322
2323                result
2324            };
2325
2326            let (complete, checkpoint) = prev_checkpoint
2327                .map_or_else(
2328                    || deterministic::Runner::timed(Duration::from_secs(180)),
2329                    deterministic::Runner::from,
2330                )
2331                .start_and_recover(f);
2332
2333            // Check if we should exit
2334            if complete {
2335                break;
2336            }
2337
2338            prev_checkpoint = Some(checkpoint);
2339        }
2340    }
2341
2342    test_for_all_fixtures!(unclean_shutdown);
2343
2344    /// Regression test: with stable leaders and optimistic validation, a
2345    /// whole-cluster crash can leave a mid-term view without any certificate
2346    /// while a higher same-term notarization survives in some journals. Nodes
2347    /// stuck below that view must be able to fetch the exact-view notarization
2348    /// (a higher-view floor cannot substitute for certification's per-view
2349    /// parent requirement) or the cluster wedges permanently (see
2350    /// [`resolver::State::get`]).
2351    #[test_group("slow")]
2352    #[test_traced]
2353    fn test_unclean_shutdown_stable_leader_optimistic() {
2354        unclean_shutdown_with_term::<_, _, RoundRobin>(
2355            RoundRobin::default().with_term(
2356                TermLength::new(NZU32!(5)),
2357                Duration::from_secs(13),
2358                ViewDelta::new(2),
2359            ),
2360            ed25519::fixture,
2361        );
2362    }
2363
2364    fn backfill<S, F, L>(mut fixture: F, elector: L)
2365    where
2366        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
2367        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
2368        L: elector::Config<S>,
2369    {
2370        // Create context
2371        let n = 4;
2372        let required_containers = View::new(100);
2373        let view_retention = ViewDelta::new(10);
2374        let skip_timeout = Duration::from_secs(11);
2375        let namespace = b"consensus".to_vec();
2376        let executor = deterministic::Runner::timed(Duration::from_secs(240));
2377        executor.start(|mut context| async move {
2378            // Register participants
2379            let Fixture {
2380                participants,
2381                schemes,
2382                ..
2383            } = fixture(&mut context, &namespace, n);
2384            let mut oracle =
2385                start_test_network_with_peers(context.child("network"), participants.clone(), true)
2386                    .await;
2387            let mut registrations = register_validators(&mut oracle, &participants).await;
2388
2389            // Link all validators except first
2390            let link = Link {
2391                latency: Duration::from_millis(10),
2392                jitter: Duration::from_millis(1),
2393                success_rate: probability!(1.0),
2394            };
2395            link_validators(
2396                &mut oracle,
2397                &participants,
2398                Action::Link(link),
2399                Some(|_, i, j| ![i, j].contains(&0usize)),
2400            )
2401            .await;
2402
2403            // Create engines
2404            let elector = elector.clone();
2405            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
2406            let mut reporters = Vec::new();
2407            let mut engine_handlers = Vec::new();
2408            for (idx_scheme, validator) in participants.iter().enumerate() {
2409                // Skip first peer
2410                if idx_scheme == 0 {
2411                    continue;
2412                }
2413
2414                // Create scheme context
2415                let context = context
2416                    .child("validator")
2417                    .with_attribute("public_key", validator);
2418
2419                // Configure engine
2420                let reporter_config = mocks::reporter::Config {
2421                    participants: participants.clone().try_into().unwrap(),
2422                    scheme: schemes[idx_scheme].clone(),
2423                    elector: elector.clone(),
2424                };
2425                let reporter =
2426                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
2427                reporters.push(reporter.clone());
2428                let application_cfg = mocks::application::Config::<Sha256, _> {
2429                    relay: relay.clone(),
2430                    me: validator.clone(),
2431                    propose_latency: (10.0, 5.0),
2432                    verify_latency: (10.0, 5.0),
2433                    certify_latency: (10.0, 5.0),
2434                    should_certify: mocks::application::Certifier::Always,
2435                };
2436                let (actor, application) = mocks::application::Application::new(
2437                    context.child("application"),
2438                    application_cfg,
2439                );
2440                actor.start();
2441                let blocker = oracle.control(validator.clone());
2442                let cfg = config::Config {
2443                    scheme: schemes[idx_scheme].clone(),
2444                    elector: elector.clone(),
2445                    blocker,
2446                    automaton: application.clone(),
2447                    relay: application.clone(),
2448                    reporter: reporter.clone(),
2449                    strategy: Sequential,
2450                    partition: validator.to_string(),
2451                    mailbox_size: NZUsize!(1024),
2452                    epoch: Epoch::new(333),
2453                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
2454                        Epoch::new(333),
2455                    )),
2456                    leader_timeout: Duration::from_secs(1),
2457                    certification_timeout: Duration::from_secs(2),
2458                    timeout_retry: Duration::from_secs(10),
2459                    fetch_timeout: Duration::from_secs(1),
2460                    view_retention,
2461                    skip: SkipPolicy::Enabled {
2462                        timeout: skip_timeout,
2463                        budget: SkipBudget::Participants,
2464                    },
2465                    replay_buffer: NZUsize!(1024 * 1024),
2466                    write_buffer: NZUsize!(1024 * 1024),
2467                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2468                    forward: ForwardPolicy::Disabled,
2469                    track_historical_votes: false,
2470                };
2471                let engine = Engine::new(context.child("engine"), cfg);
2472
2473                // Start engine
2474                let (pending, recovered, resolver) = registrations
2475                    .remove(validator)
2476                    .expect("validator should be registered");
2477                engine_handlers.push(engine.start(pending, recovered, resolver));
2478            }
2479
2480            // Wait for all engines to finish
2481            let mut finalizers = Vec::new();
2482            for reporter in reporters.iter_mut() {
2483                let (mut latest, mut monitor) = reporter.subscribe().await;
2484                finalizers.push(context.child("finalizer").spawn(move |_| async move {
2485                    while latest < required_containers {
2486                        latest = monitor.recv().await.expect("event missing");
2487                    }
2488                }));
2489            }
2490            join_all(finalizers).await;
2491
2492            // Degrade network connections for online peers
2493            let link = Link {
2494                latency: Duration::from_secs(3),
2495                jitter: Duration::from_millis(0),
2496                success_rate: probability!(1.0),
2497            };
2498            link_validators(
2499                &mut oracle,
2500                &participants,
2501                Action::Update(link.clone()),
2502                Some(|_, i, j| ![i, j].contains(&0usize)),
2503            )
2504            .await;
2505
2506            // Wait for nullifications to accrue
2507            context.sleep(Duration::from_secs(60)).await;
2508
2509            // Unlink second peer from all (except first)
2510            link_validators(
2511                &mut oracle,
2512                &participants,
2513                Action::Unlink,
2514                Some(|_, i, j| [i, j].contains(&1usize) && ![i, j].contains(&0usize)),
2515            )
2516            .await;
2517
2518            // Configure engine for first peer
2519            let me = participants[0].clone();
2520            let context = context.child("validator").with_attribute("public_key", &me);
2521
2522            // Link first peer to all (except second)
2523            link_validators(
2524                &mut oracle,
2525                &participants,
2526                Action::Link(link),
2527                Some(|_, i, j| [i, j].contains(&0usize) && ![i, j].contains(&1usize)),
2528            )
2529            .await;
2530
2531            // Restore network connections for all online peers
2532            let link = Link {
2533                latency: Duration::from_millis(10),
2534                jitter: Duration::from_millis(3),
2535                success_rate: probability!(1.0),
2536            };
2537            link_validators(
2538                &mut oracle,
2539                &participants,
2540                Action::Update(link),
2541                Some(|_, i, j| ![i, j].contains(&1usize)),
2542            )
2543            .await;
2544
2545            // Configure engine
2546            let reporter_config = mocks::reporter::Config {
2547                participants: participants.clone().try_into().unwrap(),
2548                scheme: schemes[0].clone(),
2549                elector: elector.clone(),
2550            };
2551            let mut reporter =
2552                mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
2553            reporters.push(reporter.clone());
2554            let application_cfg = mocks::application::Config::<Sha256, _> {
2555                relay: relay.clone(),
2556                me: me.clone(),
2557                propose_latency: (10.0, 5.0),
2558                verify_latency: (10.0, 5.0),
2559                certify_latency: (10.0, 5.0),
2560                should_certify: mocks::application::Certifier::Always,
2561            };
2562            let (actor, application) =
2563                mocks::application::Application::new(context.child("application"), application_cfg);
2564            actor.start();
2565            let blocker = oracle.control(me.clone());
2566            let cfg = config::Config {
2567                scheme: schemes[0].clone(),
2568                elector: elector.clone(),
2569                blocker,
2570                automaton: application.clone(),
2571                relay: application.clone(),
2572                reporter: reporter.clone(),
2573                strategy: Sequential,
2574                partition: me.to_string(),
2575                mailbox_size: NZUsize!(1024),
2576                epoch: Epoch::new(333),
2577                floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(Epoch::new(
2578                    333,
2579                ))),
2580                leader_timeout: Duration::from_secs(1),
2581                certification_timeout: Duration::from_secs(2),
2582                timeout_retry: Duration::from_secs(10),
2583                fetch_timeout: Duration::from_secs(1),
2584                view_retention,
2585                skip: SkipPolicy::Enabled {
2586                    timeout: skip_timeout,
2587                    budget: SkipBudget::Participants,
2588                },
2589                replay_buffer: NZUsize!(1024 * 1024),
2590                write_buffer: NZUsize!(1024 * 1024),
2591                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2592                forward: ForwardPolicy::Disabled,
2593                track_historical_votes: false,
2594            };
2595            let engine = Engine::new(context.child("engine"), cfg);
2596
2597            // Start engine
2598            let (pending, recovered, resolver) = registrations
2599                .remove(&me)
2600                .expect("validator should be registered");
2601            engine_handlers.push(engine.start(pending, recovered, resolver));
2602
2603            // Wait for new engine to finalize required
2604            let (mut latest, mut monitor) = reporter.subscribe().await;
2605            while latest < required_containers {
2606                latest = monitor.recv().await.expect("event missing");
2607            }
2608
2609            // Ensure no blocked connections
2610            let blocked = oracle.blocked().await.unwrap();
2611            assert!(blocked.is_empty());
2612        });
2613    }
2614
2615    test_for_all_fixtures!(backfill);
2616
2617    #[test_group("slow")]
2618    #[test_traced]
2619    fn test_backfill_stable_leader_optimistic() {
2620        backfill::<_, _, RoundRobin>(
2621            ed25519::fixture,
2622            // Keep the stall timeout long so the healthy prefix of the run
2623            // (finalizing with one validator offline) never stall-nullifies.
2624            RoundRobin::default().with_term(
2625                TermLength::new(NZU32!(5)),
2626                Duration::from_secs(51),
2627                ViewDelta::new(2),
2628            ),
2629        );
2630    }
2631
2632    fn one_offline<S, F, L>(fixture: F, elector: L)
2633    where
2634        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
2635        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
2636        L: elector::Config<S>,
2637    {
2638        one_offline_with_term::<S, F, L>(elector, fixture);
2639    }
2640
2641    fn one_offline_with_term<S, F, L>(elector: L, mut fixture: F)
2642    where
2643        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
2644        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
2645        L: elector::Config<S>,
2646    {
2647        // Create context
2648        let n = 5;
2649        let quorum = quorum(n) as usize;
2650        let required_containers = View::new(100);
2651        let view_retention = ViewDelta::new(10);
2652        let skip_timeout = Duration::from_secs(11);
2653        let max_exceptions = 10;
2654        let namespace = b"consensus".to_vec();
2655        let executor = deterministic::Runner::timed(Duration::from_secs(300));
2656        executor.start(|mut context| async move {
2657            // Register participants
2658            let Fixture {
2659                participants,
2660                schemes,
2661                ..
2662            } = fixture(&mut context, &namespace, n);
2663            let mut oracle =
2664                start_test_network_with_peers(context.child("network"), participants.clone(), true)
2665                    .await;
2666            let mut registrations = register_validators(&mut oracle, &participants).await;
2667
2668            // Link all validators except first
2669            let link = Link {
2670                latency: Duration::from_millis(10),
2671                jitter: Duration::from_millis(1),
2672                success_rate: probability!(1.0),
2673            };
2674            link_validators(
2675                &mut oracle,
2676                &participants,
2677                Action::Link(link),
2678                Some(|_, i, j| ![i, j].contains(&0usize)),
2679            )
2680            .await;
2681
2682            // Create engines
2683            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
2684            let mut reporters = Vec::new();
2685            let mut engine_handlers = Vec::new();
2686            for (idx_scheme, validator) in participants.iter().enumerate() {
2687                // Skip first peer
2688                if idx_scheme == 0 {
2689                    continue;
2690                }
2691
2692                // Create scheme context
2693                let context = context
2694                    .child("validator")
2695                    .with_attribute("public_key", validator);
2696
2697                // Configure engine
2698                let reporter_config = mocks::reporter::Config {
2699                    participants: participants.clone().try_into().unwrap(),
2700                    scheme: schemes[idx_scheme].clone(),
2701                    elector: elector.clone(),
2702                };
2703                let reporter =
2704                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
2705                reporters.push(reporter.clone());
2706                let application_cfg = mocks::application::Config::<Sha256, _> {
2707                    relay: relay.clone(),
2708                    me: validator.clone(),
2709                    propose_latency: (10.0, 5.0),
2710                    verify_latency: (10.0, 5.0),
2711                    certify_latency: (10.0, 5.0),
2712                    should_certify: mocks::application::Certifier::Always,
2713                };
2714                let (actor, application) = mocks::application::Application::new(
2715                    context.child("application"),
2716                    application_cfg,
2717                );
2718                actor.start();
2719                let blocker = oracle.control(validator.clone());
2720                let cfg = config::Config {
2721                    scheme: schemes[idx_scheme].clone(),
2722                    elector: elector.clone(),
2723                    blocker,
2724                    automaton: application.clone(),
2725                    relay: application.clone(),
2726                    reporter: reporter.clone(),
2727                    strategy: Sequential,
2728                    partition: validator.to_string(),
2729                    mailbox_size: NZUsize!(1024),
2730                    epoch: Epoch::new(333),
2731                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
2732                        Epoch::new(333),
2733                    )),
2734                    leader_timeout: Duration::from_secs(1),
2735                    certification_timeout: Duration::from_secs(2),
2736                    timeout_retry: Duration::from_secs(10),
2737                    fetch_timeout: Duration::from_secs(1),
2738                    view_retention,
2739                    skip: SkipPolicy::Enabled {
2740                        timeout: skip_timeout,
2741                        budget: SkipBudget::Participants,
2742                    },
2743                    replay_buffer: NZUsize!(1024 * 1024),
2744                    write_buffer: NZUsize!(1024 * 1024),
2745                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2746                    forward: ForwardPolicy::Disabled,
2747                    track_historical_votes: false,
2748                };
2749                let engine = Engine::new(context.child("engine"), cfg);
2750
2751                // Start engine
2752                let (pending, recovered, resolver) = registrations
2753                    .remove(validator)
2754                    .expect("validator should be registered");
2755                engine_handlers.push(engine.start(pending, recovered, resolver));
2756            }
2757
2758            // Wait for all engines to finish
2759            let mut finalizers = Vec::new();
2760            for reporter in reporters.iter_mut() {
2761                let (mut latest, mut monitor) = reporter.subscribe().await;
2762                finalizers.push(context.child("finalizer").spawn(move |_| async move {
2763                    while latest < required_containers {
2764                        latest = monitor.recv().await.expect("event missing");
2765                    }
2766                }));
2767            }
2768            join_all(finalizers).await;
2769
2770            // Check reporters for correct activity
2771            let exceptions = 0;
2772            let offline = &participants[0];
2773            for reporter in reporters.iter() {
2774                // Ensure no faults
2775                reporter.assert_no_faults();
2776
2777                // Ensure no invalid signatures
2778                reporter.assert_no_invalid();
2779
2780                // Ensure offline node is never active
2781                let mut exceptions = 0;
2782                {
2783                    let notarizes = reporter.notarizes.lock();
2784                    for (view, payloads) in notarizes.iter() {
2785                        for participants in payloads.values() {
2786                            if participants.contains(offline) {
2787                                panic!("view: {view}");
2788                            }
2789                        }
2790                    }
2791                }
2792                {
2793                    let nullifies = reporter.nullifies.lock();
2794                    for (view, participants) in nullifies.iter() {
2795                        if participants.contains(offline) {
2796                            panic!("view: {view}");
2797                        }
2798                    }
2799                }
2800                {
2801                    let finalizes = reporter.finalizes.lock();
2802                    for (view, payloads) in finalizes.iter() {
2803                        for finalizers in payloads.values() {
2804                            if finalizers.contains(offline) {
2805                                panic!("view: {view}");
2806                            }
2807                        }
2808                    }
2809                }
2810
2811                // Identify offline views
2812                let mut offline_views = Vec::new();
2813                {
2814                    let leaders = reporter.leaders.lock();
2815                    for (view, leader) in leaders.iter() {
2816                        if leader == offline {
2817                            offline_views.push(*view);
2818                        }
2819                    }
2820                }
2821                assert!(!offline_views.is_empty());
2822
2823                // Ensure nullifies/nullification collected for offline node
2824                {
2825                    let nullifies = reporter.nullifies.lock();
2826                    for view in offline_views.iter() {
2827                        let nullifies = nullifies.get(view).map_or(0, |n| n.len());
2828                        if nullifies < quorum {
2829                            warn!("missing expected view nullifies: {}", view);
2830                            exceptions += 1;
2831                        }
2832                    }
2833                }
2834                {
2835                    let nullifications = reporter.nullifications.lock();
2836                    for view in offline_views.iter() {
2837                        if !nullifications.contains_key(view) {
2838                            warn!("missing expected view nullifies: {}", view);
2839                            exceptions += 1;
2840                        }
2841                    }
2842                }
2843
2844                // Ensure exceptions within allowed
2845                assert!(exceptions <= max_exceptions);
2846            }
2847            assert!(exceptions <= max_exceptions);
2848
2849            // Ensure no blocked connections
2850            let blocked = oracle.blocked().await.unwrap();
2851            assert!(blocked.is_empty());
2852
2853            // Ensure online nodes are recording timeouts/nullifications for the offline leader
2854            let encoded = context.encode();
2855            let leader_label = format!("leader=\"{}\"", offline);
2856            assert!(
2857                count_nonzero_metric_lines(&encoded, &["_timeouts", &leader_label]) >= n - 1,
2858                "expected timeout metrics for offline leader"
2859            );
2860            assert_eq!(
2861                count_nonzero_metric_lines(&encoded, &["_nullifications", &leader_label]),
2862                n - 1,
2863                "expected all online nodes to record _nullifications for offline leader"
2864            );
2865        });
2866    }
2867
2868    test_for_all_fixtures!(one_offline);
2869
2870    #[test_group("slow")]
2871    #[test_traced]
2872    fn test_one_offline_stable_leader() {
2873        one_offline_with_term::<_, _, RoundRobin>(
2874            RoundRobin::default().with_term(
2875                TermLength::new(NZU32!(3)),
2876                Duration::from_secs(12),
2877                ViewDelta::new(0),
2878            ),
2879            scheme_mocks::fixture,
2880        );
2881    }
2882
2883    fn slow_validator<S, F, L>(mut fixture: F, elector: L)
2884    where
2885        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
2886        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
2887        L: elector::Config<S>,
2888    {
2889        // Create context
2890        let n = 5;
2891        let required_containers = View::new(50);
2892        let view_retention = ViewDelta::new(10);
2893        let skip_timeout = Duration::from_secs(11);
2894        let namespace = b"consensus".to_vec();
2895        let executor = deterministic::Runner::timed(Duration::from_secs(300));
2896        executor.start(|mut context| async move {
2897            // Register participants
2898            let Fixture {
2899                participants,
2900                schemes,
2901                ..
2902            } = fixture(&mut context, &namespace, n);
2903            let mut oracle =
2904                start_test_network_with_peers(context.child("network"), participants.clone(), true)
2905                    .await;
2906            let mut registrations = register_validators(&mut oracle, &participants).await;
2907
2908            // Link all validators
2909            let link = Link {
2910                latency: Duration::from_millis(10),
2911                jitter: Duration::from_millis(1),
2912                success_rate: probability!(1.0),
2913            };
2914            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
2915
2916            // Create engines
2917            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
2918            let mut reporters = Vec::new();
2919            let mut engine_handlers = Vec::new();
2920            for (idx_scheme, validator) in participants.iter().enumerate() {
2921                // Create scheme context
2922                let context = context
2923                    .child("validator")
2924                    .with_attribute("public_key", validator);
2925
2926                // Configure engine
2927                let reporter_config = mocks::reporter::Config {
2928                    participants: participants.clone().try_into().unwrap(),
2929                    scheme: schemes[idx_scheme].clone(),
2930                    elector: elector.clone(),
2931                };
2932                let reporter =
2933                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
2934                reporters.push(reporter.clone());
2935                let application_cfg = if idx_scheme == 0 {
2936                    mocks::application::Config::<Sha256, _> {
2937                        relay: relay.clone(),
2938                        me: validator.clone(),
2939                        propose_latency: (10_000.0, 0.0),
2940                        verify_latency: (10_000.0, 5.0),
2941                        certify_latency: (10_000.0, 5.0),
2942                        should_certify: mocks::application::Certifier::Always,
2943                    }
2944                } else {
2945                    mocks::application::Config::<Sha256, _> {
2946                        relay: relay.clone(),
2947                        me: validator.clone(),
2948                        propose_latency: (10.0, 5.0),
2949                        verify_latency: (10.0, 5.0),
2950                        certify_latency: (10.0, 5.0),
2951                        should_certify: mocks::application::Certifier::Always,
2952                    }
2953                };
2954                let (actor, application) = mocks::application::Application::new(
2955                    context.child("application"),
2956                    application_cfg,
2957                );
2958                actor.start();
2959                let blocker = oracle.control(validator.clone());
2960                let cfg = config::Config {
2961                    scheme: schemes[idx_scheme].clone(),
2962                    elector: elector.clone(),
2963                    blocker,
2964                    automaton: application.clone(),
2965                    relay: application.clone(),
2966                    reporter: reporter.clone(),
2967                    strategy: Sequential,
2968                    partition: validator.to_string(),
2969                    mailbox_size: NZUsize!(1024),
2970                    epoch: Epoch::new(333),
2971                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
2972                        Epoch::new(333),
2973                    )),
2974                    leader_timeout: Duration::from_secs(1),
2975                    certification_timeout: Duration::from_secs(2),
2976                    timeout_retry: Duration::from_secs(10),
2977                    fetch_timeout: Duration::from_secs(1),
2978                    view_retention,
2979                    skip: SkipPolicy::Enabled {
2980                        timeout: skip_timeout,
2981                        budget: SkipBudget::Participants,
2982                    },
2983                    replay_buffer: NZUsize!(1024 * 1024),
2984                    write_buffer: NZUsize!(1024 * 1024),
2985                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
2986                    forward: ForwardPolicy::Disabled,
2987                    track_historical_votes: false,
2988                };
2989                let engine = Engine::new(context.child("engine"), cfg);
2990
2991                // Start engine
2992                let (pending, recovered, resolver) = registrations
2993                    .remove(validator)
2994                    .expect("validator should be registered");
2995                engine_handlers.push(engine.start(pending, recovered, resolver));
2996            }
2997
2998            // Wait for all engines to finish
2999            let mut finalizers = Vec::new();
3000            for reporter in reporters.iter_mut() {
3001                let (mut latest, mut monitor) = reporter.subscribe().await;
3002                finalizers.push(context.child("finalizer").spawn(move |_| async move {
3003                    while latest < required_containers {
3004                        latest = monitor.recv().await.expect("event missing");
3005                    }
3006                }));
3007            }
3008            join_all(finalizers).await;
3009
3010            // Check reporters for correct activity
3011            let slow = &participants[0];
3012            for reporter in reporters.iter() {
3013                // Ensure no faults
3014                reporter.assert_no_faults();
3015
3016                // Ensure no invalid signatures
3017                reporter.assert_no_invalid();
3018
3019                // Ensure the slow validator never emits notarize or finalize
3020                // votes. It may still emit nullifies after timing out.
3021                {
3022                    let notarizes = reporter.notarizes.lock();
3023                    assert!(notarizes.values().all(|payloads| {
3024                        payloads
3025                            .values()
3026                            .all(|participants| !participants.contains(slow))
3027                    }));
3028                }
3029                {
3030                    let finalizes = reporter.finalizes.lock();
3031                    assert!(finalizes.values().all(|payloads| {
3032                        payloads
3033                            .values()
3034                            .all(|participants| !participants.contains(slow))
3035                    }));
3036                }
3037
3038                // Ensure every reporter observes finalization progress to at least the target view.
3039                {
3040                    let finalizations = reporter.finalizations.lock();
3041                    assert!(
3042                        finalizations
3043                            .keys()
3044                            .any(|view| *view >= required_containers)
3045                    );
3046                }
3047            }
3048
3049            // Ensure no blocked connections
3050            let blocked = oracle.blocked().await.unwrap();
3051            assert!(blocked.is_empty());
3052        });
3053    }
3054
3055    test_for_all_fixtures!(slow_validator);
3056
3057    fn all_recovery<S, F, L>(mut fixture: F, elector: L)
3058    where
3059        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3060        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
3061        L: elector::Config<S>,
3062    {
3063        // Create context
3064        let n = 5;
3065        let required_containers = View::new(100);
3066        let view_retention = ViewDelta::new(10);
3067        let skip_timeout = Duration::from_secs(11);
3068        let namespace = b"consensus".to_vec();
3069        let executor = deterministic::Runner::timed(Duration::from_secs(1800));
3070        executor.start(|mut context| async move {
3071            // Register participants
3072            let Fixture {
3073                participants,
3074                schemes,
3075                ..
3076            } = fixture(&mut context, &namespace, n);
3077            let mut oracle =
3078                start_test_network_with_peers(context.child("network"), participants.clone(), true)
3079                    .await;
3080            let mut registrations = register_validators(&mut oracle, &participants).await;
3081
3082            // Link all validators
3083            let link = Link {
3084                latency: Duration::from_secs(3),
3085                jitter: Duration::from_millis(0),
3086                success_rate: probability!(1.0),
3087            };
3088            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
3089
3090            // Create engines
3091            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
3092            let mut reporters = Vec::new();
3093            let mut engine_handlers = Vec::new();
3094            for (idx, validator) in participants.iter().enumerate() {
3095                // Create scheme context
3096                let context = context
3097                    .child("validator")
3098                    .with_attribute("public_key", validator);
3099
3100                // Configure engine
3101                let reporter_config = mocks::reporter::Config {
3102                    participants: participants.clone().try_into().unwrap(),
3103                    scheme: schemes[idx].clone(),
3104                    elector: elector.clone(),
3105                };
3106                let reporter =
3107                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
3108                reporters.push(reporter.clone());
3109                let application_cfg = mocks::application::Config::<Sha256, _> {
3110                    relay: relay.clone(),
3111                    me: validator.clone(),
3112                    propose_latency: (10.0, 5.0),
3113                    verify_latency: (10.0, 5.0),
3114                    certify_latency: (10.0, 5.0),
3115                    should_certify: mocks::application::Certifier::Always,
3116                };
3117                let (actor, application) = mocks::application::Application::new(
3118                    context.child("application"),
3119                    application_cfg,
3120                );
3121                actor.start();
3122                let blocker = oracle.control(validator.clone());
3123                let cfg = config::Config {
3124                    scheme: schemes[idx].clone(),
3125                    elector: elector.clone(),
3126                    blocker,
3127                    automaton: application.clone(),
3128                    relay: application.clone(),
3129                    reporter: reporter.clone(),
3130                    strategy: Sequential,
3131                    partition: validator.to_string(),
3132                    mailbox_size: NZUsize!(1024),
3133                    epoch: Epoch::new(333),
3134                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
3135                        Epoch::new(333),
3136                    )),
3137                    leader_timeout: Duration::from_secs(1),
3138                    certification_timeout: Duration::from_secs(2),
3139                    timeout_retry: Duration::from_secs(10),
3140                    fetch_timeout: Duration::from_secs(1),
3141                    view_retention,
3142                    skip: SkipPolicy::Enabled {
3143                        timeout: skip_timeout,
3144                        budget: SkipBudget::Participants,
3145                    },
3146                    replay_buffer: NZUsize!(1024 * 1024),
3147                    write_buffer: NZUsize!(1024 * 1024),
3148                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3149                    forward: ForwardPolicy::Disabled,
3150                    track_historical_votes: false,
3151                };
3152                let engine = Engine::new(context.child("engine"), cfg);
3153
3154                // Start engine
3155                let (pending, recovered, resolver) = registrations
3156                    .remove(validator)
3157                    .expect("validator should be registered");
3158                engine_handlers.push(engine.start(pending, recovered, resolver));
3159            }
3160
3161            // Wait for a few virtual minutes (shouldn't finalize anything)
3162            let mut finalizers = Vec::new();
3163            for reporter in reporters.iter_mut() {
3164                let (_, mut monitor) = reporter.subscribe().await;
3165                finalizers.push(context.child("finalizer").spawn(move |context| async move {
3166                    select! {
3167                        _timeout = context.sleep(Duration::from_secs(60)) => {},
3168                        _done = monitor.recv() => {
3169                            panic!("engine should not notarize or finalize anything");
3170                        },
3171                    }
3172                }));
3173            }
3174            join_all(finalizers).await;
3175
3176            // Unlink all validators to get latest view
3177            link_validators(&mut oracle, &participants, Action::Unlink, None).await;
3178
3179            // Wait for a virtual minute (nothing should happen)
3180            context.sleep(Duration::from_secs(60)).await;
3181
3182            // Get latest view
3183            let mut latest = View::zero();
3184            for reporter in reporters.iter() {
3185                let nullifies = reporter.nullifies.lock();
3186                let max = nullifies.keys().max().unwrap();
3187                if *max > latest {
3188                    latest = *max;
3189                }
3190            }
3191
3192            // Update links
3193            let link = Link {
3194                latency: Duration::from_millis(10),
3195                jitter: Duration::from_millis(1),
3196                success_rate: probability!(1.0),
3197            };
3198            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
3199
3200            // Wait for all engines to finish
3201            let mut finalizers = Vec::new();
3202            for reporter in reporters.iter_mut() {
3203                let (mut latest, mut monitor) = reporter.subscribe().await;
3204                finalizers.push(context.child("finalizer").spawn(move |_| async move {
3205                    while latest < required_containers {
3206                        latest = monitor.recv().await.expect("event missing");
3207                    }
3208                }));
3209            }
3210            join_all(finalizers).await;
3211
3212            // Check reporters for correct activity
3213            for reporter in reporters.iter() {
3214                // Ensure no faults
3215                reporter.assert_no_faults();
3216
3217                // Ensure no invalid signatures
3218                reporter.assert_no_invalid();
3219
3220                // Ensure quick recovery.
3221                //
3222                // If the skip timeout isn't implemented correctly, we may go many views before participants
3223                // start to notarize a validator's proposal.
3224                {
3225                    // Ensure nearly all views around latest are notarized.
3226                    // We don't check for finalization since some of the blocks may fail to be
3227                    // certified for the purposes of testing.
3228                    let mut found = 0;
3229                    let notarizations = reporter.notarizations.lock();
3230                    for view in View::range(latest, latest.saturating_add(view_retention)) {
3231                        if notarizations.contains_key(&view) {
3232                            found += 1;
3233                        }
3234                    }
3235                    // A few views may still nullify while lagging validators
3236                    // catch up after relinking, but a working skip timeout
3237                    // bounds that to a handful of views, not the viewport.
3238                    let tolerated_missing = 3;
3239                    assert!(
3240                        found >= view_retention.get().saturating_sub(tolerated_missing),
3241                        "found: {found}"
3242                    );
3243                }
3244            }
3245
3246            // Ensure no blocked connections
3247            let blocked = oracle.blocked().await.unwrap();
3248            assert!(blocked.is_empty());
3249        });
3250    }
3251
3252    test_for_all_fixtures!(all_recovery);
3253
3254    fn all_crash_after_nullify<S, F, L>(mut fixture: F, elector: L)
3255    where
3256        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3257        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
3258        L: elector::Config<S>,
3259    {
3260        // Create context
3261        let n = 4;
3262        let required_containers = View::new(10);
3263        let view_retention = ViewDelta::new(10);
3264        let skip_timeout = Duration::from_secs(11);
3265        let namespace = b"consensus".to_vec();
3266        let executor = deterministic::Runner::timed(Duration::from_secs(3600));
3267        executor.start(|mut context| async move {
3268            // Register participants
3269            let Fixture {
3270                participants,
3271                schemes,
3272                ..
3273            } = fixture(&mut context, &namespace, n);
3274            let mut oracle =
3275                start_test_network_with_peers(context.child("network"), participants.clone(), true)
3276                    .await;
3277            let mut registrations = register_validators(&mut oracle, &participants).await;
3278
3279            // Participant 0 never starts an engine and no links exist yet, so no
3280            // view can produce a certificate before the crash below.
3281            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
3282            let mut reporters = Vec::new();
3283            let mut engine_handlers = Vec::new();
3284            for (idx_scheme, validator) in participants.iter().enumerate() {
3285                // Skip first peer
3286                if idx_scheme == 0 {
3287                    continue;
3288                }
3289
3290                // Create scheme context
3291                let context = context
3292                    .child("validator")
3293                    .with_attribute("public_key", validator);
3294
3295                // Configure engine
3296                let reporter_config = mocks::reporter::Config {
3297                    participants: participants.clone().try_into().unwrap(),
3298                    scheme: schemes[idx_scheme].clone(),
3299                    elector: elector.clone(),
3300                };
3301                let reporter =
3302                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
3303                reporters.push(reporter.clone());
3304                let application_cfg = mocks::application::Config::<Sha256, _> {
3305                    relay: relay.clone(),
3306                    me: validator.clone(),
3307                    propose_latency: (10.0, 5.0),
3308                    verify_latency: (10.0, 5.0),
3309                    certify_latency: (10.0, 5.0),
3310                    should_certify: mocks::application::Certifier::Always,
3311                };
3312                let (actor, application) = mocks::application::Application::new(
3313                    context.child("application"),
3314                    application_cfg,
3315                );
3316                actor.start();
3317                let blocker = oracle.control(validator.clone());
3318                let cfg = config::Config {
3319                    scheme: schemes[idx_scheme].clone(),
3320                    elector: elector.clone(),
3321                    blocker,
3322                    automaton: application.clone(),
3323                    relay: application.clone(),
3324                    reporter: reporter.clone(),
3325                    strategy: Sequential,
3326                    partition: validator.to_string(),
3327                    mailbox_size: NZUsize!(1024),
3328                    epoch: Epoch::new(333),
3329                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
3330                        Epoch::new(333),
3331                    )),
3332                    leader_timeout: Duration::from_secs(1),
3333                    certification_timeout: Duration::from_secs(2),
3334                    timeout_retry: Duration::from_secs(10),
3335                    fetch_timeout: Duration::from_secs(1),
3336                    view_retention,
3337                    skip: SkipPolicy::Enabled {
3338                        timeout: skip_timeout,
3339                        budget: SkipBudget::Participants,
3340                    },
3341                    replay_buffer: NZUsize!(1024 * 1024),
3342                    write_buffer: NZUsize!(1024 * 1024),
3343                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3344                    forward: ForwardPolicy::Disabled,
3345                    track_historical_votes: false,
3346                };
3347                let engine = Engine::new(context.child("engine"), cfg);
3348
3349                // Start engine
3350                let (pending, recovered, resolver) = registrations
3351                    .remove(validator)
3352                    .expect("validator should be registered");
3353                engine_handlers.push(engine.start(pending, recovered, resolver));
3354            }
3355
3356            // Wait for every online validator to construct its nullify vote for
3357            // view 1.
3358            let stalled = View::new(1);
3359            loop {
3360                let nullified = reporters.iter().zip(participants.iter().skip(1)).all(
3361                    |(reporter, validator)| {
3362                        reporter
3363                            .nullifies
3364                            .lock()
3365                            .get(&stalled)
3366                            .is_some_and(|nullifiers| nullifiers.contains(validator))
3367                    },
3368                );
3369                if nullified {
3370                    break;
3371                }
3372                context.sleep(Duration::from_millis(100)).await;
3373            }
3374
3375            // The reporter observes our vote via the batcher, which can run ahead
3376            // of the voter's journal sync in the same instant. Wait one more tick
3377            // so every vote is durable before crashing.
3378            context.sleep(Duration::from_secs(1)).await;
3379
3380            // Crash every online validator before any nullification certificate
3381            // can circulate.
3382            for handle in engine_handlers.drain(..) {
3383                handle.abort();
3384                let _ = handle.await;
3385            }
3386            relay.deregister_all();
3387
3388            // Restore connectivity between the online validators.
3389            let link = Link {
3390                latency: Duration::from_millis(10),
3391                jitter: Duration::from_millis(1),
3392                success_rate: probability!(1.0),
3393            };
3394            link_validators(
3395                &mut oracle,
3396                &participants,
3397                Action::Link(link),
3398                Some(|_, i, j| ![i, j].contains(&0usize)),
3399            )
3400            .await;
3401
3402            // Restart every online validator from its journal.
3403            let mut reporters = Vec::new();
3404            for (idx_scheme, validator) in participants.iter().enumerate() {
3405                // Skip first peer
3406                if idx_scheme == 0 {
3407                    continue;
3408                }
3409
3410                // Create scheme context
3411                let context = context
3412                    .child("validator_restarted")
3413                    .with_attribute("public_key", validator);
3414
3415                // Configure engine
3416                let reporter_config = mocks::reporter::Config {
3417                    participants: participants.clone().try_into().unwrap(),
3418                    scheme: schemes[idx_scheme].clone(),
3419                    elector: elector.clone(),
3420                };
3421                let reporter =
3422                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
3423                reporters.push(reporter.clone());
3424                let application_cfg = mocks::application::Config::<Sha256, _> {
3425                    relay: relay.clone(),
3426                    me: validator.clone(),
3427                    propose_latency: (10.0, 5.0),
3428                    verify_latency: (10.0, 5.0),
3429                    certify_latency: (10.0, 5.0),
3430                    should_certify: mocks::application::Certifier::Always,
3431                };
3432                let (actor, application) = mocks::application::Application::new(
3433                    context.child("application"),
3434                    application_cfg,
3435                );
3436                actor.start();
3437                let blocker = oracle.control(validator.clone());
3438                let cfg = config::Config {
3439                    scheme: schemes[idx_scheme].clone(),
3440                    elector: elector.clone(),
3441                    blocker,
3442                    automaton: application.clone(),
3443                    relay: application.clone(),
3444                    reporter: reporter.clone(),
3445                    strategy: Sequential,
3446                    partition: validator.to_string(),
3447                    mailbox_size: NZUsize!(1024),
3448                    epoch: Epoch::new(333),
3449                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
3450                        Epoch::new(333),
3451                    )),
3452                    leader_timeout: Duration::from_secs(1),
3453                    certification_timeout: Duration::from_secs(2),
3454                    timeout_retry: Duration::from_secs(10),
3455                    fetch_timeout: Duration::from_secs(1),
3456                    view_retention,
3457                    skip: SkipPolicy::Enabled {
3458                        timeout: skip_timeout,
3459                        budget: SkipBudget::Participants,
3460                    },
3461                    replay_buffer: NZUsize!(1024 * 1024),
3462                    write_buffer: NZUsize!(1024 * 1024),
3463                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3464                    forward: ForwardPolicy::Disabled,
3465                    track_historical_votes: false,
3466                };
3467                let engine = Engine::new(context.child("engine"), cfg);
3468
3469                // Start engine
3470                let (pending, recovered, resolver) =
3471                    register_validator(&mut oracle, validator.clone()).await;
3472                engine.start(pending, recovered, resolver);
3473            }
3474
3475            // The restarted validators must reconstruct a nullification for view 1
3476            // to make progress (participant 0 never votes, so every remaining vote
3477            // is required to reach quorum).
3478            let mut finalizers = Vec::new();
3479            for reporter in reporters.iter_mut() {
3480                let (mut latest, mut monitor) = reporter.subscribe().await;
3481                finalizers.push(context.child("finalizer").spawn(move |_| async move {
3482                    while latest < required_containers {
3483                        latest = monitor.recv().await.expect("event missing");
3484                    }
3485                }));
3486            }
3487            join_all(finalizers).await;
3488        });
3489    }
3490
3491    test_for_all_fixtures!(all_crash_after_nullify);
3492
3493    fn partition<S, F, L>(fixture: F, elector: L)
3494    where
3495        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3496        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
3497        L: elector::Config<S>,
3498    {
3499        partition_with_term::<S, F, L>(elector, fixture);
3500    }
3501
3502    fn partition_with_term<S, F, L>(elector: L, mut fixture: F)
3503    where
3504        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3505        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
3506        L: elector::Config<S>,
3507    {
3508        // Create context
3509        let n = 10;
3510        let required_containers = View::new(50);
3511        let view_retention = ViewDelta::new(10);
3512        let skip_timeout = Duration::from_secs(11);
3513        let namespace = b"consensus".to_vec();
3514        let executor = deterministic::Runner::timed(Duration::from_secs(900));
3515        executor.start(|mut context| async move {
3516            // Register participants
3517            let Fixture {
3518                participants,
3519                schemes,
3520                ..
3521            } = fixture(&mut context, &namespace, n);
3522            let mut oracle =
3523                start_test_network_with_peers(context.child("network"), participants.clone(), true)
3524                    .await;
3525            let mut registrations = register_validators(&mut oracle, &participants).await;
3526
3527            // Link all validators
3528            let link = Link {
3529                latency: Duration::from_millis(10),
3530                jitter: Duration::from_millis(1),
3531                success_rate: probability!(1.0),
3532            };
3533            link_validators(&mut oracle, &participants, Action::Link(link.clone()), None).await;
3534
3535            // Create engines
3536            let elector = elector.clone();
3537            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
3538            let mut reporters = Vec::new();
3539            let mut engine_handlers = Vec::new();
3540            for (idx, validator) in participants.iter().enumerate() {
3541                // Create scheme context
3542                let context = context
3543                    .child("validator")
3544                    .with_attribute("public_key", validator);
3545
3546                // Configure engine
3547                let reporter_config = mocks::reporter::Config {
3548                    participants: participants.clone().try_into().unwrap(),
3549                    scheme: schemes[idx].clone(),
3550                    elector: elector.clone(),
3551                };
3552                let reporter =
3553                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
3554                reporters.push(reporter.clone());
3555                let application_cfg = mocks::application::Config::<Sha256, _> {
3556                    relay: relay.clone(),
3557                    me: validator.clone(),
3558                    propose_latency: (10.0, 5.0),
3559                    verify_latency: (10.0, 5.0),
3560                    certify_latency: (10.0, 5.0),
3561                    should_certify: mocks::application::Certifier::Always,
3562                };
3563                let (actor, application) = mocks::application::Application::new(
3564                    context.child("application"),
3565                    application_cfg,
3566                );
3567                actor.start();
3568                let blocker = oracle.control(validator.clone());
3569                let cfg = config::Config {
3570                    scheme: schemes[idx].clone(),
3571                    elector: elector.clone(),
3572                    blocker,
3573                    automaton: application.clone(),
3574                    relay: application.clone(),
3575                    reporter: reporter.clone(),
3576                    strategy: Sequential,
3577                    partition: validator.to_string(),
3578                    mailbox_size: NZUsize!(1024),
3579                    epoch: Epoch::new(333),
3580                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
3581                        Epoch::new(333),
3582                    )),
3583                    leader_timeout: Duration::from_secs(1),
3584                    certification_timeout: Duration::from_secs(2),
3585                    timeout_retry: Duration::from_secs(10),
3586                    fetch_timeout: Duration::from_secs(1),
3587                    view_retention,
3588                    skip: SkipPolicy::Enabled {
3589                        timeout: skip_timeout,
3590                        budget: SkipBudget::Participants,
3591                    },
3592                    replay_buffer: NZUsize!(1024 * 1024),
3593                    write_buffer: NZUsize!(1024 * 1024),
3594                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3595                    forward: ForwardPolicy::Disabled,
3596                    track_historical_votes: false,
3597                };
3598                let engine = Engine::new(context.child("engine"), cfg);
3599
3600                // Start engine
3601                let (pending, recovered, resolver) = registrations
3602                    .remove(validator)
3603                    .expect("validator should be registered");
3604                engine_handlers.push(engine.start(pending, recovered, resolver));
3605            }
3606
3607            // Wait for all engines to finish
3608            let mut finalizers = Vec::new();
3609            for reporter in reporters.iter_mut() {
3610                let (mut latest, mut monitor) = reporter.subscribe().await;
3611                finalizers.push(context.child("finalizer").spawn(move |_| async move {
3612                    while latest < required_containers {
3613                        latest = monitor.recv().await.expect("event missing");
3614                    }
3615                }));
3616            }
3617            join_all(finalizers).await;
3618
3619            // Cut all links between validator halves
3620            fn separated(n: usize, a: usize, b: usize) -> bool {
3621                let m = n / 2;
3622                (a < m && b >= m) || (a >= m && b < m)
3623            }
3624            link_validators(&mut oracle, &participants, Action::Unlink, Some(separated)).await;
3625
3626            // Wait for any in-progress notarizations/finalizations to finish
3627            context.sleep(Duration::from_secs(10)).await;
3628
3629            // Wait for a few virtual minutes (shouldn't finalize anything)
3630            let mut finalizers = Vec::new();
3631            for reporter in reporters.iter_mut() {
3632                let (_, mut monitor) = reporter.subscribe().await;
3633                finalizers.push(context.child("finalizer").spawn(move |context| async move {
3634                    select! {
3635                        _timeout = context.sleep(Duration::from_secs(60)) => {},
3636                        _done = monitor.recv() => {
3637                            panic!("engine should not notarize or finalize anything");
3638                        },
3639                    }
3640                }));
3641            }
3642            join_all(finalizers).await;
3643
3644            // Restore links
3645            link_validators(
3646                &mut oracle,
3647                &participants,
3648                Action::Link(link),
3649                Some(separated),
3650            )
3651            .await;
3652
3653            // Wait for all engines to finish
3654            let mut finalizers = Vec::new();
3655            for reporter in reporters.iter_mut() {
3656                let (mut latest, mut monitor) = reporter.subscribe().await;
3657                let required = latest.saturating_add(ViewDelta::new(required_containers.get()));
3658                finalizers.push(context.child("finalizer").spawn(move |_| async move {
3659                    while latest < required {
3660                        latest = monitor.recv().await.expect("event missing");
3661                    }
3662                }));
3663            }
3664            join_all(finalizers).await;
3665
3666            // Check reporters for correct activity
3667            for reporter in reporters.iter() {
3668                // Ensure no faults
3669                reporter.assert_no_faults();
3670
3671                // Ensure no invalid signatures
3672                reporter.assert_no_invalid();
3673            }
3674
3675            // Ensure no blocked connections
3676            let blocked = oracle.blocked().await.unwrap();
3677            assert!(blocked.is_empty());
3678        });
3679    }
3680
3681    test_for_all_fixtures!(partition);
3682
3683    #[test_group("slow")]
3684    #[test_traced]
3685    fn test_partition_stable_leader_optimistic() {
3686        partition_with_term::<_, _, RoundRobin>(
3687            RoundRobin::default().with_term(
3688                TermLength::new(NZU32!(5)),
3689                Duration::from_secs(13),
3690                ViewDelta::new(2),
3691            ),
3692            ed25519::fixture,
3693        );
3694    }
3695
3696    fn slow_and_lossy_links_seeded<S, F, L>(seed: u64, fixture: F, elector: L) -> String
3697    where
3698        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3699        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
3700        L: elector::Config<S>,
3701    {
3702        slow_and_lossy_links_seeded_with_term::<S, F, L>(elector, seed, fixture)
3703    }
3704
3705    fn slow_and_lossy_links_seeded_with_term<S, F, L>(
3706        elector: L,
3707        seed: u64,
3708        mut fixture: F,
3709    ) -> String
3710    where
3711        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3712        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
3713        L: elector::Config<S>,
3714    {
3715        // Create context
3716        let n = 5;
3717        let required_containers = View::new(50);
3718        let view_retention = ViewDelta::new(10);
3719        let skip_timeout = Duration::from_secs(11);
3720        let namespace = b"consensus".to_vec();
3721        let cfg = deterministic::Config::new()
3722            .with_seed(seed)
3723            .with_timeout(Some(Duration::from_secs(5_000)));
3724        let executor = deterministic::Runner::new(cfg);
3725        executor.start(|mut context| async move {
3726            // Register participants
3727            let Fixture {
3728                participants,
3729                schemes,
3730                ..
3731            } = fixture(&mut context, &namespace, n);
3732            let mut oracle =
3733                start_test_network_with_peers(context.child("network"), participants.clone(), true)
3734                    .await;
3735            let mut registrations = register_validators(&mut oracle, &participants).await;
3736
3737            // Link all validators
3738            let degraded_link = Link {
3739                latency: Duration::from_millis(200),
3740                jitter: Duration::from_millis(150),
3741                success_rate: probability!(0.5),
3742            };
3743            link_validators(
3744                &mut oracle,
3745                &participants,
3746                Action::Link(degraded_link),
3747                None,
3748            )
3749            .await;
3750
3751            // Create engines
3752            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
3753            let mut reporters = Vec::new();
3754            let mut engine_handlers = Vec::new();
3755            for (idx, validator) in participants.iter().enumerate() {
3756                // Create scheme context
3757                let context = context
3758                    .child("validator")
3759                    .with_attribute("public_key", validator);
3760
3761                // Configure engine
3762                let reporter_config = mocks::reporter::Config {
3763                    participants: participants.clone().try_into().unwrap(),
3764                    scheme: schemes[idx].clone(),
3765                    elector: elector.clone(),
3766                };
3767                let reporter =
3768                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
3769                reporters.push(reporter.clone());
3770                let application_cfg = mocks::application::Config::<Sha256, _> {
3771                    relay: relay.clone(),
3772                    me: validator.clone(),
3773                    propose_latency: (10.0, 5.0),
3774                    verify_latency: (10.0, 5.0),
3775                    certify_latency: (10.0, 5.0),
3776                    should_certify: mocks::application::Certifier::Always,
3777                };
3778                let (actor, application) = mocks::application::Application::new(
3779                    context.child("application"),
3780                    application_cfg,
3781                );
3782                actor.start();
3783                let blocker = oracle.control(validator.clone());
3784                let cfg = config::Config {
3785                    scheme: schemes[idx].clone(),
3786                    elector: elector.clone(),
3787                    blocker,
3788                    automaton: application.clone(),
3789                    relay: application.clone(),
3790                    reporter: reporter.clone(),
3791                    strategy: Sequential,
3792                    partition: validator.to_string(),
3793                    mailbox_size: NZUsize!(1024),
3794                    epoch: Epoch::new(333),
3795                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
3796                        Epoch::new(333),
3797                    )),
3798                    leader_timeout: Duration::from_secs(1),
3799                    certification_timeout: Duration::from_secs(2),
3800                    timeout_retry: Duration::from_secs(10),
3801                    fetch_timeout: Duration::from_secs(1),
3802                    view_retention,
3803                    skip: SkipPolicy::Enabled {
3804                        timeout: skip_timeout,
3805                        budget: SkipBudget::Participants,
3806                    },
3807                    replay_buffer: NZUsize!(1024 * 1024),
3808                    write_buffer: NZUsize!(1024 * 1024),
3809                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
3810                    forward: ForwardPolicy::Disabled,
3811                    track_historical_votes: false,
3812                };
3813                let engine = Engine::new(context.child("engine"), cfg);
3814
3815                // Start engine
3816                let (pending, recovered, resolver) = registrations
3817                    .remove(validator)
3818                    .expect("validator should be registered");
3819                engine_handlers.push(engine.start(pending, recovered, resolver));
3820            }
3821
3822            // Wait for all engines to finish
3823            let mut finalizers = Vec::new();
3824            for reporter in reporters.iter_mut() {
3825                let (mut latest, mut monitor) = reporter.subscribe().await;
3826                finalizers.push(context.child("finalizer").spawn(move |_| async move {
3827                    while latest < required_containers {
3828                        latest = monitor.recv().await.expect("event missing");
3829                    }
3830                }));
3831            }
3832            join_all(finalizers).await;
3833
3834            // Check reporters for correct activity
3835            for reporter in reporters.iter() {
3836                // Ensure no faults
3837                reporter.assert_no_faults();
3838
3839                // Ensure no invalid signatures
3840                reporter.assert_no_invalid();
3841            }
3842
3843            // Ensure no blocked connections
3844            let blocked = oracle.blocked().await.unwrap();
3845            assert!(blocked.is_empty());
3846
3847            context.auditor().state()
3848        })
3849    }
3850
3851    fn slow_and_lossy_links<S, F, L>(fixture: F, elector: L) -> String
3852    where
3853        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3854        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
3855        L: elector::Config<S>,
3856    {
3857        slow_and_lossy_links_seeded::<_, _, L>(6, fixture, elector)
3858    }
3859
3860    test_for_all_fixtures!(slow_and_lossy_links);
3861
3862    #[test_group("slow")]
3863    #[test_traced]
3864    fn test_slow_and_lossy_links_stable_leader_optimistic() {
3865        slow_and_lossy_links_seeded_with_term::<_, _, RoundRobin>(
3866            RoundRobin::default().with_term(
3867                TermLength::new(NZU32!(5)),
3868                Duration::from_secs(13),
3869                ViewDelta::new(2),
3870            ),
3871            6,
3872            ed25519::fixture,
3873        );
3874    }
3875
3876    fn determinism<S, F, L>(seed: u64, fixture: F, elector: L)
3877    where
3878        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3879        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S> + Copy,
3880        L: elector::Config<S>,
3881    {
3882        // We use slow and lossy links as the deterministic test
3883        // because it is the most complex test.
3884        assert_eq!(
3885            slow_and_lossy_links_seeded::<_, _, L>(seed, fixture, elector.clone()),
3886            slow_and_lossy_links_seeded::<_, _, L>(seed, fixture, elector),
3887        );
3888    }
3889
3890    test_for_all_fixtures!(determinism, seeds = 5);
3891
3892    #[test_group("slow")]
3893    #[test_traced]
3894    fn test_distinct_states() {
3895        // Sanity check that different schemes produce different audit states.
3896        macro_rules! collect {
3897            ($vec:ident, $suffix:ident, $elector:ty, $fixture:expr, $elector_config:expr) => {
3898                $vec.push((
3899                    stringify!($suffix),
3900                    slow_and_lossy_links_seeded::<_, _, $elector>(7, $fixture, $elector_config),
3901                ));
3902            };
3903        }
3904        let mut states = Vec::new();
3905        for_each_fixture!(collect!(states));
3906        for pair in states.windows(2) {
3907            assert_ne!(
3908                pair[0].1, pair[1].1,
3909                "state {} equals state {}",
3910                pair[0].0, pair[1].0
3911            );
3912        }
3913    }
3914
3915    fn conflicter<S, F, L>(seed: u64, mut fixture: F, elector: L)
3916    where
3917        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
3918        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
3919        L: elector::Config<S>,
3920    {
3921        // Create context
3922        let n = 4;
3923        let required_containers = View::new(50);
3924        let view_retention = ViewDelta::new(10);
3925        let skip_timeout = Duration::from_secs(11);
3926        let namespace = b"consensus".to_vec();
3927        let cfg = deterministic::Config::new()
3928            .with_seed(seed)
3929            .with_timeout(Some(Duration::from_secs(30)));
3930        let executor = deterministic::Runner::new(cfg);
3931        executor.start(|mut context| async move {
3932            // Register participants
3933            let Fixture {
3934                participants,
3935                schemes,
3936                ..
3937            } = fixture(&mut context, &namespace, n);
3938            let mut oracle =
3939                start_test_network_with_peers(context.child("network"), participants.clone(), true)
3940                    .await;
3941            let mut registrations = register_validators(&mut oracle, &participants).await;
3942
3943            // Link all validators
3944            let link = Link {
3945                latency: Duration::from_millis(10),
3946                jitter: Duration::from_millis(1),
3947                success_rate: probability!(1.0),
3948            };
3949            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
3950
3951            // Create engines
3952            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
3953            let mut reporters = Vec::new();
3954            for (idx_scheme, validator) in participants.iter().enumerate() {
3955                // Create scheme context
3956                let context = context
3957                    .child("validator")
3958                    .with_attribute("public_key", validator);
3959
3960                // Start engine
3961                let reporter_config = mocks::reporter::Config {
3962                    participants: participants.clone().try_into().unwrap(),
3963                    scheme: schemes[idx_scheme].clone(),
3964                    elector: elector.clone(),
3965                };
3966                let reporter =
3967                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
3968                let (pending, recovered, resolver) = registrations
3969                    .remove(validator)
3970                    .expect("validator should be registered");
3971                if idx_scheme == 0 {
3972                    let cfg = mocks::conflicter::Config {
3973                        scheme: schemes[idx_scheme].clone(),
3974                    };
3975
3976                    let engine: mocks::conflicter::Conflicter<_, _, Sha256> =
3977                        mocks::conflicter::Conflicter::new(context.child("byzantine_engine"), cfg);
3978                    engine.start(pending);
3979                } else {
3980                    reporters.push(reporter.clone());
3981                    let application_cfg = mocks::application::Config::<Sha256, _> {
3982                        relay: relay.clone(),
3983                        me: validator.clone(),
3984                        propose_latency: (10.0, 5.0),
3985                        verify_latency: (10.0, 5.0),
3986                        certify_latency: (10.0, 5.0),
3987                        should_certify: mocks::application::Certifier::Always,
3988                    };
3989                    let (actor, application) = mocks::application::Application::new(
3990                        context.child("application"),
3991                        application_cfg,
3992                    );
3993                    actor.start();
3994                    let blocker = oracle.control(validator.clone());
3995                    let cfg = config::Config {
3996                        scheme: schemes[idx_scheme].clone(),
3997                        elector: elector.clone(),
3998                        blocker,
3999                        automaton: application.clone(),
4000                        relay: application.clone(),
4001                        reporter: reporter.clone(),
4002                        strategy: Sequential,
4003                        partition: validator.to_string(),
4004                        mailbox_size: NZUsize!(1024),
4005                        epoch: Epoch::new(333),
4006                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
4007                            Epoch::new(333),
4008                        )),
4009                        leader_timeout: Duration::from_secs(1),
4010                        certification_timeout: Duration::from_secs(2),
4011                        timeout_retry: Duration::from_secs(10),
4012                        fetch_timeout: Duration::from_secs(1),
4013                        view_retention,
4014                        skip: SkipPolicy::Enabled {
4015                            timeout: skip_timeout,
4016                            budget: SkipBudget::Participants,
4017                        },
4018                        replay_buffer: NZUsize!(1024 * 1024),
4019                        write_buffer: NZUsize!(1024 * 1024),
4020                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
4021                        forward: ForwardPolicy::Disabled,
4022                        track_historical_votes: true,
4023                    };
4024                    let engine = Engine::new(context.child("engine"), cfg);
4025                    engine.start(pending, recovered, resolver);
4026                }
4027            }
4028
4029            // Wait for all engines to finish
4030            let mut finalizers = Vec::new();
4031            for reporter in reporters.iter_mut() {
4032                let (mut latest, mut monitor) = reporter.subscribe().await;
4033                finalizers.push(context.child("finalizer").spawn(move |_| async move {
4034                    while latest < required_containers {
4035                        latest = monitor.recv().await.expect("event missing");
4036                    }
4037                }));
4038            }
4039            join_all(finalizers).await;
4040
4041            // Check reporters for correct activity
4042            let byz = &participants[0];
4043            let mut count_conflicting = 0;
4044            for reporter in reporters.iter() {
4045                // Ensure only faults for byz
4046                {
4047                    let faults = reporter.faults.lock();
4048                    assert_eq!(faults.len(), 1);
4049                    let faulter = faults.get(byz).expect("byzantine party is not faulter");
4050                    for faults in faulter.values() {
4051                        for fault in faults.iter() {
4052                            match fault {
4053                                Activity::ConflictingNotarize(_) => {
4054                                    count_conflicting += 1;
4055                                }
4056                                Activity::ConflictingFinalize(_) => {
4057                                    count_conflicting += 1;
4058                                }
4059                                _ => panic!("unexpected fault: {fault:?}"),
4060                            }
4061                        }
4062                    }
4063                }
4064
4065                // Ensure no invalid signatures
4066                reporter.assert_no_invalid();
4067            }
4068            assert!(count_conflicting > 0);
4069
4070            // Ensure conflicter is blocked
4071            let blocked = oracle.blocked().await.unwrap();
4072            assert!(!blocked.is_empty());
4073            for (a, b) in blocked {
4074                assert_ne!(&a, byz);
4075                assert_eq!(&b, byz);
4076            }
4077        });
4078    }
4079
4080    test_for_all_fixtures!(conflicter, seeds = 5);
4081
4082    fn invalid<S, F, L>(seed: u64, mut fixture: F, elector: L)
4083    where
4084        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
4085        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
4086        L: elector::Config<S>,
4087    {
4088        // Create context
4089        let n = 4;
4090        let required_containers = View::new(50);
4091        let view_retention = ViewDelta::new(10);
4092        let skip_timeout = Duration::from_secs(11);
4093        let namespace = b"consensus".to_vec();
4094        let cfg = deterministic::Config::new()
4095            .with_seed(seed)
4096            .with_timeout(Some(Duration::from_secs(30)));
4097        let executor = deterministic::Runner::new(cfg);
4098        executor.start(|mut context| async move {
4099            // Register participants
4100            let Fixture {
4101                participants,
4102                schemes,
4103                ..
4104            } = fixture(&mut context, &namespace, n);
4105
4106            let schemes: Vec<_> = schemes
4107                .into_iter()
4108                .enumerate()
4109                .map(|(idx, scheme)| {
4110                    let is_byzantine = idx == 0;
4111                    let behavior = if is_byzantine {
4112                        wrapped::Behavior::CorruptSignature
4113                    } else {
4114                        wrapped::Behavior::Honest
4115                    };
4116                    wrapped::Scheme::new(scheme, behavior)
4117                })
4118                .collect();
4119
4120            let mut oracle =
4121                start_test_network_with_peers(context.child("network"), participants.clone(), true)
4122                    .await;
4123            let mut registrations = register_validators(&mut oracle, &participants).await;
4124
4125            // Link all validators
4126            let link = Link {
4127                latency: Duration::from_millis(10),
4128                jitter: Duration::from_millis(1),
4129                success_rate: probability!(1.0),
4130            };
4131            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
4132
4133            // Create engines
4134            let elector = wrapped::Config(elector);
4135            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
4136            let mut reporters = Vec::new();
4137            for (idx_scheme, validator) in participants.iter().enumerate() {
4138                // Create scheme context
4139                let context = context
4140                    .child("validator")
4141                    .with_attribute("public_key", validator);
4142
4143                let reporter_config = mocks::reporter::Config {
4144                    participants: participants.clone().try_into().unwrap(),
4145                    scheme: schemes[idx_scheme].clone(),
4146                    elector: elector.clone(),
4147                };
4148                let reporter =
4149                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
4150                reporters.push(reporter.clone());
4151
4152                let application_cfg = mocks::application::Config::<Sha256, _> {
4153                    relay: relay.clone(),
4154                    me: validator.clone(),
4155                    propose_latency: (10.0, 5.0),
4156                    verify_latency: (10.0, 5.0),
4157                    certify_latency: (10.0, 5.0),
4158                    should_certify: mocks::application::Certifier::Always,
4159                };
4160                let (actor, application) = mocks::application::Application::new(
4161                    context.child("application"),
4162                    application_cfg,
4163                );
4164                actor.start();
4165                let blocker = oracle.control(validator.clone());
4166                let cfg = config::Config {
4167                    scheme: schemes[idx_scheme].clone(),
4168                    elector: elector.clone(),
4169                    blocker,
4170                    automaton: application.clone(),
4171                    relay: application.clone(),
4172                    reporter: reporter.clone(),
4173                    strategy: Sequential,
4174                    partition: validator.clone().to_string(),
4175                    mailbox_size: NZUsize!(1024),
4176                    epoch: Epoch::new(333),
4177                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
4178                        Epoch::new(333),
4179                    )),
4180                    leader_timeout: Duration::from_secs(1),
4181                    certification_timeout: Duration::from_secs(2),
4182                    timeout_retry: Duration::from_secs(10),
4183                    fetch_timeout: Duration::from_secs(1),
4184                    view_retention,
4185                    skip: SkipPolicy::Enabled {
4186                        timeout: skip_timeout,
4187                        budget: SkipBudget::Participants,
4188                    },
4189                    replay_buffer: NZUsize!(1024 * 1024),
4190                    write_buffer: NZUsize!(1024 * 1024),
4191                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
4192                    forward: ForwardPolicy::Disabled,
4193                    track_historical_votes: false,
4194                };
4195                let engine = Engine::new(context.child("engine"), cfg);
4196                let (pending, recovered, resolver) = registrations
4197                    .remove(validator)
4198                    .expect("validator should be registered");
4199                engine.start(pending, recovered, resolver);
4200            }
4201
4202            // Wait for all engines to finish.
4203            // The byzantine node will not finish since it will mark any finalization
4204            // certificates it creates (using its own invalid signature) as invalid.
4205            let mut finalizers = Vec::new();
4206            for reporter in reporters.iter_mut().skip(1) {
4207                let (mut latest, mut monitor) = reporter.subscribe().await;
4208                finalizers.push(context.child("finalizer").spawn(move |_| async move {
4209                    while latest < required_containers {
4210                        latest = monitor.recv().await.expect("event missing");
4211                    }
4212                }));
4213            }
4214            join_all(finalizers).await;
4215
4216            // Check all reporters for activity
4217            for (i, reporter) in reporters.iter().enumerate() {
4218                // Ensure no faults
4219                reporter.assert_no_faults();
4220
4221                // All nodes see invalid signatures since the honest reporters get unfiltered votes
4222                // once they pass the view.
4223                assert!(*reporter.invalid_votes.lock() > 0);
4224
4225                // Only the byzantine node sees invalid certificates since it constructs them from
4226                // its own invalid vote. The honest nodes reject them before reaching the reporter.
4227                let is_byzantine = i == 0;
4228                if is_byzantine {
4229                    assert!(*reporter.invalid_certificates.lock() > 0);
4230                } else {
4231                    assert_eq!(*reporter.invalid_certificates.lock(), 0);
4232                }
4233            }
4234
4235            // Ensure byzantine node is blocked by honest nodes.
4236            // The ">=" is because the Byzantine node may block itself.
4237            let blocked = oracle.blocked().await.unwrap();
4238            assert!(blocked.len() >= participants.len() - 1);
4239            let byz = &participants[0];
4240            for (_, b) in blocked {
4241                // Assert only the byzantine node is blocked.
4242                assert_eq!(&b, byz);
4243            }
4244        });
4245    }
4246
4247    test_for_all_fixtures!(invalid, seeds = 5);
4248
4249    // Test that when a node receives finalizations, it reports them.
4250    fn received_certificates_are_reported<S, F, L>(mut fixture: F, elector: L)
4251    where
4252        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
4253        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
4254        L: elector::Config<S>,
4255    {
4256        let n = 4;
4257        let required_containers = View::new(10);
4258        let view_retention = ViewDelta::new(10);
4259        let skip_timeout = Duration::from_secs(11);
4260        let namespace = b"consensus".to_vec();
4261        let cfg = deterministic::Config::new()
4262            .with_seed(0)
4263            .with_timeout(Some(Duration::from_secs(30)));
4264        let executor = deterministic::Runner::new(cfg);
4265        executor.start(|mut context| async move {
4266            let Fixture {
4267                participants,
4268                schemes,
4269                ..
4270            } = fixture(&mut context, &namespace, n);
4271
4272            let mut oracle = start_test_network_with_peers(
4273                context.child("network"),
4274                participants.clone(),
4275                false,
4276            )
4277            .await;
4278            let mut registrations = register_validators(&mut oracle, &participants).await;
4279
4280            // Link all honest nodes. Only link node 0 to node 1.
4281            //
4282            // Node 0 cannot locally form a certificate because it only sees itself plus one honest
4283            // peer, but it should still receive the certificates relayed by that peer.
4284            let link = Link {
4285                latency: Duration::from_millis(100),
4286                jitter: Duration::from_millis(1),
4287                success_rate: probability!(1.0),
4288            };
4289            fn link_graph(_: usize, i: usize, j: usize) -> bool {
4290                if i == 0 || j == 0 {
4291                    return i == 1 || j == 1;
4292                }
4293                true
4294            }
4295            link_validators(
4296                &mut oracle,
4297                &participants,
4298                Action::Link(link),
4299                Some(link_graph),
4300            )
4301            .await;
4302
4303            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
4304            let mut reporters = Vec::new();
4305            for (idx_scheme, validator) in participants.iter().enumerate() {
4306                let context = context
4307                    .child("validator")
4308                    .with_attribute("public_key", validator);
4309                let reporter_config = mocks::reporter::Config {
4310                    participants: participants.clone().try_into().unwrap(),
4311                    scheme: schemes[idx_scheme].clone(),
4312                    elector: elector.clone(),
4313                };
4314                let reporter =
4315                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
4316                reporters.push(reporter.clone());
4317
4318                let application_cfg = mocks::application::Config::<Sha256, _> {
4319                    relay: relay.clone(),
4320                    me: validator.clone(),
4321                    propose_latency: (10.0, 5.0),
4322                    verify_latency: (10.0, 5.0),
4323                    certify_latency: (10.0, 5.0),
4324                    should_certify: mocks::application::Certifier::Always,
4325                };
4326                let (actor, application) = mocks::application::Application::new(
4327                    context.child("application"),
4328                    application_cfg,
4329                );
4330                actor.start();
4331                let blocker = oracle.control(validator.clone());
4332                let cfg = config::Config {
4333                    scheme: schemes[idx_scheme].clone(),
4334                    elector: elector.clone(),
4335                    blocker,
4336                    automaton: application.clone(),
4337                    relay: application.clone(),
4338                    reporter: reporter.clone(),
4339                    strategy: Sequential,
4340                    partition: validator.clone().to_string(),
4341                    mailbox_size: NZUsize!(1024),
4342                    epoch: Epoch::new(333),
4343                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
4344                        Epoch::new(333),
4345                    )),
4346                    leader_timeout: Duration::from_secs(1),
4347                    certification_timeout: Duration::from_secs(2),
4348                    timeout_retry: Duration::from_secs(10),
4349                    fetch_timeout: Duration::from_secs(1),
4350                    view_retention,
4351                    skip: SkipPolicy::Enabled {
4352                        timeout: skip_timeout,
4353                        budget: SkipBudget::Participants,
4354                    },
4355                    replay_buffer: NZUsize!(1024 * 1024),
4356                    write_buffer: NZUsize!(1024 * 1024),
4357                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
4358                    forward: ForwardPolicy::Disabled,
4359                    track_historical_votes: false,
4360                };
4361                let engine = Engine::new(context.child("engine"), cfg);
4362                let (pending, recovered, resolver) = registrations
4363                    .remove(validator)
4364                    .expect("validator should be registered");
4365                engine.start(pending, recovered, resolver);
4366            }
4367            // Wait for an honest node to observe the finalizations
4368            let excluded_reporter = reporters[0].clone();
4369            let mut honest_reporter = reporters[1].clone();
4370            let (mut honest_latest, mut honest_monitor) = honest_reporter.subscribe().await;
4371            while honest_latest < required_containers {
4372                honest_latest = honest_monitor.recv().await.expect("event missing");
4373            }
4374
4375            // Wait for all in-flight certificates to arrive at excluded node and be reported.
4376            context.sleep(Duration::from_secs(1)).await;
4377
4378            // It should have received similar certificates to the honest node (with some
4379            // tolerance for initial views in which may not have yet been connected).
4380            let honest_notarized = {
4381                let notarizations = honest_reporter.notarizations.lock();
4382                View::range(View::new(1), required_containers.next())
4383                    .filter(|view| notarizations.contains_key(view))
4384                    .count()
4385            };
4386            let excluded_notarized = {
4387                let notarizations = excluded_reporter.notarizations.lock();
4388                View::range(View::new(1), required_containers.next())
4389                    .filter(|view| notarizations.contains_key(view))
4390                    .count()
4391            };
4392            assert!(
4393                excluded_notarized >= honest_notarized.saturating_sub(2),
4394                "honest_notarized: {honest_notarized}, excluded_notarized: {excluded_notarized}"
4395            );
4396
4397            let honest_finalized = {
4398                let finalizations = honest_reporter.finalizations.lock();
4399                View::range(View::new(1), required_containers.next())
4400                    .filter(|view| finalizations.contains_key(view))
4401                    .count()
4402            };
4403            let excluded_finalized = {
4404                let finalizations = excluded_reporter.finalizations.lock();
4405                View::range(View::new(1), required_containers.next())
4406                    .filter(|view| finalizations.contains_key(view))
4407                    .count()
4408            };
4409            assert!(
4410                excluded_finalized >= honest_finalized.saturating_sub(2),
4411                "honest_finalized: {honest_finalized}, excluded_finalized: {excluded_finalized}"
4412            );
4413        });
4414    }
4415
4416    test_for_all_fixtures!(received_certificates_are_reported);
4417
4418    fn survives_burst<S, F, L>(mut fixture: F, elector: L)
4419    where
4420        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
4421        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
4422        L: elector::Config<S>,
4423    {
4424        let n = 4;
4425        let epoch = Epoch::new(333);
4426        let namespace = b"mailbox_size_one_certificate_burst".to_vec();
4427        let executor = deterministic::Runner::default();
4428        executor.start(|mut context| async move {
4429            let Fixture {
4430                participants,
4431                schemes,
4432                ..
4433            } = fixture(&mut context, &namespace, n);
4434            let me = participants[0].clone();
4435            let mut oracle =
4436                start_test_network_with_peers(context.child("network"), participants.clone(), true)
4437                    .await;
4438            let (pending, recovered, resolver) = register_validator(&mut oracle, me.clone()).await;
4439
4440            let injector_pk = PrivateKey::from_seed(9_000_000).public_key();
4441            let (mut injector_sender, _injector_receiver) = oracle
4442                .control(injector_pk.clone())
4443                .register(1, TEST_QUOTA)
4444                .await
4445                .unwrap();
4446            let link = Link {
4447                latency: Duration::from_millis(0),
4448                jitter: Duration::from_millis(0),
4449                success_rate: probability!(1.0),
4450            };
4451            oracle
4452                .add_link(injector_pk.clone(), me.clone(), link)
4453                .await
4454                .unwrap();
4455            oracle.manager().track(
4456                1,
4457                TrackedPeers::new(
4458                    Set::from_iter_dedup(std::iter::once(me.clone())),
4459                    Set::from_iter_dedup(std::iter::once(injector_pk.clone())),
4460                ),
4461            );
4462            context.sleep(Duration::from_millis(1)).await;
4463
4464            let quorum = quorum(n) as usize;
4465            let notarization = |view: View, parent: View, payload: &[u8]| {
4466                let proposal =
4467                    Proposal::new(Round::new(epoch, view), parent, Sha256::hash(&[payload]));
4468                let votes: Vec<_> = schemes
4469                    .iter()
4470                    .take(quorum)
4471                    .map(|scheme| TNotarize::sign(scheme, proposal.clone()).unwrap())
4472                    .collect();
4473                TNotarization::from_notarizes(&schemes[0], non_empty![@votes.iter()], &Sequential)
4474                    .expect("notarization requires quorum")
4475            };
4476            let finalization = |view: View, parent: View, payload: &[u8]| {
4477                let proposal =
4478                    Proposal::new(Round::new(epoch, view), parent, Sha256::hash(&[payload]));
4479                let votes: Vec<_> = schemes
4480                    .iter()
4481                    .take(quorum)
4482                    .map(|scheme| TFinalize::sign(scheme, proposal.clone()).unwrap())
4483                    .collect();
4484                TFinalization::from_finalizes(&schemes[0], non_empty![@votes.iter()], &Sequential)
4485                    .expect("finalization requires quorum")
4486            };
4487
4488            // Load the network with certificates that the batcher will want to pass to the voter
4489            for certificate in [
4490                Certificate::Notarization(notarization(View::new(1), View::zero(), b"payload-1")),
4491                Certificate::Notarization(notarization(View::new(2), View::new(1), b"payload-2")),
4492                Certificate::Notarization(notarization(View::new(3), View::new(2), b"payload-3")),
4493                Certificate::Finalization(finalization(View::new(3), View::new(2), b"payload-3")),
4494            ] {
4495                injector_sender.send(Recipients::One(me.clone()), certificate.encode(), true);
4496            }
4497
4498            let reporter_config = mocks::reporter::Config {
4499                participants: participants.clone().try_into().unwrap(),
4500                scheme: schemes[0].clone(),
4501                elector: elector.clone(),
4502            };
4503            let reporter =
4504                mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
4505            let mut monitor_reporter = reporter.clone();
4506            let (mut latest, mut monitor) = monitor_reporter.subscribe().await;
4507
4508            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
4509            let application_cfg = mocks::application::Config::<Sha256, _> {
4510                relay: relay.clone(),
4511                me: me.clone(),
4512                propose_latency: (0.0, 0.0),
4513                verify_latency: (0.0, 0.0),
4514                certify_latency: (0.0, 0.0),
4515                should_certify: mocks::application::Certifier::Always,
4516            };
4517            let (mut application_actor, application) =
4518                mocks::application::Application::new(context.child("application"), application_cfg);
4519            application_actor.set_stall_proposals(true);
4520            application_actor.start();
4521
4522            let cfg = config::Config {
4523                scheme: schemes[0].clone(),
4524                elector,
4525                blocker: oracle.control(me.clone()),
4526                automaton: application.clone(),
4527                relay: application,
4528                reporter,
4529                strategy: Sequential,
4530                partition: me.to_string(),
4531                mailbox_size: NZUsize!(1),
4532                epoch,
4533                floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(epoch)),
4534                leader_timeout: Duration::from_secs(1),
4535                certification_timeout: Duration::from_secs(2),
4536                timeout_retry: Duration::from_secs(10),
4537                fetch_timeout: Duration::from_secs(1),
4538                view_retention: ViewDelta::new(10),
4539                skip: SkipPolicy::Enabled {
4540                    timeout: Duration::from_secs(11),
4541                    budget: SkipBudget::Participants,
4542                },
4543                replay_buffer: NZUsize!(1024 * 1024),
4544                write_buffer: NZUsize!(1024 * 1024),
4545                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
4546                forward: ForwardPolicy::Disabled,
4547                track_historical_votes: false,
4548            };
4549            let engine = Engine::new(context.child("engine"), cfg);
4550            engine.start(pending, recovered, resolver);
4551
4552            while latest < View::new(3) {
4553                latest = monitor.recv().await.expect("finalization event missing");
4554            }
4555        });
4556    }
4557
4558    test_for_all_fixtures!(survives_burst);
4559
4560    fn impersonator<S, F, L>(seed: u64, mut fixture: F, elector: L)
4561    where
4562        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
4563        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
4564        L: elector::Config<S>,
4565    {
4566        // Create context
4567        let n = 4;
4568        let required_containers = View::new(50);
4569        let view_retention = ViewDelta::new(10);
4570        let skip_timeout = Duration::from_secs(11);
4571        let namespace = b"consensus".to_vec();
4572        let cfg = deterministic::Config::new()
4573            .with_seed(seed)
4574            .with_timeout(Some(Duration::from_secs(30)));
4575        let executor = deterministic::Runner::new(cfg);
4576        executor.start(|mut context| async move {
4577            // Register participants
4578            let Fixture {
4579                participants,
4580                schemes,
4581                ..
4582            } = fixture(&mut context, &namespace, n);
4583            let mut oracle =
4584                start_test_network_with_peers(context.child("network"), participants.clone(), true)
4585                    .await;
4586            let mut registrations = register_validators(&mut oracle, &participants).await;
4587
4588            // Link all validators
4589            let link = Link {
4590                latency: Duration::from_millis(10),
4591                jitter: Duration::from_millis(1),
4592                success_rate: probability!(1.0),
4593            };
4594            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
4595
4596            // Create engines
4597            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
4598            let mut reporters = Vec::new();
4599            for (idx_scheme, validator) in participants.iter().enumerate() {
4600                // Create scheme context
4601                let context = context
4602                    .child("validator")
4603                    .with_attribute("public_key", validator);
4604
4605                // Start engine
4606                let reporter_config = mocks::reporter::Config {
4607                    participants: participants.clone().try_into().unwrap(),
4608                    scheme: schemes[idx_scheme].clone(),
4609                    elector: elector.clone(),
4610                };
4611                let reporter =
4612                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
4613                let (pending, recovered, resolver) = registrations
4614                    .remove(validator)
4615                    .expect("validator should be registered");
4616                if idx_scheme == 0 {
4617                    let cfg = mocks::impersonator::Config {
4618                        scheme: schemes[idx_scheme].clone(),
4619                    };
4620
4621                    let engine: mocks::impersonator::Impersonator<_, _, Sha256> =
4622                        mocks::impersonator::Impersonator::new(
4623                            context.child("byzantine_engine"),
4624                            cfg,
4625                        );
4626                    engine.start(pending);
4627                } else {
4628                    reporters.push(reporter.clone());
4629                    let application_cfg = mocks::application::Config::<Sha256, _> {
4630                        relay: relay.clone(),
4631                        me: validator.clone(),
4632                        propose_latency: (10.0, 5.0),
4633                        verify_latency: (10.0, 5.0),
4634                        certify_latency: (10.0, 5.0),
4635                        should_certify: mocks::application::Certifier::Always,
4636                    };
4637                    let (actor, application) = mocks::application::Application::new(
4638                        context.child("application"),
4639                        application_cfg,
4640                    );
4641                    actor.start();
4642                    let blocker = oracle.control(validator.clone());
4643                    let cfg = config::Config {
4644                        scheme: schemes[idx_scheme].clone(),
4645                        elector: elector.clone(),
4646                        blocker,
4647                        automaton: application.clone(),
4648                        relay: application.clone(),
4649                        reporter: reporter.clone(),
4650                        strategy: Sequential,
4651                        partition: validator.clone().to_string(),
4652                        mailbox_size: NZUsize!(1024),
4653                        epoch: Epoch::new(333),
4654                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
4655                            Epoch::new(333),
4656                        )),
4657                        leader_timeout: Duration::from_secs(1),
4658                        certification_timeout: Duration::from_secs(2),
4659                        timeout_retry: Duration::from_secs(10),
4660                        fetch_timeout: Duration::from_secs(1),
4661                        view_retention,
4662                        skip: SkipPolicy::Enabled {
4663                            timeout: skip_timeout,
4664                            budget: SkipBudget::Participants,
4665                        },
4666                        replay_buffer: NZUsize!(1024 * 1024),
4667                        write_buffer: NZUsize!(1024 * 1024),
4668                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
4669                        forward: ForwardPolicy::Disabled,
4670                        track_historical_votes: false,
4671                    };
4672                    let engine = Engine::new(context.child("engine"), cfg);
4673                    engine.start(pending, recovered, resolver);
4674                }
4675            }
4676
4677            // Wait for all engines to finish
4678            let mut finalizers = Vec::new();
4679            for reporter in reporters.iter_mut() {
4680                let (mut latest, mut monitor) = reporter.subscribe().await;
4681                finalizers.push(context.child("finalizer").spawn(move |_| async move {
4682                    while latest < required_containers {
4683                        latest = monitor.recv().await.expect("event missing");
4684                    }
4685                }));
4686            }
4687            join_all(finalizers).await;
4688
4689            // Check reporters for correct activity
4690            let byz = &participants[0];
4691            for reporter in reporters.iter() {
4692                // Ensure no faults
4693                reporter.assert_no_faults();
4694
4695                // Ensure no invalid signatures
4696                reporter.assert_no_invalid();
4697            }
4698
4699            // Ensure invalid is blocked
4700            let blocked = oracle.blocked().await.unwrap();
4701            assert!(!blocked.is_empty());
4702            for (a, b) in blocked {
4703                assert_ne!(&a, byz);
4704                assert_eq!(&b, byz);
4705            }
4706        });
4707    }
4708
4709    test_for_all_fixtures!(impersonator, seeds = 5);
4710
4711    fn equivocator_seeded<S, F, L>(seed: u64, fixture: F, elector: L) -> bool
4712    where
4713        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
4714        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
4715        L: elector::Config<S>,
4716    {
4717        equivocator_seeded_with_term::<S, F, L>(seed, elector, fixture)
4718    }
4719
4720    fn equivocator_seeded_with_term<S, F, L>(seed: u64, elector: L, mut fixture: F) -> bool
4721    where
4722        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
4723        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
4724        L: elector::Config<S>,
4725    {
4726        // Create context
4727        let n = 7;
4728        let required_containers = View::new(50);
4729        let view_retention = ViewDelta::new(10);
4730        let skip_timeout = Duration::from_secs(11);
4731        let namespace = b"consensus".to_vec();
4732        let cfg = deterministic::Config::new()
4733            .with_seed(seed)
4734            .with_timeout(Some(Duration::from_secs(60)));
4735        let executor = deterministic::Runner::new(cfg);
4736        executor.start(|mut context| async move {
4737            // Register participants
4738            let Fixture {
4739                participants,
4740                schemes,
4741                ..
4742            } = fixture(&mut context, &namespace, n);
4743            let mut oracle =
4744                start_test_network_with_peers(context.child("network"), participants.clone(), true)
4745                    .await;
4746            let mut registrations = register_validators(&mut oracle, &participants).await;
4747
4748            // Link all validators
4749            let link = Link {
4750                latency: Duration::from_millis(10),
4751                jitter: Duration::from_millis(1),
4752                success_rate: probability!(1.0),
4753            };
4754            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
4755
4756            // Create engines
4757            let elector = elector.clone();
4758            let mut engines = Vec::new();
4759            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
4760            let mut reporters = Vec::new();
4761            for (idx_scheme, validator) in participants.iter().enumerate() {
4762                // Create scheme context
4763                let context = context
4764                    .child("validator")
4765                    .with_attribute("public_key", validator);
4766
4767                // Start engine
4768                let reporter_config = mocks::reporter::Config {
4769                    participants: participants.clone().try_into().unwrap(),
4770                    scheme: schemes[idx_scheme].clone(),
4771                    elector: elector.clone(),
4772                };
4773                let reporter =
4774                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
4775                reporters.push(reporter.clone());
4776                let (pending, recovered, resolver) = registrations
4777                    .remove(validator)
4778                    .expect("validator should be registered");
4779                if idx_scheme == 0 {
4780                    let cfg = mocks::equivocator::Config::<_, _, Sha256> {
4781                        scheme: schemes[idx_scheme].clone(),
4782                        epoch: Epoch::new(333),
4783                        relay: relay.clone(),
4784                        elector: elector.clone(),
4785                    };
4786
4787                    let engine = mocks::equivocator::Equivocator::new(
4788                        context.child("byzantine_engine"),
4789                        cfg,
4790                    );
4791                    engines.push(engine.start(pending, recovered));
4792                } else {
4793                    let application_cfg = mocks::application::Config::<Sha256, _> {
4794                        relay: relay.clone(),
4795                        me: validator.clone(),
4796                        propose_latency: (10.0, 5.0),
4797                        verify_latency: (10.0, 5.0),
4798                        certify_latency: (10.0, 5.0),
4799                        should_certify: mocks::application::Certifier::Always,
4800                    };
4801                    let (actor, application) = mocks::application::Application::new(
4802                        context.child("application"),
4803                        application_cfg,
4804                    );
4805                    actor.start();
4806                    let blocker = oracle.control(validator.clone());
4807                    let cfg = config::Config {
4808                        scheme: schemes[idx_scheme].clone(),
4809                        elector: elector.clone(),
4810                        blocker,
4811                        automaton: application.clone(),
4812                        relay: application.clone(),
4813                        reporter: reporter.clone(),
4814                        strategy: Sequential,
4815                        partition: validator.to_string(),
4816                        mailbox_size: NZUsize!(1024),
4817                        epoch: Epoch::new(333),
4818                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
4819                            Epoch::new(333),
4820                        )),
4821                        leader_timeout: Duration::from_secs(1),
4822                        certification_timeout: Duration::from_secs(2),
4823                        timeout_retry: Duration::from_secs(10),
4824                        fetch_timeout: Duration::from_secs(1),
4825                        view_retention,
4826                        skip: SkipPolicy::Enabled {
4827                            timeout: skip_timeout,
4828                            budget: SkipBudget::Participants,
4829                        },
4830                        replay_buffer: NZUsize!(1024 * 1024),
4831                        write_buffer: NZUsize!(1024 * 1024),
4832                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
4833                        forward: ForwardPolicy::Disabled,
4834                        track_historical_votes: false,
4835                    };
4836                    let engine = Engine::new(context.child("engine"), cfg);
4837                    engines.push(engine.start(pending, recovered, resolver));
4838                }
4839            }
4840
4841            // Wait for all engines to hit required containers
4842            let mut finalizers = Vec::new();
4843            for reporter in reporters.iter_mut().skip(1) {
4844                let (mut latest, mut monitor) = reporter.subscribe().await;
4845                finalizers.push(context.child("finalizer").spawn(move |_| async move {
4846                    while latest < required_containers {
4847                        latest = monitor.recv().await.expect("event missing");
4848                    }
4849                }));
4850            }
4851            join_all(finalizers).await;
4852
4853            // Abort a validator
4854            let idx = context.random_range(1..engines.len()); // skip byzantine validator
4855            let validator = &participants[idx];
4856            let handle = engines.remove(idx);
4857            handle.abort();
4858            let _ = handle.await;
4859            reporters.remove(idx);
4860            info!(idx, ?validator, "aborted validator");
4861
4862            // Wait for all engines to hit required containers
4863            let mut finalizers = Vec::new();
4864            for reporter in reporters.iter_mut().skip(1) {
4865                let (mut latest, mut monitor) = reporter.subscribe().await;
4866                finalizers.push(context.child("finalizer").spawn(move |_| async move {
4867                    while latest < View::new(required_containers.get() * 2) {
4868                        latest = monitor.recv().await.expect("event missing");
4869                    }
4870                }));
4871            }
4872            join_all(finalizers).await;
4873
4874            // Recreate engine
4875            info!(idx, ?validator, "restarting validator");
4876            let context = context
4877                .child("validator_restarted")
4878                .with_attribute("public_key", validator);
4879
4880            // Start engine
4881            let reporter_config = mocks::reporter::Config {
4882                participants: participants.clone().try_into().unwrap(),
4883                scheme: schemes[idx].clone(),
4884                elector: elector.clone(),
4885            };
4886            let reporter =
4887                mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
4888            let (pending, recovered, resolver) =
4889                register_validator(&mut oracle, validator.clone()).await;
4890            reporters.push(reporter.clone());
4891            let application_cfg = mocks::application::Config::<Sha256, _> {
4892                relay: relay.clone(),
4893                me: validator.clone(),
4894                propose_latency: (10.0, 5.0),
4895                verify_latency: (10.0, 5.0),
4896                certify_latency: (10.0, 5.0),
4897                should_certify: mocks::application::Certifier::Always,
4898            };
4899            let (actor, application) =
4900                mocks::application::Application::new(context.child("application"), application_cfg);
4901            actor.start();
4902            let blocker = oracle.control(validator.clone());
4903            let cfg = config::Config {
4904                scheme: schemes[idx].clone(),
4905                elector: elector.clone(),
4906                blocker,
4907                automaton: application.clone(),
4908                relay: application.clone(),
4909                reporter: reporter.clone(),
4910                strategy: Sequential,
4911                partition: validator.to_string(),
4912                mailbox_size: NZUsize!(1024),
4913                epoch: Epoch::new(333),
4914                floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(Epoch::new(
4915                    333,
4916                ))),
4917                leader_timeout: Duration::from_secs(1),
4918                certification_timeout: Duration::from_secs(2),
4919                timeout_retry: Duration::from_secs(10),
4920                fetch_timeout: Duration::from_secs(1),
4921                view_retention,
4922                skip: SkipPolicy::Enabled {
4923                    timeout: skip_timeout,
4924                    budget: SkipBudget::Participants,
4925                },
4926                replay_buffer: NZUsize!(1024 * 1024),
4927                write_buffer: NZUsize!(1024 * 1024),
4928                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
4929                forward: ForwardPolicy::Disabled,
4930                track_historical_votes: false,
4931            };
4932            let engine = Engine::new(context.child("engine"), cfg);
4933            engine.start(pending, recovered, resolver);
4934
4935            // Wait for all engines to hit required containers
4936            let mut finalizers = Vec::new();
4937            for reporter in reporters.iter_mut().skip(1) {
4938                let (mut latest, mut monitor) = reporter.subscribe().await;
4939                finalizers.push(context.child("finalizer").spawn(move |_| async move {
4940                    while latest < View::new(required_containers.get() * 3) {
4941                        latest = monitor.recv().await.expect("event missing");
4942                    }
4943                }));
4944            }
4945            join_all(finalizers).await;
4946
4947            // Check equivocator blocking (we aren't guaranteed a fault will be produced
4948            // because it may not be possible to extract a conflicting vote from the certificate
4949            // we receive)
4950            let byz = &participants[0];
4951            let blocked = oracle.blocked().await.unwrap();
4952            for (a, b) in &blocked {
4953                assert_ne!(a, byz);
4954                assert_eq!(b, byz);
4955            }
4956            !blocked.is_empty()
4957        })
4958    }
4959
4960    fn equivocator<S, F, L>(fixture: F, elector: L)
4961    where
4962        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
4963        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S> + Copy,
4964        L: elector::Config<S>,
4965    {
4966        let detected =
4967            (0..5).any(|seed| equivocator_seeded::<_, _, L>(seed, fixture, elector.clone()));
4968        assert!(
4969            detected,
4970            "expected at least one seed to detect equivocation"
4971        );
4972    }
4973
4974    test_for_all_fixtures!(equivocator);
4975
4976    #[test_group("slow")]
4977    #[test_traced]
4978    fn test_equivocator_stable_leader_optimistic() {
4979        let detected = (0..5).any(|seed| {
4980            equivocator_seeded_with_term::<_, _, RoundRobin>(
4981                seed,
4982                RoundRobin::default().with_term(
4983                    TermLength::new(NZU32!(5)),
4984                    Duration::from_secs(13),
4985                    ViewDelta::new(2),
4986                ),
4987                ed25519::fixture,
4988            )
4989        });
4990        assert!(
4991            detected,
4992            "expected at least one seed to detect equivocation"
4993        );
4994    }
4995
4996    fn reconfigurer<S, F, L>(seed: u64, mut fixture: F, elector: L)
4997    where
4998        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
4999        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5000        L: elector::Config<S>,
5001    {
5002        // Create context
5003        let n = 4;
5004        let required_containers = View::new(50);
5005        let view_retention = ViewDelta::new(10);
5006        let skip_timeout = Duration::from_secs(11);
5007        let namespace = b"consensus".to_vec();
5008        let cfg = deterministic::Config::new()
5009            .with_seed(seed)
5010            .with_timeout(Some(Duration::from_secs(30)));
5011        let executor = deterministic::Runner::new(cfg);
5012        executor.start(|mut context| async move {
5013            // Register participants
5014            let Fixture {
5015                participants,
5016                schemes,
5017                ..
5018            } = fixture(&mut context, &namespace, n);
5019            let mut oracle =
5020                start_test_network_with_peers(context.child("network"), participants.clone(), true)
5021                    .await;
5022            let mut registrations = register_validators(&mut oracle, &participants).await;
5023
5024            // Link all validators
5025            let link = Link {
5026                latency: Duration::from_millis(10),
5027                jitter: Duration::from_millis(1),
5028                success_rate: probability!(1.0),
5029            };
5030            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
5031
5032            // Create engines
5033            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
5034            let mut reporters = Vec::new();
5035            for (idx_scheme, validator) in participants.iter().enumerate() {
5036                // Create scheme context
5037                let context = context
5038                    .child("validator")
5039                    .with_attribute("public_key", validator);
5040
5041                // Start engine
5042                let reporter_config = mocks::reporter::Config {
5043                    participants: participants.clone().try_into().unwrap(),
5044                    scheme: schemes[idx_scheme].clone(),
5045                    elector: elector.clone(),
5046                };
5047                let reporter =
5048                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
5049                let (pending, recovered, resolver) = registrations
5050                    .remove(validator)
5051                    .expect("validator should be registered");
5052                if idx_scheme == 0 {
5053                    let cfg = mocks::reconfigurer::Config {
5054                        scheme: schemes[idx_scheme].clone(),
5055                    };
5056                    let engine: mocks::reconfigurer::Reconfigurer<_, _, Sha256> =
5057                        mocks::reconfigurer::Reconfigurer::new(
5058                            context.child("byzantine_engine"),
5059                            cfg,
5060                        );
5061                    engine.start(pending);
5062                } else {
5063                    reporters.push(reporter.clone());
5064                    let application_cfg = mocks::application::Config::<Sha256, _> {
5065                        relay: relay.clone(),
5066                        me: validator.clone(),
5067                        propose_latency: (10.0, 5.0),
5068                        verify_latency: (10.0, 5.0),
5069                        certify_latency: (10.0, 5.0),
5070                        should_certify: mocks::application::Certifier::Always,
5071                    };
5072                    let (actor, application) = mocks::application::Application::new(
5073                        context.child("application"),
5074                        application_cfg,
5075                    );
5076                    actor.start();
5077                    let blocker = oracle.control(validator.clone());
5078                    let cfg = config::Config {
5079                        scheme: schemes[idx_scheme].clone(),
5080                        elector: elector.clone(),
5081                        blocker,
5082                        automaton: application.clone(),
5083                        relay: application.clone(),
5084                        reporter: reporter.clone(),
5085                        strategy: Sequential,
5086                        partition: validator.to_string(),
5087                        mailbox_size: NZUsize!(1024),
5088                        epoch: Epoch::new(333),
5089                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
5090                            Epoch::new(333),
5091                        )),
5092                        leader_timeout: Duration::from_secs(1),
5093                        certification_timeout: Duration::from_secs(2),
5094                        timeout_retry: Duration::from_secs(10),
5095                        fetch_timeout: Duration::from_secs(1),
5096                        view_retention,
5097                        skip: SkipPolicy::Enabled {
5098                            timeout: skip_timeout,
5099                            budget: SkipBudget::Participants,
5100                        },
5101                        replay_buffer: NZUsize!(1024 * 1024),
5102                        write_buffer: NZUsize!(1024 * 1024),
5103                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
5104                        forward: ForwardPolicy::Disabled,
5105                        track_historical_votes: false,
5106                    };
5107                    let engine = Engine::new(context.child("engine"), cfg);
5108                    engine.start(pending, recovered, resolver);
5109                }
5110            }
5111
5112            // Wait for all engines to finish
5113            let mut finalizers = Vec::new();
5114            for reporter in reporters.iter_mut() {
5115                let (mut latest, mut monitor) = reporter.subscribe().await;
5116                finalizers.push(context.child("finalizer").spawn(move |_| async move {
5117                    while latest < required_containers {
5118                        latest = monitor.recv().await.expect("event missing");
5119                    }
5120                }));
5121            }
5122            join_all(finalizers).await;
5123
5124            // Check reporters for correct activity
5125            let byz = &participants[0];
5126            for reporter in reporters.iter() {
5127                // Ensure no faults
5128                reporter.assert_no_faults();
5129
5130                // Ensure no invalid signatures
5131                reporter.assert_no_invalid();
5132            }
5133
5134            // Ensure reconfigurer is blocked (epoch mismatch)
5135            let blocked = oracle.blocked().await.unwrap();
5136            assert!(!blocked.is_empty());
5137            for (a, b) in blocked {
5138                assert_ne!(&a, byz);
5139                assert_eq!(&b, byz);
5140            }
5141        });
5142    }
5143
5144    test_for_all_fixtures!(reconfigurer, seeds = 5);
5145
5146    fn nuller<S, F, L>(seed: u64, mut fixture: F, elector: L)
5147    where
5148        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
5149        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5150        L: elector::Config<S>,
5151    {
5152        // Create context
5153        let n = 4;
5154        let required_containers = View::new(50);
5155        let view_retention = ViewDelta::new(10);
5156        let skip_timeout = Duration::from_secs(11);
5157        let namespace = b"consensus".to_vec();
5158        let cfg = deterministic::Config::new()
5159            .with_seed(seed)
5160            .with_timeout(Some(Duration::from_secs(30)));
5161        let executor = deterministic::Runner::new(cfg);
5162        executor.start(|mut context| async move {
5163            // Register participants
5164            let Fixture {
5165                participants,
5166                schemes,
5167                ..
5168            } = fixture(&mut context, &namespace, n);
5169            let mut oracle =
5170                start_test_network_with_peers(context.child("network"), participants.clone(), true)
5171                    .await;
5172            let mut registrations = register_validators(&mut oracle, &participants).await;
5173
5174            // Link all validators
5175            let link = Link {
5176                latency: Duration::from_millis(10),
5177                jitter: Duration::from_millis(1),
5178                success_rate: probability!(1.0),
5179            };
5180            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
5181
5182            // Create engines
5183            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
5184            let mut reporters = Vec::new();
5185            for (idx_scheme, validator) in participants.iter().enumerate() {
5186                // Create scheme context
5187                let context = context
5188                    .child("validator")
5189                    .with_attribute("public_key", validator);
5190
5191                // Start engine
5192                let reporter_config = mocks::reporter::Config {
5193                    participants: participants.clone().try_into().unwrap(),
5194                    scheme: schemes[idx_scheme].clone(),
5195                    elector: elector.clone(),
5196                };
5197                let reporter =
5198                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
5199                let (pending, recovered, resolver) = registrations
5200                    .remove(validator)
5201                    .expect("validator should be registered");
5202                if idx_scheme == 0 {
5203                    let cfg = mocks::nuller::Config {
5204                        scheme: schemes[idx_scheme].clone(),
5205                    };
5206                    let engine: mocks::nuller::Nuller<_, _, Sha256> =
5207                        mocks::nuller::Nuller::new(context.child("byzantine_engine"), cfg);
5208                    engine.start(pending);
5209                } else {
5210                    reporters.push(reporter.clone());
5211                    let application_cfg = mocks::application::Config::<Sha256, _> {
5212                        relay: relay.clone(),
5213                        me: validator.clone(),
5214                        propose_latency: (10.0, 5.0),
5215                        verify_latency: (10.0, 5.0),
5216                        certify_latency: (10.0, 5.0),
5217                        should_certify: mocks::application::Certifier::Always,
5218                    };
5219                    let (actor, application) = mocks::application::Application::new(
5220                        context.child("application"),
5221                        application_cfg,
5222                    );
5223                    actor.start();
5224                    let blocker = oracle.control(validator.clone());
5225                    let cfg = config::Config {
5226                        scheme: schemes[idx_scheme].clone(),
5227                        elector: elector.clone(),
5228                        blocker,
5229                        automaton: application.clone(),
5230                        relay: application.clone(),
5231                        reporter: reporter.clone(),
5232                        strategy: Sequential,
5233                        partition: validator.clone().to_string(),
5234                        mailbox_size: NZUsize!(1024),
5235                        epoch: Epoch::new(333),
5236                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
5237                            Epoch::new(333),
5238                        )),
5239                        leader_timeout: Duration::from_secs(1),
5240                        certification_timeout: Duration::from_secs(2),
5241                        timeout_retry: Duration::from_secs(10),
5242                        fetch_timeout: Duration::from_secs(1),
5243                        view_retention,
5244                        skip: SkipPolicy::Enabled {
5245                            timeout: skip_timeout,
5246                            budget: SkipBudget::Participants,
5247                        },
5248                        replay_buffer: NZUsize!(1024 * 1024),
5249                        write_buffer: NZUsize!(1024 * 1024),
5250                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
5251                        forward: ForwardPolicy::Disabled,
5252                        track_historical_votes: true,
5253                    };
5254                    let engine = Engine::new(context.child("engine"), cfg);
5255                    engine.start(pending, recovered, resolver);
5256                }
5257            }
5258
5259            // Wait for all engines to finish
5260            let mut finalizers = Vec::new();
5261            for reporter in reporters.iter_mut() {
5262                let (mut latest, mut monitor) = reporter.subscribe().await;
5263                finalizers.push(context.child("finalizer").spawn(move |_| async move {
5264                    while latest < required_containers {
5265                        latest = monitor.recv().await.expect("event missing");
5266                    }
5267                }));
5268            }
5269            join_all(finalizers).await;
5270
5271            // Check reporters for correct activity
5272            let byz = &participants[0];
5273            let mut count_nullify_and_finalize = 0;
5274            for reporter in reporters.iter() {
5275                // Ensure only faults for byz
5276                {
5277                    let faults = reporter.faults.lock();
5278                    assert_eq!(faults.len(), 1);
5279                    let faulter = faults.get(byz).expect("byzantine party is not faulter");
5280                    for faults in faulter.values() {
5281                        for fault in faults.iter() {
5282                            match fault {
5283                                Activity::NullifyFinalize(_) => {
5284                                    count_nullify_and_finalize += 1;
5285                                }
5286                                _ => panic!("unexpected fault: {fault:?}"),
5287                            }
5288                        }
5289                    }
5290                }
5291
5292                // Ensure no invalid signatures
5293                reporter.assert_no_invalid();
5294            }
5295            assert!(count_nullify_and_finalize > 0);
5296
5297            // Ensure nullifier is blocked
5298            let blocked = oracle.blocked().await.unwrap();
5299            assert!(!blocked.is_empty());
5300            for (a, b) in blocked {
5301                assert_ne!(&a, byz);
5302                assert_eq!(&b, byz);
5303            }
5304        });
5305    }
5306
5307    test_for_all_fixtures!(nuller, seeds = 5);
5308
5309    fn outdated<S, F, L>(seed: u64, mut fixture: F, elector: L)
5310    where
5311        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
5312        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5313        L: elector::Config<S>,
5314    {
5315        // Create context
5316        let n = 4;
5317        let required_containers = View::new(100);
5318        let view_retention = ViewDelta::new(10);
5319        let skip_timeout = Duration::from_secs(11);
5320        let namespace = b"consensus".to_vec();
5321        let cfg = deterministic::Config::new()
5322            .with_seed(seed)
5323            .with_timeout(Some(Duration::from_secs(60)));
5324        let executor = deterministic::Runner::new(cfg);
5325        executor.start(|mut context| async move {
5326            // Register participants
5327            let Fixture {
5328                participants,
5329                schemes,
5330                ..
5331            } = fixture(&mut context, &namespace, n);
5332            let mut oracle =
5333                start_test_network_with_peers(context.child("network"), participants.clone(), true)
5334                    .await;
5335            let mut registrations = register_validators(&mut oracle, &participants).await;
5336
5337            // Link all validators
5338            let link = Link {
5339                latency: Duration::from_millis(10),
5340                jitter: Duration::from_millis(1),
5341                success_rate: probability!(1.0),
5342            };
5343            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
5344
5345            // Create engines
5346            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
5347            let mut reporters = Vec::new();
5348            for (idx_scheme, validator) in participants.iter().enumerate() {
5349                // Create scheme context
5350                let context = context
5351                    .child("validator")
5352                    .with_attribute("public_key", validator);
5353
5354                // Start engine
5355                let reporter_config = mocks::reporter::Config {
5356                    participants: participants.clone().try_into().unwrap(),
5357                    scheme: schemes[idx_scheme].clone(),
5358                    elector: elector.clone(),
5359                };
5360                let reporter =
5361                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
5362                let (pending, recovered, resolver) = registrations
5363                    .remove(validator)
5364                    .expect("validator should be registered");
5365                if idx_scheme == 0 {
5366                    let cfg = mocks::outdated::Config {
5367                        scheme: schemes[idx_scheme].clone(),
5368                        view_delta: ViewDelta::new(view_retention.get().saturating_mul(4)),
5369                    };
5370                    let engine: mocks::outdated::Outdated<_, _, Sha256> =
5371                        mocks::outdated::Outdated::new(context.child("byzantine_engine"), cfg);
5372                    engine.start(pending);
5373                } else {
5374                    reporters.push(reporter.clone());
5375                    let application_cfg = mocks::application::Config::<Sha256, _> {
5376                        relay: relay.clone(),
5377                        me: validator.clone(),
5378                        propose_latency: (10.0, 5.0),
5379                        verify_latency: (10.0, 5.0),
5380                        certify_latency: (10.0, 5.0),
5381                        should_certify: mocks::application::Certifier::Always,
5382                    };
5383                    let (actor, application) = mocks::application::Application::new(
5384                        context.child("application"),
5385                        application_cfg,
5386                    );
5387                    actor.start();
5388                    let blocker = oracle.control(validator.clone());
5389                    let cfg = config::Config {
5390                        scheme: schemes[idx_scheme].clone(),
5391                        elector: elector.clone(),
5392                        blocker,
5393                        automaton: application.clone(),
5394                        relay: application.clone(),
5395                        reporter: reporter.clone(),
5396                        strategy: Sequential,
5397                        partition: validator.clone().to_string(),
5398                        mailbox_size: NZUsize!(1024),
5399                        epoch: Epoch::new(333),
5400                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
5401                            Epoch::new(333),
5402                        )),
5403                        leader_timeout: Duration::from_secs(1),
5404                        certification_timeout: Duration::from_secs(2),
5405                        timeout_retry: Duration::from_secs(10),
5406                        fetch_timeout: Duration::from_secs(1),
5407                        view_retention,
5408                        skip: SkipPolicy::Enabled {
5409                            timeout: skip_timeout,
5410                            budget: SkipBudget::Participants,
5411                        },
5412                        replay_buffer: NZUsize!(1024 * 1024),
5413                        write_buffer: NZUsize!(1024 * 1024),
5414                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
5415                        forward: ForwardPolicy::Disabled,
5416                        track_historical_votes: false,
5417                    };
5418                    let engine = Engine::new(context.child("engine"), cfg);
5419                    engine.start(pending, recovered, resolver);
5420                }
5421            }
5422
5423            // Wait for all engines to finish
5424            let mut finalizers = Vec::new();
5425            for reporter in reporters.iter_mut() {
5426                let (mut latest, mut monitor) = reporter.subscribe().await;
5427                finalizers.push(context.child("finalizer").spawn(move |_| async move {
5428                    while latest < required_containers {
5429                        latest = monitor.recv().await.expect("event missing");
5430                    }
5431                }));
5432            }
5433            join_all(finalizers).await;
5434
5435            // Check reporters for correct activity
5436            for reporter in reporters.iter() {
5437                // Ensure no faults
5438                reporter.assert_no_faults();
5439
5440                // Ensure no invalid signatures
5441                reporter.assert_no_invalid();
5442            }
5443
5444            // Ensure no blocked connections
5445            let blocked = oracle.blocked().await.unwrap();
5446            assert!(blocked.is_empty());
5447        });
5448    }
5449
5450    test_for_all_fixtures!(outdated, seeds = 5);
5451
5452    fn run_1k<S, F, L>(mut fixture: F, elector: L)
5453    where
5454        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
5455        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5456        L: elector::Config<S>,
5457    {
5458        // Create context
5459        let n = 10;
5460        let required_containers = View::new(1_000);
5461        let view_retention = ViewDelta::new(10);
5462        let skip_timeout = Duration::from_secs(11);
5463        let namespace = b"consensus".to_vec();
5464        let cfg = deterministic::Config::new();
5465        let executor = deterministic::Runner::new(cfg);
5466        executor.start(|mut context| async move {
5467            // Register participants
5468            let Fixture {
5469                participants,
5470                schemes,
5471                ..
5472            } = fixture(&mut context, &namespace, n);
5473            let mut oracle =
5474                start_test_network_with_peers(context.child("network"), participants.clone(), true)
5475                    .await;
5476            let mut registrations = register_validators(&mut oracle, &participants).await;
5477
5478            // Link all validators
5479            let link = Link {
5480                latency: Duration::from_millis(80),
5481                jitter: Duration::from_millis(10),
5482                success_rate: probability!(0.98),
5483            };
5484            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
5485
5486            // Create engines
5487            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
5488            let mut reporters = Vec::new();
5489            let mut engine_handlers = Vec::new();
5490            for (idx, validator) in participants.iter().enumerate() {
5491                // Create scheme context
5492                let context = context
5493                    .child("validator")
5494                    .with_attribute("public_key", validator);
5495
5496                // Configure engine
5497                let reporter_config = mocks::reporter::Config {
5498                    participants: participants.clone().try_into().unwrap(),
5499                    scheme: schemes[idx].clone(),
5500                    elector: elector.clone(),
5501                };
5502                let reporter =
5503                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
5504                reporters.push(reporter.clone());
5505                let application_cfg = mocks::application::Config::<Sha256, _> {
5506                    relay: relay.clone(),
5507                    me: validator.clone(),
5508                    propose_latency: (100.0, 50.0),
5509                    verify_latency: (50.0, 40.0),
5510                    certify_latency: (50.0, 40.0),
5511                    should_certify: mocks::application::Certifier::Always,
5512                };
5513                let (actor, application) = mocks::application::Application::new(
5514                    context.child("application"),
5515                    application_cfg,
5516                );
5517                actor.start();
5518                let blocker = oracle.control(validator.clone());
5519                let cfg = config::Config {
5520                    scheme: schemes[idx].clone(),
5521                    elector: elector.clone(),
5522                    blocker,
5523                    automaton: application.clone(),
5524                    relay: application.clone(),
5525                    reporter: reporter.clone(),
5526                    strategy: Sequential,
5527                    partition: validator.to_string(),
5528                    mailbox_size: NZUsize!(1024),
5529                    epoch: Epoch::new(333),
5530                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
5531                        Epoch::new(333),
5532                    )),
5533                    leader_timeout: Duration::from_secs(1),
5534                    certification_timeout: Duration::from_secs(2),
5535                    timeout_retry: Duration::from_secs(10),
5536                    fetch_timeout: Duration::from_secs(1),
5537                    view_retention,
5538                    skip: SkipPolicy::Enabled {
5539                        timeout: skip_timeout,
5540                        budget: SkipBudget::Participants,
5541                    },
5542                    replay_buffer: NZUsize!(1024 * 1024),
5543                    write_buffer: NZUsize!(1024 * 1024),
5544                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
5545                    forward: ForwardPolicy::Disabled,
5546                    track_historical_votes: false,
5547                };
5548                let engine = Engine::new(context.child("engine"), cfg);
5549
5550                // Start engine
5551                let (pending, recovered, resolver) = registrations
5552                    .remove(validator)
5553                    .expect("validator should be registered");
5554                engine_handlers.push(engine.start(pending, recovered, resolver));
5555            }
5556
5557            // Wait for all engines to finish
5558            let mut finalizers = Vec::new();
5559            for reporter in reporters.iter_mut() {
5560                let (mut latest, mut monitor) = reporter.subscribe().await;
5561                finalizers.push(context.child("finalizer").spawn(move |_| async move {
5562                    while latest < required_containers {
5563                        latest = monitor.recv().await.expect("event missing");
5564                    }
5565                }));
5566            }
5567            join_all(finalizers).await;
5568
5569            // Check reporters for correct activity
5570            for reporter in reporters.iter() {
5571                // Ensure no faults
5572                reporter.assert_no_faults();
5573
5574                // Ensure no invalid signatures
5575                reporter.assert_no_invalid();
5576            }
5577
5578            // Ensure no blocked connections
5579            let blocked = oracle.blocked().await.unwrap();
5580            assert!(blocked.is_empty());
5581        })
5582    }
5583
5584    #[test_group("slow")]
5585    #[test_traced]
5586    fn test_1k() {
5587        run_1k::<_, _, RoundRobin>(scheme_mocks::fixture, RoundRobin::default());
5588    }
5589
5590    fn engine_shutdown<S, F, L>(seed: u64, mut fixture: F, elector: L, graceful: bool)
5591    where
5592        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
5593        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5594        L: elector::Config<S>,
5595    {
5596        let n = 1;
5597        let namespace = b"consensus".to_vec();
5598        let cfg = deterministic::Config::default()
5599            .with_seed(seed)
5600            .with_timeout(Some(Duration::from_secs(10)));
5601        let executor = deterministic::Runner::new(cfg);
5602        executor.start(|mut context| async move {
5603            // Register a single participant
5604            let Fixture {
5605                participants,
5606                schemes,
5607                ..
5608            } = fixture(&mut context, &namespace, n);
5609            let mut oracle =
5610                start_test_network_with_peers(context.child("network"), participants.clone(), true)
5611                    .await;
5612            let mut registrations = register_validators(&mut oracle, &participants).await;
5613
5614            // Link the single validator to itself (no-ops for completeness)
5615            let link = Link {
5616                latency: Duration::from_millis(1),
5617                jitter: Duration::from_millis(0),
5618                success_rate: probability!(1.0),
5619            };
5620            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
5621
5622            // Create engine
5623            let reporter_config = mocks::reporter::Config {
5624                participants: participants.clone().try_into().unwrap(),
5625                scheme: schemes[0].clone(),
5626                elector: elector.clone(),
5627            };
5628            let reporter =
5629                mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
5630            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
5631            let application_cfg = mocks::application::Config::<Sha256, _> {
5632                relay: relay.clone(),
5633                me: participants[0].clone(),
5634                propose_latency: (1.0, 0.0),
5635                verify_latency: (1.0, 0.0),
5636                certify_latency: (1.0, 0.0),
5637                should_certify: mocks::application::Certifier::Always,
5638            };
5639            let (actor, application) =
5640                mocks::application::Application::new(context.child("application"), application_cfg);
5641            actor.start();
5642            let blocker = oracle.control(participants[0].clone());
5643            let cfg = config::Config {
5644                scheme: schemes[0].clone(),
5645                elector: elector.clone(),
5646                blocker,
5647                automaton: application.clone(),
5648                relay: application.clone(),
5649                reporter: reporter.clone(),
5650                strategy: Sequential,
5651                partition: participants[0].clone().to_string(),
5652                mailbox_size: NZUsize!(64),
5653                epoch: Epoch::new(333),
5654                floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(Epoch::new(
5655                    333,
5656                ))),
5657                leader_timeout: Duration::from_millis(50),
5658                certification_timeout: Duration::from_millis(100),
5659                timeout_retry: Duration::from_millis(250),
5660                fetch_timeout: Duration::from_millis(50),
5661                view_retention: ViewDelta::new(4),
5662                skip: SkipPolicy::Enabled {
5663                    timeout: Duration::from_secs(2),
5664                    budget: SkipBudget::Participants,
5665                },
5666                replay_buffer: NZUsize!(1024 * 16),
5667                write_buffer: NZUsize!(1024 * 16),
5668                page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
5669                forward: ForwardPolicy::Disabled,
5670                track_historical_votes: false,
5671            };
5672            let engine = Engine::new(context.child("engine"), cfg);
5673
5674            // Start engine
5675            let (pending, recovered, resolver) = registrations
5676                .remove(&participants[0])
5677                .expect("validator should be registered");
5678            let handle = engine.start(pending, recovered, resolver);
5679
5680            // Allow tasks to start
5681            context.sleep(Duration::from_millis(1000)).await;
5682
5683            // Count running tasks under the engine prefix
5684            let running_before = count_running_tasks(&context, "engine");
5685            assert!(
5686                running_before > 0,
5687                "at least one engine task should be running"
5688            );
5689
5690            // Make sure the engine is still running after some time
5691            context.sleep(Duration::from_millis(1500)).await;
5692            assert!(
5693                count_running_tasks(&context, "engine") > 0,
5694                "engine tasks should still be running"
5695            );
5696
5697            // Shutdown engine and ensure children stop
5698            let running_after = if graceful {
5699                let result = context
5700                    .child("stop")
5701                    .stop(0, Some(Duration::from_secs(5)))
5702                    .await;
5703                assert!(
5704                    result.is_ok(),
5705                    "graceful shutdown should complete: {result:?}"
5706                );
5707                count_running_tasks(&context, "engine")
5708            } else {
5709                handle.abort();
5710                let _ = handle.await; // ensure parent tear-down runs
5711
5712                // Give the runtime a tick to process aborts
5713                context.sleep(Duration::from_millis(1000)).await;
5714                count_running_tasks(&context, "engine")
5715            };
5716            assert_eq!(
5717                running_after, 0,
5718                "all engine tasks should be stopped, but {running_after} still running"
5719            );
5720        });
5721    }
5722
5723    fn children_shutdown_on_engine_abort<S, F, L>(seed: u64, fixture: F, elector: L)
5724    where
5725        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
5726        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5727        L: elector::Config<S>,
5728    {
5729        engine_shutdown::<S, F, L>(seed, fixture, elector, false);
5730    }
5731
5732    test_for_all_fixtures!(children_shutdown_on_engine_abort, seeds = 10);
5733
5734    fn graceful_shutdown<S, F, L>(seed: u64, fixture: F, elector: L)
5735    where
5736        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
5737        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5738        L: elector::Config<S>,
5739    {
5740        engine_shutdown::<S, F, L>(seed, fixture, elector, true);
5741    }
5742
5743    test_for_all_fixtures!(graceful_shutdown, seeds = 10);
5744
5745    fn attributable_reporter_filtering<S, F, L>(mut fixture: F, elector: L)
5746    where
5747        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
5748        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5749        L: elector::Config<S>,
5750    {
5751        let n = 3;
5752        let required_containers = View::new(10);
5753        let view_retention = ViewDelta::new(10);
5754        let skip_timeout = Duration::from_secs(11);
5755        let namespace = b"consensus".to_vec();
5756        let executor = deterministic::Runner::timed(Duration::from_secs(30));
5757        executor.start(|mut context| async move {
5758            // Register participants
5759            let Fixture {
5760                participants,
5761                schemes,
5762                ..
5763            } = fixture(&mut context, &namespace, n);
5764            let mut oracle = start_test_network_with_peers(
5765                context.child("network"),
5766                participants.clone(),
5767                false,
5768            )
5769            .await;
5770            let mut registrations = register_validators(&mut oracle, &participants).await;
5771
5772            // Link all validators
5773            let link = Link {
5774                latency: Duration::from_millis(10),
5775                jitter: Duration::from_millis(1),
5776                success_rate: probability!(1.0),
5777            };
5778            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
5779
5780            // Create engines with `AttributableReporter` wrapper
5781            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
5782            let mut reporters = Vec::new();
5783            for (idx, validator) in participants.iter().enumerate() {
5784                let context = context
5785                    .child("validator")
5786                    .with_attribute("public_key", validator);
5787
5788                let reporter_config = mocks::reporter::Config {
5789                    participants: participants.clone().try_into().unwrap(),
5790                    scheme: schemes[idx].clone(),
5791                    elector: elector.clone(),
5792                };
5793                let mock_reporter =
5794                    mocks::reporter::Reporter::new(context.child("mock_reporter"), reporter_config);
5795
5796                // Wrap with `AttributableReporter`
5797                let attributable_reporter = scheme::reporter::AttributableReporter::new(
5798                    context.child("rng"),
5799                    schemes[idx].clone(),
5800                    mock_reporter.clone(),
5801                    Sequential,
5802                    true, // Enable verification
5803                );
5804                reporters.push(mock_reporter.clone());
5805
5806                let application_cfg = mocks::application::Config::<Sha256, _> {
5807                    relay: relay.clone(),
5808                    me: validator.clone(),
5809                    propose_latency: (10.0, 5.0),
5810                    verify_latency: (10.0, 5.0),
5811                    certify_latency: (10.0, 5.0),
5812                    should_certify: mocks::application::Certifier::Always,
5813                };
5814                let (actor, application) = mocks::application::Application::new(
5815                    context.child("application"),
5816                    application_cfg,
5817                );
5818                actor.start();
5819                let blocker = oracle.control(validator.clone());
5820                let cfg = config::Config {
5821                    scheme: schemes[idx].clone(),
5822                    elector: elector.clone(),
5823                    blocker,
5824                    automaton: application.clone(),
5825                    relay: application.clone(),
5826                    reporter: attributable_reporter,
5827                    strategy: Sequential,
5828                    partition: validator.to_string(),
5829                    mailbox_size: NZUsize!(1024),
5830                    epoch: Epoch::new(333),
5831                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
5832                        Epoch::new(333),
5833                    )),
5834                    leader_timeout: Duration::from_secs(1),
5835                    certification_timeout: Duration::from_secs(2),
5836                    timeout_retry: Duration::from_secs(10),
5837                    fetch_timeout: Duration::from_secs(1),
5838                    view_retention,
5839                    skip: SkipPolicy::Enabled {
5840                        timeout: skip_timeout,
5841                        budget: SkipBudget::Participants,
5842                    },
5843                    replay_buffer: NZUsize!(1024 * 1024),
5844                    write_buffer: NZUsize!(1024 * 1024),
5845                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
5846                    forward: ForwardPolicy::Disabled,
5847                    track_historical_votes: false,
5848                };
5849                let engine = Engine::new(context.child("engine"), cfg);
5850
5851                // Start engine
5852                let (pending, recovered, resolver) = registrations
5853                    .remove(validator)
5854                    .expect("validator should be registered");
5855                engine.start(pending, recovered, resolver);
5856            }
5857
5858            // Wait for all engines to finish
5859            let mut finalizers = Vec::new();
5860            for reporter in reporters.iter_mut() {
5861                let (mut latest, mut monitor) = reporter.subscribe().await;
5862                finalizers.push(context.child("finalizer").spawn(move |_| async move {
5863                    while latest < required_containers {
5864                        latest = monitor.recv().await.expect("event missing");
5865                    }
5866                }));
5867            }
5868            join_all(finalizers).await;
5869
5870            // Verify filtering behavior based on scheme attributability
5871            for reporter in reporters.iter() {
5872                // Ensure no faults (normal operation)
5873                reporter.assert_no_faults();
5874
5875                // Ensure no invalid signatures
5876                reporter.assert_no_invalid();
5877
5878                // Check that we have certificates reported
5879                {
5880                    let notarizations = reporter.notarizations.lock();
5881                    let finalizations = reporter.finalizations.lock();
5882                    assert!(
5883                        !notarizations.is_empty() || !finalizations.is_empty(),
5884                        "Certificates should be reported"
5885                    );
5886                }
5887
5888                // Check notarizes
5889                let notarizes = reporter.notarizes.lock();
5890                let last_view = notarizes.keys().max().cloned().unwrap_or_default();
5891                for (view, payloads) in notarizes.iter() {
5892                    if *view == last_view {
5893                        continue; // Skip last view
5894                    }
5895
5896                    let signers: usize = payloads.values().map(|signers| signers.len()).sum();
5897
5898                    // For attributable schemes, we should see peer activities
5899                    if S::is_attributable() {
5900                        assert!(signers > 1, "view {view}: {signers}");
5901                    } else {
5902                        // For non-attributable, we shouldn't see any peer activities
5903                        assert_eq!(signers, 0);
5904                    }
5905                }
5906
5907                // Check finalizes
5908                let finalizes = reporter.finalizes.lock();
5909                for payloads in finalizes.values() {
5910                    let signers: usize = payloads.values().map(|signers| signers.len()).sum();
5911
5912                    // For attributable schemes, we should see peer activities
5913                    if S::is_attributable() {
5914                        assert!(signers > 1);
5915                    } else {
5916                        // For non-attributable, we shouldn't see any peer activities
5917                        assert_eq!(signers, 0);
5918                    }
5919                }
5920            }
5921
5922            // Ensure no blocked connections (normal operation)
5923            let blocked = oracle.blocked().await.unwrap();
5924            assert!(blocked.is_empty());
5925        });
5926    }
5927
5928    test_for_all_fixtures!(attributable_reporter_filtering);
5929
5930    fn split_views_no_lockup<S, F, L>(mut fixture: F, elector: L)
5931    where
5932        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
5933        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
5934        L: elector::Config<S>,
5935    {
5936        // Scenario:
5937        // - View F: Finalization of B_1 seen by all participants.
5938        // - View F+1:
5939        //   - Nullification seen by honest (4..=6,7) and all 3 byzantines
5940        //   - Notarization of B_2A seen by honest (1..=3)
5941        // - View F+2:
5942        //   - Nullification seen by honest (1..=3,7) and all 3 byzantines
5943        //   - Notarization of B_2B seen by honest (4..=6)
5944        // - View F+3: Nullification. Seen by all participants.
5945        // - Then ensure progress resumes beyond F+3 after reconnecting
5946
5947        // Define participant types
5948        enum ParticipantType {
5949            Group1,    // receives notarization for f+1, nullification for f+2
5950            Group2,    // receives nullification for f+1, notarization for f+2
5951            Ignorant,  // receives nullification for f+1 and f+2
5952            Byzantine, // nullify-only
5953        }
5954        let get_type = |idx: usize| -> ParticipantType {
5955            match idx {
5956                0..3 => ParticipantType::Group1,
5957                3..6 => ParticipantType::Group2,
5958                6 => ParticipantType::Ignorant,
5959                7..10 => ParticipantType::Byzantine,
5960                _ => unreachable!(),
5961            }
5962        };
5963
5964        // Create context
5965        let n = 10;
5966        let quorum = quorum(n) as usize;
5967        assert_eq!(quorum, 7);
5968        let view_retention = ViewDelta::new(10);
5969        let skip_timeout = Duration::from_secs(12);
5970        let namespace = b"consensus".to_vec();
5971        let executor = deterministic::Runner::timed(Duration::from_secs(300));
5972        executor.start(|mut context| async move {
5973            // Register participants
5974            let Fixture {
5975                participants,
5976                schemes,
5977                ..
5978            } = fixture(&mut context, &namespace, n);
5979            let mut oracle = start_test_network_with_peers(
5980                context.child("network"),
5981                participants.clone(),
5982                false,
5983            )
5984            .await;
5985            let mut registrations = register_validators(&mut oracle, &participants).await;
5986
5987            // ========== Build the certificates manually ==========
5988
5989            // Helper: assemble finalization from explicit signer indices
5990            let build_finalization = |proposal: &Proposal<D>| -> TFinalization<_, D> {
5991                let votes: Vec<_> = (0..=quorum)
5992                    .map(|i| TFinalize::sign(&schemes[i], proposal.clone()).unwrap())
5993                    .collect();
5994                TFinalization::from_finalizes(&schemes[0], non_empty![@&votes], &Sequential)
5995                    .expect("finalization quorum")
5996            };
5997            // Helper: assemble notarization from explicit signer indices
5998            let build_notarization = |proposal: &Proposal<D>| -> TNotarization<_, D> {
5999                let votes: Vec<_> = (0..=quorum)
6000                    .map(|i| TNotarize::sign(&schemes[i], proposal.clone()).unwrap())
6001                    .collect();
6002                TNotarization::from_notarizes(&schemes[0], non_empty![@&votes], &Sequential)
6003                    .expect("notarization quorum")
6004            };
6005            let build_nullification = |round: Round| -> TNullification<_> {
6006                let votes: Vec<_> = (0..=quorum)
6007                    .map(|i| TNullify::sign::<D>(&schemes[i], round).unwrap())
6008                    .collect();
6009                TNullification::from_nullifies(&schemes[0], non_empty![@&votes], &Sequential)
6010                    .expect("nullification quorum")
6011            };
6012            // Choose F=1 and construct B_1, B_2A, B_2B
6013            let f_view = 1;
6014            let round_f = Round::new(Epoch::new(333), View::new(f_view));
6015            let payload_b0 = Sha256::hash(&[b"B_F"]);
6016            let proposal_b0 = Proposal::new(round_f, View::new(f_view - 1), payload_b0);
6017            let payload_b1a = Sha256::hash(&[b"B_G1"]);
6018            let proposal_b1a = Proposal::new(
6019                Round::new(Epoch::new(333), View::new(f_view + 1)),
6020                View::new(f_view),
6021                payload_b1a,
6022            );
6023            let payload_b1b = Sha256::hash(&[b"B_G2"]);
6024            let proposal_b1b = Proposal::new(
6025                Round::new(Epoch::new(333), View::new(f_view + 2)),
6026                View::new(f_view),
6027                payload_b1b,
6028            );
6029
6030            // Build notarization and finalization for the first block
6031            let b0_notarization = build_notarization(&proposal_b0);
6032            let b0_finalization = build_finalization(&proposal_b0);
6033            // Build notarizations for F+1 and F+2
6034            let b1a_notarization = build_notarization(&proposal_b1a);
6035            let b1b_notarization = build_notarization(&proposal_b1b);
6036            // Build nullifications for F+1 and F+2
6037            let null_a = build_nullification(Round::new(Epoch::new(333), View::new(f_view + 1)));
6038            let null_b = build_nullification(Round::new(Epoch::new(333), View::new(f_view + 2)));
6039
6040            // Create an 11th non-participant injector with one-way links to all participants
6041            let link = Link {
6042                latency: Duration::from_millis(10),
6043                jitter: Duration::from_millis(0),
6044                success_rate: probability!(1.0),
6045            };
6046            let mut injector_sender =
6047                start_certificate_injector(&context, &mut oracle, &participants, &link).await;
6048
6049            // ========== Broadcast certificates over recovered network. ==========
6050
6051            // View F:
6052            let msg = Certificate::<_, D>::Notarization(b0_notarization).encode();
6053            injector_sender.send(Recipients::All, msg, true);
6054            let msg = Certificate::<_, D>::Finalization(b0_finalization).encode();
6055            injector_sender.send(Recipients::All, msg, true);
6056            // View F+1:
6057            let notarization_msg = Certificate::<_, D>::Notarization(b1a_notarization);
6058            let nullification_msg = Certificate::<_, D>::Nullification(null_a.clone());
6059            for (i, participant) in participants.iter().enumerate() {
6060                let recipient = Recipients::One(participant.clone());
6061                let msg = match get_type(i) {
6062                    ParticipantType::Group1 => notarization_msg.encode(),
6063                    _ => nullification_msg.encode(),
6064                };
6065                injector_sender.send(recipient, msg, true);
6066            }
6067            // View F+2:
6068            let notarization_msg = Certificate::<_, D>::Notarization(b1b_notarization);
6069            let nullification_msg = Certificate::<_, D>::Nullification(null_b.clone());
6070            for (i, participant) in participants.iter().enumerate() {
6071                let recipient = Recipients::One(participant.clone());
6072                let msg = match get_type(i) {
6073                    ParticipantType::Group2 => notarization_msg.encode(),
6074                    _ => nullification_msg.encode(),
6075                };
6076                injector_sender.send(recipient, msg, true);
6077            }
6078
6079            // ========== Create engines ==========
6080
6081            // Start engines after preloading certificates into each participant's
6082            // recovered channel (ensuring processing before any leader attempts to issue a
6083            // conflicting vote).
6084            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
6085            let mut honest_reporters = Vec::new();
6086            for (idx, validator) in participants.iter().enumerate() {
6087                let (pending, recovered, resolver) = registrations
6088                    .remove(validator)
6089                    .expect("validator should be registered");
6090                let participant_type = get_type(idx);
6091                if matches!(participant_type, ParticipantType::Byzantine) {
6092                    // Byzantine engines
6093                    let cfg = mocks::nullify_only::Config {
6094                        scheme: schemes[idx].clone(),
6095                    };
6096                    let engine: mocks::nullify_only::NullifyOnly<_, _, Sha256> =
6097                        mocks::nullify_only::NullifyOnly::new(
6098                            context
6099                                .child("byzantine")
6100                                .with_attribute("public_key", validator),
6101                            cfg,
6102                        );
6103                    engine.start(pending);
6104                    // Recovered/resolver channels are unused for byzantine actors.
6105                    drop(recovered);
6106                    drop(resolver);
6107                } else {
6108                    // Honest engines
6109                    let reporter_config = mocks::reporter::Config {
6110                        participants: participants.clone().try_into().unwrap(),
6111                        scheme: schemes[idx].clone(),
6112                        elector: elector.clone(),
6113                    };
6114                    let reporter = mocks::reporter::Reporter::new(
6115                        context
6116                            .child("reporter")
6117                            .with_attribute("public_key", validator),
6118                        reporter_config,
6119                    );
6120                    honest_reporters.push(reporter.clone());
6121
6122                    let application_cfg = mocks::application::Config::<Sha256, _> {
6123                        relay: relay.clone(),
6124                        me: validator.clone(),
6125                        propose_latency: (250.0, 50.0), // ensure we process certificates first
6126                        verify_latency: (10.0, 5.0),
6127                        certify_latency: (10.0, 5.0),
6128                        should_certify: mocks::application::Certifier::Always,
6129                    };
6130                    let (actor, application) = mocks::application::Application::new(
6131                        context
6132                            .child("application")
6133                            .with_attribute("public_key", validator),
6134                        application_cfg,
6135                    );
6136                    actor.start();
6137                    let blocker = oracle.control(validator.clone());
6138                    let cfg = config::Config {
6139                        scheme: schemes[idx].clone(),
6140                        elector: elector.clone(),
6141                        blocker,
6142                        automaton: application.clone(),
6143                        relay: application.clone(),
6144                        reporter: reporter.clone(),
6145                        strategy: Sequential,
6146                        partition: validator.to_string(),
6147                        mailbox_size: NZUsize!(1024),
6148                        epoch: Epoch::new(333),
6149                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
6150                            Epoch::new(333),
6151                        )),
6152                        leader_timeout: Duration::from_secs(10),
6153                        certification_timeout: Duration::from_secs(11),
6154                        timeout_retry: Duration::from_secs(10),
6155                        fetch_timeout: Duration::from_secs(1),
6156                        view_retention,
6157                        skip: SkipPolicy::Enabled {
6158                            timeout: skip_timeout,
6159                            budget: SkipBudget::Participants,
6160                        },
6161                        replay_buffer: NZUsize!(1024 * 1024),
6162                        write_buffer: NZUsize!(1024 * 1024),
6163                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
6164                        forward: ForwardPolicy::Disabled,
6165                        track_historical_votes: false,
6166                    };
6167                    let engine = Engine::new(
6168                        context
6169                            .child("engine")
6170                            .with_attribute("public_key", validator),
6171                        cfg,
6172                    );
6173                    engine.start(pending, recovered, resolver);
6174                }
6175            }
6176
6177            // Allow started engines to consume preloaded certificates.
6178            context.sleep(Duration::from_secs(2)).await;
6179
6180            // ========== Assert the exact certificates are seen in each view ==========
6181
6182            // Assert the exact certificates in view F
6183            // All participants should have finalized B_0
6184            let view = View::new(f_view);
6185            for reporter in honest_reporters.iter() {
6186                let finalizations = reporter.finalizations.lock();
6187                assert!(finalizations.contains_key(&view));
6188            }
6189
6190            // Assert the exact certificates in view F+1
6191            // Group 1 should have notarized B_1A only
6192            // All other participants should have nullified F+1
6193            let view = View::new(f_view + 1);
6194            for (i, reporter) in honest_reporters.iter().enumerate() {
6195                let finalizations = reporter.finalizations.lock();
6196                assert!(!finalizations.contains_key(&view));
6197                let nullifications = reporter.nullifications.lock();
6198                let notarizations = reporter.notarizations.lock();
6199                match get_type(i) {
6200                    ParticipantType::Group1 => {
6201                        assert!(notarizations.contains_key(&view));
6202                        assert!(!nullifications.contains_key(&view));
6203                    }
6204                    _ => {
6205                        assert!(nullifications.contains_key(&view));
6206                        assert!(!notarizations.contains_key(&view));
6207                    }
6208                }
6209            }
6210
6211            // Assert the exact certificates in view F+2
6212            // Group 2 should have notarized B_1B only
6213            // All other participants should have nullified F+2
6214            let view = View::new(f_view + 2);
6215            for (i, reporter) in honest_reporters.iter().enumerate() {
6216                let finalizations = reporter.finalizations.lock();
6217                assert!(!finalizations.contains_key(&view));
6218                let nullifications = reporter.nullifications.lock();
6219                let notarizations = reporter.notarizations.lock();
6220                match get_type(i) {
6221                    ParticipantType::Group2 => {
6222                        assert!(notarizations.contains_key(&view));
6223                        assert!(!nullifications.contains_key(&view));
6224                    }
6225                    _ => {
6226                        assert!(nullifications.contains_key(&view));
6227                        assert!(!notarizations.contains_key(&view));
6228                    }
6229                }
6230            }
6231
6232            // Assert no members have yet nullified view F+3
6233            let next_view = View::new(f_view + 3);
6234            for (i, reporter) in honest_reporters.iter().enumerate() {
6235                let nullifies = reporter.nullifies.lock();
6236                assert!(!nullifies.contains_key(&next_view), "reporter {i}");
6237            }
6238
6239            // ========== Reconnect all participants ==========
6240
6241            // Reconnect all participants fully using the helper
6242            link_validators(&mut oracle, &participants, Action::Link(link.clone()), None).await;
6243
6244            // Wait until all honest reporters finalize strictly past F+2 (e.g., at least F+3)
6245            {
6246                let target = View::new(f_view + 3);
6247                let mut finalizers = Vec::new();
6248                for reporter in honest_reporters.iter_mut() {
6249                    let (mut latest, mut monitor) = reporter.subscribe().await;
6250                    finalizers.push(
6251                        context
6252                            .child("resume_finalizer")
6253                            .spawn(move |_| async move {
6254                                while latest < target {
6255                                    latest = monitor.recv().await.expect("event missing");
6256                                }
6257                            }),
6258                    );
6259                }
6260                join_all(finalizers).await;
6261            }
6262
6263            // Sanity checks: no faults/invalid signatures, and no peers blocked
6264            for reporter in honest_reporters.iter() {
6265                reporter.assert_no_faults();
6266                reporter.assert_no_invalid();
6267            }
6268            let blocked = oracle.blocked().await.unwrap();
6269            assert!(blocked.is_empty(), "blocked peers: {blocked:?}");
6270        });
6271    }
6272
6273    test_for_all_fixtures!(split_views_no_lockup);
6274
6275    type CertifiedSplitReporter<S, L> =
6276        mocks::reporter::Reporter<deterministic::Context, S, L, Sha256Digest>;
6277
6278    struct CertifiedSplitEngineConfig<'a, S, L> {
6279        oracle: &'a Oracle<PublicKey, deterministic::Context>,
6280        participants: &'a [PublicKey],
6281        schemes: &'a [S],
6282        registrations: &'a mut TestRegistrations,
6283        silent: usize,
6284        elector: &'a L,
6285        epoch: Epoch,
6286        view_retention: ViewDelta,
6287        skip_timeout: Duration,
6288    }
6289
6290    /// Registers a non-committee peer that can preload certificates while
6291    /// validators are partitioned from one another.
6292    async fn start_certificate_injector(
6293        context: &deterministic::Context,
6294        oracle: &mut Oracle<PublicKey, deterministic::Context>,
6295        participants: &[PublicKey],
6296        link: &Link,
6297    ) -> Sender<PublicKey, deterministic::Context> {
6298        let injector = PrivateKey::from_seed(1_000_000).public_key();
6299        let (sender, _receiver) = oracle
6300            .control(injector.clone())
6301            .register(1, TEST_QUOTA)
6302            .await
6303            .unwrap();
6304        for participant in participants {
6305            oracle
6306                .add_link(injector.clone(), participant.clone(), link.clone())
6307                .await
6308                .unwrap();
6309        }
6310        oracle.manager().track(
6311            1,
6312            TrackedPeers::new(
6313                Set::from_iter_dedup(participants.iter().cloned()),
6314                Set::from_iter_dedup(std::iter::once(injector)),
6315            ),
6316        );
6317        context.sleep(Duration::from_millis(10)).await;
6318        sender
6319    }
6320
6321    /// Starts every validator except `silent`, whose network registration is
6322    /// dropped.
6323    fn start_certified_split_engines<S, L>(
6324        context: &deterministic::Context,
6325        cfg: CertifiedSplitEngineConfig<'_, S, L>,
6326    ) -> HashMap<usize, CertifiedSplitReporter<S, L>>
6327    where
6328        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
6329        L: elector::Config<S>,
6330    {
6331        let CertifiedSplitEngineConfig {
6332            oracle,
6333            participants,
6334            schemes,
6335            registrations,
6336            silent,
6337            elector,
6338            epoch,
6339            view_retention,
6340            skip_timeout,
6341        } = cfg;
6342        let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
6343        let mut reporters = HashMap::new();
6344
6345        for (idx, validator) in participants.iter().enumerate() {
6346            let registration = registrations
6347                .remove(validator)
6348                .expect("validator should be registered");
6349            if idx == silent {
6350                drop(registration);
6351                continue;
6352            }
6353
6354            let reporter_config = mocks::reporter::Config {
6355                participants: participants.to_vec().try_into().unwrap(),
6356                scheme: schemes[idx].clone(),
6357                elector: elector.clone(),
6358            };
6359            let reporter = mocks::reporter::Reporter::new(
6360                context
6361                    .child("reporter")
6362                    .with_attribute("public_key", validator),
6363                reporter_config,
6364            );
6365            reporters.insert(idx, reporter.clone());
6366
6367            let application_cfg = mocks::application::Config::<Sha256, _> {
6368                relay: relay.clone(),
6369                me: validator.clone(),
6370                propose_latency: (250.0, 50.0), // ensure we process certificates first
6371                verify_latency: (10.0, 5.0),
6372                certify_latency: (10.0, 5.0),
6373                should_certify: mocks::application::Certifier::Always,
6374            };
6375            let (actor, application) = mocks::application::Application::new(
6376                context
6377                    .child("application")
6378                    .with_attribute("public_key", validator),
6379                application_cfg,
6380            );
6381            actor.start();
6382
6383            let cfg = config::Config {
6384                scheme: schemes[idx].clone(),
6385                elector: elector.clone(),
6386                blocker: oracle.control(validator.clone()),
6387                automaton: application.clone(),
6388                relay: application.clone(),
6389                reporter: reporter.clone(),
6390                strategy: Sequential,
6391                partition: validator.to_string(),
6392                mailbox_size: NZUsize!(1024),
6393                epoch,
6394                floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(epoch)),
6395                leader_timeout: Duration::from_secs(10),
6396                certification_timeout: Duration::from_secs(11),
6397                timeout_retry: Duration::from_secs(10),
6398                fetch_timeout: Duration::from_secs(1),
6399                view_retention,
6400                skip: SkipPolicy::Enabled {
6401                    timeout: skip_timeout,
6402                    budget: SkipBudget::Participants,
6403                },
6404                replay_buffer: NZUsize!(1024 * 1024),
6405                write_buffer: NZUsize!(1024 * 1024),
6406                page_cache: CacheRef::from_pooler(context, PAGE_SIZE, PAGE_CACHE_SIZE),
6407                forward: ForwardPolicy::Disabled,
6408                track_historical_votes: false,
6409            };
6410            let engine = Engine::new(
6411                context
6412                    .child("engine")
6413                    .with_attribute("public_key", validator),
6414                cfg,
6415            );
6416            let (pending, recovered, resolver) = registration;
6417            engine.start(pending, recovered, resolver);
6418        }
6419
6420        reporters
6421    }
6422
6423    /// Heals a certified-notarization/nullification split in a group-led view.
6424    ///
6425    /// One honest validator certifies Notarization(3), two hold Nullification(3), and
6426    /// the fourth Byzantine validator stays silent. A group leader builds on parent 2.
6427    /// Targeted repair supplies the missing nullification, so group-led view 11
6428    /// becomes the first new finalization.
6429    fn certified_split_heals_in_group_led_view<S, F, L>(mut fixture: F, elector: L)
6430    where
6431        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
6432        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
6433        L: elector::Config<S>,
6434    {
6435        let n = 4;
6436        let quorum = quorum(n) as usize;
6437        assert_eq!(quorum, 3);
6438        let view_retention = ViewDelta::new(10);
6439        let skip_timeout = Duration::from_secs(12);
6440        let namespace = b"consensus".to_vec();
6441        let executor = deterministic::Runner::timed(Duration::from_secs(300));
6442        executor.start(|mut context| async move {
6443            let Fixture {
6444                participants,
6445                schemes,
6446                ..
6447            } = fixture(&mut context, &namespace, n);
6448            let mut oracle = start_test_network_with_peers(
6449                context.child("network"),
6450                participants.clone(),
6451                false,
6452            )
6453            .await;
6454            let mut registrations = register_validators(&mut oracle, &participants).await;
6455
6456            // This schedule is independent of the entry certificate. The silent
6457            // participant leads view 10, the group leads views 11..=12, and the
6458            // lone participant leads view 13.
6459            let epoch = Epoch::new(333);
6460            let participant_set: Set<PublicKey> = participants.clone().try_into().unwrap();
6461            let schedule = elector.clone().build(&participant_set);
6462            let leader_of =
6463                |view: u64| usize::from(schedule.elect(Round::new(epoch, View::new(view)), None));
6464            let byzantine = leader_of(10);
6465            let group = [leader_of(11), leader_of(12)];
6466            let lone = leader_of(13);
6467            assert!(!group.contains(&byzantine) && !group.contains(&lone) && byzantine != lone);
6468
6469            // Delivery roles, not the arbitrary quorum signers, define the split.
6470            let build_notarization = |proposal: &Proposal<D>| -> TNotarization<_, D> {
6471                let votes: Vec<_> = (0..quorum)
6472                    .map(|i| TNotarize::sign(&schemes[i], proposal.clone()).unwrap())
6473                    .collect();
6474                TNotarization::from_notarizes(&schemes[0], non_empty![@&votes], &Sequential)
6475                    .expect("notarization quorum")
6476            };
6477            let build_finalization = |proposal: &Proposal<D>| -> TFinalization<_, D> {
6478                let votes: Vec<_> = (0..quorum)
6479                    .map(|i| TFinalize::sign(&schemes[i], proposal.clone()).unwrap())
6480                    .collect();
6481                TFinalization::from_finalizes(&schemes[0], non_empty![@&votes], &Sequential)
6482                    .expect("finalization quorum")
6483            };
6484            let build_nullification = |view: u64| -> TNullification<_> {
6485                let round = Round::new(epoch, View::new(view));
6486                let votes: Vec<_> = (0..quorum)
6487                    .map(|i| TNullify::sign::<D>(&schemes[i], round).unwrap())
6488                    .collect();
6489                TNullification::from_nullifies(&schemes[0], non_empty![@&votes], &Sequential)
6490                    .expect("nullification quorum")
6491            };
6492            let payload_b2 = Sha256::hash(&[b"B_2"]);
6493            let proposal_b2 =
6494                Proposal::new(Round::new(epoch, View::new(2)), View::new(1), payload_b2);
6495            let payload_b3 = Sha256::hash(&[b"B_3"]);
6496            let proposal_b3 =
6497                Proposal::new(Round::new(epoch, View::new(3)), View::new(2), payload_b3);
6498            let b2_notarization = build_notarization(&proposal_b2);
6499            let b2_finalization = build_finalization(&proposal_b2);
6500            let b3_notarization = build_notarization(&proposal_b3);
6501            let null_3 = build_nullification(3);
6502
6503            let link = Link {
6504                latency: Duration::from_millis(10),
6505                jitter: Duration::from_millis(0),
6506                success_rate: probability!(1.0),
6507            };
6508            let mut injector_sender =
6509                start_certificate_injector(&context, &mut oracle, &participants, &link).await;
6510
6511            // Split the view-3 certificates by role and share all other evidence.
6512            let msg = Certificate::<_, D>::Notarization(b2_notarization).encode();
6513            injector_sender.send(Recipients::All, msg, true);
6514            let msg = Certificate::<_, D>::Finalization(b2_finalization).encode();
6515            injector_sender.send(Recipients::All, msg, true);
6516            let msg = Certificate::<_, D>::Notarization(b3_notarization).encode();
6517            injector_sender.send(Recipients::One(participants[lone].clone()), msg, true);
6518            let msg = Certificate::<_, D>::Nullification(null_3).encode();
6519            for idx in group {
6520                injector_sender.send(
6521                    Recipients::One(participants[idx].clone()),
6522                    msg.clone(),
6523                    true,
6524                );
6525            }
6526            for view in 4..=9 {
6527                let msg = Certificate::<_, D>::Nullification(build_nullification(view)).encode();
6528                injector_sender.send(Recipients::All, msg, true);
6529            }
6530
6531            // Start honest engines before GST so preload rebroadcasts are lost.
6532            let mut honest_reporters = start_certified_split_engines(
6533                &context,
6534                CertifiedSplitEngineConfig {
6535                    oracle: &oracle,
6536                    participants: &participants,
6537                    schemes: &schemes,
6538                    registrations: &mut registrations,
6539                    silent: byzantine,
6540                    elector: &elector,
6541                    epoch,
6542                    view_retention,
6543                    skip_timeout,
6544                },
6545            );
6546
6547            // Drain the preload before checking the split.
6548            context.sleep(Duration::from_secs(2)).await;
6549
6550            // Confirm the intended view-3 split before GST.
6551            let view_2 = View::new(2);
6552            let view_3 = View::new(3);
6553            for (idx, reporter) in honest_reporters.iter() {
6554                assert!(
6555                    reporter.finalizations.lock().contains_key(&view_2),
6556                    "reporter {idx} missing finalization for view 2"
6557                );
6558                let notarizations = reporter.notarizations.lock();
6559                let nullifications = reporter.nullifications.lock();
6560                if *idx == lone {
6561                    assert!(notarizations.contains_key(&view_3));
6562                    assert!(!nullifications.contains_key(&view_3));
6563                    assert!(reporter.certifications.lock().contains_key(&view_3));
6564                } else {
6565                    assert!(nullifications.contains_key(&view_3), "reporter {idx}");
6566                    assert!(!notarizations.contains_key(&view_3), "reporter {idx}");
6567                }
6568            }
6569
6570            // End the partition.
6571            link_validators(&mut oracle, &participants, Action::Link(link.clone()), None).await;
6572
6573            {
6574                let target = View::new(3);
6575                let mut finalizers = Vec::new();
6576                for reporter in honest_reporters.values_mut() {
6577                    let (mut latest, mut monitor) = reporter.subscribe().await;
6578                    finalizers.push(
6579                        context
6580                            .child("resume_finalizer")
6581                            .spawn(move |_| async move {
6582                                while latest < target {
6583                                    latest = monitor.recv().await.expect("event missing");
6584                                }
6585                            }),
6586                    );
6587                }
6588                join_all(finalizers).await;
6589            }
6590
6591            // Group-led view 11 is the first new finalization.
6592            for (idx, reporter) in honest_reporters.iter() {
6593                let first = {
6594                    let finalizations = reporter.finalizations.lock();
6595                    finalizations
6596                        .keys()
6597                        .filter(|view| **view > view_2)
6598                        .min()
6599                        .copied()
6600                        .expect("no finalization past the preload")
6601                };
6602                assert_eq!(
6603                    first,
6604                    View::new(11),
6605                    "reporter {idx} did not finalize the first honest-led view"
6606                );
6607            }
6608            // Background repair considers view 3 complete, so this proves targeted delivery.
6609            assert!(
6610                honest_reporters[&lone]
6611                    .nullifications
6612                    .lock()
6613                    .contains_key(&view_3),
6614                "lone did not resolve Nullification(3) from the group leader"
6615            );
6616
6617            for reporter in honest_reporters.values() {
6618                reporter.assert_no_faults();
6619                reporter.assert_no_invalid();
6620            }
6621            let blocked = oracle.blocked().await.unwrap();
6622            assert!(blocked.is_empty(), "blocked peers: {blocked:?}");
6623        });
6624    }
6625
6626    #[test_group("slow")]
6627    #[test_traced]
6628    fn test_certified_split_heals_in_group_led_view() {
6629        certified_split_heals_in_group_led_view::<_, _, RoundRobin>(
6630            ed25519::fixture,
6631            RoundRobin::default(),
6632        );
6633    }
6634
6635    /// Heals a certified-notarization/nullification split when the holder leads first.
6636    ///
6637    /// The group fetches and certifies Notarization(3) from the leader. The
6638    /// recovered parent becomes the first new finalization.
6639    fn certified_split_heals_when_lone_holder_leads_first<S, F, L>(mut fixture: F, elector: L)
6640    where
6641        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
6642        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
6643        L: elector::Config<S>,
6644    {
6645        let n = 4;
6646        let quorum = quorum(n) as usize;
6647        assert_eq!(quorum, 3);
6648        let view_retention = ViewDelta::new(10);
6649        let skip_timeout = Duration::from_secs(12);
6650        let namespace = b"consensus".to_vec();
6651        let executor = deterministic::Runner::timed(Duration::from_secs(300));
6652        executor.start(|mut context| async move {
6653            let Fixture {
6654                participants,
6655                schemes,
6656                ..
6657            } = fixture(&mut context, &namespace, n);
6658            let mut oracle = start_test_network_with_peers(
6659                context.child("network"),
6660                participants.clone(),
6661                false,
6662            )
6663            .await;
6664            let mut registrations = register_validators(&mut oracle, &participants).await;
6665
6666            // This schedule is independent of the entry certificate. The silent
6667            // participant leads view 10, the lone participant leads view 11, and
6668            // the group leads views 12..=13.
6669            let epoch = Epoch::new(333);
6670            let participant_set: Set<PublicKey> = participants.clone().try_into().unwrap();
6671            let schedule = elector.clone().build(&participant_set);
6672            let leader_of =
6673                |view: u64| usize::from(schedule.elect(Round::new(epoch, View::new(view)), None));
6674            let byzantine = leader_of(10);
6675            let lone = leader_of(11);
6676            let group = [leader_of(12), leader_of(13)];
6677            assert!(!group.contains(&byzantine) && !group.contains(&lone) && byzantine != lone);
6678
6679            // Delivery roles, not the arbitrary quorum signers, define the split.
6680            let build_notarization = |proposal: &Proposal<D>| -> TNotarization<_, D> {
6681                let votes: Vec<_> = (0..quorum)
6682                    .map(|i| TNotarize::sign(&schemes[i], proposal.clone()).unwrap())
6683                    .collect();
6684                TNotarization::from_notarizes(&schemes[0], non_empty![@&votes], &Sequential)
6685                    .expect("notarization quorum")
6686            };
6687            let build_finalization = |proposal: &Proposal<D>| -> TFinalization<_, D> {
6688                let votes: Vec<_> = (0..quorum)
6689                    .map(|i| TFinalize::sign(&schemes[i], proposal.clone()).unwrap())
6690                    .collect();
6691                TFinalization::from_finalizes(&schemes[0], non_empty![@&votes], &Sequential)
6692                    .expect("finalization quorum")
6693            };
6694            let build_nullification = |view: u64| -> TNullification<_> {
6695                let round = Round::new(epoch, View::new(view));
6696                let votes: Vec<_> = (0..quorum)
6697                    .map(|i| TNullify::sign::<D>(&schemes[i], round).unwrap())
6698                    .collect();
6699                TNullification::from_nullifies(&schemes[0], non_empty![@&votes], &Sequential)
6700                    .expect("nullification quorum")
6701            };
6702            let payload_b2 = Sha256::hash(&[b"B_2"]);
6703            let proposal_b2 =
6704                Proposal::new(Round::new(epoch, View::new(2)), View::new(1), payload_b2);
6705            let payload_b3 = Sha256::hash(&[b"B_3"]);
6706            let proposal_b3 =
6707                Proposal::new(Round::new(epoch, View::new(3)), View::new(2), payload_b3);
6708            let b2_notarization = build_notarization(&proposal_b2);
6709            let b2_finalization = build_finalization(&proposal_b2);
6710            let b3_notarization = build_notarization(&proposal_b3);
6711            let null_3 = build_nullification(3);
6712
6713            let link = Link {
6714                latency: Duration::from_millis(10),
6715                jitter: Duration::from_millis(0),
6716                success_rate: probability!(1.0),
6717            };
6718            let mut injector_sender =
6719                start_certificate_injector(&context, &mut oracle, &participants, &link).await;
6720
6721            // Split the view-3 certificates by role and share all other evidence.
6722            let msg = Certificate::<_, D>::Notarization(b2_notarization).encode();
6723            injector_sender.send(Recipients::All, msg, true);
6724            let msg = Certificate::<_, D>::Finalization(b2_finalization).encode();
6725            injector_sender.send(Recipients::All, msg, true);
6726            let msg = Certificate::<_, D>::Notarization(b3_notarization).encode();
6727            injector_sender.send(Recipients::One(participants[lone].clone()), msg, true);
6728            let msg = Certificate::<_, D>::Nullification(null_3).encode();
6729            for idx in group {
6730                injector_sender.send(
6731                    Recipients::One(participants[idx].clone()),
6732                    msg.clone(),
6733                    true,
6734                );
6735            }
6736            for view in 4..=9 {
6737                let msg = Certificate::<_, D>::Nullification(build_nullification(view)).encode();
6738                injector_sender.send(Recipients::All, msg, true);
6739            }
6740
6741            // Start honest engines before GST so preload rebroadcasts are lost.
6742            let mut honest_reporters = start_certified_split_engines(
6743                &context,
6744                CertifiedSplitEngineConfig {
6745                    oracle: &oracle,
6746                    participants: &participants,
6747                    schemes: &schemes,
6748                    registrations: &mut registrations,
6749                    silent: byzantine,
6750                    elector: &elector,
6751                    epoch,
6752                    view_retention,
6753                    skip_timeout,
6754                },
6755            );
6756
6757            // Drain the preload before checking the split.
6758            context.sleep(Duration::from_secs(2)).await;
6759
6760            // Confirm the intended view-3 split before GST.
6761            let view_2 = View::new(2);
6762            let view_3 = View::new(3);
6763            for (idx, reporter) in honest_reporters.iter() {
6764                assert!(
6765                    reporter.finalizations.lock().contains_key(&view_2),
6766                    "reporter {idx} missing finalization for view 2"
6767                );
6768                let notarizations = reporter.notarizations.lock();
6769                let nullifications = reporter.nullifications.lock();
6770                if *idx == lone {
6771                    assert!(notarizations.contains_key(&view_3));
6772                    assert!(!nullifications.contains_key(&view_3));
6773                    assert!(reporter.certifications.lock().contains_key(&view_3));
6774                } else {
6775                    assert!(nullifications.contains_key(&view_3), "reporter {idx}");
6776                    assert!(!notarizations.contains_key(&view_3), "reporter {idx}");
6777                }
6778            }
6779
6780            // End the partition.
6781            link_validators(&mut oracle, &participants, Action::Link(link.clone()), None).await;
6782
6783            {
6784                let target = View::new(3);
6785                let mut finalizers = Vec::new();
6786                for reporter in honest_reporters.values_mut() {
6787                    let (mut latest, mut monitor) = reporter.subscribe().await;
6788                    finalizers.push(
6789                        context
6790                            .child("resume_finalizer")
6791                            .spawn(move |_| async move {
6792                                while latest < target {
6793                                    latest = monitor.recv().await.expect("event missing");
6794                                }
6795                            }),
6796                    );
6797                }
6798                join_all(finalizers).await;
6799            }
6800
6801            // The recovered parent is the first new finalization.
6802            for (idx, reporter) in honest_reporters.iter() {
6803                let first = {
6804                    let finalizations = reporter.finalizations.lock();
6805                    finalizations
6806                        .keys()
6807                        .filter(|view| **view > view_2)
6808                        .min()
6809                        .copied()
6810                        .expect("no finalization past the preload")
6811                };
6812                assert_eq!(
6813                    first, view_3,
6814                    "reporter {idx} did not finalize the recovered parent view"
6815                );
6816            }
6817
6818            // Background repair considers view 3 complete, so this proves targeted delivery.
6819            for idx in group {
6820                let reporter = &honest_reporters[&idx];
6821                assert!(
6822                    reporter.notarizations.lock().contains_key(&view_3),
6823                    "group reporter {idx} did not resolve Notarization(3) from lone"
6824                );
6825                assert!(
6826                    reporter.certifications.lock().contains_key(&view_3),
6827                    "group reporter {idx} did not certify Notarization(3)"
6828                );
6829            }
6830
6831            for reporter in honest_reporters.values() {
6832                reporter.assert_no_faults();
6833                reporter.assert_no_invalid();
6834            }
6835            let blocked = oracle.blocked().await.unwrap();
6836            assert!(blocked.is_empty(), "blocked peers: {blocked:?}");
6837        });
6838    }
6839
6840    #[test_group("slow")]
6841    #[test_traced]
6842    fn test_certified_split_heals_when_lone_holder_leads_first() {
6843        certified_split_heals_when_lone_holder_leads_first::<_, _, RoundRobin>(
6844            ed25519::fixture,
6845            RoundRobin::default(),
6846        );
6847    }
6848
6849    /// Repairs ancestry gaps below a displaced certified view.
6850    ///
6851    /// One validator certifies Notarization(5) but lacks Nullification(3..=5),
6852    /// which the group holds. Targeted repair fetches each gap in order and lets
6853    /// the first group-led proposal finalize.
6854    fn certified_split_heals_with_displaced_certified_view<S, F, L>(mut fixture: F, elector: L)
6855    where
6856        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
6857        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
6858        L: elector::Config<S>,
6859    {
6860        let n = 4;
6861        let quorum = quorum(n) as usize;
6862        assert_eq!(quorum, 3);
6863        let view_retention = ViewDelta::new(10);
6864        let skip_timeout = Duration::from_secs(12);
6865        let namespace = b"consensus".to_vec();
6866        let executor = deterministic::Runner::timed(Duration::from_secs(300));
6867        executor.start(|mut context| async move {
6868            let Fixture {
6869                participants,
6870                schemes,
6871                ..
6872            } = fixture(&mut context, &namespace, n);
6873            let mut oracle = start_test_network_with_peers(
6874                context.child("network"),
6875                participants.clone(),
6876                false,
6877            )
6878            .await;
6879            let mut registrations = register_validators(&mut oracle, &participants).await;
6880
6881            // This schedule is independent of the entry certificate. The silent
6882            // participant leads view 10, the group leads views 11..=12, and the
6883            // lone participant leads view 13.
6884            let epoch = Epoch::new(333);
6885            let participant_set: Set<PublicKey> = participants.clone().try_into().unwrap();
6886            let schedule = elector.clone().build(&participant_set);
6887            let leader_of =
6888                |view: u64| usize::from(schedule.elect(Round::new(epoch, View::new(view)), None));
6889            let byzantine = leader_of(10);
6890            let group = [leader_of(11), leader_of(12)];
6891            let lone = leader_of(13);
6892            assert!(!group.contains(&byzantine) && !group.contains(&lone) && byzantine != lone);
6893
6894            // Delivery roles, not the arbitrary quorum signers, define the split.
6895            let build_notarization = |proposal: &Proposal<D>| -> TNotarization<_, D> {
6896                let votes: Vec<_> = (0..quorum)
6897                    .map(|i| TNotarize::sign(&schemes[i], proposal.clone()).unwrap())
6898                    .collect();
6899                TNotarization::from_notarizes(&schemes[0], non_empty![@&votes], &Sequential)
6900                    .expect("notarization quorum")
6901            };
6902            let build_finalization = |proposal: &Proposal<D>| -> TFinalization<_, D> {
6903                let votes: Vec<_> = (0..quorum)
6904                    .map(|i| TFinalize::sign(&schemes[i], proposal.clone()).unwrap())
6905                    .collect();
6906                TFinalization::from_finalizes(&schemes[0], non_empty![@&votes], &Sequential)
6907                    .expect("finalization quorum")
6908            };
6909            let build_nullification = |view: u64| -> TNullification<_> {
6910                let round = Round::new(epoch, View::new(view));
6911                let votes: Vec<_> = (0..quorum)
6912                    .map(|i| TNullify::sign::<D>(&schemes[i], round).unwrap())
6913                    .collect();
6914                TNullification::from_nullifies(&schemes[0], non_empty![@&votes], &Sequential)
6915                    .expect("nullification quorum")
6916            };
6917            let payload_b2 = Sha256::hash(&[b"B_2"]);
6918            let proposal_b2 =
6919                Proposal::new(Round::new(epoch, View::new(2)), View::new(1), payload_b2);
6920            let payload_b5 = Sha256::hash(&[b"B_5"]);
6921            let proposal_b5 =
6922                Proposal::new(Round::new(epoch, View::new(5)), View::new(2), payload_b5);
6923            let b2_notarization = build_notarization(&proposal_b2);
6924            let b2_finalization = build_finalization(&proposal_b2);
6925            let b5_notarization = build_notarization(&proposal_b5);
6926
6927            let link = Link {
6928                latency: Duration::from_millis(10),
6929                jitter: Duration::from_millis(0),
6930                success_rate: probability!(1.0),
6931            };
6932            let mut injector_sender =
6933                start_certificate_injector(&context, &mut oracle, &participants, &link).await;
6934
6935            // Split view-5 notarization from nullifications 3 through 5.
6936            let msg = Certificate::<_, D>::Notarization(b2_notarization).encode();
6937            injector_sender.send(Recipients::All, msg, true);
6938            let msg = Certificate::<_, D>::Finalization(b2_finalization).encode();
6939            injector_sender.send(Recipients::All, msg, true);
6940            let msg = Certificate::<_, D>::Notarization(b5_notarization).encode();
6941            injector_sender.send(Recipients::One(participants[lone].clone()), msg, true);
6942            for view in 3..=5 {
6943                let msg = Certificate::<_, D>::Nullification(build_nullification(view)).encode();
6944                for idx in group {
6945                    injector_sender.send(
6946                        Recipients::One(participants[idx].clone()),
6947                        msg.clone(),
6948                        true,
6949                    );
6950                }
6951            }
6952            for view in 6..=9 {
6953                let msg = Certificate::<_, D>::Nullification(build_nullification(view)).encode();
6954                injector_sender.send(Recipients::All, msg, true);
6955            }
6956
6957            // Start honest engines before GST so preload rebroadcasts are lost.
6958            let mut honest_reporters = start_certified_split_engines(
6959                &context,
6960                CertifiedSplitEngineConfig {
6961                    oracle: &oracle,
6962                    participants: &participants,
6963                    schemes: &schemes,
6964                    registrations: &mut registrations,
6965                    silent: byzantine,
6966                    elector: &elector,
6967                    epoch,
6968                    view_retention,
6969                    skip_timeout,
6970                },
6971            );
6972
6973            // Drain the preload before checking the split.
6974            context.sleep(Duration::from_secs(2)).await;
6975
6976            // Confirm the intended certificate split before GST.
6977            let view_2 = View::new(2);
6978            let view_5 = View::new(5);
6979            for (idx, reporter) in honest_reporters.iter() {
6980                assert!(
6981                    reporter.finalizations.lock().contains_key(&view_2),
6982                    "reporter {idx} missing finalization for view 2"
6983                );
6984                let notarizations = reporter.notarizations.lock();
6985                let nullifications = reporter.nullifications.lock();
6986                if *idx == lone {
6987                    assert!(notarizations.contains_key(&view_5));
6988                    assert!(reporter.certifications.lock().contains_key(&view_5));
6989                    for view in 3..=5 {
6990                        assert!(!nullifications.contains_key(&View::new(view)));
6991                    }
6992                } else {
6993                    for view in 3..=5 {
6994                        assert!(
6995                            nullifications.contains_key(&View::new(view)),
6996                            "reporter {idx}"
6997                        );
6998                    }
6999                    assert!(!notarizations.contains_key(&view_5), "reporter {idx}");
7000                }
7001            }
7002
7003            // End the partition.
7004            link_validators(&mut oracle, &participants, Action::Link(link.clone()), None).await;
7005
7006            {
7007                let target = View::new(3);
7008                let mut finalizers = Vec::new();
7009                for reporter in honest_reporters.values_mut() {
7010                    let (mut latest, mut monitor) = reporter.subscribe().await;
7011                    finalizers.push(
7012                        context
7013                            .child("resume_finalizer")
7014                            .spawn(move |_| async move {
7015                                while latest < target {
7016                                    latest = monitor.recv().await.expect("event missing");
7017                                }
7018                            }),
7019                    );
7020                }
7021                join_all(finalizers).await;
7022            }
7023
7024            // Group-led view 11 is the first new finalization after all three repairs.
7025            for (idx, reporter) in honest_reporters.iter() {
7026                let first = {
7027                    let finalizations = reporter.finalizations.lock();
7028                    finalizations
7029                        .keys()
7030                        .filter(|view| **view > view_2)
7031                        .min()
7032                        .copied()
7033                        .expect("no finalization past the preload")
7034                };
7035                assert_eq!(
7036                    first,
7037                    View::new(11),
7038                    "reporter {idx} did not finalize the first honest-led view"
7039                );
7040            }
7041            let lone_reporter = &honest_reporters[&lone];
7042            for view in 3..=5 {
7043                assert!(
7044                    lone_reporter
7045                        .nullifications
7046                        .lock()
7047                        .contains_key(&View::new(view)),
7048                    "lone did not resolve Nullification({view}) from the group leader"
7049                );
7050            }
7051
7052            for reporter in honest_reporters.values() {
7053                reporter.assert_no_faults();
7054                reporter.assert_no_invalid();
7055            }
7056            let blocked = oracle.blocked().await.unwrap();
7057            assert!(blocked.is_empty(), "blocked peers: {blocked:?}");
7058        });
7059    }
7060
7061    #[test_group("slow")]
7062    #[test_traced]
7063    fn test_certified_split_heals_with_displaced_certified_view() {
7064        certified_split_heals_with_displaced_certified_view::<_, _, RoundRobin>(
7065            ed25519::fixture,
7066            RoundRobin::default(),
7067        );
7068    }
7069
7070    /// Two terms led by an offline validator give the honest validators
7071    /// different certified floors. Progress requires a missing parent
7072    /// notarization from an available validator.
7073    ///
7074    /// Terms are five views long and the offline participant leads term 1
7075    /// (views 1..=5) and term 5 (views 21..=25).
7076    ///
7077    /// An injector builds this pre-GST state while validator links are down:
7078    /// - `b` and `c` certify views 1 and 2 (term 1). `a` never sees them.
7079    /// - `a` and `b` certify views 21 and 22 (term 5). `c` never sees them.
7080    /// - Terms 2..=4 are nullified at their term starts for everyone.
7081    ///
7082    /// After GST, the three honest participants form the only quorum. Each
7083    /// proposal names its proposer's highest certified view. One honest peer
7084    /// lacks the named chain.
7085    ///
7086    /// If `suppress_backfill` is true, the deprived participants receive
7087    /// term-start nullifications that suppress background repair. Otherwise,
7088    /// the nullifications occur one view past the notarized heads (views 3 and
7089    /// 23), so repair can see each head.
7090    fn stable_leader_cross_term_certified_split<S, F, L>(
7091        mut fixture: F,
7092        elector: L,
7093        suppress_backfill: bool,
7094    ) where
7095        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
7096        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
7097        L: elector::Config<S>,
7098    {
7099        let n = 4;
7100        let quorum = quorum(n) as usize;
7101        assert_eq!(quorum, 3);
7102        let view_retention = ViewDelta::new(10);
7103        let skip_timeout = Duration::from_secs(12);
7104        let namespace = b"consensus".to_vec();
7105        let executor = deterministic::Runner::timed(Duration::from_secs(300));
7106        executor.start(|mut context| async move {
7107            let Fixture {
7108                participants,
7109                schemes,
7110                ..
7111            } = fixture(&mut context, &namespace, n);
7112            let mut oracle = start_test_network_with_peers(
7113                context.child("network"),
7114                participants.clone(),
7115                false,
7116            )
7117            .await;
7118            let mut registrations = register_validators(&mut oracle, &participants).await;
7119
7120            // Choose an epoch where the same participant leads terms 1 and 5.
7121            let epoch = Epoch::new(2);
7122            let participant_set: Set<PublicKey> = participants.clone().try_into().unwrap();
7123            let schedule = elector.clone().build(&participant_set);
7124            let leader_of =
7125                |view: u64| usize::from(schedule.elect(Round::new(epoch, View::new(view)), None));
7126            let offline = leader_of(1);
7127            assert_eq!(offline, leader_of(21), "offline must lead terms 1 and 5");
7128            let honest: Vec<usize> = (0..n as usize).filter(|idx| *idx != offline).collect();
7129            let (a, b, c) = (honest[0], honest[1], honest[2]);
7130            for view in [6u64, 11, 16, 26, 31, 36] {
7131                assert_ne!(leader_of(view), offline, "term start must be honest");
7132            }
7133            // The view-26 leader must have certified view 22.
7134            assert_ne!(
7135                leader_of(26),
7136                c,
7137                "view-26 leader must hold certification 22"
7138            );
7139            // The halt detector ends when term 9 returns to the offline validator.
7140            assert_eq!(leader_of(41), offline, "term 9 must return to offline");
7141
7142            let build_notarization =
7143                |proposal: &Proposal<D>, signers: [usize; 3]| -> TNotarization<_, D> {
7144                    let votes: Vec<_> = signers
7145                        .iter()
7146                        .map(|i| TNotarize::sign(&schemes[*i], proposal.clone()).unwrap())
7147                        .collect();
7148                    TNotarization::from_notarizes(&schemes[0], non_empty![@&votes], &Sequential)
7149                        .expect("notarization quorum")
7150                };
7151            let build_nullification = |view: u64, signers: [usize; 3]| -> TNullification<_> {
7152                let round = Round::new(epoch, View::new(view));
7153                let votes: Vec<_> = signers
7154                    .iter()
7155                    .map(|i| TNullify::sign::<D>(&schemes[*i], round).unwrap())
7156                    .collect();
7157                TNullification::from_nullifies(&schemes[0], non_empty![@&votes], &Sequential)
7158                    .expect("nullification quorum")
7159            };
7160
7161            // Term 1 chain (views 1 and 2), certified by `b` and `c`.
7162            let payload_1 = Sha256::hash(&[b"V1"]);
7163            let proposal_1 =
7164                Proposal::new(Round::new(epoch, View::new(1)), View::new(0), payload_1);
7165            let payload_2 = Sha256::hash(&[b"V2"]);
7166            let proposal_2 =
7167                Proposal::new(Round::new(epoch, View::new(2)), View::new(1), payload_2);
7168            // Term 5 chain (views 21 and 22), certified by `a` and `b`. The
7169            // view-21 proposal names genesis, so `a` can vote without
7170            // certifying term 1.
7171            let payload_21 = Sha256::hash(&[b"V21"]);
7172            let proposal_21 =
7173                Proposal::new(Round::new(epoch, View::new(21)), View::new(0), payload_21);
7174            let payload_22 = Sha256::hash(&[b"V22"]);
7175            let proposal_22 =
7176                Proposal::new(Round::new(epoch, View::new(22)), View::new(21), payload_22);
7177
7178            let link = Link {
7179                latency: Duration::from_millis(10),
7180                jitter: Duration::from_millis(0),
7181                success_rate: probability!(1.0),
7182            };
7183            let mut injector_sender =
7184                start_certificate_injector(&context, &mut oracle, &participants, &link).await;
7185
7186            {
7187                let mut send_to = |certificate: Certificate<S, D>, targets: &[usize]| {
7188                    let msg = certificate.encode();
7189                    for idx in targets {
7190                        injector_sender.send(
7191                            Recipients::One(participants[*idx].clone()),
7192                            msg.clone(),
7193                            true,
7194                        );
7195                    }
7196                };
7197
7198                // Term 1: `b` and `c` follow the chain; `a` only learns the
7199                // term was abandoned.
7200                send_to(
7201                    Certificate::Notarization(build_notarization(&proposal_1, [b, c, offline])),
7202                    &[b, c],
7203                );
7204                send_to(
7205                    Certificate::Notarization(build_notarization(&proposal_2, [b, c, offline])),
7206                    &[b, c],
7207                );
7208                send_to(
7209                    Certificate::Nullification(build_nullification(3, [b, c, offline])),
7210                    &[b, c],
7211                );
7212                let a_skip = if suppress_backfill { 1 } else { 3 };
7213                send_to(
7214                    Certificate::Nullification(build_nullification(a_skip, [a, c, offline])),
7215                    &[a, b],
7216                );
7217
7218                // Terms 2..=4 are abandoned at their term starts by everyone.
7219                for view in [6u64, 11, 16] {
7220                    send_to(
7221                        Certificate::Nullification(build_nullification(view, [a, b, c])),
7222                        &[a, b, c],
7223                    );
7224                }
7225
7226                // Term 5: `a` and `b` follow the chain; `c` only learns the
7227                // term was abandoned.
7228                send_to(
7229                    Certificate::Notarization(build_notarization(&proposal_21, [a, b, offline])),
7230                    &[a, b],
7231                );
7232                send_to(
7233                    Certificate::Notarization(build_notarization(&proposal_22, [a, b, offline])),
7234                    &[a, b],
7235                );
7236                let c_skip = if suppress_backfill { 21 } else { 23 };
7237                send_to(
7238                    Certificate::Nullification(build_nullification(c_skip, [a, c, offline])),
7239                    &[c],
7240                );
7241                send_to(
7242                    Certificate::Nullification(build_nullification(23, [a, b, offline])),
7243                    &[a, b],
7244                );
7245            }
7246
7247            // Start honest engines before GST. The partition drops their
7248            // preload broadcasts.
7249            let mut honest_reporters = start_certified_split_engines(
7250                &context,
7251                CertifiedSplitEngineConfig {
7252                    oracle: &oracle,
7253                    participants: &participants,
7254                    schemes: &schemes,
7255                    registrations: &mut registrations,
7256                    silent: offline,
7257                    elector: &elector,
7258                    epoch,
7259                    view_retention,
7260                    skip_timeout,
7261                },
7262            );
7263
7264            // Drain the preload before checking the split.
7265            context.sleep(Duration::from_secs(2)).await;
7266
7267            // Confirm the intended cross-term split before GST.
7268            for (idx, reporter) in honest_reporters.iter() {
7269                let certifications = reporter.certifications.lock();
7270                let has_term1 = certifications.contains_key(&View::new(2));
7271                let has_term5 = certifications.contains_key(&View::new(22));
7272                if *idx == a {
7273                    assert!(!has_term1 && has_term5, "a: {has_term1} {has_term5}");
7274                } else if *idx == b {
7275                    assert!(has_term1 && has_term5, "b: {has_term1} {has_term5}");
7276                } else {
7277                    assert!(has_term1 && !has_term5, "c: {has_term1} {has_term5}");
7278                }
7279                assert!(
7280                    reporter.finalizations.lock().is_empty(),
7281                    "reporter {idx} finalized during preload"
7282                );
7283            }
7284
7285            // End the partition (GST).
7286            link_validators(&mut oracle, &participants, Action::Link(link.clone()), None).await;
7287
7288            // Wait until every honest validator finalizes after GST.
7289            let target = View::new(26);
7290            let mut finalizers = Vec::new();
7291            for (idx, reporter) in honest_reporters.iter_mut() {
7292                let (mut latest, mut monitor) = reporter.subscribe().await;
7293                finalizers.push(
7294                    context
7295                        .child("finalizer")
7296                        .with_attribute("index", idx)
7297                        .spawn(move |_| async move {
7298                            while latest < target {
7299                                latest = monitor.recv().await.expect("finalization missing");
7300                            }
7301                        }),
7302                );
7303            }
7304            // Reaching the next offline-led term proves each honest validator
7305            // led a full term without finalizing.
7306            let progress_reporters: Vec<_> = honest_reporters.values().cloned().collect();
7307            let next_byzantine_term =
7308                context
7309                    .child("next_byzantine_term")
7310                    .spawn(move |context| async move {
7311                        loop {
7312                            let reached = progress_reporters.iter().all(|reporter| {
7313                                reporter
7314                                    .nullifications
7315                                    .lock()
7316                                    .keys()
7317                                    .any(|view| *view >= View::new(41))
7318                            });
7319                            if reached {
7320                                return;
7321                            }
7322                            context.sleep(Duration::from_secs(1)).await;
7323                        }
7324                    });
7325            let finalized = select! {
7326                _ = join_all(finalizers) => true,
7327                _ = next_byzantine_term => false,
7328            };
7329
7330            for reporter in honest_reporters.values() {
7331                reporter.assert_no_faults();
7332                reporter.assert_no_invalid();
7333            }
7334            let blocked = oracle.blocked().await.unwrap();
7335            assert!(blocked.is_empty(), "blocked peers: {blocked:?}");
7336
7337            if !finalized {
7338                // Each deprived validator fetched the proposal parent from an
7339                // honest proposer but lacks the preceding notarization needed
7340                // to certify it.
7341                let a_reporter = &honest_reporters[&a];
7342                assert!(
7343                    a_reporter.notarizations.lock().contains_key(&View::new(2)),
7344                    "halted, but a is missing notarization(2)"
7345                );
7346                assert!(
7347                    !a_reporter.notarizations.lock().contains_key(&View::new(1)),
7348                    "halted, but a repaired notarization(1)"
7349                );
7350                assert!(
7351                    !a_reporter.certifications.lock().contains_key(&View::new(2)),
7352                    "halted, but a certified view 2"
7353                );
7354
7355                let c_reporter = &honest_reporters[&c];
7356                assert!(
7357                    c_reporter.notarizations.lock().contains_key(&View::new(22)),
7358                    "halted, but c is missing notarization(22)"
7359                );
7360                assert!(
7361                    !c_reporter.notarizations.lock().contains_key(&View::new(21)),
7362                    "halted, but c repaired notarization(21)"
7363                );
7364                assert!(
7365                    !c_reporter.certifications.lock().contains_key(&View::new(22)),
7366                    "halted, but c certified view 22"
7367                );
7368
7369                panic!(
7370                    "all honest leaders exhausted a term, but recursive parent repair never finalized"
7371                );
7372            }
7373            for (idx, reporter) in honest_reporters.iter() {
7374                assert!(
7375                    reporter
7376                        .finalizations
7377                        .lock()
7378                        .keys()
7379                        .any(|view| *view >= target),
7380                    "reporter {idx} never finalized after GST"
7381                );
7382            }
7383            let c_certifications = honest_reporters[&c].certifications.lock();
7384            for view in [View::new(21), View::new(22)] {
7385                assert!(
7386                    c_certifications.contains_key(&view),
7387                    "c did not recover and certify view {view} from an honest validator"
7388                );
7389            }
7390        });
7391    }
7392
7393    #[test_group("slow")]
7394    #[test_traced]
7395    fn test_stable_leader_cross_term_certified_split_recovers() {
7396        stable_leader_cross_term_certified_split::<_, _, RoundRobin>(
7397            ed25519::fixture,
7398            RoundRobin::default().with_term(
7399                TermLength::new(NZU32!(5)),
7400                Duration::from_secs(12),
7401                ViewDelta::new(0),
7402            ),
7403            true,
7404        );
7405    }
7406
7407    #[test_group("slow")]
7408    #[test_traced]
7409    fn test_stable_leader_cross_term_certified_split_backfillable() {
7410        stable_leader_cross_term_certified_split::<_, _, RoundRobin>(
7411            ed25519::fixture,
7412            RoundRobin::default().with_term(
7413                TermLength::new(NZU32!(5)),
7414                Duration::from_secs(12),
7415                ViewDelta::new(0),
7416            ),
7417            false,
7418        );
7419    }
7420
7421    fn tle<V, L>(elector: L)
7422    where
7423        V: Variant,
7424        L: elector::Config<bls12381_threshold_vrf::Scheme<PublicKey, V>>,
7425    {
7426        // Create context
7427        let n = 4;
7428        let namespace = b"consensus".to_vec();
7429        let view_retention = ViewDelta::new(100);
7430        let skip_timeout = Duration::from_secs(50);
7431        let executor = deterministic::Runner::timed(Duration::from_secs(30));
7432        executor.start(|mut context| async move {
7433            // Register participants
7434            let Fixture {
7435                participants,
7436                schemes,
7437                ..
7438            } = bls12381_threshold_vrf::fixture::<V, _>(&mut context, &namespace, n);
7439            let mut oracle =
7440                start_test_network_with_peers(context.child("network"), participants.clone(), true)
7441                    .await;
7442            let mut registrations = register_validators(&mut oracle, &participants).await;
7443
7444            // Link all validators
7445            let link = Link {
7446                latency: Duration::from_millis(10),
7447                jitter: Duration::from_millis(5),
7448                success_rate: probability!(1.0),
7449            };
7450            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
7451
7452            // Create engines and reporters
7453            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
7454            let mut reporters = Vec::new();
7455            let mut engine_handlers = Vec::new();
7456            let monitor_reporter = Arc::new(Mutex::new(None));
7457            for (idx, validator) in participants.iter().enumerate() {
7458                // Create scheme context
7459                let context = context
7460                    .child("validator")
7461                    .with_attribute("public_key", validator);
7462
7463                // Store first reporter for monitoring
7464                let reporter_config = mocks::reporter::Config {
7465                    participants: participants.clone().try_into().unwrap(),
7466                    scheme: schemes[idx].clone(),
7467                    elector: elector.clone(),
7468                };
7469                let reporter =
7470                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
7471                reporters.push(reporter.clone());
7472                if idx == 0 {
7473                    *monitor_reporter.lock() = Some(reporter.clone());
7474                }
7475
7476                // Configure application
7477                let application_cfg = mocks::application::Config::<Sha256, _> {
7478                    relay: relay.clone(),
7479                    me: validator.clone(),
7480                    propose_latency: (10.0, 5.0),
7481                    verify_latency: (10.0, 5.0),
7482                    certify_latency: (10.0, 5.0),
7483                    should_certify: mocks::application::Certifier::Always,
7484                };
7485                let (actor, application) = mocks::application::Application::new(
7486                    context.child("application"),
7487                    application_cfg,
7488                );
7489                actor.start();
7490                let blocker = oracle.control(validator.clone());
7491                let cfg = config::Config {
7492                    scheme: schemes[idx].clone(),
7493                    elector: elector.clone(),
7494                    blocker,
7495                    automaton: application.clone(),
7496                    relay: application.clone(),
7497                    reporter: reporter.clone(),
7498                    strategy: Sequential,
7499                    partition: validator.to_string(),
7500                    mailbox_size: NZUsize!(1024),
7501                    epoch: Epoch::new(333),
7502                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
7503                        Epoch::new(333),
7504                    )),
7505                    leader_timeout: Duration::from_millis(100),
7506                    certification_timeout: Duration::from_millis(200),
7507                    timeout_retry: Duration::from_millis(500),
7508                    fetch_timeout: Duration::from_millis(100),
7509                    view_retention,
7510                    skip: SkipPolicy::Enabled {
7511                        timeout: skip_timeout,
7512                        budget: SkipBudget::Participants,
7513                    },
7514                    replay_buffer: NZUsize!(1024 * 1024),
7515                    write_buffer: NZUsize!(1024 * 1024),
7516                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
7517                    forward: ForwardPolicy::Disabled,
7518                    track_historical_votes: false,
7519                };
7520                let engine = Engine::new(context.child("engine"), cfg);
7521
7522                // Start engine
7523                let (pending, recovered, resolver) = registrations
7524                    .remove(validator)
7525                    .expect("validator should be registered");
7526                engine_handlers.push(engine.start(pending, recovered, resolver));
7527            }
7528
7529            // Prepare TLE test data
7530            let target = Round::new(Epoch::new(333), View::new(10)); // Encrypt for round (epoch 333, view 10)
7531            let message = b"Secret message for future view10"; // 32 bytes
7532
7533            // Encrypt message
7534            let ciphertext = schemes[0]
7535                .encrypt(&mut context, target, *message)
7536                .expect("valid TLE encryption inputs");
7537
7538            // Wait for consensus to reach the target view and then decrypt
7539            let reporter = monitor_reporter.lock().clone().unwrap();
7540            loop {
7541                // Wait for notarization
7542                context.sleep(Duration::from_millis(100)).await;
7543                let notarizations = reporter.notarizations.lock();
7544                let Some(notarization) = notarizations.get(&target.view()) else {
7545                    continue;
7546                };
7547
7548                // Decrypt the message using the seed
7549                let seed = notarization.seed();
7550                let decrypted = seed
7551                    .decrypt(&ciphertext)
7552                    .expect("Decryption should succeed with valid seed signature");
7553                assert_eq!(
7554                    message,
7555                    decrypted.as_ref(),
7556                    "Decrypted message should match original message"
7557                );
7558                break;
7559            }
7560        });
7561    }
7562
7563    #[test_traced]
7564    fn test_tle() {
7565        tle::<MinPk, Random>(Random::new(RandomVersion::V1));
7566        tle::<MinSig, Random>(Random::new(RandomVersion::V1));
7567    }
7568
7569    fn run_hailstorm<S, F, L>(
7570        seed: u64,
7571        shutdowns: usize,
7572        interval: ViewDelta,
7573        elector: L,
7574        mut fixture: F,
7575    ) -> String
7576    where
7577        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
7578        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
7579        L: elector::Config<S>,
7580    {
7581        // Create context
7582        let n = 5;
7583        let view_retention = ViewDelta::new(10);
7584        let skip_timeout = Duration::from_secs(11);
7585        let namespace = b"consensus".to_vec();
7586        let cfg = deterministic::Config::new().with_seed(seed);
7587        let executor = deterministic::Runner::new(cfg);
7588        executor.start(|mut context| async move {
7589            // Register participants
7590            let Fixture {
7591                participants,
7592                schemes,
7593                ..
7594            } = fixture(&mut context, &namespace, n);
7595            let mut oracle =
7596                start_test_network_with_peers(context.child("network"), participants.clone(), true)
7597                    .await;
7598            let mut registrations = register_validators(&mut oracle, &participants).await;
7599
7600            // Link all validators
7601            let link = Link {
7602                latency: Duration::from_millis(10),
7603                jitter: Duration::from_millis(1),
7604                success_rate: probability!(1.0),
7605            };
7606            link_validators(&mut oracle, &participants, Action::Link(link), None).await;
7607
7608            // Create engines
7609            let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
7610            let mut reporters = BTreeMap::new();
7611            let mut engine_handlers = BTreeMap::new();
7612            for (idx, validator) in participants.iter().enumerate() {
7613                // Create scheme context
7614                let context = context
7615                    .child("validator")
7616                    .with_attribute("public_key", validator);
7617
7618                // Configure engine
7619                let reporter_config = mocks::reporter::Config {
7620                    participants: participants.clone().try_into().unwrap(),
7621                    scheme: schemes[idx].clone(),
7622                    elector: elector.clone(),
7623                };
7624                let reporter =
7625                    mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
7626                reporters.insert(idx, reporter.clone());
7627                let application_cfg = mocks::application::Config::<Sha256, _> {
7628                    relay: relay.clone(),
7629                    me: validator.clone(),
7630                    propose_latency: (10.0, 5.0),
7631                    verify_latency: (10.0, 5.0),
7632                    certify_latency: (10.0, 5.0),
7633                    should_certify: mocks::application::Certifier::Always,
7634                };
7635                let (actor, application) = mocks::application::Application::new(
7636                    context.child("application"),
7637                    application_cfg,
7638                );
7639                actor.start();
7640                let blocker = oracle.control(validator.clone());
7641                let cfg = config::Config {
7642                    scheme: schemes[idx].clone(),
7643                    elector: elector.clone(),
7644                    blocker,
7645                    automaton: application.clone(),
7646                    relay: application.clone(),
7647                    reporter: reporter.clone(),
7648                    strategy: Sequential,
7649                    partition: validator.to_string(),
7650                    mailbox_size: NZUsize!(1024),
7651                    epoch: Epoch::new(333),
7652                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
7653                        Epoch::new(333),
7654                    )),
7655                    leader_timeout: Duration::from_secs(1),
7656                    certification_timeout: Duration::from_secs(2),
7657                    timeout_retry: Duration::from_secs(10),
7658                    fetch_timeout: Duration::from_secs(1),
7659                    view_retention,
7660                    skip: SkipPolicy::Enabled {
7661                        timeout: skip_timeout,
7662                        budget: SkipBudget::Participants,
7663                    },
7664                    replay_buffer: NZUsize!(1024 * 1024),
7665                    write_buffer: NZUsize!(1024 * 1024),
7666                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
7667                    forward: ForwardPolicy::Disabled,
7668                    track_historical_votes: false,
7669                };
7670                let engine = Engine::new(context.child("engine"), cfg);
7671
7672                // Start engine
7673                let (pending, recovered, resolver) = registrations
7674                    .remove(validator)
7675                    .expect("validator should be registered");
7676                engine_handlers.insert(idx, engine.start(pending, recovered, resolver));
7677            }
7678
7679            // Run shutdowns
7680            let mut target = View::zero();
7681            for i in 0..shutdowns {
7682                // Update target
7683                target = target.saturating_add(interval);
7684
7685                // Wait for all engines to finish
7686                let mut finalizers = Vec::new();
7687                for reporter in reporters.values_mut() {
7688                    let (mut latest, mut monitor) = reporter.subscribe().await;
7689                    finalizers.push(context.child("finalizer").spawn(move |_| async move {
7690                        while latest < target {
7691                            latest = monitor.recv().await.expect("event missing");
7692                        }
7693                    }));
7694                }
7695                join_all(finalizers).await;
7696                target = target.saturating_add(interval);
7697
7698                // Select a random engine to shutdown
7699                let idx = context.random_range(0..engine_handlers.len());
7700                let validator = &participants[idx];
7701                let handle = engine_handlers.remove(&idx).unwrap();
7702                handle.abort();
7703                let _ = handle.await;
7704                let selected_reporter = reporters.remove(&idx).unwrap();
7705                info!(idx, ?validator, "shutdown validator");
7706
7707                // Wait for all engines to finish
7708                let mut finalizers = Vec::new();
7709                for reporter in reporters.values_mut() {
7710                    let (mut latest, mut monitor) = reporter.subscribe().await;
7711                    finalizers.push(context.child("finalizer").spawn(move |_| async move {
7712                        while latest < target {
7713                            latest = monitor.recv().await.expect("event missing");
7714                        }
7715                    }));
7716                }
7717                join_all(finalizers).await;
7718                target = target.saturating_add(interval);
7719
7720                // Recreate engine
7721                info!(idx, ?validator, "restarting validator");
7722                let context = context
7723                    .child("validator_restarted")
7724                    .with_attribute("public_key", validator)
7725                    .with_attribute("restart", i);
7726
7727                // Start engine
7728                let (pending, recovered, resolver) =
7729                    register_validator(&mut oracle, validator.clone()).await;
7730                let application_cfg = mocks::application::Config::<Sha256, _> {
7731                    relay: relay.clone(),
7732                    me: validator.clone(),
7733                    propose_latency: (10.0, 5.0),
7734                    verify_latency: (10.0, 5.0),
7735                    certify_latency: (10.0, 5.0),
7736                    should_certify: mocks::application::Certifier::Always,
7737                };
7738                let (actor, application) = mocks::application::Application::new(
7739                    context.child("application"),
7740                    application_cfg,
7741                );
7742                actor.start();
7743                reporters.insert(idx, selected_reporter.clone());
7744                let blocker = oracle.control(validator.clone());
7745                let cfg = config::Config {
7746                    scheme: schemes[idx].clone(),
7747                    elector: elector.clone(),
7748                    blocker,
7749                    automaton: application.clone(),
7750                    relay: application.clone(),
7751                    reporter: selected_reporter,
7752                    strategy: Sequential,
7753                    partition: validator.to_string(),
7754                    mailbox_size: NZUsize!(1024),
7755                    epoch: Epoch::new(333),
7756                    floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
7757                        Epoch::new(333),
7758                    )),
7759                    leader_timeout: Duration::from_secs(1),
7760                    certification_timeout: Duration::from_secs(2),
7761                    timeout_retry: Duration::from_secs(10),
7762                    fetch_timeout: Duration::from_secs(1),
7763                    view_retention,
7764                    skip: SkipPolicy::Enabled {
7765                        timeout: skip_timeout,
7766                        budget: SkipBudget::Participants,
7767                    },
7768                    replay_buffer: NZUsize!(1024 * 1024),
7769                    write_buffer: NZUsize!(1024 * 1024),
7770                    page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
7771                    forward: ForwardPolicy::Disabled,
7772                    track_historical_votes: false,
7773                };
7774                let engine = Engine::new(context.child("engine"), cfg);
7775                engine_handlers.insert(idx, engine.start(pending, recovered, resolver));
7776
7777                // Wait for all engines to hit required containers
7778                let mut finalizers = Vec::new();
7779                for reporter in reporters.values_mut() {
7780                    let (mut latest, mut monitor) = reporter.subscribe().await;
7781                    finalizers.push(context.child("finalizer").spawn(move |_| async move {
7782                        while latest < target {
7783                            latest = monitor.recv().await.expect("event missing");
7784                        }
7785                    }));
7786                }
7787                join_all(finalizers).await;
7788                info!(idx, ?validator, "validator recovered");
7789            }
7790
7791            // Check reporters for correct activity
7792            let latest_complete = target.saturating_sub(view_retention);
7793            for reporter in reporters.values() {
7794                // Ensure no faults
7795                reporter.assert_no_faults();
7796
7797                // Ensure no invalid signatures
7798                reporter.assert_no_invalid();
7799
7800                // Ensure no forks
7801                let mut notarized = HashMap::new();
7802                let mut finalized = HashMap::new();
7803                {
7804                    let notarizes = reporter.notarizes.lock();
7805                    for view in View::range(View::new(1), latest_complete) {
7806                        // Ensure only one payload proposed per view
7807                        let Some(payloads) = notarizes.get(&view) else {
7808                            continue;
7809                        };
7810                        if payloads.len() > 1 {
7811                            panic!("view: {view}");
7812                        }
7813                        let (digest, _) = payloads.iter().next().unwrap();
7814                        notarized.insert(view, *digest);
7815                    }
7816                }
7817                {
7818                    let notarizations = reporter.notarizations.lock();
7819                    for view in View::range(View::new(1), latest_complete) {
7820                        // Ensure notarization matches digest from notarizes
7821                        let Some(notarization) = notarizations.get(&view) else {
7822                            continue;
7823                        };
7824                        let Some(digest) = notarized.get(&view) else {
7825                            continue;
7826                        };
7827                        assert_eq!(&notarization.proposal.payload, digest);
7828                    }
7829                }
7830                {
7831                    let finalizes = reporter.finalizes.lock();
7832                    for view in View::range(View::new(1), latest_complete) {
7833                        // Ensure only one payload proposed per view
7834                        let Some(payloads) = finalizes.get(&view) else {
7835                            continue;
7836                        };
7837                        if payloads.len() > 1 {
7838                            panic!("view: {view}");
7839                        }
7840                        let (digest, _) = payloads.iter().next().unwrap();
7841                        finalized.insert(view, *digest);
7842
7843                        // Only check at views below timeout
7844                        if view > latest_complete {
7845                            continue;
7846                        }
7847
7848                        // Ensure no nullifies for any finalizers
7849                        let nullifies = reporter.nullifies.lock();
7850                        let Some(nullifies) = nullifies.get(&view) else {
7851                            continue;
7852                        };
7853                        for finalizers in payloads.values() {
7854                            for finalizer in finalizers.iter() {
7855                                if nullifies.contains(finalizer) {
7856                                    panic!("should not nullify and finalize at same view");
7857                                }
7858                            }
7859                        }
7860                    }
7861                }
7862                {
7863                    let finalizations = reporter.finalizations.lock();
7864                    for view in View::range(View::new(1), latest_complete) {
7865                        // Ensure finalization matches digest from finalizes
7866                        let Some(finalization) = finalizations.get(&view) else {
7867                            continue;
7868                        };
7869                        let Some(digest) = finalized.get(&view) else {
7870                            continue;
7871                        };
7872                        assert_eq!(&finalization.proposal.payload, digest);
7873                    }
7874                }
7875            }
7876
7877            // Ensure no blocked connections
7878            let blocked = oracle.blocked().await.unwrap();
7879            assert!(blocked.is_empty());
7880
7881            // Return state for audit
7882            context.auditor().state()
7883        })
7884    }
7885
7886    // The hailstorm run must be deterministic: two runs with identical inputs
7887    // must produce identical audit state.
7888    fn hailstorm<S, F, L>(fixture: F, elector: L)
7889    where
7890        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
7891        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S> + Copy,
7892        L: elector::Config<S>,
7893    {
7894        assert_eq!(
7895            run_hailstorm::<_, _, L>(0, 10, ViewDelta::new(15), elector.clone(), fixture,),
7896            run_hailstorm::<_, _, L>(0, 10, ViewDelta::new(15), elector, fixture),
7897        );
7898    }
7899
7900    test_for_all_fixtures!(hailstorm);
7901
7902    #[test_group("slow")]
7903    #[test_traced]
7904    fn test_hailstorm_stable_leader_ed25519() {
7905        assert_eq!(
7906            run_hailstorm::<_, _, RoundRobin>(
7907                0,
7908                10,
7909                ViewDelta::new(15),
7910                RoundRobin::default().with_term(
7911                    TermLength::new(NZU32!(3)),
7912                    Duration::from_secs(12),
7913                    ViewDelta::new(0)
7914                ),
7915                ed25519::fixture
7916            ),
7917            run_hailstorm::<_, _, RoundRobin>(
7918                0,
7919                10,
7920                ViewDelta::new(15),
7921                RoundRobin::default().with_term(
7922                    TermLength::new(NZU32!(3)),
7923                    Duration::from_secs(12),
7924                    ViewDelta::new(0)
7925                ),
7926                ed25519::fixture
7927            )
7928        );
7929    }
7930
7931    /// Configuration for a Twins testing campaign.
7932    ///
7933    /// A campaign generates adversarial primary/secondary recipient-set
7934    /// scenarios, splits Byzantine participants into twin halves, and verifies
7935    /// that honest nodes still finalize blocks after the adversarial prefix
7936    /// ends.
7937    ///
7938    /// # Fields
7939    ///
7940    /// - `n`: Total participants. The number of faults is derived as
7941    ///   `N3f1::max_faults(n)`. Larger `n` increases the per-scenario
7942    ///   compromised-set space but also makes each case slower to execute.
7943    ///
7944    /// - `rounds`: Number of adversarial rounds that form the attack prefix.
7945    ///   Each round independently places participants relative to the primary
7946    ///   and secondary recipient sets: outside both, both-halves,
7947    ///   primary-only, or secondary-only. The two recipient sets may overlap;
7948    ///   a participant in `both-halves` is visible to both twins in that view.
7949    ///   After these rounds, the network becomes fully synchronous. More
7950    ///   rounds exponentially increase the canonical scenario space.
7951    ///
7952    /// - `mode`: How multi-round scenarios are constructed. `Sampled` picks
7953    ///   independent recipient sets per round; `Sustained` repeats a single
7954    ///   recipient-set pattern across all rounds (modeling a persistent
7955    ///   adversarial split).
7956    ///
7957    /// - `max_cases`: Upper bound on the total emitted cases. Each case is a
7958    ///   (scenario, compromised-assignment) pair. Also caps scenario
7959    ///   enumeration (sampling uniformly when the space is larger). Cases
7960    ///   are shuffled and truncated to this budget.
7961    ///
7962    /// - `trailing_finalizations`: Number of finalizations each honest node
7963    ///   must produce *after* the adversarial prefix before the case is
7964    ///   considered successful. This is the liveness assertion: it ensures
7965    ///   the protocol actually commits blocks under synchrony, not just
7966    ///   reaches a high view via nullifications.
7967    ///
7968    /// The term structure (length and optimistic lookahead) comes from the
7969    /// elector passed to [twins_campaign]: multi-view terms exercise the
7970    /// stable-leader finalize gate under equivocation, and a nonzero
7971    /// lookahead exercises optimistic validation.
7972    #[derive(Clone, Copy, Debug)]
7973    struct TwinsCampaign {
7974        n: u32,
7975        rounds: usize,
7976        mode: twins::Mode,
7977        max_cases: usize,
7978        trailing_finalizations: usize,
7979    }
7980
7981    fn twins_campaign<S, F, L>(
7982        rng: &mut impl CryptoRng,
7983        campaign: TwinsCampaign,
7984        elector: L,
7985        link: Link,
7986        mut fixture: F,
7987    ) where
7988        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
7989        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
7990        L: elector::Config<S>,
7991    {
7992        let n = campaign.n;
7993        let faults = N3f1::max_faults(n) as usize;
7994        let cases = twins::cases(
7995            rng,
7996            twins::Framework {
7997                participants: n as usize,
7998                faults,
7999                rounds: campaign.rounds,
8000                mode: campaign.mode,
8001                max_cases: campaign.max_cases,
8002            },
8003        );
8004        assert!(
8005            !cases.is_empty(),
8006            "twins campaign should generate at least one case"
8007        );
8008        for case in cases {
8009            let scenario = case.scenario.clone();
8010            let twin_indices = case.compromised.clone();
8011            assert_eq!(
8012                twin_indices.len(),
8013                faults,
8014                "unexpected twins count for n={n} (expected f={faults})",
8015            );
8016
8017            let view_retention = ViewDelta::new(10);
8018            let skip_timeout = Duration::from_secs(11);
8019            let namespace = b"consensus".to_vec();
8020            let link = link.clone();
8021            let trailing_finalizations = campaign.trailing_finalizations;
8022            let elector = elector.clone();
8023            let mut case_fixture =
8024                |ctx: &mut deterministic::Context, ns: &[u8], n: u32| fixture(ctx, ns, n);
8025            let rng: deterministic::BoxDynRng = Box::new(StdRng::from_rng(&mut *rng));
8026            let cfg = deterministic::Config::new().with_rng(rng);
8027            let executor = deterministic::Runner::new(cfg);
8028            executor.start(|mut context| async move {
8029                let Fixture {
8030                    participants,
8031                    schemes,
8032                    ..
8033                } = case_fixture(&mut context, &namespace, n);
8034                let participants: Arc<[_]> = participants.into();
8035                let mut oracle = start_test_network_with_peers(
8036                    context.child("network"),
8037                    participants.iter().cloned(),
8038                    false,
8039                )
8040                .await;
8041                let mut registrations = register_validators(&mut oracle, &participants).await;
8042                link_validators(&mut oracle, &participants, Action::Link(link), None).await;
8043
8044                // The elector is the single source of the term structure:
8045                // twin routing derives its term length from the built elector,
8046                // the same way the engine does.
8047                let term_length = elector
8048                    .clone()
8049                    .build(schemes[0].participants())
8050                    .terms()
8051                    .length();
8052                let elector = TwinsElector::new(
8053                    elector.clone(),
8054                    &scenario,
8055                    n as usize,
8056                );
8057                let relay = Arc::new(mocks::relay::Relay::<Sha256Digest, _>::new());
8058                let mut reporters = Vec::new();
8059                let mut engine_handlers = Vec::new();
8060                let twin_index_set: HashSet<usize> = twin_indices.iter().copied().collect();
8061
8062                // Create twin engines (f Byzantine twins).
8063                for idx in twin_indices.iter().copied() {
8064                    let validator = &participants[idx];
8065                    let (
8066                        (vote_sender, vote_receiver),
8067                        (certificate_sender, certificate_receiver),
8068                        (_resolver_sender, _resolver_receiver),
8069                    ) = registrations
8070                        .remove(validator)
8071                        .expect("validator should be registered");
8072
8073                    let make_vote_forwarder = || {
8074                        let participants = participants.clone();
8075                        let scenario = scenario.clone();
8076                        move |origin: SplitOrigin, _: &Recipients<_>, message: &IoBuf| {
8077                            let msg: Vote<S, D> = Vote::decode(message.clone()).unwrap();
8078                            let (primary, secondary) =
8079                                scenario.partitions(msg.view(), term_length, participants.as_ref());
8080                            match origin {
8081                                SplitOrigin::Primary => Some(Recipients::Some(primary)),
8082                                SplitOrigin::Secondary => Some(Recipients::Some(secondary)),
8083                            }
8084                        }
8085                    };
8086                    let make_certificate_forwarder = || {
8087                        let codec = schemes[idx].certificate_codec_config();
8088                        let participants = participants.clone();
8089                        let scenario = scenario.clone();
8090                        move |origin: SplitOrigin, _: &Recipients<_>, message: &IoBuf| {
8091                            let msg: Certificate<S, D> =
8092                                Certificate::decode_cfg(&mut message.as_ref(), &codec).unwrap();
8093                            let (primary, secondary) =
8094                                scenario.partitions(msg.view(), term_length, participants.as_ref());
8095                            match origin {
8096                                SplitOrigin::Primary => Some(Recipients::Some(primary)),
8097                                SplitOrigin::Secondary => Some(Recipients::Some(secondary)),
8098                            }
8099                        }
8100                    };
8101                    let make_vote_router = || {
8102                        let participants = participants.clone();
8103                        let scenario = scenario.clone();
8104                        move |(sender, message): &(_, IoBuf)| {
8105                            let msg: Vote<S, D> = Vote::decode(message.clone()).unwrap();
8106                            scenario.route(msg.view(), term_length, sender, participants.as_ref())
8107                        }
8108                    };
8109                    let make_certificate_router = || {
8110                        let codec = schemes[idx].certificate_codec_config();
8111                        let participants = participants.clone();
8112                        let scenario = scenario.clone();
8113                        move |(sender, message): &(_, IoBuf)| {
8114                            let msg: Certificate<S, D> =
8115                                Certificate::decode_cfg(&mut message.as_ref(), &codec).unwrap();
8116                            scenario.route(msg.view(), term_length, sender, participants.as_ref())
8117                        }
8118                    };
8119                    let (vote_sender_primary, vote_sender_secondary) =
8120                        vote_sender.split_with(make_vote_forwarder());
8121                    let (vote_receiver_primary, vote_receiver_secondary) = vote_receiver
8122                        .split_with(
8123                            context.child("pending_split").with_attribute("index", idx),
8124                            make_vote_router(),
8125                        );
8126                    let (certificate_sender_primary, certificate_sender_secondary) =
8127                        certificate_sender.split_with(make_certificate_forwarder());
8128                    let (certificate_receiver_primary, certificate_receiver_secondary) =
8129                        certificate_receiver.split_with(
8130                            context
8131                                .child("recovered_split")
8132                                .with_attribute("index", idx),
8133                            make_certificate_router(),
8134                        );
8135
8136                    for (twin_label, pending, recovered) in [
8137                        (
8138                            "primary",
8139                            (vote_sender_primary, vote_receiver_primary),
8140                            (certificate_sender_primary, certificate_receiver_primary),
8141                        ),
8142                        (
8143                            "secondary",
8144                            (vote_sender_secondary, vote_receiver_secondary),
8145                            (certificate_sender_secondary, certificate_receiver_secondary),
8146                        ),
8147                    ] {
8148                        let partition = format!("twin_{idx}_{twin_label}");
8149                        let context = context
8150                            .child("twin")
8151                            .with_attribute("index", idx)
8152                            .with_attribute("side", twin_label);
8153
8154                        let reporter_config = mocks::reporter::Config {
8155                            participants: participants.as_ref().try_into().unwrap(),
8156                            scheme: schemes[idx].clone(),
8157                            elector: elector.clone(),
8158                        };
8159                        let reporter = mocks::reporter::Reporter::new(
8160                            context.child("reporter"),
8161                            reporter_config,
8162                        );
8163                        reporters.push(reporter.clone());
8164
8165                        let application_cfg = mocks::application::Config::<Sha256, _> {
8166                            relay: relay.clone(),
8167                            me: validator.clone(),
8168                            propose_latency: (10.0, 5.0),
8169                            verify_latency: (10.0, 5.0),
8170                            certify_latency: (10.0, 5.0),
8171                            should_certify: mocks::application::Certifier::Always,
8172                        };
8173                        let (actor, application) = mocks::application::Application::new(
8174                            context.child("application"),
8175                            application_cfg,
8176                        );
8177                        actor.start();
8178
8179                        let blocker = oracle.control(validator.clone());
8180                        let cfg = config::Config {
8181                            scheme: schemes[idx].clone(),
8182                            elector: elector.clone(),
8183                            blocker,
8184                            automaton: application.clone(),
8185                            relay: application.clone(),
8186                            reporter: reporter.clone(),
8187                            strategy: Sequential,
8188                            partition,
8189                            mailbox_size: NZUsize!(1024),
8190                            epoch: Epoch::new(333),
8191                            floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
8192                                Epoch::new(333),
8193                            )),
8194                            leader_timeout: Duration::from_secs(1),
8195                            certification_timeout: Duration::from_millis(1_500),
8196                            timeout_retry: Duration::from_secs(10),
8197                            fetch_timeout: Duration::from_secs(1),
8198                            view_retention,
8199                            skip: SkipPolicy::Enabled {
8200                                timeout: skip_timeout,
8201                                budget: SkipBudget::Participants,
8202                            },
8203                            replay_buffer: NZUsize!(1024 * 1024),
8204                            write_buffer: NZUsize!(1024 * 1024),
8205                            page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
8206                            forward: ForwardPolicy::Disabled,
8207                            track_historical_votes: false,
8208                        };
8209                        let engine = Engine::new(context.child("engine"), cfg);
8210                        engine_handlers.push(engine.start(
8211                            pending,
8212                            recovered,
8213                            inert_channel(participants.as_ref()),
8214                        ));
8215                    }
8216                }
8217
8218                // Create honest engines.
8219                let honest_start = reporters.len();
8220                for (idx, validator) in participants.iter().enumerate() {
8221                    if twin_index_set.contains(&idx) {
8222                        continue;
8223                    }
8224
8225                    let partition = format!("honest_{idx}");
8226                    let context = context.child("honest").with_attribute("index", idx);
8227
8228                    let reporter_config = mocks::reporter::Config {
8229                        participants: participants.as_ref().try_into().unwrap(),
8230                        scheme: schemes[idx].clone(),
8231                        elector: elector.clone(),
8232                    };
8233                    let reporter =
8234                        mocks::reporter::Reporter::new(context.child("reporter"), reporter_config);
8235                    reporters.push(reporter.clone());
8236
8237                    let application_cfg = mocks::application::Config::<Sha256, _> {
8238                        relay: relay.clone(),
8239                        me: validator.clone(),
8240                        propose_latency: (10.0, 5.0),
8241                        verify_latency: (10.0, 5.0),
8242                        certify_latency: (10.0, 5.0),
8243                        should_certify: mocks::application::Certifier::Always,
8244                    };
8245                    let (actor, application) = mocks::application::Application::new(
8246                        context.child("application"),
8247                        application_cfg,
8248                    );
8249                    actor.start();
8250
8251                    let blocker = oracle.control(validator.clone());
8252                    let cfg = config::Config {
8253                        scheme: schemes[idx].clone(),
8254                        elector: elector.clone(),
8255                        blocker,
8256                        automaton: application.clone(),
8257                        relay: application.clone(),
8258                        reporter: reporter.clone(),
8259                        strategy: Sequential,
8260                        partition,
8261                        mailbox_size: NZUsize!(1024),
8262                        epoch: Epoch::new(333),
8263                        floor: config::Floor::Genesis(mocks::application::genesis::<Sha256>(
8264                            Epoch::new(333),
8265                        )),
8266                        leader_timeout: Duration::from_secs(1),
8267                        certification_timeout: Duration::from_millis(1_500),
8268                        timeout_retry: Duration::from_secs(10),
8269                        fetch_timeout: Duration::from_secs(1),
8270                        view_retention,
8271                        skip: SkipPolicy::Enabled {
8272                            timeout: skip_timeout,
8273                            budget: SkipBudget::Participants,
8274                        },
8275                        replay_buffer: NZUsize!(1024 * 1024),
8276                        write_buffer: NZUsize!(1024 * 1024),
8277                        page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
8278                        forward: ForwardPolicy::Disabled,
8279                        track_historical_votes: false,
8280                    };
8281                    let engine = Engine::new(context.child("engine"), cfg);
8282
8283                    let (
8284                        (pending_sender, pending_receiver),
8285                        (recovered_sender, recovered_receiver),
8286                        _,
8287                    ) = registrations
8288                        .remove(validator)
8289                        .expect("validator should be registered");
8290                    engine_handlers.push(engine.start(
8291                        (pending_sender, pending_receiver),
8292                        (recovered_sender, recovered_receiver),
8293                        inert_channel(participants.as_ref()),
8294                    ));
8295                }
8296
8297                // Wait for progress (liveness check) across honest replicas only.
8298                //
8299                // Only count finalizations after the adversarial prefix so we
8300                // verify the protocol actually recovers and makes progress under
8301                // synchrony. Finalizations during the prefix may be artifacts of
8302                // the attack setup and do not demonstrate liveness.
8303                //
8304                // Twin halves are Byzantine test machinery and are not required to
8305                // make progress for the campaign to establish honest-node liveness.
8306                //
8307                // Each scripted round drives one full leader term, so the
8308                // adversarial prefix spans `rounds * term_length` views.
8309                let prefix_end = View::new(scenario.rounds().len() as u64 * term_length.get());
8310                let mut finalizers = Vec::new();
8311                for (i, reporter) in reporters.iter_mut().skip(honest_start).enumerate() {
8312                    let (_latest, mut monitor) = reporter.subscribe().await;
8313                    let required = trailing_finalizations;
8314                    finalizers.push(context.child("finalizer").with_attribute("index", i).spawn(
8315                        move |_| async move {
8316                            let mut count = 0usize;
8317                            while count < required {
8318                                let view = monitor.recv().await.expect("event missing");
8319                                if view > prefix_end {
8320                                    count += 1;
8321                                }
8322                            }
8323                        },
8324                    ));
8325                }
8326                join_all(finalizers).await;
8327
8328                // Verify safety: no conflicting finalizations across honest reporters.
8329                let mut finalized_at_view: BTreeMap<View, D> = BTreeMap::new();
8330                for reporter in reporters.iter().skip(honest_start) {
8331                    let finalizations = reporter.finalizations.lock();
8332                    for (view, finalization) in finalizations.iter() {
8333                        let digest = finalization.proposal.payload;
8334                        if let Some(existing) = finalized_at_view.get(view) {
8335                            assert_eq!(
8336                                existing, &digest,
8337                                "safety violation: conflicting finalizations at view {view}"
8338                            );
8339                        } else {
8340                            finalized_at_view.insert(*view, digest);
8341                        }
8342                    }
8343                }
8344
8345                // Verify no invalid signatures were observed by honest replicas.
8346                for reporter in reporters.iter().skip(honest_start) {
8347                    reporter.assert_no_invalid();
8348                }
8349
8350                // Ensure no honest signer appears under multiple payloads for the same view.
8351                let twin_keys: HashSet<_> = twin_indices
8352                    .iter()
8353                    .map(|idx| participants[*idx].clone())
8354                    .collect();
8355                let mut notarized_by_signer: BTreeMap<View, HashMap<PublicKey, D>> =
8356                    BTreeMap::new();
8357                let mut finalized_by_signer: BTreeMap<View, HashMap<PublicKey, D>> =
8358                    BTreeMap::new();
8359                for reporter in reporters.iter().skip(honest_start) {
8360                    let notarizes = reporter.notarizes.lock();
8361                    for (view, payloads) in notarizes.iter() {
8362                        let signers = notarized_by_signer.entry(*view).or_default();
8363                        for (digest, payload_signers) in payloads.iter() {
8364                            for signer in payload_signers.iter() {
8365                                if twin_keys.contains(signer) {
8366                                    continue;
8367                                }
8368                                if let Some(existing) = signers.insert(signer.clone(), *digest) {
8369                                    assert_eq!(
8370                                    existing, *digest,
8371                                    "honest signer produced conflicting notarizes at view {view}"
8372                                );
8373                                }
8374                            }
8375                        }
8376                    }
8377
8378                    let finalizes = reporter.finalizes.lock();
8379                    for (view, payloads) in finalizes.iter() {
8380                        let signers = finalized_by_signer.entry(*view).or_default();
8381                        for (digest, payload_signers) in payloads.iter() {
8382                            for signer in payload_signers.iter() {
8383                                if twin_keys.contains(signer) {
8384                                    continue;
8385                                }
8386                                if let Some(existing) = signers.insert(signer.clone(), *digest) {
8387                                    assert_eq!(
8388                                    existing, *digest,
8389                                    "honest signer produced conflicting finalizes at view {view}"
8390                                );
8391                                }
8392                            }
8393                        }
8394                    }
8395                }
8396
8397                // Ensure faults are attributable to twins.
8398                for reporter in reporters.iter().skip(honest_start) {
8399                    let faults = reporter.faults.lock();
8400                    for faulter in faults.keys() {
8401                        assert!(
8402                            twin_keys.contains(faulter),
8403                            "fault from non-twin participant"
8404                        );
8405                    }
8406                }
8407
8408                let blocked = oracle.blocked().await.unwrap();
8409                for (_, faulter) in blocked {
8410                    assert!(
8411                        twin_keys.contains(&faulter),
8412                        "blocked peer attributed to non-twin participant"
8413                    );
8414                }
8415            });
8416        }
8417    }
8418
8419    const TWINS_CAMPAIGN: TwinsCampaign = TwinsCampaign {
8420        n: 5,
8421        rounds: 3,
8422        mode: twins::Mode::Sampled,
8423        max_cases: 20,
8424        trailing_finalizations: 10,
8425    };
8426
8427    const TWINS_LINK: Link = Link {
8428        latency: Duration::from_millis(500),
8429        jitter: Duration::from_millis(500),
8430        success_rate: probability!(1.0),
8431    };
8432
8433    /// Runs `campaign` with `elector` over a fast link and the slow [TWINS_LINK].
8434    fn twins_campaign_all_links(campaign: TwinsCampaign, elector: RoundRobin) {
8435        for link in [
8436            Link {
8437                latency: Duration::from_millis(10),
8438                jitter: Duration::from_millis(10),
8439                success_rate: probability!(1.0),
8440            },
8441            TWINS_LINK,
8442        ] {
8443            twins_campaign::<_, _, RoundRobin>(
8444                &mut test_rng(),
8445                campaign,
8446                elector.clone(),
8447                link,
8448                scheme_mocks::fixture,
8449            );
8450        }
8451    }
8452
8453    #[test_group("slow")]
8454    #[test_traced("INFO")]
8455    fn test_twins_sampled() {
8456        twins_campaign_all_links(TWINS_CAMPAIGN, RoundRobin::default());
8457    }
8458
8459    #[test_group("slow")]
8460    #[test_traced("INFO")]
8461    fn test_twins_sustained() {
8462        twins_campaign_all_links(
8463            TwinsCampaign {
8464                mode: twins::Mode::Sustained,
8465                ..TWINS_CAMPAIGN
8466            },
8467            RoundRobin::default(),
8468        );
8469    }
8470
8471    #[test_group("slow")]
8472    #[test_traced("INFO")]
8473    fn test_twins_stable_leader() {
8474        twins_campaign_all_links(
8475            TWINS_CAMPAIGN,
8476            RoundRobin::default().with_term(
8477                TermLength::new(NZU32!(3)),
8478                Duration::from_secs(12),
8479                ViewDelta::new(0),
8480            ),
8481        );
8482    }
8483
8484    #[test_group("slow")]
8485    #[test_traced("INFO")]
8486    fn test_twins_stable_leader_optimistic() {
8487        twins_campaign_all_links(
8488            TWINS_CAMPAIGN,
8489            RoundRobin::default().with_term(
8490                TermLength::new(NZU32!(3)),
8491                Duration::from_secs(12),
8492                ViewDelta::new(2),
8493            ),
8494        );
8495    }
8496
8497    #[test_group("slow")]
8498    #[test_traced("INFO")]
8499    fn test_twins_large_sampled() {
8500        let campaign = TwinsCampaign {
8501            n: 10,
8502            rounds: 5,
8503            ..TWINS_CAMPAIGN
8504        };
8505        twins_campaign::<_, _, RoundRobin>(
8506            &mut test_rng(),
8507            campaign,
8508            RoundRobin::default(),
8509            TWINS_LINK,
8510            scheme_mocks::fixture,
8511        );
8512    }
8513
8514    #[test_group("slow")]
8515    #[test_traced("INFO")]
8516    fn test_twins_large_sustained() {
8517        let campaign = TwinsCampaign {
8518            n: 10,
8519            rounds: 5,
8520            mode: twins::Mode::Sustained,
8521            ..TWINS_CAMPAIGN
8522        };
8523        twins_campaign::<_, _, RoundRobin>(
8524            &mut test_rng(),
8525            campaign,
8526            RoundRobin::default(),
8527            TWINS_LINK,
8528            scheme_mocks::fixture,
8529        );
8530    }
8531
8532    fn twins<S, F, L>(fixture: F, elector: L)
8533    where
8534        S: Scheme<Sha256Digest, PublicKey = PublicKey>,
8535        F: FnMut(&mut deterministic::Context, &[u8], u32) -> Fixture<S>,
8536        L: elector::Config<S>,
8537    {
8538        twins_campaign::<_, _, L>(
8539            &mut test_rng(),
8540            TWINS_CAMPAIGN,
8541            elector,
8542            TWINS_LINK,
8543            fixture,
8544        );
8545    }
8546
8547    test_for_all_fixtures!(twins, level = "INFO");
8548
8549    #[test]
8550    fn test_viewport() {
8551        let viewport = Viewport {
8552            finalized: View::new(20),
8553            current: View::new(25),
8554            view_retention: ViewDelta::new(10),
8555            lookahead: Lookahead {
8556                term_length: TermLength::new(commonware_utils::NZU32!(10)),
8557                optimistic_views: ViewDelta::new(0),
8558            },
8559        };
8560
8561        // Genesis is never tracked
8562        assert!(!viewport.retains(View::zero()));
8563
8564        // Retention floor is view_retention below finalized
8565        assert_eq!(viewport.floor(), View::new(10));
8566        assert!(!viewport.retains(View::new(9)));
8567        assert!(viewport.retains(View::new(10)));
8568
8569        // Votes are admitted up to the next view or the next term start
8570        assert!(viewport.admits_vote(View::new(10)));
8571        assert!(viewport.admits_vote(View::new(25)));
8572        assert!(viewport.admits_vote(View::new(26)));
8573        assert!(viewport.admits_vote(View::new(31)));
8574        assert!(!viewport.admits_vote(View::new(5)));
8575        assert!(!viewport.admits_vote(View::new(27)));
8576        assert!(!viewport.admits_vote(View::new(34)));
8577
8578        // Certificates are admitted from arbitrarily far ahead but still
8579        // respect the retention floor
8580        assert!(!viewport.admits_certificate(View::new(9)));
8581        assert!(viewport.admits_certificate(View::new(10_000)));
8582
8583        // With an optimistic window, votes are additionally admitted up to
8584        // `optimistic_views` ahead within the current term
8585        let optimistic = Viewport {
8586            lookahead: Lookahead {
8587                optimistic_views: ViewDelta::new(2),
8588                ..viewport.lookahead
8589            },
8590            ..viewport
8591        };
8592        assert!(optimistic.admits_vote(View::new(26)));
8593        assert!(optimistic.admits_vote(View::new(27)));
8594        assert!(!optimistic.admits_vote(View::new(28)));
8595        assert!(optimistic.admits_vote(View::new(31)));
8596
8597        // The window never crosses the term boundary (term [21, 30])
8598        let term_edge = Viewport {
8599            current: View::new(29),
8600            lookahead: Lookahead {
8601                optimistic_views: ViewDelta::new(5),
8602                ..viewport.lookahead
8603            },
8604            ..viewport
8605        };
8606        assert!(term_edge.admits_vote(View::new(30)));
8607        assert!(term_edge.admits_vote(View::new(31)));
8608        assert!(!term_edge.admits_vote(View::new(32)));
8609    }
8610}