Skip to main content

asupersync/distributed/consensus/
pbft.rs

1//! Practical Byzantine Fault Tolerance (PBFT) consensus algorithm.
2//!
3//! This implements the PBFT protocol as described in "Practical Byzantine
4//! Fault Tolerance" by Castro and Liskov. The protocol provides safety
5//! and liveness guarantees in partially synchronous networks with up to
6//! f Byzantine faults in a system of 3f+1 replicas.
7//!
8//! # Protocol Overview
9//!
10//! PBFT operates in views, where each view has a designated primary replica
11//! that orders client requests. The protocol consists of three phases:
12//!
13//! 1. **Pre-prepare**: Primary proposes ordering for a batch of requests
14//! 2. **Prepare**: Replicas agree on the ordering proposed by the primary
15//! 3. **Commit**: Replicas commit to executing the ordered requests
16//!
17//! View changes occur when the primary is suspected of being faulty.
18//!
19//! # Experimental — not Byzantine-fault-tolerant yet
20//!
21//! This implementation is **experimental and incomplete**. The normal-case
22//! three-phase path (pre-prepare/prepare/commit) is implemented, but
23//! view-change/new-view handling is **not** (the handlers fail closed rather
24//! than silently succeed), and there is no message authentication, no
25//! watermark/checkpoint stability, and no log pruning. As a result it does
26//! **not** provide liveness under primary failure or safety against a
27//! Byzantine primary. Do not rely on it for fault tolerance. Tracked by
28//! `asupersync-v8mszr`.
29
30use crate::cx::Cx;
31use crate::error::{Error, ErrorKind, Result};
32use crate::time::timeout;
33use crate::types::{Outcome, Time};
34use serde::{Deserialize, Serialize};
35use std::collections::{HashMap, VecDeque};
36use std::sync::{Arc, Mutex};
37use std::time::Duration;
38
39use super::types::{
40    ConsensusBatch, ConsensusRequest, ConsensusResponse, MessageCertificate, MessageDigest,
41    PhaseKind, ReplicaId, SequenceNumber, ViewNumber,
42};
43
44/// Configuration for PBFT consensus.
45#[derive(Debug, Clone)]
46pub struct PbftConfig {
47    /// Total number of replicas in the system.
48    pub replica_count: usize,
49    /// Maximum number of Byzantine faults tolerated.
50    pub fault_tolerance: usize,
51    /// Timeout for pre-prepare phase.
52    pub preprepare_timeout: Duration,
53    /// Timeout for prepare phase.
54    pub prepare_timeout: Duration,
55    /// Timeout for commit phase.
56    pub commit_timeout: Duration,
57    /// Timeout for view change.
58    pub view_change_timeout: Duration,
59    /// Maximum batch size for requests.
60    pub max_batch_size: usize,
61    /// Batch timeout - max time to wait for full batch.
62    pub batch_timeout: Duration,
63}
64
65impl PbftConfig {
66    /// Create configuration for n replicas with f Byzantine faults.
67    pub fn new(replica_count: usize, fault_tolerance: usize) -> Result<Self> {
68        if replica_count < 3 * fault_tolerance + 1 {
69            return Err(Error::new(ErrorKind::InvalidInput));
70        }
71
72        Ok(Self {
73            replica_count,
74            fault_tolerance,
75            preprepare_timeout: Duration::from_secs(5),
76            prepare_timeout: Duration::from_secs(5),
77            commit_timeout: Duration::from_secs(5),
78            view_change_timeout: Duration::from_secs(10),
79            max_batch_size: 100,
80            batch_timeout: Duration::from_millis(10),
81        })
82    }
83
84    /// Check if we have enough replicas for given fault tolerance.
85    pub fn is_valid(&self) -> bool {
86        self.replica_count > 3 * self.fault_tolerance
87    }
88
89    /// Get the minimum number of signatures needed for a quorum.
90    pub fn quorum_size(&self) -> usize {
91        2 * self.fault_tolerance + 1
92    }
93}
94
95/// PBFT protocol message types.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub enum PbftMessage {
98    /// Client request for consensus.
99    Request(ConsensusRequest),
100    /// Primary proposes ordering (pre-prepare phase).
101    PrePrepare {
102        view: ViewNumber,
103        sequence: SequenceNumber,
104        digest: MessageDigest,
105        batch: ConsensusBatch,
106        replica_id: ReplicaId,
107    },
108    /// Replica agrees with ordering (prepare phase).
109    Prepare {
110        view: ViewNumber,
111        sequence: SequenceNumber,
112        digest: MessageDigest,
113        replica_id: ReplicaId,
114    },
115    /// Replica commits to execution (commit phase).
116    Commit {
117        view: ViewNumber,
118        sequence: SequenceNumber,
119        digest: MessageDigest,
120        replica_id: ReplicaId,
121    },
122    /// View change request.
123    ViewChange {
124        new_view: ViewNumber,
125        replica_id: ReplicaId,
126        certificates: Vec<MessageCertificate>,
127    },
128    /// New view establishment.
129    NewView {
130        view: ViewNumber,
131        view_change_msgs: Vec<PbftMessage>,
132        preprepare_msgs: Vec<PbftMessage>,
133    },
134}
135
136impl PbftMessage {
137    /// Compute cryptographic digest of this message.
138    pub fn digest(&self) -> Result<MessageDigest> {
139        MessageDigest::of(self)
140    }
141
142    /// Get the phase kind of this message.
143    pub fn phase(&self) -> PhaseKind {
144        match self {
145            PbftMessage::PrePrepare { .. } => PhaseKind::PrePrepare,
146            PbftMessage::Prepare { .. } => PhaseKind::Prepare,
147            PbftMessage::Commit { .. } => PhaseKind::Commit,
148            PbftMessage::ViewChange { .. } => PhaseKind::ViewChange,
149            PbftMessage::NewView { .. } => PhaseKind::NewView,
150            PbftMessage::Request(_) => PhaseKind::PrePrepare, // Requests trigger pre-prepare
151        }
152    }
153}
154
155/// Current state of a PBFT replica.
156#[derive(Debug, Clone)]
157pub struct PbftState {
158    /// Current view number.
159    pub view: ViewNumber,
160    /// Next sequence number to assign.
161    pub sequence: SequenceNumber,
162    /// Request batches in various phases.
163    pub log: HashMap<SequenceNumber, LogEntry>,
164    /// Pending client requests.
165    pub pending_requests: VecDeque<ConsensusRequest>,
166    /// Last executed sequence number.
167    pub last_executed: SequenceNumber,
168    /// View change state.
169    pub view_change_state: Option<ViewChangeState>,
170}
171
172/// Entry in the consensus log for tracking message phases.
173#[derive(Debug, Clone)]
174pub struct LogEntry {
175    /// The batch of requests.
176    pub batch: ConsensusBatch,
177    /// Digest of the batch.
178    pub digest: MessageDigest,
179    /// View number when created.
180    pub view: ViewNumber,
181    /// Pre-prepare received.
182    pub preprepared: bool,
183    /// Prepare messages received.
184    pub prepare_msgs: HashMap<ReplicaId, PbftMessage>,
185    /// Commit messages received.
186    pub commit_msgs: HashMap<ReplicaId, PbftMessage>,
187    /// Execution result if completed.
188    pub result: Option<Outcome<Vec<u8>, String>>,
189}
190
191/// State during view change protocol.
192#[derive(Debug, Clone)]
193pub struct ViewChangeState {
194    /// Target view number.
195    pub target_view: ViewNumber,
196    /// View change messages received.
197    pub view_change_msgs: HashMap<ReplicaId, PbftMessage>,
198    /// Whether this replica sent view change.
199    pub sent_view_change: bool,
200    /// Timestamp when view change started.
201    pub started_at: Time,
202}
203
204/// Transport interface for PBFT message delivery.
205pub trait PbftTransport: Send + Sync {
206    /// Send message to a specific replica.
207    fn send_to_replica(
208        &self,
209        replica_id: &ReplicaId,
210        message: PbftMessage,
211    ) -> impl std::future::Future<Output = Result<()>> + Send;
212
213    /// Broadcast message to all replicas.
214    fn broadcast(
215        &self,
216        message: PbftMessage,
217    ) -> impl std::future::Future<Output = Result<()>> + Send;
218
219    /// Receive next message (blocking).
220    fn receive(&self) -> impl std::future::Future<Output = Result<PbftMessage>> + Send;
221}
222
223/// State machine for PBFT consensus node.
224pub struct PbftNode<T: PbftTransport> {
225    /// Replica identifier for this node.
226    replica_id: ReplicaId,
227    /// Canonical numeric index for this replica in the configured replica set.
228    replica_index: usize,
229    /// Configuration parameters.
230    config: PbftConfig,
231    /// Current state.
232    state: Arc<Mutex<PbftState>>,
233    /// Transport for message delivery.
234    transport: T,
235}
236
237impl<T: PbftTransport> PbftNode<T> {
238    /// Create a new PBFT node.
239    pub fn new(replica_id: ReplicaId, config: PbftConfig, transport: T) -> Result<Self> {
240        if !config.is_valid() {
241            return Err(Error::new(ErrorKind::InvalidInput));
242        }
243        let replica_index = parse_replica_index(&replica_id, config.replica_count)?;
244
245        let state = PbftState {
246            view: ViewNumber::new(0),
247            // Sequence numbers are assigned starting at 1. `last_executed` is the
248            // watermark of the highest sequence already executed, and starts at 0
249            // to mean "nothing executed yet". Execution is gap-free and gated on
250            // `sequence == last_executed.next()` (see `handle_commit`), so the
251            // first batch MUST be sequence 1 — otherwise `0 == 0.next() == 1` is
252            // never satisfied and the first batch (and thus the whole pipeline)
253            // can never execute.
254            sequence: SequenceNumber::new(1),
255            log: HashMap::new(),
256            pending_requests: VecDeque::new(),
257            last_executed: SequenceNumber::new(0),
258            view_change_state: None,
259        };
260
261        Ok(Self {
262            replica_id,
263            replica_index,
264            config,
265            state: Arc::new(Mutex::new(state)),
266            transport,
267        })
268    }
269
270    /// Check if this replica is the primary for the current view.
271    pub fn is_primary(&self) -> bool {
272        let state = self.state.lock().unwrap();
273        let primary_idx = state.view.primary(self.config.replica_count);
274        self.replica_index == primary_idx
275    }
276
277    /// The highest sequence number this replica has executed. Execution is
278    /// gap-free, so every sequence in `1..=last_executed` has been applied.
279    pub fn last_executed(&self) -> SequenceNumber {
280        self.state.lock().unwrap().last_executed
281    }
282
283    /// Submit a client request for consensus.
284    pub async fn submit_request(&self, cx: &Cx, request: ConsensusRequest) -> Result<()> {
285        {
286            let mut state = self.state.lock().unwrap();
287            state.pending_requests.push_back(request);
288        }
289
290        // If we're the primary, try to create a batch
291        if self.is_primary() {
292            self.try_create_batch(cx).await?;
293        }
294
295        Ok(())
296    }
297
298    /// Try to create a batch of pending requests.
299    async fn try_create_batch(&self, cx: &Cx) -> Result<()> {
300        let (batch, sequence, view) = {
301            let mut state = self.state.lock().unwrap();
302
303            if state.pending_requests.is_empty() {
304                return Ok(()); // No requests to batch
305            }
306
307            // Collect requests for batch
308            let mut requests = Vec::new();
309            while requests.len() < self.config.max_batch_size && !state.pending_requests.is_empty()
310            {
311                if let Some(request) = state.pending_requests.pop_front() {
312                    requests.push(request);
313                }
314            }
315
316            let batch = ConsensusBatch::new(requests);
317            let sequence = state.sequence;
318            let view = state.view;
319
320            (batch, sequence, view)
321        };
322
323        let result = self
324            .send_preprepare(cx, view, sequence, batch.clone())
325            .await;
326        let mut state = self.state.lock().unwrap();
327        match result {
328            Ok(()) => {
329                if state.sequence == sequence {
330                    state.sequence = state.sequence.next();
331                }
332                Ok(())
333            }
334            Err(err) => {
335                if state.sequence == sequence.next() {
336                    state.sequence = sequence;
337                }
338                if let Ok(digest) = MessageDigest::of(&batch) {
339                    if state
340                        .log
341                        .get(&sequence)
342                        .is_some_and(|entry| entry.view == view && entry.digest == digest)
343                    {
344                        state.log.remove(&sequence);
345                    }
346                }
347                for request in batch.requests.iter().rev() {
348                    state.pending_requests.push_front(request.clone());
349                }
350                Err(err)
351            }
352        }
353    }
354
355    /// Send pre-prepare message as primary.
356    async fn send_preprepare(
357        &self,
358        _cx: &Cx,
359        view: ViewNumber,
360        sequence: SequenceNumber,
361        batch: ConsensusBatch,
362    ) -> Result<()> {
363        let digest = MessageDigest::of(&batch)?;
364
365        // Create log entry
366        {
367            let mut state = self.state.lock().unwrap();
368            if state.log.contains_key(&sequence) {
369                return Err(
370                    Error::new(ErrorKind::InvalidStateTransition).with_message(format!(
371                        "PBFT pre-prepare sequence {sequence} already has a log entry"
372                    )),
373                );
374            }
375            let entry = LogEntry {
376                batch: batch.clone(),
377                digest: digest.clone(),
378                view,
379                preprepared: true,
380                prepare_msgs: HashMap::new(),
381                commit_msgs: HashMap::new(),
382                result: None,
383            };
384            state.log.insert(sequence, entry);
385        }
386
387        let message = PbftMessage::PrePrepare {
388            view,
389            sequence,
390            digest,
391            batch,
392            replica_id: self.replica_id.clone(),
393        };
394
395        // Broadcast pre-prepare to all replicas
396        timeout(
397            Time::from_millis(0),
398            self.config.preprepare_timeout,
399            self.transport.broadcast(message),
400        )
401        .await
402        .map_err(|_| Error::new(ErrorKind::DeadlineExceeded))?
403    }
404
405    /// Process an incoming PBFT message.
406    pub async fn process_message(&self, cx: &Cx, message: PbftMessage) -> Result<()> {
407        match message {
408            PbftMessage::Request(request) => self.submit_request(cx, request).await,
409            PbftMessage::PrePrepare {
410                view,
411                sequence,
412                digest,
413                batch,
414                replica_id,
415            } => {
416                self.handle_preprepare(cx, view, sequence, digest, batch, replica_id)
417                    .await
418            }
419            PbftMessage::Prepare {
420                view,
421                sequence,
422                digest,
423                replica_id,
424            } => {
425                self.handle_prepare(cx, view, sequence, digest, replica_id)
426                    .await
427            }
428            PbftMessage::Commit {
429                view,
430                sequence,
431                digest,
432                replica_id,
433            } => {
434                self.handle_commit(cx, view, sequence, digest, replica_id)
435                    .await
436            }
437            PbftMessage::ViewChange {
438                new_view,
439                replica_id,
440                certificates,
441            } => {
442                self.handle_view_change(cx, new_view, replica_id, certificates)
443                    .await
444            }
445            PbftMessage::NewView {
446                view,
447                view_change_msgs,
448                preprepare_msgs,
449            } => {
450                self.handle_new_view(cx, view, view_change_msgs, preprepare_msgs)
451                    .await
452            }
453        }
454    }
455
456    /// Handle pre-prepare message from primary.
457    async fn handle_preprepare(
458        &self,
459        _cx: &Cx,
460        view: ViewNumber,
461        sequence: SequenceNumber,
462        digest: MessageDigest,
463        batch: ConsensusBatch,
464        replica_id: ReplicaId,
465    ) -> Result<()> {
466        self.validate_preprepare_primary(view, &replica_id)?;
467        // Validate view and primary
468        {
469            let mut state = self.state.lock().unwrap();
470            if view != state.view {
471                return Err(Error::new(ErrorKind::InvalidInput));
472            }
473            if sequence <= state.last_executed {
474                return Err(
475                    Error::new(ErrorKind::InvalidStateTransition).with_message(format!(
476                        "PBFT pre-prepare sequence {sequence} is at or below executed watermark {}",
477                        state.last_executed
478                    )),
479                );
480            }
481
482            if let Some(entry) = state.log.get_mut(&sequence) {
483                if entry.view != view || entry.digest != digest {
484                    return Err(Error::new(ErrorKind::InvalidStateTransition).with_message(
485                        format!("PBFT pre-prepare equivocation for {sequence} in {view}"),
486                    ));
487                }
488                if entry.preprepared {
489                    return Ok(());
490                }
491            }
492        }
493
494        // Verify digest
495        let computed_digest = MessageDigest::of(&batch)?;
496        if digest != computed_digest {
497            return Err(Error::new(ErrorKind::InvalidInput));
498        }
499
500        // Create or mark the log entry without overwriting accumulated messages.
501        {
502            let mut state = self.state.lock().unwrap();
503            if let Some(entry) = state.log.get_mut(&sequence) {
504                entry.batch = batch;
505                entry.preprepared = true;
506            } else {
507                let entry = LogEntry {
508                    batch,
509                    digest: digest.clone(),
510                    view,
511                    preprepared: true,
512                    prepare_msgs: HashMap::new(),
513                    commit_msgs: HashMap::new(),
514                    result: None,
515                };
516                state.log.insert(sequence, entry);
517            }
518        }
519
520        // Send prepare message
521        let prepare_msg = PbftMessage::Prepare {
522            view,
523            sequence,
524            digest,
525            replica_id: self.replica_id.clone(),
526        };
527
528        timeout(
529            Time::from_millis(0),
530            self.config.prepare_timeout,
531            self.transport.broadcast(prepare_msg),
532        )
533        .await
534        .map_err(|_| Error::new(ErrorKind::DeadlineExceeded))?
535    }
536
537    /// Handle prepare message from replica.
538    async fn handle_prepare(
539        &self,
540        _cx: &Cx,
541        view: ViewNumber,
542        sequence: SequenceNumber,
543        digest: MessageDigest,
544        replica_id: ReplicaId,
545    ) -> Result<()> {
546        self.validate_remote_replica(&replica_id)?;
547        let should_commit = {
548            let mut state = self.state.lock().unwrap();
549
550            // Find log entry
551            let entry = match state.log.get_mut(&sequence) {
552                Some(entry) if entry.view == view && entry.digest == digest => entry,
553                _ => return Ok(()), // Ignore if no matching entry
554            };
555
556            // Add prepare message
557            let msg = PbftMessage::Prepare {
558                view,
559                sequence,
560                digest: digest.clone(),
561                replica_id: replica_id.clone(),
562            };
563            entry.prepare_msgs.insert(replica_id, msg);
564
565            // Check if we have enough prepares (2f+1 including our own).
566            entry.preprepared && entry.prepare_msgs.len() + 1 >= self.config.quorum_size()
567        };
568
569        // Send commit message if we have quorum
570        if should_commit {
571            let commit_msg = PbftMessage::Commit {
572                view,
573                sequence,
574                digest,
575                replica_id: self.replica_id.clone(),
576            };
577
578            timeout(
579                Time::from_millis(0),
580                self.config.commit_timeout,
581                self.transport.broadcast(commit_msg),
582            )
583            .await
584            .map_err(|_| Error::new(ErrorKind::DeadlineExceeded))??;
585        }
586
587        Ok(())
588    }
589
590    /// Handle commit message from replica.
591    async fn handle_commit(
592        &self,
593        _cx: &Cx,
594        view: ViewNumber,
595        sequence: SequenceNumber,
596        digest: MessageDigest,
597        replica_id: ReplicaId,
598    ) -> Result<()> {
599        self.validate_remote_replica(&replica_id)?;
600        let should_execute = {
601            let mut state = self.state.lock().unwrap();
602            let next_to_execute = state.last_executed.next();
603
604            // Find log entry
605            let entry = match state.log.get_mut(&sequence) {
606                Some(entry) if entry.view == view && entry.digest == digest => entry,
607                _ => return Ok(()), // Ignore if no matching entry
608            };
609
610            // Add commit message
611            let msg = PbftMessage::Commit {
612                view,
613                sequence,
614                digest: digest.clone(),
615                replica_id: replica_id.clone(),
616            };
617            entry.commit_msgs.insert(replica_id, msg);
618
619            let prepared =
620                entry.preprepared && entry.prepare_msgs.len() + 1 >= self.config.quorum_size();
621            let committed = entry.commit_msgs.len() + 1 >= self.config.quorum_size();
622
623            prepared && committed && sequence == next_to_execute && entry.result.is_none()
624        };
625
626        // Execute the batch if we have quorum and it's the next in sequence,
627        // then drain any successors whose commit-quorum completed out of order.
628        // Execution is otherwise only re-triggered by the arrival of a NEW
629        // commit for the exact `last_executed.next()`, so a higher sequence that
630        // already holds a full commit certificate would stall permanently behind
631        // a lower one — ordinary under network reordering, not just adversarial.
632        if should_execute {
633            self.execute_batch(sequence).await?;
634            while let Some(next) = self.next_executable_sequence() {
635                self.execute_batch(next).await?;
636            }
637        }
638
639        Ok(())
640    }
641
642    /// The next sequence that is prepared, committed, and not yet executed, if
643    /// any. Used to drain successors whose commit-quorum was reached before the
644    /// lower sequence executed (`handle_commit` only fires execution on the
645    /// exact `last_executed.next()`, so out-of-order quorum completion would
646    /// otherwise wedge the pipeline).
647    fn next_executable_sequence(&self) -> Option<SequenceNumber> {
648        let state = self.state.lock().unwrap();
649        let next = state.last_executed.next();
650        let entry = state.log.get(&next)?;
651        let prepared =
652            entry.preprepared && entry.prepare_msgs.len() + 1 >= self.config.quorum_size();
653        let committed = entry.commit_msgs.len() + 1 >= self.config.quorum_size();
654        (prepared && committed && entry.result.is_none()).then_some(next)
655    }
656
657    /// Execute a batch of requests.
658    async fn execute_batch(&self, sequence: SequenceNumber) -> Result<()> {
659        let batch = {
660            let mut state = self.state.lock().unwrap();
661
662            if sequence != state.last_executed.next() {
663                return Ok(());
664            }
665
666            let batch = {
667                let entry = state.log.get_mut(&sequence).ok_or_else(|| {
668                    Error::new(ErrorKind::InvalidStateTransition).with_message(format!(
669                        "PBFT cannot execute missing log entry for {sequence}"
670                    ))
671                })?;
672                if entry.result.is_some() {
673                    return Ok(());
674                }
675                let batch = entry.batch.clone();
676
677                // For simplicity, just simulate execution
678                let result = Outcome::Ok(b"executed".to_vec());
679                entry.result = Some(result);
680                batch
681            };
682
683            state.last_executed = sequence;
684            batch
685        };
686
687        let batch_size = batch.len();
688
689        // In a real implementation, this would execute the actual state machine.
690        // With tracing disabled, keep the execution path side-effect free.
691        #[cfg(feature = "tracing-integration")]
692        tracing::info!(
693            replica_id = %self.replica_id,
694            sequence = %sequence,
695            batch_size,
696            "Executed consensus batch"
697        );
698        #[cfg(not(feature = "tracing-integration"))]
699        let _ = batch_size;
700
701        Ok(())
702    }
703
704    fn validate_remote_replica(&self, replica_id: &ReplicaId) -> Result<()> {
705        let index = parse_replica_index(replica_id, self.config.replica_count)?;
706        if index == self.replica_index {
707            return Err(Error::new(ErrorKind::InvalidInput).with_message(format!(
708                "PBFT rejected self-authored remote quorum message from {replica_id}"
709            )));
710        }
711        Ok(())
712    }
713
714    fn validate_preprepare_primary(&self, view: ViewNumber, replica_id: &ReplicaId) -> Result<()> {
715        let index = parse_replica_index(replica_id, self.config.replica_count)?;
716        let expected = view.primary(self.config.replica_count);
717        if index != expected {
718            return Err(Error::new(ErrorKind::InvalidInput).with_message(format!(
719                "PBFT rejected pre-prepare from {replica_id}; primary for {view} is replica:{expected}"
720            )));
721        }
722        Ok(())
723    }
724
725    /// Handle view change message.
726    ///
727    /// **Not implemented.** A correct PBFT view-change requires validated
728    /// view-change certificates, watermark/checkpoint stability, and new-view
729    /// construction. Until that lands this returns an explicit error rather
730    /// than silently succeeding — a silent `Ok(())` here would let a caller
731    /// believe primary-failure recovery occurred when it did not. See the
732    /// experimental warning on [`PbftConsensus`].
733    async fn handle_view_change(
734        &self,
735        _cx: &Cx,
736        _new_view: ViewNumber,
737        _replica_id: ReplicaId,
738        _certificates: Vec<MessageCertificate>,
739    ) -> Result<()> {
740        Err(Error::new(ErrorKind::InvalidStateTransition).with_message(
741            "PBFT view-change is not implemented (experimental consensus; no Byzantine \
742             fault tolerance under primary failure)",
743        ))
744    }
745
746    /// Handle new view message.
747    ///
748    /// **Not implemented** — see [`Self::handle_view_change`]. Fails closed
749    /// rather than pretending to install a new view.
750    async fn handle_new_view(
751        &self,
752        _cx: &Cx,
753        _view: ViewNumber,
754        _view_change_msgs: Vec<PbftMessage>,
755        _preprepare_msgs: Vec<PbftMessage>,
756    ) -> Result<()> {
757        Err(Error::new(ErrorKind::InvalidStateTransition).with_message(
758            "PBFT new-view is not implemented (experimental consensus; no Byzantine \
759             fault tolerance under primary failure)",
760        ))
761    }
762}
763
764fn parse_replica_index(replica_id: &ReplicaId, replica_count: usize) -> Result<usize> {
765    let index = replica_id.as_str().parse::<usize>().map_err(|_| {
766        Error::new(ErrorKind::InvalidInput).with_message(format!(
767            "PBFT replica id {replica_id} must be a numeric index"
768        ))
769    })?;
770    if index >= replica_count {
771        return Err(Error::new(ErrorKind::InvalidInput).with_message(format!(
772            "PBFT replica id {replica_id} is outside configured replica set size {replica_count}"
773        )));
774    }
775    Ok(index)
776}
777
778/// High-level PBFT consensus interface.
779pub struct PbftConsensus<T: PbftTransport> {
780    node: PbftNode<T>,
781}
782
783impl<T: PbftTransport> PbftConsensus<T> {
784    /// Create a new PBFT consensus instance.
785    pub fn new(replica_id: ReplicaId, config: PbftConfig, transport: T) -> Result<Self> {
786        let node = PbftNode::new(replica_id, config, transport)?;
787        Ok(Self { node })
788    }
789
790    /// Submit a request for consensus.
791    pub async fn submit(&self, cx: &Cx, request: ConsensusRequest) -> Result<ConsensusResponse> {
792        self.node.submit_request(cx, request.clone()).await?;
793
794        // For simplicity, return a dummy response
795        // A real implementation would wait for execution and return the result
796        Ok(ConsensusResponse {
797            view: ViewNumber::new(0),
798            sequence: SequenceNumber::new(0),
799            result: Outcome::Ok(b"consensus result".to_vec()),
800            replica_id: self.node.replica_id.clone(),
801            timestamp: Time::from_millis(0),
802        })
803    }
804
805    /// Run the consensus protocol message loop.
806    pub async fn run(&self, cx: &Cx) -> Result<()> {
807        loop {
808            // Receive and process messages
809            let message = self.node.transport.receive().await?;
810            self.node.process_message(cx, message).await?;
811        }
812    }
813}