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}