Skip to main content

commonware_consensus/simplex/
config.rs

1use super::{
2    elector,
3    types::{Activity, Context, Finalization},
4};
5use crate::{
6    CertifiableAutomaton, Epochable, Relay, Reporter, Viewable,
7    types::{Epoch, View, ViewDelta},
8};
9use commonware_cryptography::{Digest, certificate::Scheme};
10use commonware_p2p::Blocker;
11use commonware_parallel::Strategy;
12use commonware_runtime::buffer::paged::CacheRef;
13use commonware_utils::NonZeroDuration;
14use rand_core::CryptoRng;
15use std::{
16    num::{NonZeroU64, NonZeroUsize},
17    time::Duration,
18};
19
20/// Selects the maximum number of unfinalized terms that may be skipped.
21#[derive(Debug, Clone, Copy, Default)]
22pub enum SkipBudget {
23    /// Uses the participant count as the budget.
24    #[default]
25    Participants,
26    /// Uses the specified budget.
27    Fixed(NonZeroU64),
28}
29
30impl SkipBudget {
31    /// Resolves the configured budget for a participant count.
32    pub(crate) const fn resolve(self, participants: usize) -> u64 {
33        match self {
34            Self::Participants => participants as u64,
35            Self::Fixed(budget) => budget.get(),
36        }
37    }
38}
39
40/// Controls whether `nullify(v)` may be broadcast before the normal round deadlines.
41///
42/// Normal round deadlines remain active when the policy does not permit a skip.
43#[derive(Debug, Clone, Copy)]
44pub enum SkipPolicy {
45    /// Disables skips.
46    Disabled,
47    /// Enables skips under the configured timeout and budget.
48    Enabled {
49        /// Duration after which an inactive leader may trigger a skip.
50        ///
51        /// This timeout must be greater than the certification timeout and timeout retry.
52        timeout: Duration,
53        /// Maximum number of unfinalized terms that may be skipped.
54        budget: SkipBudget,
55    },
56}
57
58/// Controls whether and how the engine proactively forwards blocks when
59/// entering the next view.
60///
61/// Forwarding is a best-effort liveness aid. When enabled, the batcher
62/// broadcasts on entering the next view for a proposal that is finalized, or
63/// notarized without a failed certification. Targets are chosen from votes observed
64/// locally, so a certificate signer whose vote never reached us still counts
65/// as silent.
66#[derive(Debug, Clone, Copy)]
67pub enum ForwardPolicy {
68    /// Do nothing when a proposal becomes eligible for forwarding.
69    Disabled,
70    /// Forward the block to all participants whose matching vote was not
71    /// observed locally.
72    ///
73    /// To only send to the leader of the newly entered view, see [ForwardPolicy::SilentLeader].
74    SilentVoters,
75    /// Forward the block to the leader of the newly entered view if the
76    /// leader's matching vote was not observed locally.
77    ///
78    /// To forward to all such participants, see [ForwardPolicy::SilentVoters].
79    SilentLeader,
80}
81
82impl ForwardPolicy {
83    /// Returns true if the policy is enabled.
84    pub const fn is_enabled(&self) -> bool {
85        !matches!(self, Self::Disabled)
86    }
87}
88
89/// The certified root from which a Simplex instance starts.
90///
91/// The voter records a finalized floor in its journal, so a later start over the
92/// same partition resumes from the highest floor or finalization recorded there
93/// even if it is configured with a lower floor. Keep the journal intact across
94/// restarts. A lower floor then neither rolls the voter back nor re-enters views
95/// whose vote records were pruned.
96#[derive(Clone, Debug)]
97pub enum Floor<S: Scheme, D: Digest> {
98    /// Start from the epoch genesis payload at view 0.
99    Genesis(D),
100    /// Start from an already-finalized proposal.
101    Finalized(Finalization<S, D>),
102}
103
104impl<S: Scheme, D: Digest> Floor<S, D> {
105    /// The finalized view the engine starts from (`View::zero()` for genesis).
106    pub(crate) fn view(&self) -> View {
107        match self {
108            Self::Genesis(_) => View::zero(),
109            Self::Finalized(finalization) => finalization.view(),
110        }
111    }
112
113    fn assert<Rng>(&self, epoch: Epoch, rng: &mut Rng, scheme: &S, strategy: &impl Strategy)
114    where
115        Rng: CryptoRng,
116        S: super::scheme::Scheme<D>,
117    {
118        if let Self::Finalized(finalization) = self {
119            assert_eq!(
120                finalization.epoch(),
121                epoch,
122                "floor finalization must be in the configured epoch"
123            );
124            assert!(
125                !finalization.view().is_zero(),
126                "use Floor::Genesis for the genesis view"
127            );
128            assert!(
129                finalization.verify(rng, scheme, strategy),
130                "floor finalization must verify"
131            );
132        }
133    }
134}
135
136/// Configuration for the consensus engine.
137pub struct Config<S, L, B, D, A, R, F, T>
138where
139    S: Scheme,
140    L: elector::Config<S>,
141    B: Blocker<PublicKey = S::PublicKey>,
142    D: Digest,
143    A: CertifiableAutomaton<Context = Context<D, S::PublicKey>>,
144    R: Relay,
145    F: Reporter<Activity = Activity<S, D>>,
146    T: Strategy,
147{
148    /// Signing scheme for the consensus engine.
149    ///
150    /// Consensus messages can be signed with a cryptosystem that differs from the static
151    /// participant identity keys exposed in `participants`. For example, we can authenticate peers
152    /// on the network with [commonware_cryptography::ed25519] keys while signing votes with shares distributed
153    /// via [commonware_cryptography::bls12381::dkg] (which change each epoch). The scheme implementation is
154    /// responsible for reusing the exact participant ordering carried by `participants` so that signer indices
155    /// remain stable across both key spaces; if the order diverges, validators will reject votes as coming from
156    /// the wrong validator.
157    ///
158    /// Schemes must provide deterministic signatures (a participant must produce the same
159    /// signature encoding every time it signs the same subject) because Simplex compares
160    /// encoded votes when detecting equivocation.
161    pub scheme: S,
162
163    /// Leader election configuration.
164    ///
165    /// Determines how leaders are selected for each view. Built-in options include
166    /// [`RoundRobin`](super::elector::RoundRobin) for deterministic rotation and
167    /// [`Random`](super::elector::Random) for unpredictable selection using BLS
168    /// threshold signatures.
169    pub elector: L,
170
171    /// Blocker for the network.
172    ///
173    /// Blocking is handled by [commonware_p2p].
174    pub blocker: B,
175
176    /// Automaton for the consensus engine.
177    pub automaton: A,
178
179    /// Relay for the consensus engine.
180    pub relay: R,
181
182    /// Reporter for the consensus engine.
183    ///
184    /// Activity is exported for every tracked view, including votes that arrive up to
185    /// `view_retention` views below the highest finalized view; votes below that window
186    /// are dropped without being reported. Reported votes are not guaranteed to be
187    /// verified (see [`crate::simplex::types::Activity`]). Consider wrapping with
188    /// [`crate::simplex::scheme::reporter::AttributableReporter`] to automatically filter
189    /// and verify activities based on scheme attributability.
190    ///
191    /// Locally constructed votes are exported only after their journal entries are
192    /// durable. Network votes are not persisted, so an equivocating sender may have
193    /// different votes exported before and after a restart.
194    pub reporter: F,
195
196    /// Track individual votes after certification and for skipped views.
197    ///
198    /// By default, full vote evidence is released once the corresponding certificate
199    /// is constructed or received, and votes for views this node skipped are only
200    /// reported and deduplicated, so later conflict reporting and peer blocking are
201    /// best effort. Enabling this retains each recorded vote until its round is
202    /// pruned, increasing memory usage.
203    pub track_historical_votes: bool,
204
205    /// Strategy for parallel operations.
206    pub strategy: T,
207
208    /// Partition for the consensus engine.
209    ///
210    /// The partition must be unique per epoch, since journal artifacts written under
211    /// another epoch are not valid state for this engine.
212    pub partition: String,
213
214    /// Maximum number of messages held in the ready queue of each mailbox inside the
215    /// consensus engine. Enqueueing never blocks, and once the ready queue is full the
216    /// mailbox overflow policy applies.
217    pub mailbox_size: NonZeroUsize,
218
219    /// Epoch for the consensus engine. Each running engine should have a unique epoch.
220    pub epoch: Epoch,
221
222    /// Certified root for the consensus engine.
223    pub floor: Floor<S, D>,
224
225    /// Number of bytes to buffer when replaying during startup.
226    pub replay_buffer: NonZeroUsize,
227
228    /// The size of the write buffer to use for each blob in the journal.
229    pub write_buffer: NonZeroUsize,
230
231    /// Page cache for the journal.
232    pub page_cache: CacheRef,
233
234    /// Amount of time to wait for a leader to propose a payload
235    /// in a view.
236    pub leader_timeout: Duration,
237
238    /// Amount of time to wait for certification progress in a view
239    /// before attempting to skip the view.
240    ///
241    /// This timeout must be greater than the leader timeout.
242    pub certification_timeout: Duration,
243
244    /// Amount of time to wait before retrying a nullify broadcast if
245    /// stuck in a view.
246    pub timeout_retry: Duration,
247
248    /// Number of views behind the finalized tip to track (in memory and in the
249    /// journal) for recent activity.
250    pub view_retention: ViewDelta,
251
252    /// Policy governing whether `nullify(v)` may be broadcast before the normal round deadlines.
253    pub skip: SkipPolicy,
254
255    /// Timeout to wait for a peer to respond to a request.
256    pub fetch_timeout: NonZeroDuration,
257
258    /// Delay after an unresolved attempt before retrying its fetch.
259    /// Zero permits immediate retries, subject to the sender's rate limit.
260    pub fetch_retry_delay: Duration,
261
262    /// Policy for proactively forwarding blocks when entering the next view.
263    pub forward: ForwardPolicy,
264}
265
266impl<
267    S: Scheme,
268    L: elector::Config<S>,
269    B: Blocker<PublicKey = S::PublicKey>,
270    D: Digest,
271    A: CertifiableAutomaton<Context = Context<D, S::PublicKey>>,
272    R: Relay,
273    F: Reporter<Activity = Activity<S, D>>,
274    T: Strategy,
275> Config<S, L, B, D, A, R, F, T>
276{
277    /// Assert enforces that all configuration values are valid.
278    ///
279    /// The RNG is used to verify finalized floor certificates.
280    pub fn assert<Rng>(&self, rng: &mut Rng)
281    where
282        Rng: CryptoRng,
283        S: super::scheme::Scheme<D>,
284    {
285        assert!(
286            !self.scheme.participants().is_empty(),
287            "there must be at least one participant"
288        );
289
290        // Vote-to-nullify timeouts.
291        // certification_timeout > leader_timeout > 0.
292        // skip timeout > certification_timeout and timeout_retry, when enabled.
293        assert!(
294            self.leader_timeout > Duration::default(),
295            "leader timeout must be greater than zero"
296        );
297        assert!(
298            self.certification_timeout > self.leader_timeout,
299            "certification timeout must be greater than leader timeout"
300        );
301
302        if let SkipPolicy::Enabled { timeout, .. } = self.skip {
303            assert!(
304                timeout > self.certification_timeout,
305                "skip timeout must be greater than certification timeout"
306            );
307            assert!(
308                timeout > self.timeout_retry,
309                "skip timeout must be greater than timeout retry"
310            );
311        }
312        assert!(
313            self.timeout_retry > Duration::default(),
314            "timeout retry broadcast must be greater than zero"
315        );
316        assert!(
317            !self.view_retention.is_zero(),
318            "view retention timeout must be greater than zero"
319        );
320        self.floor
321            .assert(self.epoch, rng, &self.scheme, &self.strategy);
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::SkipBudget;
328    use std::num::NonZeroU64;
329
330    #[test]
331    fn skip_budget_resolves() {
332        assert_eq!(SkipBudget::default().resolve(4), 4);
333        assert_eq!(SkipBudget::Participants.resolve(7), 7);
334        assert_eq!(SkipBudget::Fixed(NonZeroU64::new(9).unwrap()).resolve(4), 9);
335    }
336}