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}