Skip to main content

tendermint_machine/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![deny(missing_docs)]
4#![no_std]
5
6#[cfg(feature = "alloc")]
7extern crate alloc;
8#[cfg(feature = "std")]
9extern crate std;
10
11use core::{time::Duration, future::Future};
12
13mod or;
14use or::*;
15
16mod timeout;
17use timeout::*;
18
19mod validators;
20pub use validators::*;
21
22mod commit;
23pub use commit::*;
24
25mod blockchain;
26pub use blockchain::*;
27
28mod message;
29pub use message::*;
30
31mod slash_reason;
32pub use slash_reason::*;
33
34#[cfg(feature = "std")]
35mod state;
36#[cfg(feature = "std")]
37use state::*;
38
39/// A view over the network used for consensus.
40pub trait Network<V: Validator, S: Signature, A: AggregateSignature, B: Block> {
41  /// The expected average-case latency of the network.
42  ///
43  /// This should be sufficient for a supermajority of validators to communicate, in the average
44  /// case, as necessary to perform the consensus protocol. If the real-world latency exceeds this
45  /// amount of time, the attempt to finalize a block will fail, and a new round will begin with an
46  /// increased amount of latency. Accordingly, setting this to too optimistic of a value will
47  /// increase the amount of failures.
48  ///
49  /// The derivatives of this MAY be limited to the bounds of the internal representations used for
50  /// time.
51  const LATENCY_TIME: Duration;
52
53  /// The expected maximum amount of time to download a block.
54  ///
55  /// The derivatives of this MAY be limited to the bounds of the internal representations used for
56  /// time.
57  const BLOCK_DOWNLOADING_TIME: Duration;
58
59  /// The expected maximum amount of time to process a block.
60  ///
61  /// This is also used as a timeout for fetching the block proposal, if it isn't ready yet.
62  ///
63  /// The derivatives of this MAY be limited to the bounds of the internal representations used for
64  /// time.
65  const BLOCK_PROCESSING_TIME: Duration;
66
67  /// The future returned by [`Network::sleep`].
68  type Sleep: Future<Output = ()>;
69
70  /// An asynchronous implementation of [`std::thread::sleep`].
71  ///
72  /// This MUST be cancel-safe.
73  fn sleep(duration: Duration) -> Self::Sleep;
74
75  /// Broadcast a message to the other validators.
76  ///
77  /// Examples of broadcast include sending this message via peer-to-peer channels to all other
78  /// validators, or publishing this message to a gossip network where it can be reasonably assumed
79  /// to be delivered to all other validators. It does not have to achieve a formal definition of
80  /// broadcast with any specific security properties, though failure for this message to be
81  /// delivered to a sufficient amount of other validators in a timely fashion will cause consensus
82  /// to stall until the timeout for a round exceeds the latency of this network.
83  ///
84  /// This DOES NOT have to be cancel-safe.
85  fn broadcast(&mut self, message: Message<V, S, A, B>) -> impl Send + Future<Output = ()>;
86}
87
88#[cfg(feature = "std")]
89mod tendermint {
90  use core::{
91    sync::atomic::{Ordering, AtomicU64},
92    future::Future,
93    num::NonZero,
94  };
95  use alloc::sync::Arc;
96
97  /*
98    We use `async-channel`, not `futures-channel`, as neither document/guarantee cancel safety.
99    However, `async-channel` has [this issue](https://github.com/smol-rs/async-channel/issues/111)
100    for which notgull confirms [`async_channel::Recv`] is cancel-safe:
101
102    https://github.com/smol-rs/async-channel/issues/111#issuecomment-3459124415
103
104    We expect this property, causing us to use `async-channel`.
105  */
106  use async_channel::{Sender, Receiver};
107
108  use borsh::{BorshSerialize, BorshDeserialize};
109  use serai_db::{Transaction as _, Db};
110
111  use crate::{
112    BlockNumber, ValidatorSet as _, SignatureScheme, Block, CommitFor, Blockchain, MessageFor,
113    MessageError, Signer, Network, State, Or,
114  };
115
116  /// The Tendermint process.
117  ///
118  /// This is expected to drive the underlying blockchain, adding blocks to it as produced by
119  /// itself _and as received from an external sync loop_ (which is not implemented here).
120  pub struct Tendermint;
121
122  /// The handle for the Tendermint process.
123  pub struct TendermintHandle<B: Blockchain> {
124    block_number: Arc<AtomicU64>,
125    observed_block_number: Arc<AtomicU64>,
126    sync: Sender<(B::Block, CommitFor<B>)>,
127    message: Sender<MessageFor<B>>,
128  }
129
130  struct TendermintProcess<B: Blockchain, S, D, N> {
131    sync: Receiver<(B::Block, CommitFor<B>)>,
132    message: Receiver<MessageFor<B>>,
133
134    state: State<B>,
135    blockchain: B,
136    signer: S,
137    db: D,
138    network: N,
139  }
140
141  impl<
142      B: Blockchain<
143        Validator: BorshSerialize + BorshDeserialize,
144        SignatureScheme: SignatureScheme<
145          Signature: BorshSerialize + BorshDeserialize,
146          AggregateSignature: BorshSerialize + BorshDeserialize,
147        >,
148        Block: BorshSerialize + BorshDeserialize + Block<Hash: BorshSerialize + BorshDeserialize>,
149      >,
150      S: Signer<
151        Validator = B::Validator,
152        Signature = <B::SignatureScheme as SignatureScheme>::Signature,
153      >,
154      D: Db,
155      N: Network<
156        B::Validator,
157        <B::SignatureScheme as SignatureScheme>::Signature,
158        <B::SignatureScheme as SignatureScheme>::AggregateSignature,
159        B::Block,
160      >,
161    > TendermintProcess<B, S, D, N>
162  {
163    async fn run(mut self) {
164      loop {
165        /*
166          We use our `Or` future to implement a select, where our `Or` is documented to prefer `f1`
167          to `f2`, causing these to be polled in the order they're written.
168
169          Receiving from the channels is cancel-safe, as is our own future to check for timeouts.
170        */
171        let tick = (Or {
172          f1: Or {
173            // Check for a synced block, which makes this entire consensus process immaterial
174            f1: self.sync.recv(),
175            /*
176              Check if any timeouts have expired.
177
178              We give this priority so if we reload from the database, this is consistently handled
179              independent of any messages are available.
180            */
181            f2: self.state.timeout::<N, _>(&self.blockchain, &self.signer),
182          },
183          // Check for a received message, which may advance this consensus process
184          f2: self.message.recv(),
185        })
186        .await;
187
188        match tick {
189          crate::or::Either::L(crate::or::Either::L(Ok((ref block, ref commit)))) => {
190            // TODO: Remove these `Clone`s
191            let block = block.clone();
192            let commit = commit.clone();
193            drop(tick);
194
195            if commit.block_number != self.state.block_number() {
196              continue;
197            }
198            if !commit.verify(
199              self.blockchain.validator_set(),
200              self.blockchain.signature_scheme(),
201              self.blockchain.genesis(),
202              block.hash(),
203            ) {
204              continue;
205            }
206
207            let mut txn = self.db.txn();
208            let messages = self
209              .state
210              .commit::<N>(&mut self.blockchain, &self.signer, &mut txn, block, commit)
211              .await;
212            txn.commit();
213
214            for message in messages {
215              self.network.broadcast(message).await;
216            }
217
218            continue;
219          }
220
221          /*
222            We do not have to interact with the database here as, on reboot, these timeouts will
223            immediately expire and cause this behavior to occur once again (as this future has
224            priority over incoming messages).
225          */
226          crate::or::Either::L(crate::or::Either::R(timeout)) => {
227            let mut txn = self.db.txn();
228            let messages = timeout.respond::<N>(&mut txn).await;
229            txn.commit();
230
231            for message in messages {
232              self.network.broadcast(message).await;
233            }
234          }
235
236          crate::or::Either::R(Ok(ref message)) => {
237            // TODO: Remove this `Clone`
238            let message = message.clone();
239            drop(tick);
240
241            let mut txn = self.db.txn();
242            let validator = message.validator;
243            match self.state.message::<N>(&self.blockchain, &self.signer, &mut txn, message).await {
244              Ok(messages) => {
245                txn.commit();
246                for message in messages {
247                  self.network.broadcast(message).await;
248                }
249              }
250              Err(MessageError::Invalid(slash_reason)) => {
251                self.blockchain.slash(validator, slash_reason);
252                continue;
253              }
254              Err(
255                MessageError::Stale |
256                MessageError::Future |
257                MessageError::NotValidator |
258                MessageError::InvalidOuterSignature |
259                MessageError::AlreadyHandled,
260              ) => continue,
261            }
262          }
263          // If our channels have been closed, terminate the consensus process
264          crate::or::Either::L(crate::or::Either::L(Err(async_channel::RecvError))) |
265          crate::or::Either::R(Err(async_channel::RecvError)) => return,
266        }
267
268        // Attempt to form a commit
269        {
270          let mut txn = self.db.txn();
271          let messages =
272            self.state.attempt_commit::<N>(&mut self.blockchain, &self.signer, &mut txn).await;
273          txn.commit();
274
275          for message in messages {
276            self.network.broadcast(message).await;
277          }
278        }
279      }
280    }
281  }
282
283  /// The Tendermint process was terminated.
284  ///
285  /// This occurs when the future terminates, either due to being itself dropped or due to its
286  /// handle being dropped.
287  #[derive(Debug)]
288  pub struct ProcessTerminated;
289
290  impl Tendermint {
291    async fn internal<
292      B: Blockchain<
293        Validator: BorshSerialize + BorshDeserialize,
294        SignatureScheme: SignatureScheme<
295          Signature: BorshSerialize + BorshDeserialize,
296          AggregateSignature: BorshSerialize + BorshDeserialize,
297        >,
298        Block: BorshSerialize + BorshDeserialize + Block<Hash: BorshSerialize + BorshDeserialize>,
299      >,
300      S: Signer<
301        Validator = B::Validator,
302        Signature = <B::SignatureScheme as SignatureScheme>::Signature,
303      >,
304      D: Db,
305      N: Network<
306        B::Validator,
307        <B::SignatureScheme as SignatureScheme>::Signature,
308        <B::SignatureScheme as SignatureScheme>::AggregateSignature,
309        B::Block,
310      >,
311    >(
312      blockchain: B,
313      signer: S,
314      mut db: D,
315      mut network: N,
316      proposal: B::Block,
317    ) -> (TendermintHandle<B>, TendermintProcess<B, S, D, N>) {
318      // Sync blocks with a capacity of the amount of blocks per 5 minutes
319      let (sync_send, sync_recv) = async_channel::bounded({
320        use core::time::Duration;
321        let time_per_block = N::BLOCK_DOWNLOADING_TIME
322          .saturating_add(N::BLOCK_PROCESSING_TIME)
323          .saturating_add(N::LATENCY_TIME.saturating_mul(3));
324        let blocks_per_minute =
325          Duration::from_mins(5).as_millis().div_ceil(time_per_block.as_millis().max(1));
326        // Limit this to a sane range
327        usize::try_from(blocks_per_minute).unwrap_or(usize::MAX).clamp(1, 64)
328      });
329
330      /*
331        Limit the amount of messages proportionally to the amount of validators.
332
333        So long as validators are honest and only send one message within a period, this bound
334        will only be hit if either:
335        1) Messages aren't drained faster than the network's declared latency time
336        2) This validators is slower than the supermajority of validators
337        where either cases are reasonable to establish the bound regarding.
338
339        We do further scale the capacity by `2` to handle _some_ overages.
340      */
341      let (message_send, message_recv) =
342        async_channel::bounded(2 * blockchain.validator_set().validators().into_iter().count());
343
344      let mut txn = db.txn();
345      let (state, messages) = State::new::<N>(&blockchain, &signer, &mut txn, proposal).await;
346      txn.commit();
347
348      for message in messages {
349        network.broadcast(message).await;
350      }
351
352      (
353        TendermintHandle {
354          block_number: state.block_number_ref(),
355          observed_block_number: state.observed_block_number_ref(),
356          sync: sync_send,
357          message: message_send,
358        },
359        (TendermintProcess {
360          sync: sync_recv,
361          message: message_recv,
362
363          state,
364          blockchain,
365          signer,
366          db,
367          network,
368        }),
369      )
370    }
371
372    /// Initialize the Tendermint process.
373    ///
374    /// The returned future will run until its corresponding channels are closed. The future is NOT
375    /// cancel-safe, and will have an undefined state in memory if cancelled. It MUST be polled
376    /// until either:
377    /// 1) It is of no further use, and accordingly its state may be undefined
378    /// 2) The process terminates, so that the Tendermint process will be re-initialized from the
379    ///    disk before any further use
380    pub fn process<
381      B: Send
382        + Sync
383        + Blockchain<
384          Validator: Send + Sync + BorshSerialize + BorshDeserialize,
385          ValidatorSet: Sync,
386          SignatureScheme: SignatureScheme<
387            Signature: Send + BorshSerialize + BorshDeserialize,
388            AggregateSignature: Send + BorshSerialize + BorshDeserialize,
389          >,
390          Genesis: Sync,
391          Block: Send
392                   + Sync
393                   + BorshSerialize
394                   + BorshDeserialize
395                   + Block<Hash: Send + BorshSerialize + BorshDeserialize>,
396          BlockProposal: Send,
397        >,
398      S: Send
399        + Sync
400        + Signer<
401          Validator = B::Validator,
402          Signature = <B::SignatureScheme as SignatureScheme>::Signature,
403          SignFuture: Send,
404        >,
405      D: Send + for<'db> Db<Transaction<'db>: Send>,
406      N: Send
407        + Network<
408          B::Validator,
409          <B::SignatureScheme as SignatureScheme>::Signature,
410          <B::SignatureScheme as SignatureScheme>::AggregateSignature,
411          B::Block,
412          Sleep: Send,
413        >,
414    >(
415      blockchain: B,
416      signer: S,
417      db: D,
418      network: N,
419      proposal: B::Block,
420    ) -> impl Send + Future<Output = (TendermintHandle<B>, impl Send + Future<Output = ()>)> {
421      async move {
422        let (handle, process) = Self::internal(blockchain, signer, db, network, proposal).await;
423        (handle, process.run())
424      }
425    }
426
427    /// A variant of [`Tendermint::process`] which supports `!Send`, `!Sync` arguments.
428    ///
429    /// This is intended to enable support for single-threaded runtimes which do not require such
430    /// bounds.
431    #[expect(clippy::manual_async_fn)]
432    pub fn process_single_threaded<
433      B: Blockchain<
434        Validator: BorshSerialize + BorshDeserialize,
435        SignatureScheme: SignatureScheme<
436          Signature: BorshSerialize + BorshDeserialize,
437          AggregateSignature: BorshSerialize + BorshDeserialize,
438        >,
439        Block: BorshSerialize + BorshDeserialize + Block<Hash: BorshSerialize + BorshDeserialize>,
440      >,
441      S: Signer<
442        Validator = B::Validator,
443        Signature = <B::SignatureScheme as SignatureScheme>::Signature,
444      >,
445      D: Db,
446      N: Network<
447        B::Validator,
448        <B::SignatureScheme as SignatureScheme>::Signature,
449        <B::SignatureScheme as SignatureScheme>::AggregateSignature,
450        B::Block,
451      >,
452    >(
453      blockchain: B,
454      signer: S,
455      db: D,
456      network: N,
457      proposal: B::Block,
458    ) -> impl Future<Output = (TendermintHandle<B>, impl Future<Output = ()>)> {
459      async move {
460        let (handle, process) = Self::internal(blockchain, signer, db, network, proposal).await;
461        (handle, process.run())
462      }
463    }
464  }
465
466  impl<B: Blockchain> TendermintHandle<B> {
467    /// The number for the block we're currently attempting to achieve consensus over.
468    ///
469    /// This is implemented in a lock-free manner and MAY momentarily desynchronize from the block
470    /// number internal to the Tendermint process, or the blockchain, accordingly.
471    pub fn block_number(&self) -> BlockNumber {
472      BlockNumber(
473        NonZero::new(self.block_number.load(Ordering::Acquire))
474          .expect("block number was corrupted"),
475      )
476    }
477
478    /// The greatest block number we've observed validators attempting to achieve consensus over.
479    ///
480    /// This is the greatest block number which `f + 1` validators have been observed to be
481    /// attempting to obtain consensus over, meaning there presumably is consensus over all prior
482    /// blocks (or the amount of faulty validators exceeds the fault threshold). This is intended
483    /// to be used to allow the larger application to realize it should explicitly sync up to this
484    /// block (as this library does not implement a block sync loop itself).
485    ///
486    /// This is implemented in a lock-free manner and MAY momentarily desynchronize from other
487    /// representations of the state accordingly. A best-effort attempt is made to ensure this will
488    /// always be greater than or equal to the value yielded by [`TendermintHandle::block_number`]
489    /// but this is not guaranteed.
490    pub fn observed_block_number(&self) -> BlockNumber {
491      BlockNumber(
492        NonZero::new(self.observed_block_number.load(Ordering::Acquire))
493          .expect("observed block number was corrupted"),
494      )
495    }
496
497    /// Sync a block by its commit.
498    ///
499    /// The Tendermint implementation will validate the commit as necessary.
500    ///
501    /// This function returns `Ok(())` if the Tendermint process is still running and `Err(())`
502    /// otherwise. This function implements backpressure and will wait until the process has the
503    /// capacity to receive this.
504    pub async fn sync(
505      &mut self,
506      block: B::Block,
507      commit: CommitFor<B>,
508    ) -> Result<(), ProcessTerminated> {
509      self.sync.send((block, commit)).await.map_err(|_| ProcessTerminated)
510    }
511
512    /// Handle a message received from the network.
513    ///
514    /// The Tendermint implementation will validate the message as necessary.
515    ///
516    /// This function returns `Ok(())` if the Tendermint process is still running and `Err(())`
517    /// otherwise. This function implements backpressure and will wait until the process has the
518    /// capacity to receive this.
519    pub async fn message(&mut self, message: MessageFor<B>) -> Result<(), ProcessTerminated> {
520      self.message.send(message).await.map_err(|_| ProcessTerminated)
521    }
522  }
523}
524#[cfg(feature = "std")]
525pub use tendermint::*;