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