Skip to main content

rhiza_quepaxa/
lib.rs

1#![doc = include_str!("../README.md")]
2
3#[cfg(unix)]
4use std::os::unix::fs::MetadataExt;
5#[cfg(any(
6    target_os = "linux",
7    target_os = "android",
8    target_os = "macos",
9    target_os = "ios",
10    target_os = "freebsd",
11    target_os = "openbsd",
12    target_os = "netbsd",
13    target_os = "dragonfly"
14))]
15use std::os::unix::fs::OpenOptionsExt;
16use std::{
17    cmp::Ordering as CmpOrdering,
18    collections::{hash_map, BTreeMap, BTreeSet, HashMap},
19    fmt, fs,
20    io::{self, Read, Write},
21    path::{Path, PathBuf},
22    sync::{
23        atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
24        Arc, Mutex,
25    },
26    thread,
27    time::{Duration, Instant},
28};
29
30use rhiza_core::canonical_membership_digest;
31
32pub use rhiza_core::{
33    ClusterId, Command, CommandKind, ConfigChange, ConfigId, EntryType, Epoch, LogEntry, LogHash,
34    LogIndex, NodeId, StoredCommand,
35};
36
37pub type Result<T> = std::result::Result<T, Error>;
38pub type Slot = u64;
39pub type Round = u64;
40pub type Phase = u8;
41pub type Step = u64;
42pub type Priority = u128;
43
44/// Read-only classification of a recorder root before startup recovery.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum RecorderPreflight {
47    Missing,
48    Valid,
49    Recoverable,
50}
51
52const RECORDER_STATE_VERSION: u16 = 4;
53const CONFIGURATION_STATE_VERSION: u16 = 3;
54const RECORD_WORKER_QUEUE_CAPACITY: usize = 1;
55const CONTROL_WORKER_QUEUE_CAPACITY: usize = 1;
56
57// The largest replicated command supported by the bounded WAL rotation policy below. The
58// remaining limits follow directly from the on-disk envelopes: seven membership entries,
59// two durable slot snapshots, and at most one oversized WAL frame past the soft limit.
60const MAX_REPLICATED_COMMAND_BYTES: usize = 512 * 1024;
61const MAX_COMMAND_CACHE_BYTES: usize = 4 + 2 + 1 + 8 + MAX_REPLICATED_COMMAND_BYTES + 32;
62const MAX_CONFIGURATION_BYTES: usize = 512 * 1024;
63const MAX_RECORDER_STATE_BYTES: usize = 1024 * 1024;
64const MAX_RECORDED_HEAD_BYTES: usize = 2 * MAX_RECORDER_STATE_BYTES + MAX_CONFIGURATION_BYTES;
65const MAX_TRANSITION_INTENT_BYTES: usize =
66    MAX_RECORDER_STATE_BYTES + MAX_CONFIGURATION_BYTES + 4 + 2 + 8 + 2 + 2;
67const MAX_CONFIGURATION_HEAD_INTENT_BYTES: usize =
68    MAX_CONFIGURATION_BYTES + MAX_RECORDED_HEAD_BYTES + 4 + 2 + 8 + 8;
69const MAX_RECORDER_WAL_BYTES: usize = 64 * 1024 * 1024 + 2 * 1024 * 1024;
70
71#[cfg(any(target_os = "linux", target_os = "android"))]
72const O_NOFOLLOW_FLAG: i32 = 0o400000;
73#[cfg(any(
74    target_os = "macos",
75    target_os = "ios",
76    target_os = "freebsd",
77    target_os = "openbsd",
78    target_os = "netbsd",
79    target_os = "dragonfly"
80))]
81const O_NOFOLLOW_FLAG: i32 = 0x0100;
82
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum Error {
85    ChainConflict {
86        slot: Slot,
87        expected_prev_hash: LogHash,
88        actual_prev_hash: LogHash,
89    },
90    CommandHashMismatch,
91    CommandUnavailable,
92    Cancelled,
93    ConflictingCertificates,
94    Decode(String),
95    DuplicateRecorderIdentity,
96    EmptyRecorderIdentity,
97    EmptyFixedMembership,
98    InvalidFixedMembershipSize,
99    InvalidRecoveredTip,
100    Io(String),
101    MigrationRequired {
102        format: &'static str,
103        version: u16,
104    },
105    NoQuorum,
106    ProposeFailed,
107    RandomnessUnavailable(String),
108    RecorderRootLocked(PathBuf),
109    Rejected(RejectReason),
110    ReadFenceUnsupported,
111    TypedProofInstallRequired,
112    TypedRecordRequired,
113}
114
115impl fmt::Display for Error {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        match self {
118            Self::ChainConflict { slot, .. } => {
119                write!(f, "QuePaxa predecessor conflicts at slot {slot}")
120            }
121            Self::CommandHashMismatch => write!(f, "QuePaxa command hash mismatch"),
122            Self::CommandUnavailable => write!(f, "QuePaxa command bytes unavailable"),
123            Self::Cancelled => write!(f, "QuePaxa proposal cancelled"),
124            Self::ConflictingCertificates => {
125                write!(f, "QuePaxa recovered conflicting decision certificates")
126            }
127            Self::Decode(message) => write!(f, "QuePaxa decode failed: {message}"),
128            Self::DuplicateRecorderIdentity => write!(f, "recorder identities must be unique"),
129            Self::EmptyRecorderIdentity => write!(f, "recorder identity must not be empty"),
130            Self::EmptyFixedMembership => {
131                write!(f, "fixed membership must include at least one node")
132            }
133            Self::InvalidFixedMembershipSize => {
134                write!(f, "membership requires between three and seven recorders")
135            }
136            Self::InvalidRecoveredTip => write!(f, "recovered qlog next index must be positive"),
137            Self::Io(message) => write!(f, "QuePaxa io failed: {message}"),
138            Self::MigrationRequired { format, version } => {
139                write!(f, "QuePaxa {format} version {version} requires migration")
140            }
141            Self::NoQuorum => write!(f, "QuePaxa quorum was not reached"),
142            Self::ProposeFailed => write!(f, "QuePaxa propose failed"),
143            Self::RandomnessUnavailable(message) => {
144                write!(f, "QuePaxa OS randomness unavailable: {message}")
145            }
146            Self::RecorderRootLocked(root) => {
147                write!(f, "recorder root is already owned: {}", root.display())
148            }
149            Self::Rejected(reason) => write!(f, "QuePaxa recorder rejected request: {reason:?}"),
150            Self::ReadFenceUnsupported => {
151                write!(f, "recorder does not implement context-bound read fences")
152            }
153            Self::TypedProofInstallRequired => {
154                write!(
155                    f,
156                    "recorder does not implement typed decision-proof installation"
157                )
158            }
159            Self::TypedRecordRequired => {
160                write!(f, "recorder does not implement the typed Record operation")
161            }
162        }
163    }
164}
165
166impl std::error::Error for Error {}
167
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct Membership {
170    members: Vec<NodeId>,
171    digest: LogHash,
172}
173
174impl Membership {
175    pub fn new<const N: usize>(members: [&str; N]) -> Result<Self> {
176        Self::from_voters(members.into_iter().map(String::from))
177    }
178
179    pub fn from_voters(voters: impl IntoIterator<Item = NodeId>) -> Result<Self> {
180        Self::from_members(voters.into_iter().collect())
181    }
182
183    pub fn members(&self) -> &[NodeId] {
184        &self.members
185    }
186
187    pub fn contains(&self, recorder_id: &str) -> bool {
188        self.members
189            .binary_search_by(|member| member.as_str().cmp(recorder_id))
190            .is_ok()
191    }
192
193    pub const fn digest(&self) -> LogHash {
194        self.digest
195    }
196
197    pub fn quorum_size(&self) -> usize {
198        quorum_size(self.members.len())
199    }
200
201    fn from_members(members: Vec<NodeId>) -> Result<Self> {
202        if members.is_empty() {
203            return Err(Error::EmptyFixedMembership);
204        }
205        if !(3..=7).contains(&members.len()) {
206            return Err(Error::InvalidFixedMembershipSize);
207        }
208        if members.iter().any(String::is_empty) {
209            return Err(Error::EmptyRecorderIdentity);
210        }
211        let member_count = members.len();
212        let members: Vec<_> = members
213            .into_iter()
214            .collect::<BTreeSet<_>>()
215            .into_iter()
216            .collect();
217        if members.len() != member_count {
218            return Err(Error::DuplicateRecorderIdentity);
219        }
220        Ok(Self {
221            digest: canonical_membership_digest(&members)
222                .map_err(|_| Error::InvalidFixedMembershipSize)?,
223            members,
224        })
225    }
226}
227
228pub type FixedMembership = Membership;
229
230pub trait Consensus {
231    fn propose(&self, command: Command) -> Result<LogEntry>;
232}
233
234pub fn quorum_size(n: usize) -> usize {
235    n / 2 + 1
236}
237
238#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
239pub struct Ballot {
240    pub round: Round,
241    pub priority: Priority,
242    pub proposer_id: NodeId,
243}
244
245impl Ballot {
246    pub fn new(round: Round, priority: Priority, proposer_id: impl Into<NodeId>) -> Self {
247        Self {
248            round,
249            priority,
250            proposer_id: proposer_id.into(),
251        }
252    }
253}
254
255#[derive(
256    Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize,
257)]
258pub struct LogicalStep {
259    pub round: Round,
260    pub phase: Phase,
261}
262
263impl LogicalStep {
264    pub const fn as_u64(&self) -> Step {
265        self.round * 4 + self.phase as u64
266    }
267}
268
269#[derive(
270    Clone,
271    Copy,
272    Debug,
273    Default,
274    Eq,
275    Ord,
276    PartialEq,
277    PartialOrd,
278    serde::Deserialize,
279    serde::Serialize,
280)]
281pub struct ProposalPriority(pub [u8; 32]);
282
283impl ProposalPriority {
284    pub const ZERO: Self = Self([0; 32]);
285    pub const MAX: Self = Self([u8::MAX; 32]);
286
287    pub const fn from_u64(value: u64) -> Self {
288        let mut bytes = [0; 32];
289        let encoded = value.to_be_bytes();
290        let mut index = 0;
291        while index < encoded.len() {
292            bytes[24 + index] = encoded[index];
293            index += 1;
294        }
295        Self(bytes)
296    }
297
298    fn legacy_u128(self) -> u128 {
299        u128::from_be_bytes(self.0[16..].try_into().expect("fixed priority suffix"))
300    }
301}
302
303#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
304pub struct Proposal {
305    pub priority: ProposalPriority,
306    pub proposer_id: NodeId,
307    pub proposal_id: u64,
308    pub value: Option<AcceptedValue>,
309}
310
311impl Proposal {
312    pub fn new(
313        priority: ProposalPriority,
314        proposer_id: impl Into<NodeId>,
315        proposal_id: u64,
316        value: AcceptedValue,
317    ) -> Self {
318        Self {
319            priority,
320            proposer_id: proposer_id.into(),
321            proposal_id,
322            value: Some(value),
323        }
324    }
325
326    pub fn nil() -> Self {
327        Self {
328            priority: ProposalPriority::ZERO,
329            proposer_id: String::new(),
330            proposal_id: 0,
331            value: None,
332        }
333    }
334
335    fn identity(&self) -> (ProposalPriority, &str, u64, Option<&AcceptedValue>) {
336        (
337            self.priority,
338            &self.proposer_id,
339            self.proposal_id,
340            self.value.as_ref(),
341        )
342    }
343
344    fn is_nil(&self) -> bool {
345        self.value.is_none()
346    }
347}
348
349impl PartialEq for Proposal {
350    fn eq(&self, other: &Self) -> bool {
351        self.identity() == other.identity()
352    }
353}
354
355impl Eq for Proposal {}
356
357impl PartialOrd for Proposal {
358    fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
359        Some(self.cmp(other))
360    }
361}
362
363impl Ord for Proposal {
364    fn cmp(&self, other: &Self) -> CmpOrdering {
365        match (self.is_nil(), other.is_nil()) {
366            (true, true) => CmpOrdering::Equal,
367            (true, false) => CmpOrdering::Less,
368            (false, true) => CmpOrdering::Greater,
369            (false, false) => self.identity().cmp(&other.identity()),
370        }
371    }
372}
373
374#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
375pub struct IsrState {
376    step: Step,
377    first_current: Option<Proposal>,
378    aggregate_current: Option<Proposal>,
379    aggregate_prior: Option<Proposal>,
380}
381
382#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
383pub struct IsrReply {
384    pub step: Step,
385    pub first_current: Option<Proposal>,
386    pub aggregate_prior: Option<Proposal>,
387}
388
389impl IsrState {
390    pub const fn step(&self) -> Step {
391        self.step
392    }
393
394    pub const fn first_current(&self) -> Option<&Proposal> {
395        self.first_current.as_ref()
396    }
397
398    pub const fn aggregate_current(&self) -> Option<&Proposal> {
399        self.aggregate_current.as_ref()
400    }
401
402    pub const fn aggregate_prior(&self) -> Option<&Proposal> {
403        self.aggregate_prior.as_ref()
404    }
405
406    /// Pure Algorithm 3 transition. Stale inputs return an unchanged state.
407    pub fn record(&self, step: Step, proposal: Proposal) -> (Self, IsrReply) {
408        let mut next = self.clone();
409        if step == next.step {
410            if next.first_current.is_none() {
411                next.first_current = Some(proposal.clone());
412            }
413            if next.aggregate_current.as_ref() < Some(&proposal) {
414                next.aggregate_current = Some(proposal);
415            }
416        } else if step > next.step {
417            next.aggregate_prior = if step == next.step.saturating_add(1) {
418                next.aggregate_current.take()
419            } else {
420                None
421            };
422            next.step = step;
423            next.first_current = Some(proposal.clone());
424            next.aggregate_current = Some(proposal);
425        }
426        let reply = IsrReply {
427            step: next.step,
428            first_current: next.first_current.clone(),
429            aggregate_prior: next.aggregate_prior.clone(),
430        };
431        (next, reply)
432    }
433}
434
435#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
436pub struct AcceptedValue {
437    pub command_hash: LogHash,
438    pub prev_hash: LogHash,
439    pub entry_hash: LogHash,
440}
441
442impl PartialOrd for AcceptedValue {
443    fn partial_cmp(&self, other: &Self) -> Option<CmpOrdering> {
444        Some(self.cmp(other))
445    }
446}
447
448impl Ord for AcceptedValue {
449    fn cmp(&self, other: &Self) -> CmpOrdering {
450        (
451            self.entry_hash.as_bytes(),
452            self.command_hash.as_bytes(),
453            self.prev_hash.as_bytes(),
454        )
455            .cmp(&(
456                other.entry_hash.as_bytes(),
457                other.command_hash.as_bytes(),
458                other.prev_hash.as_bytes(),
459            ))
460    }
461}
462
463impl AcceptedValue {
464    pub fn from_command(
465        cluster_id: &str,
466        slot: Slot,
467        epoch: Epoch,
468        config_id: ConfigId,
469        prev_hash: LogHash,
470        command: &StoredCommand,
471    ) -> Self {
472        Self {
473            command_hash: command.hash(),
474            prev_hash,
475            entry_hash: LogEntry::calculate_hash(
476                cluster_id,
477                slot,
478                epoch,
479                config_id,
480                command.entry_type,
481                prev_hash,
482                &command.payload,
483            ),
484        }
485    }
486}
487
488#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
489pub struct AcceptedSummary {
490    pub ballot: Ballot,
491    pub value: AcceptedValue,
492}
493
494pub type ProposalSummary = AcceptedSummary;
495
496#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
497pub struct DecisionCertificate {
498    pub slot: Slot,
499    pub epoch: Epoch,
500    pub config_id: ConfigId,
501    pub config_digest: LogHash,
502    pub ballot: Ballot,
503    pub value: AcceptedValue,
504    pub recorder_ids: Vec<NodeId>,
505}
506
507impl DecisionCertificate {
508    pub fn cluster_id(&self) -> Option<&str> {
509        decode_certificate_proposer(&self.ballot.proposer_id).map(|(cluster_id, _)| cluster_id)
510    }
511
512    pub fn validate_for_cluster(
513        &self,
514        cluster_id: &str,
515        config_id: ConfigId,
516        membership: &Membership,
517    ) -> std::result::Result<(), RejectReason> {
518        if self.cluster_id() != Some(cluster_id) {
519            return Err(RejectReason::WrongCluster);
520        }
521        self.validate_for(config_id, membership)
522    }
523
524    pub fn validate(&self, membership: &FixedMembership) -> std::result::Result<(), RejectReason> {
525        if self.config_digest != membership.digest() {
526            return Err(RejectReason::WrongConfig);
527        }
528        if self.recorder_ids.len() != membership.quorum_size()
529            || !self.recorder_ids.windows(2).all(|pair| pair[0] < pair[1])
530            || self
531                .recorder_ids
532                .iter()
533                .any(|recorder_id| !membership.contains(recorder_id))
534        {
535            return Err(RejectReason::InvalidCertificate);
536        }
537        Ok(())
538    }
539
540    pub fn validate_for(
541        &self,
542        config_id: ConfigId,
543        membership: &Membership,
544    ) -> std::result::Result<(), RejectReason> {
545        if self.config_id != config_id {
546            return Err(RejectReason::WrongConfig);
547        }
548        self.validate(membership)
549    }
550
551    fn validate_context(
552        &self,
553        slot: Slot,
554        epoch: Epoch,
555        config_id: ConfigId,
556        config_digest: LogHash,
557    ) -> std::result::Result<(), RejectReason> {
558        if self.slot != slot || self.epoch != epoch {
559            return Err(RejectReason::MalformedDecision);
560        }
561        if self.config_id != config_id || self.config_digest != config_digest {
562            return Err(RejectReason::WrongConfig);
563        }
564        Ok(())
565    }
566}
567
568pub type DecisionRecord = DecisionCertificate;
569
570#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
571pub struct RecorderSummary {
572    pub recorder_id: NodeId,
573    pub slot: Slot,
574    pub step: Step,
575    pub first_current: Option<Proposal>,
576    pub aggregate_prior: Option<Proposal>,
577}
578
579#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
580pub enum DecisionProof {
581    FastPath {
582        cluster_id: ClusterId,
583        slot: Slot,
584        epoch: Epoch,
585        config_id: ConfigId,
586        config_digest: LogHash,
587        proposal: Proposal,
588        summaries: Vec<RecorderSummary>,
589    },
590    Phase2 {
591        cluster_id: ClusterId,
592        slot: Slot,
593        epoch: Epoch,
594        config_id: ConfigId,
595        config_digest: LogHash,
596        step: Step,
597        proposal: Proposal,
598        summaries: Vec<RecorderSummary>,
599    },
600}
601
602impl DecisionProof {
603    pub fn proposal(&self) -> &Proposal {
604        match self {
605            Self::FastPath { proposal, .. } | Self::Phase2 { proposal, .. } => proposal,
606        }
607    }
608
609    pub fn validate_for(
610        &self,
611        slot: Slot,
612        epoch: Epoch,
613        config_id: ConfigId,
614        membership: &Membership,
615    ) -> std::result::Result<(), RejectReason> {
616        let (proof_slot, proof_epoch, proof_config, digest, step, proposal, summaries, fast) =
617            match self {
618                Self::FastPath {
619                    slot,
620                    epoch,
621                    config_id,
622                    config_digest,
623                    proposal,
624                    summaries,
625                    ..
626                } => (
627                    *slot,
628                    *epoch,
629                    *config_id,
630                    *config_digest,
631                    4,
632                    proposal,
633                    summaries,
634                    true,
635                ),
636                Self::Phase2 {
637                    slot,
638                    epoch,
639                    config_id,
640                    config_digest,
641                    step,
642                    proposal,
643                    summaries,
644                    ..
645                } => (
646                    *slot,
647                    *epoch,
648                    *config_id,
649                    *config_digest,
650                    *step,
651                    proposal,
652                    summaries,
653                    false,
654                ),
655            };
656        if proof_slot != slot || proof_epoch != epoch {
657            return Err(RejectReason::MalformedDecision);
658        }
659        if proof_config != config_id || digest != membership.digest() {
660            return Err(RejectReason::WrongConfig);
661        }
662        if proposal.is_nil() || proposal.value.is_none() {
663            return Err(RejectReason::InvalidCertificate);
664        }
665        if summaries.len() != membership.quorum_size()
666            || !summaries
667                .windows(2)
668                .all(|pair| pair[0].recorder_id < pair[1].recorder_id)
669            || summaries.iter().any(|summary| {
670                !membership.contains(&summary.recorder_id)
671                    || summary.slot != slot
672                    || summary.step != step
673            })
674        {
675            return Err(RejectReason::InvalidCertificate);
676        }
677        if fast {
678            if step != 4
679                || proposal.priority != ProposalPriority::MAX
680                || summaries.iter().any(|summary| {
681                    !summary
682                        .first_current
683                        .as_ref()
684                        .is_some_and(|candidate| proposal_exact(candidate, proposal))
685                })
686            {
687                return Err(RejectReason::InvalidCertificate);
688            }
689        } else {
690            if step % 4 != 2 {
691                return Err(RejectReason::InvalidCertificate);
692            }
693            let maximum = summaries
694                .iter()
695                .filter_map(|summary| summary.aggregate_prior.as_ref())
696                .max();
697            if maximum != Some(proposal)
698                || !maximum.is_some_and(|candidate| proposal_exact(candidate, proposal))
699            {
700                return Err(RejectReason::InvalidCertificate);
701            }
702        }
703        Ok(())
704    }
705
706    pub fn validate_for_cluster(
707        &self,
708        cluster_id: &str,
709        slot: Slot,
710        epoch: Epoch,
711        config_id: ConfigId,
712        membership: &Membership,
713    ) -> std::result::Result<(), RejectReason> {
714        if proof_cluster_id(self) != cluster_id {
715            return Err(RejectReason::WrongCluster);
716        }
717        self.validate_for(slot, epoch, config_id, membership)
718    }
719}
720
721fn proposal_exact(left: &Proposal, right: &Proposal) -> bool {
722    left == right && left.value == right.value
723}
724
725#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
726pub struct RecordRequest {
727    pub cluster_id: ClusterId,
728    pub epoch: Epoch,
729    pub config_id: ConfigId,
730    pub config_digest: LogHash,
731    pub slot: Slot,
732    pub step: Step,
733    pub proposal: Proposal,
734    #[serde(default)]
735    pub command: Option<StoredCommand>,
736}
737
738#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
739pub struct RecordSummary {
740    pub recorder_id: NodeId,
741    pub slot: Slot,
742    pub config_id: ConfigId,
743    pub config_digest: LogHash,
744    pub step: Step,
745    pub first_current: Option<Proposal>,
746    pub aggregate_prior: Option<Proposal>,
747    pub decided: Option<DecisionProof>,
748}
749
750#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
751pub struct ReadFenceRequest {
752    pub cluster_id: ClusterId,
753    pub epoch: Epoch,
754    pub config_id: ConfigId,
755    pub config_digest: LogHash,
756    pub slot: Slot,
757}
758
759#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
760pub enum ReadFenceSlotState {
761    Empty,
762    /// The exact slot is present, or a durable later slot proves that this
763    /// recorder has already crossed the requested position. A crossed gap has
764    /// no exact summary and must therefore fail closed as pending.
765    Occupied {
766        summary: Option<Box<RecordSummary>>,
767    },
768}
769
770#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
771pub struct ReadFenceObservation {
772    pub recorder_id: NodeId,
773    pub cluster_id: ClusterId,
774    pub epoch: Epoch,
775    pub config_id: ConfigId,
776    pub config_digest: LogHash,
777    pub slot: Slot,
778    pub max_head: Option<Slot>,
779    pub slot_state: ReadFenceSlotState,
780}
781
782fn valid_read_fence_observation(
783    observation: &ReadFenceObservation,
784    expected_recorder_id: &str,
785    request: &ReadFenceRequest,
786) -> bool {
787    if observation.recorder_id != expected_recorder_id
788        || observation.cluster_id != request.cluster_id
789        || observation.epoch != request.epoch
790        || observation.config_id != request.config_id
791        || observation.config_digest != request.config_digest
792        || observation.slot != request.slot
793    {
794        return false;
795    }
796    match &observation.slot_state {
797        ReadFenceSlotState::Empty => observation
798            .max_head
799            .is_none_or(|max_head| max_head < request.slot),
800        ReadFenceSlotState::Occupied { summary } => {
801            observation
802                .max_head
803                .is_some_and(|max_head| max_head >= request.slot)
804                && summary.as_ref().is_none_or(|summary| {
805                    summary.recorder_id == observation.recorder_id
806                        && summary.slot == request.slot
807                        && summary.config_id == request.config_id
808                        && summary.config_digest == request.config_digest
809                })
810        }
811    }
812}
813
814#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
815pub struct RecordResponse {
816    pub from: NodeId,
817    pub slot: Slot,
818    pub step: Step,
819    pub highest_promised: Option<Ballot>,
820    pub accepted: Option<AcceptedSummary>,
821    pub recorder_epoch: Epoch,
822    pub config_id: ConfigId,
823    pub config_digest: LogHash,
824}
825
826#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
827pub struct RecorderReply {
828    pub recorder_id: NodeId,
829    pub slot: Slot,
830    pub config_id: ConfigId,
831    pub config_digest: LogHash,
832    pub step: Step,
833    pub highest_promised: Option<Ballot>,
834    pub accepted: Option<AcceptedSummary>,
835    pub decided: Option<DecisionCertificate>,
836    pub command: Option<StoredCommand>,
837}
838
839#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
840pub enum RecorderRequest {
841    Identity,
842    StoreCommand {
843        cluster_id: ClusterId,
844        epoch: Epoch,
845        config_id: ConfigId,
846        config_digest: LogHash,
847        command_hash: LogHash,
848        command: StoredCommand,
849    },
850    FetchCommand {
851        cluster_id: ClusterId,
852        epoch: Epoch,
853        config_id: ConfigId,
854        config_digest: LogHash,
855        command_hash: LogHash,
856    },
857    Inspect {
858        cluster_id: ClusterId,
859        epoch: Epoch,
860        config_id: ConfigId,
861        config_digest: LogHash,
862        slot: Slot,
863    },
864    /// Legacy transport envelope; active protocol code uses `RecorderRpc::record`.
865    Observe {
866        cluster_id: ClusterId,
867        epoch: Epoch,
868        config_id: ConfigId,
869        config_digest: LogHash,
870        slot: Slot,
871        ballot: Ballot,
872    },
873    /// Legacy transport envelope; active protocol code uses `RecorderRpc::record`.
874    Converge {
875        cluster_id: ClusterId,
876        epoch: Epoch,
877        config_id: ConfigId,
878        config_digest: LogHash,
879        slot: Slot,
880        ballot: Ballot,
881        value: AcceptedValue,
882    },
883    Decide {
884        cluster_id: ClusterId,
885        epoch: Epoch,
886        config_id: ConfigId,
887        config_digest: LogHash,
888        slot: Slot,
889        decision: DecisionCertificate,
890    },
891}
892
893#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
894pub enum RejectReason {
895    StaleEpoch,
896    FutureEpoch,
897    WrongCluster,
898    WrongConfig,
899    WrongSlot,
900    AlreadyDecided,
901    MalformedDecision,
902    BallotPromised { promised: Ballot },
903    ConflictingValue,
904    InvalidValue,
905    InvalidCertificate,
906    ConfigurationSealed { stop_slot: Slot },
907    ConfigurationNotInstalled,
908    ActivationRequired,
909    TransitionInProgress,
910    InvalidTransition,
911    LocalVoterRequired,
912    StepRegression,
913    InvalidRequest,
914}
915
916#[derive(Clone, Debug, Eq, PartialEq)]
917pub struct ConfigurationSeal {
918    pub stop_slot: Slot,
919    pub command_hash: LogHash,
920    pub prefix_hash: LogHash,
921}
922
923#[derive(Clone, Debug, Eq, PartialEq)]
924pub struct ConfigurationState {
925    config_id: ConfigId,
926    config_digest: LogHash,
927    membership: Option<Membership>,
928    predecessor: Option<ConfigurationSeal>,
929    seal: Option<ConfigurationSeal>,
930    max_accepted_or_decided_slot: Option<Slot>,
931    activated: bool,
932}
933
934impl ConfigurationState {
935    pub const fn config_id(&self) -> ConfigId {
936        self.config_id
937    }
938
939    pub const fn config_digest(&self) -> LogHash {
940        self.config_digest
941    }
942
943    pub const fn membership(&self) -> Option<&Membership> {
944        self.membership.as_ref()
945    }
946
947    pub const fn predecessor(&self) -> Option<&ConfigurationSeal> {
948        self.predecessor.as_ref()
949    }
950
951    pub const fn seal(&self) -> Option<&ConfigurationSeal> {
952        self.seal.as_ref()
953    }
954
955    pub const fn is_activated(&self) -> bool {
956        self.activated
957    }
958
959    pub const fn max_accepted_or_decided_slot(&self) -> Option<Slot> {
960        self.max_accepted_or_decided_slot
961    }
962
963    fn initial(
964        config_id: ConfigId,
965        config_digest: LogHash,
966        membership: Option<Membership>,
967    ) -> Self {
968        Self {
969            config_id,
970            config_digest,
971            membership,
972            predecessor: None,
973            seal: None,
974            max_accepted_or_decided_slot: None,
975            activated: true,
976        }
977    }
978}
979
980#[derive(Clone, Copy, Debug, Eq, PartialEq)]
981pub enum SealFaultPoint {
982    AfterIntent,
983    AfterSlot,
984    AfterConfiguration,
985    BeforeRecordManifest,
986    AfterRecordManifest,
987    AfterRecordCache,
988    AfterHeadIntent,
989    AfterHeadConfiguration,
990    AfterHead,
991    AfterWalWrite,
992    AfterWalSync,
993}
994
995#[derive(Clone, Debug, Eq, PartialEq)]
996pub struct RecorderSlotState {
997    slot: Slot,
998    cluster_id: ClusterId,
999    epoch: Epoch,
1000    config_id: ConfigId,
1001    config_digest: LogHash,
1002    highest_promised: Option<Ballot>,
1003    accepted: Option<AcceptedSummary>,
1004    decided: Option<DecisionCertificate>,
1005    isr: IsrState,
1006    decided_proof: Option<DecisionProof>,
1007}
1008
1009impl RecorderSlotState {
1010    pub fn new(
1011        slot: Slot,
1012        cluster_id: impl Into<ClusterId>,
1013        epoch: Epoch,
1014        config_id: ConfigId,
1015    ) -> Self {
1016        Self::new_with_digest(slot, cluster_id, epoch, config_id, LogHash::ZERO)
1017    }
1018
1019    pub fn new_with_digest(
1020        slot: Slot,
1021        cluster_id: impl Into<ClusterId>,
1022        epoch: Epoch,
1023        config_id: ConfigId,
1024        config_digest: LogHash,
1025    ) -> Self {
1026        Self {
1027            slot,
1028            cluster_id: cluster_id.into(),
1029            epoch,
1030            config_id,
1031            config_digest,
1032            highest_promised: None,
1033            accepted: None,
1034            decided: None,
1035            isr: IsrState::default(),
1036            decided_proof: None,
1037        }
1038    }
1039
1040    pub fn apply(
1041        &mut self,
1042        request: RecorderRequest,
1043    ) -> std::result::Result<RecorderReply, RejectReason> {
1044        match request {
1045            RecorderRequest::Inspect {
1046                cluster_id,
1047                epoch,
1048                config_id,
1049                config_digest,
1050                slot,
1051            } => self.inspect(cluster_id, epoch, config_id, config_digest, slot),
1052            RecorderRequest::Observe { .. }
1053            | RecorderRequest::Converge { .. }
1054            | RecorderRequest::Decide { .. } => Err(RejectReason::InvalidRequest),
1055            RecorderRequest::Identity
1056            | RecorderRequest::StoreCommand { .. }
1057            | RecorderRequest::FetchCommand { .. } => Err(RejectReason::InvalidRequest),
1058        }
1059    }
1060
1061    pub fn decided(&self) -> Option<&DecisionCertificate> {
1062        self.decided.as_ref()
1063    }
1064
1065    pub fn decision_proof(&self) -> Option<&DecisionProof> {
1066        self.decided_proof.as_ref()
1067    }
1068
1069    pub fn isr(&self) -> &IsrState {
1070        &self.isr
1071    }
1072
1073    pub fn record(
1074        &self,
1075        request: &RecordRequest,
1076    ) -> std::result::Result<(Self, IsrReply), RejectReason> {
1077        self.validate(
1078            request.cluster_id.clone(),
1079            request.epoch,
1080            request.config_id,
1081            request.config_digest,
1082            request.slot,
1083        )?;
1084        let mut next = self.clone();
1085        let (isr, reply) = self.isr.record(request.step, request.proposal.clone());
1086        next.isr = isr;
1087        Ok((next, reply))
1088    }
1089
1090    fn install_proof(&mut self, proof: DecisionProof) -> std::result::Result<(), RejectReason> {
1091        if let Some(existing) = &self.decided_proof {
1092            if existing.proposal().value != proof.proposal().value {
1093                return Err(RejectReason::AlreadyDecided);
1094            }
1095            return Ok(());
1096        }
1097        self.decided_proof = Some(proof);
1098        Ok(())
1099    }
1100
1101    pub const fn slot(&self) -> Slot {
1102        self.slot
1103    }
1104
1105    pub fn cluster_id(&self) -> &str {
1106        &self.cluster_id
1107    }
1108
1109    pub const fn epoch(&self) -> Epoch {
1110        self.epoch
1111    }
1112
1113    pub const fn config_id(&self) -> ConfigId {
1114        self.config_id
1115    }
1116
1117    pub const fn config_digest(&self) -> LogHash {
1118        self.config_digest
1119    }
1120
1121    pub fn highest_promised(&self) -> Option<&Ballot> {
1122        self.highest_promised.as_ref()
1123    }
1124
1125    pub fn accepted(&self) -> Option<&AcceptedSummary> {
1126        self.accepted.as_ref()
1127    }
1128
1129    pub fn max_step_seen(&self) -> Step {
1130        self.highest_promised
1131            .as_ref()
1132            .map_or(0, |ballot| ballot.round)
1133    }
1134
1135    fn inspect(
1136        &self,
1137        cluster_id: ClusterId,
1138        epoch: Epoch,
1139        config_id: ConfigId,
1140        config_digest: LogHash,
1141        slot: Slot,
1142    ) -> std::result::Result<RecorderReply, RejectReason> {
1143        self.validate(cluster_id, epoch, config_id, config_digest, slot)?;
1144        Ok(self.reply())
1145    }
1146
1147    fn validate(
1148        &self,
1149        cluster_id: ClusterId,
1150        epoch: Epoch,
1151        config_id: ConfigId,
1152        config_digest: LogHash,
1153        slot: Slot,
1154    ) -> std::result::Result<(), RejectReason> {
1155        if cluster_id != self.cluster_id {
1156            return Err(RejectReason::WrongCluster);
1157        }
1158        if slot != self.slot {
1159            return Err(RejectReason::WrongSlot);
1160        }
1161        if epoch < self.epoch {
1162            return Err(RejectReason::StaleEpoch);
1163        }
1164        if epoch > self.epoch {
1165            return Err(RejectReason::FutureEpoch);
1166        }
1167        if config_id != self.config_id {
1168            return Err(RejectReason::WrongConfig);
1169        }
1170        if config_digest != self.config_digest {
1171            return Err(RejectReason::WrongConfig);
1172        }
1173        Ok(())
1174    }
1175
1176    fn reply(&self) -> RecorderReply {
1177        RecorderReply {
1178            recorder_id: String::new(),
1179            slot: self.slot,
1180            config_id: self.config_id,
1181            config_digest: self.config_digest,
1182            step: self.max_step_seen(),
1183            highest_promised: self.highest_promised.clone(),
1184            accepted: self.accepted.clone(),
1185            decided: self.decided.clone(),
1186            command: None,
1187        }
1188    }
1189}
1190
1191#[derive(Clone, Debug)]
1192pub struct RecorderFileStore {
1193    root: PathBuf,
1194    recorder_id: NodeId,
1195    cluster_id: ClusterId,
1196    epoch: Epoch,
1197    config_id: ConfigId,
1198    config_digest: LogHash,
1199    configuration: Arc<Mutex<ConfigurationState>>,
1200    recorded_head: Arc<Mutex<RecordedHeadProvenance>>,
1201    recent_slots: Arc<Mutex<Vec<DurableSlotSnapshot>>>,
1202    wal: Arc<Mutex<RecorderWal>>,
1203    seal_fault: Arc<Mutex<Option<SealFaultPoint>>>,
1204    _root_lock: Arc<fs::File>,
1205    sync: Arc<Mutex<()>>,
1206}
1207
1208const RECORDED_HEAD_MAGIC: &[u8; 4] = b"QRHD";
1209const RECORDED_HEAD_VERSION: u16 = 3;
1210const RECORDER_WAL_MAGIC: &[u8; 4] = b"QWAL";
1211const RECORDER_WAL_VERSION: u16 = 1;
1212// Keep rotation bounded while allowing 512 KiB replicated commands to amortize
1213// quorum fsyncs without forcing a synchronous checkpoint every few dozen slots.
1214const RECORDER_WAL_SOFT_BYTE_LIMIT: u64 = 64 * 1024 * 1024;
1215#[cfg(not(test))]
1216const RECORDER_WAL_HARD_FRAME_LIMIT: u64 = 1_024;
1217#[cfg(test)]
1218const RECORDER_WAL_HARD_FRAME_LIMIT: u64 = 32;
1219
1220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1221struct WalCheckpoint {
1222    generation: u64,
1223    through_sequence: u64,
1224}
1225
1226impl Default for WalCheckpoint {
1227    fn default() -> Self {
1228        Self {
1229            generation: 1,
1230            through_sequence: 0,
1231        }
1232    }
1233}
1234
1235#[derive(Debug)]
1236struct RecorderWal {
1237    checkpoint: WalCheckpoint,
1238    next_sequence: u64,
1239    last_digest: LogHash,
1240    frame_count: u64,
1241    byte_count: u64,
1242    slots: BTreeMap<Slot, Vec<u8>>,
1243    commands: HashMap<LogHash, StoredCommand>,
1244    file: Option<fs::File>,
1245    failed: bool,
1246}
1247
1248impl Default for RecorderWal {
1249    fn default() -> Self {
1250        Self {
1251            checkpoint: WalCheckpoint::default(),
1252            next_sequence: 1,
1253            last_digest: LogHash::ZERO,
1254            frame_count: 0,
1255            byte_count: 0,
1256            slots: BTreeMap::new(),
1257            commands: HashMap::new(),
1258            file: None,
1259            failed: false,
1260        }
1261    }
1262}
1263
1264#[derive(Debug)]
1265struct WalFrame {
1266    generation: u64,
1267    sequence: u64,
1268    prev_digest: LogHash,
1269    digest: LogHash,
1270    slot: Slot,
1271    slot_bytes: Vec<u8>,
1272    configuration_bytes: Vec<u8>,
1273    head: RecordedHeadProvenance,
1274    command: Option<(LogHash, StoredCommand)>,
1275}
1276
1277#[derive(Clone, Debug, Eq, PartialEq)]
1278struct DurableSlotSnapshot {
1279    slot: Slot,
1280    bytes: Vec<u8>,
1281}
1282
1283#[derive(Clone, Debug, Eq, PartialEq)]
1284enum RecordedHeadProvenance {
1285    Empty,
1286    SlotBacked {
1287        slot: Slot,
1288    },
1289    CheckpointBacked {
1290        stop_slot: Slot,
1291        prefix_hash: LogHash,
1292        recovered_tip: Slot,
1293        recovered_hash: LogHash,
1294    },
1295}
1296
1297pub trait RecorderRpc: Send + Sync {
1298    /// Performs one genuine QuePaxa Record operation.
1299    ///
1300    /// Network implementations must enforce a finite deadline and return an
1301    /// error when it expires. The same bounded-call requirement applies to all
1302    /// process-bound methods on this trait.
1303    fn record(&self, _request: RecordRequest) -> Result<RecordSummary> {
1304        Err(Error::TypedRecordRequired)
1305    }
1306
1307    fn install_decision_proof(
1308        &self,
1309        _proof: DecisionProof,
1310        _membership: &Membership,
1311    ) -> Result<()> {
1312        Err(Error::TypedProofInstallRequired)
1313    }
1314
1315    fn inspect_decision_proof(&self, _slot: Slot) -> Result<Option<DecisionProof>> {
1316        Ok(None)
1317    }
1318
1319    fn inspect_record_summary(&self, _slot: Slot) -> Result<Option<RecordSummary>> {
1320        Err(Error::TypedRecordRequired)
1321    }
1322
1323    /// Whether this recorder can atomically bind an exact slot observation to
1324    /// the durable maximum accepted-or-decided head and the requested config.
1325    fn supports_context_read_fence(&self) -> bool {
1326        false
1327    }
1328
1329    fn observe_read_fence(&self, _request: ReadFenceRequest) -> Result<ReadFenceObservation> {
1330        Err(Error::ReadFenceUnsupported)
1331    }
1332
1333    fn recorder_id(&self) -> Result<NodeId> {
1334        Err(Error::TypedRecordRequired)
1335    }
1336
1337    fn store_command(&self, command_hash: LogHash, command: StoredCommand) -> Result<()> {
1338        self.store_command_for(String::new(), 0, 0, LogHash::ZERO, command_hash, command)
1339    }
1340
1341    fn store_command_for(
1342        &self,
1343        cluster_id: ClusterId,
1344        epoch: Epoch,
1345        config_id: ConfigId,
1346        config_digest: LogHash,
1347        command_hash: LogHash,
1348        command: StoredCommand,
1349    ) -> Result<()> {
1350        let _ = (
1351            cluster_id,
1352            epoch,
1353            config_id,
1354            config_digest,
1355            command_hash,
1356            command,
1357        );
1358        Err(Error::TypedRecordRequired)
1359    }
1360
1361    fn fetch_command(&self, command_hash: LogHash) -> Result<Option<StoredCommand>> {
1362        self.fetch_command_for(String::new(), 0, 0, LogHash::ZERO, command_hash)
1363    }
1364
1365    fn fetch_command_for(
1366        &self,
1367        cluster_id: ClusterId,
1368        epoch: Epoch,
1369        config_id: ConfigId,
1370        config_digest: LogHash,
1371        command_hash: LogHash,
1372    ) -> Result<Option<StoredCommand>> {
1373        let _ = (cluster_id, epoch, config_id, config_digest, command_hash);
1374        Err(Error::TypedRecordRequired)
1375    }
1376}
1377
1378impl RecorderFileStore {
1379    /// Classifies an existing recorder root without creating, recovering, truncating, or
1380    /// rewriting local state. `Recoverable` is limited to durable normal-crash artifacts that a
1381    /// subsequent locked open knows how to finish.
1382    #[doc(hidden)]
1383    pub fn preflight_existing_with_membership_outcome(
1384        root: impl AsRef<Path>,
1385        cluster_id: &str,
1386        epoch: Epoch,
1387        config_id: ConfigId,
1388        membership: &Membership,
1389    ) -> Result<RecorderPreflight> {
1390        let root = root.as_ref();
1391        let metadata = match fs::symlink_metadata(root) {
1392            Ok(metadata) => metadata,
1393            Err(error) if error.kind() == io::ErrorKind::NotFound => {
1394                return Ok(RecorderPreflight::Missing)
1395            }
1396            Err(error) => return Err(Error::Io(error.to_string())),
1397        };
1398        if metadata.file_type().is_symlink() || !metadata.is_dir() {
1399            return Err(Error::Decode(
1400                "recorder root must be a real directory".into(),
1401            ));
1402        }
1403        let entries = fs::read_dir(root)
1404            .map_err(|error| Error::Io(error.to_string()))?
1405            .collect::<std::result::Result<Vec<_>, _>>()
1406            .map_err(|error| Error::Io(error.to_string()))?;
1407        if entries.is_empty()
1408            || entries
1409                .iter()
1410                .all(|entry| entry.file_name() == ".recorder.lock")
1411        {
1412            return Ok(RecorderPreflight::Missing);
1413        }
1414        let transition_intent =
1415            read_preflight_intent(root, "configuration.intent", MAX_TRANSITION_INTENT_BYTES)?;
1416        let configuration_head_intent = read_preflight_intent(
1417            root,
1418            "configuration-head.intent",
1419            MAX_CONFIGURATION_HEAD_INTENT_BYTES,
1420        )?;
1421        match (transition_intent, configuration_head_intent) {
1422            (Some(_), Some(_)) => {
1423                return Err(Error::Decode(
1424                    "recorder has conflicting recovery intents".into(),
1425                ))
1426            }
1427            (Some(bytes), None) => {
1428                validate_recoverable_transition_intent(
1429                    root, &bytes, cluster_id, epoch, config_id, membership,
1430                )?;
1431                validate_empty_recovery_wal(root)?;
1432                return Ok(RecorderPreflight::Recoverable);
1433            }
1434            (None, Some(bytes)) => {
1435                validate_recoverable_configuration_head_intent(
1436                    root, &bytes, cluster_id, epoch, config_id, membership,
1437                )?;
1438                validate_empty_recovery_wal(root)?;
1439                return Ok(RecorderPreflight::Recoverable);
1440            }
1441            (None, None) => {}
1442        }
1443        let name = ".recorder.lock";
1444        let path = root.join(name);
1445        let metadata = fs::symlink_metadata(&path)
1446            .map_err(|_| Error::Decode(format!("recorder is missing {name}")))?;
1447        if metadata.file_type().is_symlink() || !metadata.is_file() {
1448            return Err(Error::Decode(format!(
1449                "recorder {name} must be a regular file"
1450            )));
1451        }
1452        let configuration = decode_configuration_state(&read_regular_file_bounded(
1453            &root.join("configuration.rec"),
1454            MAX_CONFIGURATION_BYTES,
1455            "configuration.rec",
1456        )?)?;
1457        validate_preflight_configuration(&configuration, config_id, membership)?;
1458        let (head, recent_slots, checkpoint) = decode_recorded_head(
1459            &read_regular_file_bounded(
1460                &root.join("recorded-head.rec"),
1461                MAX_RECORDED_HEAD_BYTES,
1462                "recorded-head.rec",
1463            )?,
1464            cluster_id,
1465            epoch,
1466            &configuration,
1467        )?;
1468        validate_existing_snapshots(root, &recent_slots, cluster_id, epoch, &configuration)?;
1469        let torn_wal = validate_existing_wal(
1470            root,
1471            &read_regular_file_bounded(
1472                &root.join("recorder.wal"),
1473                MAX_RECORDER_WAL_BYTES,
1474                "recorder.wal",
1475            )?,
1476            cluster_id,
1477            epoch,
1478            &configuration,
1479            &head,
1480            checkpoint,
1481        )?;
1482        Ok(if torn_wal {
1483            RecorderPreflight::Recoverable
1484        } else {
1485            RecorderPreflight::Valid
1486        })
1487    }
1488
1489    pub fn new(
1490        root: impl Into<PathBuf>,
1491        cluster_id: impl Into<ClusterId>,
1492        epoch: Epoch,
1493        config_id: ConfigId,
1494    ) -> Result<Self> {
1495        let root = root.into();
1496        let recorder_id = root
1497            .file_name()
1498            .and_then(|name| name.to_str())
1499            .filter(|name| !name.is_empty())
1500            .unwrap_or("recorder")
1501            .to_string();
1502        Self::new_with_id(root, recorder_id, cluster_id, epoch, config_id)
1503    }
1504
1505    pub fn new_with_id(
1506        root: impl Into<PathBuf>,
1507        recorder_id: impl Into<NodeId>,
1508        cluster_id: impl Into<ClusterId>,
1509        epoch: Epoch,
1510        config_id: ConfigId,
1511    ) -> Result<Self> {
1512        let (store, existing_format) =
1513            Self::open_root(root, recorder_id, cluster_id, epoch, config_id)?;
1514        store.open_or_initialize_recorded_head(existing_format)?;
1515        store.open_or_replay_wal()?;
1516        Ok(store)
1517    }
1518
1519    fn open_root(
1520        root: impl Into<PathBuf>,
1521        recorder_id: impl Into<NodeId>,
1522        cluster_id: impl Into<ClusterId>,
1523        epoch: Epoch,
1524        config_id: ConfigId,
1525    ) -> Result<(Self, bool)> {
1526        let root = root.into();
1527        let head_exists = root.join("recorded-head.rec").exists();
1528        let legacy_files_exist = if root.exists() && !head_exists {
1529            fs::read_dir(&root)
1530                .map_err(|err| Error::Io(err.to_string()))?
1531                .filter_map(std::result::Result::ok)
1532                .any(|entry| {
1533                    let name = entry.file_name();
1534                    let name = name.to_string_lossy();
1535                    name == "configuration.rec"
1536                        || name.starts_with("slot-")
1537                        || name.starts_with("command-")
1538                })
1539        } else {
1540            false
1541        };
1542        let existing_format = head_exists || legacy_files_exist;
1543        let recorder_id = recorder_id.into();
1544        if recorder_id.is_empty() {
1545            return Err(Error::EmptyRecorderIdentity);
1546        }
1547        fs::create_dir_all(&root).map_err(|err| Error::Io(err.to_string()))?;
1548        let root_lock = fs::OpenOptions::new()
1549            .read(true)
1550            .write(true)
1551            .create(true)
1552            .truncate(false)
1553            .open(root.join(".recorder.lock"))
1554            .map_err(|err| Error::Io(err.to_string()))?;
1555        match root_lock.try_lock() {
1556            Ok(()) => {}
1557            Err(fs::TryLockError::WouldBlock) => {
1558                return Err(Error::RecorderRootLocked(root));
1559            }
1560            Err(fs::TryLockError::Error(err)) => return Err(Error::Io(err.to_string())),
1561        }
1562        Ok((
1563            Self {
1564                root,
1565                recorder_id,
1566                cluster_id: cluster_id.into(),
1567                epoch,
1568                config_id,
1569                config_digest: LogHash::ZERO,
1570                configuration: Arc::new(Mutex::new(ConfigurationState::initial(
1571                    config_id,
1572                    LogHash::ZERO,
1573                    None,
1574                ))),
1575                recorded_head: Arc::new(Mutex::new(RecordedHeadProvenance::Empty)),
1576                recent_slots: Arc::new(Mutex::new(Vec::new())),
1577                wal: Arc::new(Mutex::new(RecorderWal::default())),
1578                seal_fault: Arc::new(Mutex::new(None)),
1579                _root_lock: Arc::new(root_lock),
1580                sync: Arc::new(Mutex::new(())),
1581            },
1582            existing_format,
1583        ))
1584    }
1585
1586    fn open_existing_root(
1587        root: PathBuf,
1588        recorder_id: NodeId,
1589        cluster_id: ClusterId,
1590        epoch: Epoch,
1591        config_id: ConfigId,
1592    ) -> Result<(Self, bool)> {
1593        let root_metadata = match fs::symlink_metadata(&root) {
1594            Ok(metadata) => metadata,
1595            Err(error) if error.kind() == io::ErrorKind::NotFound => {
1596                return Err(Error::Decode("recorder root does not exist".into()))
1597            }
1598            Err(error) => return Err(Error::Io(error.to_string())),
1599        };
1600        if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
1601            return Err(Error::Decode(
1602                "recorder root must be an existing real directory".into(),
1603            ));
1604        }
1605        if recorder_id.is_empty() {
1606            return Err(Error::EmptyRecorderIdentity);
1607        }
1608        let root_lock_path = root.join(".recorder.lock");
1609        let root_lock = open_existing_root_lock(&root_lock_path)?;
1610        match root_lock.try_lock() {
1611            Ok(()) => {}
1612            Err(fs::TryLockError::WouldBlock) => {
1613                return Err(Error::RecorderRootLocked(root));
1614            }
1615            Err(fs::TryLockError::Error(error)) => return Err(Error::Io(error.to_string())),
1616        }
1617        let head_exists = fs::symlink_metadata(root.join("recorded-head.rec"))
1618            .map(|metadata| !metadata.file_type().is_symlink() && metadata.is_file())
1619            .unwrap_or(false);
1620        let legacy_files_exist = if !head_exists {
1621            fs::read_dir(&root)
1622                .map_err(|error| Error::Io(error.to_string()))?
1623                .filter_map(std::result::Result::ok)
1624                .any(|entry| {
1625                    let name = entry.file_name();
1626                    let name = name.to_string_lossy();
1627                    name == "configuration.rec"
1628                        || name.starts_with("slot-")
1629                        || name.starts_with("command-")
1630                })
1631        } else {
1632            false
1633        };
1634        Ok((
1635            Self {
1636                root,
1637                recorder_id,
1638                cluster_id,
1639                epoch,
1640                config_id,
1641                config_digest: LogHash::ZERO,
1642                configuration: Arc::new(Mutex::new(ConfigurationState::initial(
1643                    config_id,
1644                    LogHash::ZERO,
1645                    None,
1646                ))),
1647                recorded_head: Arc::new(Mutex::new(RecordedHeadProvenance::Empty)),
1648                recent_slots: Arc::new(Mutex::new(Vec::new())),
1649                wal: Arc::new(Mutex::new(RecorderWal::default())),
1650                seal_fault: Arc::new(Mutex::new(None)),
1651                _root_lock: Arc::new(root_lock),
1652                sync: Arc::new(Mutex::new(())),
1653            },
1654            head_exists || legacy_files_exist,
1655        ))
1656    }
1657
1658    pub fn new_with_membership(
1659        root: impl Into<PathBuf>,
1660        recorder_id: impl Into<NodeId>,
1661        cluster_id: impl Into<ClusterId>,
1662        epoch: Epoch,
1663        config_id: ConfigId,
1664        membership: Membership,
1665    ) -> Result<Self> {
1666        Self::open_with_membership(
1667            root.into(),
1668            recorder_id.into(),
1669            cluster_id.into(),
1670            epoch,
1671            config_id,
1672            membership,
1673            false,
1674        )
1675    }
1676
1677    /// Opens a recorder that must already exist. Unlike `new_with_membership`, this path never
1678    /// creates a directory, lock file, or fresh recorder state.
1679    #[doc(hidden)]
1680    pub fn open_existing_with_membership(
1681        root: impl Into<PathBuf>,
1682        recorder_id: impl Into<NodeId>,
1683        cluster_id: impl Into<ClusterId>,
1684        epoch: Epoch,
1685        config_id: ConfigId,
1686        membership: Membership,
1687    ) -> Result<Self> {
1688        Self::open_with_membership(
1689            root.into(),
1690            recorder_id.into(),
1691            cluster_id.into(),
1692            epoch,
1693            config_id,
1694            membership,
1695            true,
1696        )
1697    }
1698
1699    fn open_with_membership(
1700        root: PathBuf,
1701        recorder_id: NodeId,
1702        cluster_id: ClusterId,
1703        epoch: Epoch,
1704        config_id: ConfigId,
1705        membership: Membership,
1706        existing_only: bool,
1707    ) -> Result<Self> {
1708        let (mut store, existing_format) = if existing_only {
1709            Self::open_existing_root(root, recorder_id, cluster_id, epoch, config_id)?
1710        } else {
1711            Self::open_root(root, recorder_id, cluster_id, epoch, config_id)?
1712        };
1713        // Preflight is advisory until this process owns the recorder lock. Re-run the same
1714        // bounded, no-follow validation under that lock before an intent can rewrite authority
1715        // files, so a path replacement after startup classification cannot authorize mutation.
1716        match Self::preflight_existing_with_membership_outcome(
1717            &store.root,
1718            &store.cluster_id,
1719            store.epoch,
1720            config_id,
1721            &membership,
1722        ) {
1723            Ok(RecorderPreflight::Missing) if existing_only => {
1724                return Err(Error::Decode(
1725                    "recorder root is missing durable state".into(),
1726                ))
1727            }
1728            Ok(_) => {}
1729            // A pre-manifest recorder is legacy state. Preserve the established migration error
1730            // below; it cannot reach intent recovery because it has no complete authority set.
1731            Err(Error::Decode(message))
1732                if existing_format && message.starts_with("recorder is missing ") => {}
1733            Err(error) => return Err(error),
1734        }
1735        store.recover_configuration_head_intent()?;
1736        let configured = match read_regular_file_bounded(
1737            &store.configuration_path(),
1738            MAX_CONFIGURATION_BYTES,
1739            "configuration.rec",
1740        ) {
1741            Ok(bytes) => decode_configuration_state(&bytes)?,
1742            Err(Error::Decode(message)) if message == "recorder is missing configuration.rec" => {
1743                if existing_format {
1744                    return Err(Error::MigrationRequired {
1745                        format: "recorder durable head",
1746                        version: 2,
1747                    });
1748                }
1749                let configured =
1750                    ConfigurationState::initial(config_id, membership.digest(), Some(membership));
1751                store.commit_configuration_head_unlocked(
1752                    &configured,
1753                    &RecordedHeadProvenance::Empty,
1754                )?;
1755                configured
1756            }
1757            Err(error) => return Err(error),
1758        };
1759        if configured
1760            .membership
1761            .as_ref()
1762            .is_some_and(|current| current.digest() != configured.config_digest)
1763        {
1764            return Err(Error::Decode("installed membership digest mismatch".into()));
1765        }
1766        store.config_id = configured.config_id;
1767        store.config_digest = configured.config_digest;
1768        store.configuration = Arc::new(Mutex::new(configured));
1769        store.recover_intent()?;
1770        store.open_or_initialize_recorded_head(existing_format)?;
1771        store.open_or_replay_wal()?;
1772        Ok(store)
1773    }
1774
1775    pub fn configuration_state(&self) -> Result<ConfigurationState> {
1776        self.configuration
1777            .lock()
1778            .map(|state| state.clone())
1779            .map_err(|_| Error::Io("configuration lock poisoned".into()))
1780    }
1781
1782    #[doc(hidden)]
1783    pub fn set_seal_fault(&self, fault: Option<SealFaultPoint>) -> Result<()> {
1784        *self
1785            .seal_fault
1786            .lock()
1787            .map_err(|_| Error::Io("seal fault lock poisoned".into()))? = fault;
1788        Ok(())
1789    }
1790
1791    pub fn install_successor(
1792        &self,
1793        _next_config_id: ConfigId,
1794        _membership: Membership,
1795        _stop_certificate: &DecisionCertificate,
1796        _stop_slot: Slot,
1797        _prefix_hash: LogHash,
1798    ) -> Result<ConfigurationState> {
1799        Err(Error::Rejected(RejectReason::InvalidTransition))
1800    }
1801
1802    pub fn install_successor_from_proof(
1803        &self,
1804        membership: Membership,
1805        stop_proof: &DecisionProof,
1806    ) -> Result<ConfigurationState> {
1807        let _guard = self
1808            .sync
1809            .lock()
1810            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
1811        self.recover_intent()?;
1812        let current = self.configuration_state()?;
1813        let Some(old_membership) = current.membership.as_ref() else {
1814            return Err(Error::Rejected(RejectReason::ConfigurationNotInstalled));
1815        };
1816        let next_config_id = current
1817            .config_id
1818            .checked_add(1)
1819            .ok_or(Error::Rejected(RejectReason::InvalidTransition))?;
1820        if current.predecessor.is_some() && !current.activated {
1821            return Err(Error::Rejected(RejectReason::TransitionInProgress));
1822        }
1823        if !membership.contains(&self.recorder_id) {
1824            return Err(Error::Rejected(RejectReason::LocalVoterRequired));
1825        }
1826        if proof_cluster_id(stop_proof) != self.cluster_id {
1827            return Err(Error::Rejected(RejectReason::WrongCluster));
1828        }
1829        let (stop_slot, epoch, config_id, config_digest) = proof_context(stop_proof);
1830        if epoch != self.epoch
1831            || config_id != current.config_id
1832            || config_digest != current.config_digest
1833        {
1834            return Err(Error::Rejected(RejectReason::InvalidTransition));
1835        }
1836        stop_proof
1837            .validate_for_cluster(
1838                &self.cluster_id,
1839                stop_slot,
1840                self.epoch,
1841                current.config_id,
1842                old_membership,
1843            )
1844            .map_err(Error::Rejected)?;
1845        let stop_command = ConfigChange::bound_stop(
1846            self.cluster_id.clone(),
1847            current.config_id,
1848            current.config_digest,
1849            next_config_id,
1850            membership.members().to_vec(),
1851        )
1852        .map_err(|_| Error::Rejected(RejectReason::InvalidTransition))?
1853        .to_stored_command();
1854        let stop_value = stop_proof
1855            .proposal()
1856            .value
1857            .as_ref()
1858            .ok_or(Error::Rejected(RejectReason::InvalidCertificate))?;
1859        let expected_stop = AcceptedValue::from_command(
1860            &self.cluster_id,
1861            stop_slot,
1862            self.epoch,
1863            current.config_id,
1864            stop_value.prev_hash,
1865            &stop_command,
1866        );
1867        if *stop_value != expected_stop {
1868            return Err(Error::Rejected(RejectReason::InvalidTransition));
1869        }
1870        let prefix_hash = expected_stop.entry_hash;
1871        let expected_seal = ConfigurationSeal {
1872            stop_slot,
1873            command_hash: expected_stop.command_hash,
1874            prefix_hash,
1875        };
1876        if current
1877            .seal
1878            .as_ref()
1879            .is_some_and(|seal| seal != &expected_seal)
1880        {
1881            return Err(Error::Rejected(RejectReason::InvalidTransition));
1882        }
1883        self.checkpoint_wal_unlocked()?;
1884        self.store_command_unlocked(expected_stop.command_hash, &stop_command)?;
1885        let installed = ConfigurationState {
1886            config_id: next_config_id,
1887            config_digest: membership.digest(),
1888            membership: Some(membership),
1889            predecessor: Some(expected_seal),
1890            seal: None,
1891            max_accepted_or_decided_slot: None,
1892            activated: false,
1893        };
1894        let head = RecordedHeadProvenance::Empty;
1895        self.commit_configuration_head_unlocked(&installed, &head)?;
1896        *self
1897            .configuration
1898            .lock()
1899            .map_err(|_| Error::Io("configuration lock poisoned".into()))? = installed.clone();
1900        *self
1901            .recorded_head
1902            .lock()
1903            .map_err(|_| Error::Io("recorder head lock poisoned".into()))? = head;
1904        self.recent_slots
1905            .lock()
1906            .map_err(|_| Error::Io("recorder recent-slot lock poisoned".into()))?
1907            .clear();
1908        Ok(installed)
1909    }
1910
1911    pub fn recover_successor_activation_from_checkpoint(
1912        &self,
1913        stop_slot: Slot,
1914        prefix_hash: LogHash,
1915        recovered_tip: Slot,
1916        recovered_hash: LogHash,
1917    ) -> Result<ConfigurationState> {
1918        let _guard = self
1919            .sync
1920            .lock()
1921            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
1922        self.recover_intent()?;
1923        let current = self.configuration_state()?;
1924        let predecessor = current
1925            .predecessor
1926            .as_ref()
1927            .ok_or(Error::Rejected(RejectReason::InvalidTransition))?;
1928        if predecessor.stop_slot != stop_slot
1929            || predecessor.prefix_hash != prefix_hash
1930            || recovered_tip <= stop_slot
1931        {
1932            return Err(Error::Rejected(RejectReason::InvalidTransition));
1933        }
1934        if current.activated {
1935            if current
1936                .max_accepted_or_decided_slot
1937                .is_some_and(|slot| slot > recovered_tip)
1938            {
1939                return Err(Error::Rejected(RejectReason::InvalidTransition));
1940            }
1941            return Ok(current);
1942        }
1943        let mut recovered = current;
1944        recovered.activated = true;
1945        recovered.max_accepted_or_decided_slot = Some(recovered_tip);
1946        let head = RecordedHeadProvenance::CheckpointBacked {
1947            stop_slot,
1948            prefix_hash,
1949            recovered_tip,
1950            recovered_hash,
1951        };
1952        self.checkpoint_wal_unlocked()?;
1953        self.commit_configuration_head_unlocked(&recovered, &head)?;
1954        *self
1955            .configuration
1956            .lock()
1957            .map_err(|_| Error::Io("configuration lock poisoned".into()))? = recovered.clone();
1958        *self
1959            .recorded_head
1960            .lock()
1961            .map_err(|_| Error::Io("recorder head lock poisoned".into()))? = head;
1962        self.recent_slots
1963            .lock()
1964            .map_err(|_| Error::Io("recorder recent-slot lock poisoned".into()))?
1965            .clear();
1966        Ok(recovered)
1967    }
1968
1969    pub fn apply(&self, request: RecorderRequest) -> Result<RecorderReply> {
1970        if matches!(
1971            request,
1972            RecorderRequest::Observe { .. }
1973                | RecorderRequest::Converge { .. }
1974                | RecorderRequest::Decide { .. }
1975        ) {
1976            return Err(Error::Rejected(RejectReason::InvalidRequest));
1977        }
1978        if !matches!(request, RecorderRequest::Identity) {
1979            self.validate_request_context(&request)?;
1980        }
1981        match request {
1982            RecorderRequest::Identity => Ok(self.reply(0, None)),
1983            RecorderRequest::StoreCommand {
1984                config_id,
1985                config_digest,
1986                command_hash,
1987                command,
1988                ..
1989            } => {
1990                let _guard = self
1991                    .sync
1992                    .lock()
1993                    .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
1994                self.recover_intent()?;
1995                let context = RecorderRequest::StoreCommand {
1996                    cluster_id: self.cluster_id.clone(),
1997                    epoch: self.epoch,
1998                    config_id,
1999                    config_digest,
2000                    command_hash,
2001                    command: command.clone(),
2002                };
2003                self.validate_request_context(&context)?;
2004                self.store_command_unlocked(command_hash, &command)?;
2005                let mut reply = self.reply(0, None);
2006                reply.config_id = config_id;
2007                reply.config_digest = config_digest;
2008                Ok(reply)
2009            }
2010            RecorderRequest::FetchCommand {
2011                config_id,
2012                config_digest,
2013                command_hash,
2014                ..
2015            } => {
2016                let _guard = self
2017                    .sync
2018                    .lock()
2019                    .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
2020                self.recover_intent()?;
2021                let command = self.fetch_command_unlocked(command_hash)?;
2022                let mut reply = self.reply(0, command);
2023                reply.config_id = config_id;
2024                reply.config_digest = config_digest;
2025                Ok(reply)
2026            }
2027            request => {
2028                let slot =
2029                    request_slot(&request).ok_or(Error::Rejected(RejectReason::InvalidRequest))?;
2030                let request_digest = request_context(&request)
2031                    .ok_or(Error::Rejected(RejectReason::InvalidRequest))?
2032                    .3;
2033                let should_save = !matches!(request, RecorderRequest::Inspect { .. });
2034                let _guard = self
2035                    .sync
2036                    .lock()
2037                    .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
2038                self.recover_intent()?;
2039                self.validate_request_context(&request)?;
2040                let configuration = self.configuration_state()?;
2041                let change = match &request {
2042                    RecorderRequest::Converge { value, .. } => {
2043                        self.change_for_value_unlocked(value)?
2044                    }
2045                    RecorderRequest::Decide { decision, .. } => {
2046                        if let Some(membership) = configuration.membership.as_ref() {
2047                            decision
2048                                .validate_for(configuration.config_id, membership)
2049                                .map_err(Error::Rejected)?;
2050                        }
2051                        self.change_for_value_unlocked(&decision.value)?
2052                    }
2053                    _ => None,
2054                };
2055                if !configuration.activated
2056                    && change.is_none()
2057                    && matches!(
2058                        &request,
2059                        RecorderRequest::Converge { .. } | RecorderRequest::Decide { .. }
2060                    )
2061                {
2062                    return Err(Error::Rejected(RejectReason::ActivationRequired));
2063                }
2064                self.validate_slot_gate(&configuration, slot, change.as_ref())?;
2065                match &request {
2066                    RecorderRequest::Converge { value, .. } => {
2067                        self.validate_value_unlocked(slot, value)?;
2068                    }
2069                    RecorderRequest::Decide { decision, .. } => {
2070                        self.validate_value_unlocked(slot, &decision.value)?;
2071                    }
2072                    _ => {}
2073                }
2074                let mut state = self.load_unlocked(slot, request_digest)?;
2075                let mut reply = state.apply(request).map_err(Error::Rejected)?;
2076                let next_configuration =
2077                    self.transition_after_apply(&configuration, &state, change.as_ref(), None)?;
2078                if should_save || next_configuration != configuration {
2079                    self.persist_state_transition_unlocked(
2080                        &state,
2081                        &configuration,
2082                        &next_configuration,
2083                    )?;
2084                }
2085                reply.recorder_id = self.recorder_id.clone();
2086                Ok(reply)
2087            }
2088        }
2089    }
2090
2091    pub fn record_proposal(&self, request: RecordRequest) -> Result<RecordSummary> {
2092        self.validate_record_context(&request)?;
2093        let value = request
2094            .proposal
2095            .value
2096            .as_ref()
2097            .ok_or(Error::Rejected(RejectReason::InvalidRequest))?;
2098        let _guard = self
2099            .sync
2100            .lock()
2101            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
2102        self.recover_intent()?;
2103        let configuration = self.configuration_state()?;
2104        let command = if let Some(command) = request.command.as_ref() {
2105            self.validate_resolved_command_for_value(
2106                request.slot,
2107                configuration.config_id,
2108                value,
2109                command,
2110            )?;
2111            std::borrow::Cow::Borrowed(command)
2112        } else {
2113            std::borrow::Cow::Owned(self.command_for_value_unlocked(value)?)
2114        };
2115        let change = Self::change_for_command(&command)?;
2116        if !configuration.activated && change.is_none() {
2117            return Err(Error::Rejected(RejectReason::ActivationRequired));
2118        }
2119        self.validate_slot_gate(&configuration, request.slot, change.as_ref())?;
2120        if request.command.is_none() {
2121            self.validate_resolved_command_for_value(
2122                request.slot,
2123                configuration.config_id,
2124                value,
2125                &command,
2126            )?;
2127        }
2128        let state = self.load_unlocked(request.slot, request.config_digest)?;
2129        if let Some(proof) = state.decision_proof() {
2130            return Ok(record_summary(
2131                &self.recorder_id,
2132                &state,
2133                Some(proof.clone()),
2134            ));
2135        }
2136        let (mut next, _) = state.record(&request).map_err(Error::Rejected)?;
2137
2138        // Legacy fields are intentionally not synthesized from ISR state.
2139        next.highest_promised = next.isr.first_current().and_then(proposal_ballot);
2140        next.accepted = None;
2141        let next_configuration =
2142            self.transition_after_apply(&configuration, &next, change.as_ref(), Some(value))?;
2143        self.persist_state_transition_with_command_unlocked(
2144            &next,
2145            &configuration,
2146            &next_configuration,
2147            request
2148                .command
2149                .as_ref()
2150                .map(|command| (value.command_hash, command)),
2151        )?;
2152        Ok(record_summary(&self.recorder_id, &next, None))
2153    }
2154
2155    pub fn install_decision_proof_record(
2156        &self,
2157        proof: DecisionProof,
2158        membership: &Membership,
2159    ) -> Result<()> {
2160        let (slot, epoch, config_id, digest) = proof_context(&proof);
2161        if proof_cluster_id(&proof) != self.cluster_id {
2162            return Err(Error::Rejected(RejectReason::WrongCluster));
2163        }
2164        let _guard = self
2165            .sync
2166            .lock()
2167            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
2168        self.recover_intent()?;
2169        let configuration = self.configuration_state()?;
2170        if epoch != self.epoch
2171            || config_id != configuration.config_id
2172            || digest != configuration.config_digest
2173            || configuration.membership.as_ref() != Some(membership)
2174        {
2175            return Err(Error::Rejected(RejectReason::WrongConfig));
2176        }
2177        proof
2178            .validate_for_cluster(&self.cluster_id, slot, epoch, config_id, membership)
2179            .map_err(Error::Rejected)?;
2180        let value = proof
2181            .proposal()
2182            .value
2183            .as_ref()
2184            .ok_or(Error::Rejected(RejectReason::InvalidCertificate))?;
2185        self.validate_value_unlocked(slot, value)?;
2186        let mut state = self.load_unlocked(slot, digest)?;
2187        if state.decision_proof().is_some() {
2188            state.install_proof(proof).map_err(Error::Rejected)?;
2189            return Ok(());
2190        }
2191        let change = self.change_for_value_unlocked(value)?;
2192        self.validate_slot_gate(&configuration, slot, change.as_ref())?;
2193        state
2194            .install_proof(proof.clone())
2195            .map_err(Error::Rejected)?;
2196        let certificate = certificate_from_proof(&proof)?;
2197        if let Some(existing) = &state.decided {
2198            if existing.value != certificate.value {
2199                return Err(Error::Rejected(RejectReason::AlreadyDecided));
2200            }
2201        } else {
2202            state.decided = Some(certificate);
2203        }
2204        let next =
2205            self.transition_after_apply(&configuration, &state, change.as_ref(), Some(value))?;
2206        self.persist_state_transition_unlocked(&state, &configuration, &next)
2207    }
2208
2209    fn validate_record_context(&self, request: &RecordRequest) -> Result<()> {
2210        if request.cluster_id != self.cluster_id {
2211            return Err(Error::Rejected(RejectReason::WrongCluster));
2212        }
2213        if request.epoch != self.epoch {
2214            return Err(Error::Rejected(if request.epoch < self.epoch {
2215                RejectReason::StaleEpoch
2216            } else {
2217                RejectReason::FutureEpoch
2218            }));
2219        }
2220        let configuration = self.configuration_state()?;
2221        if request.config_id != configuration.config_id
2222            || (configuration.config_digest != LogHash::ZERO
2223                && request.config_digest != configuration.config_digest)
2224        {
2225            return Err(Error::Rejected(RejectReason::WrongConfig));
2226        }
2227        Ok(())
2228    }
2229
2230    pub fn load(&self, slot: Slot) -> Result<RecorderSlotState> {
2231        let _guard = self
2232            .sync
2233            .lock()
2234            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
2235        self.load_unlocked(slot, self.config_digest())
2236    }
2237
2238    pub fn save(&self, state: &RecorderSlotState) -> Result<()> {
2239        let _guard = self
2240            .sync
2241            .lock()
2242            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
2243        self.recover_intent()?;
2244        let configuration = self.configuration_state()?;
2245        if state.cluster_id != self.cluster_id
2246            || state.epoch != self.epoch
2247            || state.config_id != configuration.config_id
2248            || (configuration.config_digest != LogHash::ZERO
2249                && state.config_digest != configuration.config_digest)
2250        {
2251            return Err(Error::Rejected(RejectReason::WrongConfig));
2252        }
2253        let change = state
2254            .decided()
2255            .map(|decision| &decision.value)
2256            .or_else(|| state.accepted().map(|accepted| &accepted.value))
2257            .map(|value| self.change_for_value_unlocked(value))
2258            .transpose()?
2259            .flatten();
2260        self.validate_slot_gate(&configuration, state.slot(), change.as_ref())?;
2261        let applied_value = state
2262            .decided()
2263            .map(|decision| &decision.value)
2264            .or_else(|| state.accepted().map(|accepted| &accepted.value));
2265        let next =
2266            self.transition_after_apply(&configuration, state, change.as_ref(), applied_value)?;
2267        self.persist_state_transition_unlocked(state, &configuration, &next)
2268    }
2269
2270    pub fn store_command(&self, command_hash: LogHash, command: StoredCommand) -> Result<()> {
2271        let _guard = self
2272            .sync
2273            .lock()
2274            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
2275        self.store_command_unlocked(command_hash, &command)
2276    }
2277
2278    fn store_command_unlocked(&self, command_hash: LogHash, command: &StoredCommand) -> Result<()> {
2279        if command.hash() != command_hash {
2280            return Err(Error::CommandHashMismatch);
2281        }
2282        {
2283            let wal = self
2284                .wal
2285                .lock()
2286                .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
2287            match wal.commands.get(&command_hash) {
2288                Some(existing) if existing == command => return Ok(()),
2289                Some(_) => return Err(Error::CommandHashMismatch),
2290                None => {}
2291            }
2292        }
2293        self.stage_command_unlocked(command_hash, command)?;
2294        self.sync_root()
2295    }
2296
2297    fn stage_command_unlocked(&self, command_hash: LogHash, command: &StoredCommand) -> Result<()> {
2298        let path = self.command_path(command_hash);
2299        if path.exists() {
2300            return match self.fetch_command_cache_unlocked(command_hash)? {
2301                Some(existing) if existing == *command => Ok(()),
2302                _ => Err(Error::CommandHashMismatch),
2303            };
2304        }
2305        atomic_replace(&path, &encode_stored_command(command))?;
2306        Ok(())
2307    }
2308
2309    pub fn fetch_command(&self, command_hash: LogHash) -> Result<Option<StoredCommand>> {
2310        let _guard = self
2311            .sync
2312            .lock()
2313            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
2314        self.fetch_command_unlocked(command_hash)
2315    }
2316
2317    fn load_unlocked(&self, slot: Slot, config_digest: LogHash) -> Result<RecorderSlotState> {
2318        let wal = self
2319            .wal
2320            .lock()
2321            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
2322        if let Some(bytes) = wal.slots.get(&slot) {
2323            let state = decode_recorder_state(bytes)?;
2324            drop(wal);
2325            if state.cluster_id != self.cluster_id
2326                || state.epoch != self.epoch
2327                || state.config_id != self.current_config_id()
2328                || (config_digest != LogHash::ZERO && state.config_digest != config_digest)
2329            {
2330                return Err(Error::Decode("recorder WAL state identity mismatch".into()));
2331            }
2332            return Ok(state);
2333        }
2334        drop(wal);
2335        let path = self.path(slot);
2336        let bytes = match read_regular_file_bounded(&path, MAX_RECORDER_STATE_BYTES, "slot cache") {
2337            Ok(bytes) => bytes,
2338            Err(Error::Decode(message)) if message == "recorder is missing slot cache" => {
2339                return Ok(RecorderSlotState::new_with_digest(
2340                    slot,
2341                    self.cluster_id.clone(),
2342                    self.epoch,
2343                    self.current_config_id(),
2344                    config_digest,
2345                ))
2346            }
2347            Err(error) => return Err(error),
2348        };
2349        let state = decode_recorder_state(&bytes)?;
2350        if state.slot != slot
2351            || state.cluster_id != self.cluster_id
2352            || state.epoch != self.epoch
2353            || state.config_id != self.current_config_id()
2354            || (config_digest != LogHash::ZERO && state.config_digest != config_digest)
2355        {
2356            return Err(Error::Decode("recorder state identity mismatch".into()));
2357        }
2358        Ok(state)
2359    }
2360
2361    fn open_or_initialize_recorded_head(&self, existing_format: bool) -> Result<()> {
2362        let configuration = self.configuration_state()?;
2363        let (head, recent_slots, wal_checkpoint) = match read_regular_file_bounded(
2364            &self.recorded_head_path(),
2365            MAX_RECORDED_HEAD_BYTES,
2366            "recorded-head.rec",
2367        ) {
2368            Ok(bytes) => {
2369                decode_recorded_head(&bytes, &self.cluster_id, self.epoch, &configuration)?
2370            }
2371            Err(Error::Decode(message)) if message == "recorder is missing recorded-head.rec" => {
2372                if existing_format {
2373                    return Err(Error::MigrationRequired {
2374                        format: "recorder durable head",
2375                        version: 2,
2376                    });
2377                }
2378                let head = RecordedHeadProvenance::Empty;
2379                let wal_checkpoint = WalCheckpoint::default();
2380                atomic_write(
2381                    &self.recorded_head_path(),
2382                    &encode_recorded_head(
2383                        &self.cluster_id,
2384                        self.epoch,
2385                        &configuration,
2386                        &head,
2387                        &[],
2388                        wal_checkpoint,
2389                    )?,
2390                )?;
2391                (head, Vec::new(), wal_checkpoint)
2392            }
2393            Err(error) => return Err(error),
2394        };
2395        self.install_recorded_head(&configuration, head, recent_slots, wal_checkpoint)
2396    }
2397
2398    fn open_or_replay_wal(&self) -> Result<()> {
2399        let path = self.wal_path();
2400        let created = match fs::OpenOptions::new()
2401            .write(true)
2402            .create_new(true)
2403            .open(&path)
2404        {
2405            Ok(_) => true,
2406            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => false,
2407            Err(error) => return Err(Error::Io(error.to_string())),
2408        };
2409        if created {
2410            self.sync_root()?;
2411        }
2412        let bytes = read_regular_file_bounded(&path, MAX_RECORDER_WAL_BYTES, "recorder.wal")?;
2413        let checkpoint = self.wal_checkpoint()?;
2414        let mut replayed = RecorderWal {
2415            checkpoint,
2416            next_sequence: checkpoint
2417                .through_sequence
2418                .checked_add(1)
2419                .ok_or_else(|| Error::Decode("recorder WAL sequence exhausted".into()))?,
2420            ..RecorderWal::default()
2421        };
2422        let mut configuration = self.configuration_state()?;
2423        let mut head = self
2424            .recorded_head
2425            .lock()
2426            .map_err(|_| Error::Io("recorder head lock poisoned".into()))?
2427            .clone();
2428        let mut offset = 0usize;
2429        while offset < bytes.len() {
2430            let Some((frame, end)) = decode_wal_frame(&bytes, offset)? else {
2431                break;
2432            };
2433            if frame.generation < checkpoint.generation {
2434                offset = end;
2435                continue;
2436            }
2437            if frame.generation != checkpoint.generation
2438                || frame.sequence != replayed.next_sequence
2439                || frame.prev_digest != replayed.last_digest
2440            {
2441                return Err(Error::Decode(
2442                    "recorder WAL sequence or digest chain mismatch".into(),
2443                ));
2444            }
2445            let state = decode_recorder_state(&frame.slot_bytes)?;
2446            let next_configuration = decode_configuration_state(&frame.configuration_bytes)?;
2447            if state.slot() != frame.slot
2448                || state.cluster_id != self.cluster_id
2449                || state.epoch != self.epoch
2450                || state.config_id != next_configuration.config_id
2451                || state.config_digest != next_configuration.config_digest
2452                || configuration_structure_changed(&configuration, &next_configuration)
2453            {
2454                return Err(Error::Decode("recorder WAL state identity mismatch".into()));
2455            }
2456            if let Some((hash, command)) = &frame.command {
2457                if command.hash() != *hash {
2458                    return Err(Error::Decode(
2459                        "recorder WAL inline command hash mismatch".into(),
2460                    ));
2461                }
2462                upsert_wal_command(&mut replayed.commands, *hash, command)?;
2463            }
2464            for value in recorder_state_values(&state) {
2465                let cached_command;
2466                let command = match replayed.commands.get(&value.command_hash) {
2467                    Some(command) => command,
2468                    None => {
2469                        cached_command = self
2470                            .fetch_command_cache_unlocked(value.command_hash)
2471                            .ok()
2472                            .flatten()
2473                            .ok_or(Error::CommandUnavailable)?;
2474                        &cached_command
2475                    }
2476                };
2477                if AcceptedValue::from_command(
2478                    &self.cluster_id,
2479                    frame.slot,
2480                    self.epoch,
2481                    next_configuration.config_id,
2482                    value.prev_hash,
2483                    command,
2484                ) != *value
2485                {
2486                    return Err(Error::Decode("recorder WAL value mismatch".into()));
2487                }
2488            }
2489            let expected_head = if next_configuration.max_accepted_or_decided_slot
2490                == Some(frame.slot)
2491                && recorder_state_values(&state).next().is_some()
2492            {
2493                RecordedHeadProvenance::SlotBacked { slot: frame.slot }
2494            } else {
2495                head.clone()
2496            };
2497            if frame.head != expected_head {
2498                return Err(Error::Decode("recorder WAL head mismatch".into()));
2499            }
2500            replayed.slots.insert(frame.slot, frame.slot_bytes);
2501            replayed.next_sequence = replayed
2502                .next_sequence
2503                .checked_add(1)
2504                .ok_or_else(|| Error::Decode("recorder WAL sequence exhausted".into()))?;
2505            replayed.last_digest = frame.digest;
2506            replayed.frame_count += 1;
2507            configuration = next_configuration;
2508            head = frame.head;
2509            offset = end;
2510        }
2511        if offset != bytes.len() {
2512            let file = fs::OpenOptions::new()
2513                .write(true)
2514                .open(&path)
2515                .map_err(|error| Error::Io(error.to_string()))?;
2516            file.set_len(offset as u64)
2517                .and_then(|_| sync_wal_metadata(&file))
2518                .map_err(|error| Error::Io(error.to_string()))?;
2519        }
2520        replayed.file = Some(
2521            fs::OpenOptions::new()
2522                .append(true)
2523                .open(&path)
2524                .map_err(|error| Error::Io(error.to_string()))?,
2525        );
2526        replayed.byte_count = offset as u64;
2527        *self
2528            .wal
2529            .lock()
2530            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))? = replayed;
2531        *self
2532            .configuration
2533            .lock()
2534            .map_err(|_| Error::Io("configuration lock poisoned".into()))? = configuration;
2535        *self
2536            .recorded_head
2537            .lock()
2538            .map_err(|_| Error::Io("recorder head lock poisoned".into()))? = head;
2539        Ok(())
2540    }
2541
2542    fn install_recorded_head(
2543        &self,
2544        configuration: &ConfigurationState,
2545        head: RecordedHeadProvenance,
2546        recent_slots: Vec<DurableSlotSnapshot>,
2547        wal_checkpoint: WalCheckpoint,
2548    ) -> Result<()> {
2549        let mut recovered_cache = false;
2550        for snapshot in &recent_slots {
2551            let state = decode_recorder_state(&snapshot.bytes)?;
2552            if state.slot() != snapshot.slot
2553                || state.cluster_id != self.cluster_id
2554                || state.epoch != self.epoch
2555                || state.config_id != configuration.config_id
2556                || state.config_digest != configuration.config_digest
2557            {
2558                return Err(Error::Decode(
2559                    "durable recorder snapshot identity mismatch".into(),
2560                ));
2561            }
2562            for value in recorder_state_values(&state) {
2563                self.validate_value_unlocked(snapshot.slot, value)?;
2564            }
2565            let path = self.path(snapshot.slot);
2566            if read_regular_file_bounded(&path, MAX_RECORDER_STATE_BYTES, "slot cache")
2567                .ok()
2568                .as_deref()
2569                != Some(snapshot.bytes.as_slice())
2570            {
2571                atomic_replace(&path, &snapshot.bytes)?;
2572                recovered_cache = true;
2573            }
2574        }
2575        if recovered_cache {
2576            self.sync_root()?;
2577        }
2578        let recovered_max = match &head {
2579            RecordedHeadProvenance::Empty => None,
2580            RecordedHeadProvenance::SlotBacked { slot } => {
2581                let state = self.load_unlocked(*slot, configuration.config_digest)?;
2582                let mut values = recorder_state_values(&state).peekable();
2583                if values.peek().is_none() {
2584                    return Err(Error::Decode(
2585                        "slot-backed recorder head references a state without a value".into(),
2586                    ));
2587                }
2588                for value in values {
2589                    self.validate_value_unlocked(*slot, value)?;
2590                }
2591                Some(*slot)
2592            }
2593            RecordedHeadProvenance::CheckpointBacked {
2594                stop_slot,
2595                prefix_hash,
2596                recovered_tip,
2597                recovered_hash,
2598            } => {
2599                let predecessor = configuration.predecessor.as_ref().ok_or_else(|| {
2600                    Error::Decode("checkpoint-backed head has no predecessor binding".into())
2601                })?;
2602                if !configuration.activated
2603                    || predecessor.stop_slot != *stop_slot
2604                    || predecessor.prefix_hash != *prefix_hash
2605                    || recovered_tip <= stop_slot
2606                    || *recovered_hash == LogHash::ZERO
2607                    || configuration.max_accepted_or_decided_slot != Some(*recovered_tip)
2608                {
2609                    return Err(Error::Decode(
2610                        "checkpoint-backed recorder head evidence is invalid".into(),
2611                    ));
2612                }
2613                Some(*recovered_tip)
2614            }
2615        };
2616        self.configuration
2617            .lock()
2618            .map_err(|_| Error::Io("configuration lock poisoned".into()))?
2619            .max_accepted_or_decided_slot = recovered_max;
2620        *self
2621            .recorded_head
2622            .lock()
2623            .map_err(|_| Error::Io("recorder head lock poisoned".into()))? = head;
2624        *self
2625            .recent_slots
2626            .lock()
2627            .map_err(|_| Error::Io("recorder recent-slot lock poisoned".into()))? = recent_slots;
2628        let mut wal = self
2629            .wal
2630            .lock()
2631            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
2632        wal.checkpoint = wal_checkpoint;
2633        wal.next_sequence = wal_checkpoint
2634            .through_sequence
2635            .checked_add(1)
2636            .ok_or_else(|| Error::Decode("recorder WAL sequence exhausted".into()))?;
2637        Ok(())
2638    }
2639
2640    fn fetch_command_unlocked(&self, command_hash: LogHash) -> Result<Option<StoredCommand>> {
2641        let wal = self
2642            .wal
2643            .lock()
2644            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
2645        if let Some(command) = wal.commands.get(&command_hash).cloned() {
2646            return Ok(Some(command));
2647        }
2648        drop(wal);
2649        self.fetch_command_cache_unlocked(command_hash)
2650    }
2651
2652    fn fetch_command_cache_unlocked(&self, command_hash: LogHash) -> Result<Option<StoredCommand>> {
2653        #[cfg(test)]
2654        COMMAND_FILE_READS.with(|reads| reads.set(reads.get() + 1));
2655        match read_existing_command_cache(&self.root, command_hash) {
2656            Err(Error::CommandUnavailable) => Ok(None),
2657            result => result.map(Some),
2658        }
2659    }
2660
2661    fn validate_value_unlocked(&self, slot: Slot, value: &AcceptedValue) -> Result<()> {
2662        let config_id = self.current_config_id();
2663        let command = self.command_for_value_unlocked(value)?;
2664        self.validate_resolved_command_for_value(slot, config_id, value, &command)
2665    }
2666
2667    fn validate_resolved_command_for_value(
2668        &self,
2669        slot: Slot,
2670        config_id: ConfigId,
2671        value: &AcceptedValue,
2672        command: &StoredCommand,
2673    ) -> Result<()> {
2674        let expected = AcceptedValue::from_command(
2675            &self.cluster_id,
2676            slot,
2677            self.epoch,
2678            config_id,
2679            value.prev_hash,
2680            command,
2681        );
2682        if expected != *value {
2683            return Err(Error::Rejected(RejectReason::InvalidValue));
2684        }
2685        Ok(())
2686    }
2687
2688    fn change_for_value_unlocked(&self, value: &AcceptedValue) -> Result<Option<ConfigChange>> {
2689        let command = self.command_for_value_unlocked(value)?;
2690        Self::change_for_command(&command)
2691    }
2692
2693    fn change_for_command(command: &StoredCommand) -> Result<Option<ConfigChange>> {
2694        if command.entry_type != EntryType::ConfigChange {
2695            return Ok(None);
2696        }
2697        ConfigChange::recognize(command)
2698            .map_err(|_| Error::Rejected(RejectReason::InvalidRequest))
2699            .map(Some)
2700    }
2701
2702    fn command_for_value_unlocked(&self, value: &AcceptedValue) -> Result<StoredCommand> {
2703        self.fetch_command_unlocked(value.command_hash)?
2704            .ok_or(Error::CommandUnavailable)
2705    }
2706
2707    fn validate_slot_gate(
2708        &self,
2709        configuration: &ConfigurationState,
2710        slot: Slot,
2711        change: Option<&ConfigChange>,
2712    ) -> Result<()> {
2713        if let Some(predecessor) = &configuration.predecessor {
2714            if slot <= predecessor.stop_slot {
2715                return Err(Error::Rejected(RejectReason::ConfigurationNotInstalled));
2716            }
2717        }
2718        if let Some(seal) = &configuration.seal {
2719            if slot > seal.stop_slot {
2720                return Err(Error::Rejected(RejectReason::ConfigurationSealed {
2721                    stop_slot: seal.stop_slot,
2722                }));
2723            }
2724            if matches!(
2725                change,
2726                Some(ConfigChange::Stop { .. } | ConfigChange::BoundStop { .. })
2727            ) && (slot != seal.stop_slot || seal.command_hash == LogHash::ZERO)
2728            {
2729                return Err(Error::Rejected(RejectReason::TransitionInProgress));
2730            }
2731        }
2732        if matches!(
2733            change,
2734            Some(ConfigChange::Stop { .. } | ConfigChange::BoundStop { .. })
2735        ) && configuration
2736            .max_accepted_or_decided_slot
2737            .is_some_and(|accepted_slot| accepted_slot > slot)
2738        {
2739            return Err(Error::Rejected(RejectReason::InvalidTransition));
2740        }
2741        if !configuration.activated {
2742            let Some(predecessor) = &configuration.predecessor else {
2743                return Err(Error::Rejected(RejectReason::InvalidTransition));
2744            };
2745            match change {
2746                Some(ConfigChange::BoundActivationBarrier {
2747                    successor,
2748                    stop_slot,
2749                    prefix_hash,
2750                    stop_command_hash,
2751                }) if successor.cluster_id() == self.cluster_id
2752                    && successor.config_id() == configuration.config_id
2753                    && successor.digest() == configuration.config_digest
2754                    && successor.predecessor_config_id().checked_add(1)
2755                        == Some(configuration.config_id)
2756                    && *stop_slot == predecessor.stop_slot
2757                    && *prefix_hash == predecessor.prefix_hash
2758                    && *stop_command_hash == predecessor.command_hash
2759                    && slot == predecessor.stop_slot + 1 => {}
2760                None if slot == predecessor.stop_slot + 1 => {}
2761                _ => return Err(Error::Rejected(RejectReason::ActivationRequired)),
2762            }
2763        } else if matches!(
2764            change,
2765            Some(
2766                ConfigChange::ActivationBarrier { .. }
2767                    | ConfigChange::BoundActivationBarrier { .. }
2768            )
2769        ) {
2770            return Err(Error::Rejected(RejectReason::InvalidTransition));
2771        }
2772        if let Some(change) = change {
2773            let (config_id, config_digest) = change.binding();
2774            if config_id != configuration.config_id || config_digest != configuration.config_digest
2775            {
2776                return Err(Error::Rejected(RejectReason::WrongConfig));
2777            }
2778        }
2779        Ok(())
2780    }
2781
2782    fn transition_after_apply(
2783        &self,
2784        configuration: &ConfigurationState,
2785        state: &RecorderSlotState,
2786        change: Option<&ConfigChange>,
2787        applied_value: Option<&AcceptedValue>,
2788    ) -> Result<ConfigurationState> {
2789        let mut next = configuration.clone();
2790        if applied_value.is_some() {
2791            next.max_accepted_or_decided_slot = Some(
2792                next.max_accepted_or_decided_slot
2793                    .map_or(state.slot(), |current| current.max(state.slot())),
2794            );
2795        }
2796        if state.decision_proof().is_some()
2797            && next.seal.as_ref().is_some_and(|seal| {
2798                seal.stop_slot == state.slot()
2799                    && applied_value.is_some_and(|value| value.command_hash != seal.command_hash)
2800            })
2801        {
2802            next.seal = None;
2803        }
2804        match change {
2805            Some(ConfigChange::Stop { .. } | ConfigChange::BoundStop { .. })
2806                if applied_value.is_some() =>
2807            {
2808                let value = applied_value.expect("checked applied value");
2809                let proposed = ConfigurationSeal {
2810                    stop_slot: state.slot(),
2811                    command_hash: value.command_hash,
2812                    prefix_hash: value.entry_hash,
2813                };
2814                if let Some(existing) = &next.seal {
2815                    if existing != &proposed {
2816                        return Err(Error::Rejected(RejectReason::TransitionInProgress));
2817                    }
2818                } else {
2819                    next.seal = Some(proposed);
2820                }
2821            }
2822            Some(
2823                ConfigChange::ActivationBarrier { .. }
2824                | ConfigChange::BoundActivationBarrier { .. },
2825            ) if state.decision_proof().is_some() => {
2826                next.activated = true;
2827            }
2828            _ => {}
2829        }
2830        Ok(next)
2831    }
2832
2833    fn validate_request_context(&self, request: &RecorderRequest) -> Result<()> {
2834        let (cluster_id, epoch, config_id, config_digest) =
2835            request_context(request).ok_or(Error::Rejected(RejectReason::InvalidRequest))?;
2836        if cluster_id != &self.cluster_id {
2837            return Err(Error::Rejected(RejectReason::WrongCluster));
2838        }
2839        if epoch < self.epoch {
2840            return Err(Error::Rejected(RejectReason::StaleEpoch));
2841        }
2842        if epoch > self.epoch {
2843            return Err(Error::Rejected(RejectReason::FutureEpoch));
2844        }
2845        let configuration = self.configuration_state()?;
2846        if config_id != configuration.config_id {
2847            return Err(Error::Rejected(RejectReason::WrongConfig));
2848        }
2849        if configuration.config_digest != LogHash::ZERO
2850            && config_digest != configuration.config_digest
2851        {
2852            return Err(Error::Rejected(RejectReason::WrongConfig));
2853        }
2854        Ok(())
2855    }
2856
2857    fn reply(&self, slot: Slot, command: Option<StoredCommand>) -> RecorderReply {
2858        RecorderReply {
2859            recorder_id: self.recorder_id.clone(),
2860            slot,
2861            config_id: self.current_config_id(),
2862            config_digest: self.config_digest(),
2863            step: 0,
2864            highest_promised: None,
2865            accepted: None,
2866            decided: None,
2867            command,
2868        }
2869    }
2870
2871    fn path(&self, slot: Slot) -> PathBuf {
2872        self.root.join(format!("slot-{slot:020}.rec"))
2873    }
2874
2875    fn command_path(&self, command_hash: LogHash) -> PathBuf {
2876        self.root
2877            .join(format!("command-{}.cmd", command_hash.to_hex()))
2878    }
2879
2880    fn configuration_path(&self) -> PathBuf {
2881        self.root.join("configuration.rec")
2882    }
2883
2884    fn intent_path(&self) -> PathBuf {
2885        self.root.join("configuration.intent")
2886    }
2887
2888    fn configuration_head_intent_path(&self) -> PathBuf {
2889        self.root.join("configuration-head.intent")
2890    }
2891
2892    fn recorded_head_path(&self) -> PathBuf {
2893        self.root.join("recorded-head.rec")
2894    }
2895
2896    fn wal_path(&self) -> PathBuf {
2897        self.root.join("recorder.wal")
2898    }
2899
2900    fn head_after_slot_state(
2901        &self,
2902        configuration: &ConfigurationState,
2903        slot_state: &RecorderSlotState,
2904    ) -> Result<RecordedHeadProvenance> {
2905        let current = self
2906            .recorded_head
2907            .lock()
2908            .map_err(|_| Error::Io("recorder head lock poisoned".into()))?
2909            .clone();
2910        if configuration.max_accepted_or_decided_slot == Some(slot_state.slot())
2911            && recorder_state_values(slot_state).next().is_some()
2912        {
2913            Ok(RecordedHeadProvenance::SlotBacked {
2914                slot: slot_state.slot(),
2915            })
2916        } else {
2917            Ok(current)
2918        }
2919    }
2920
2921    fn recover_intent(&self) -> Result<()> {
2922        self.recover_configuration_head_intent()?;
2923        let path = self.intent_path();
2924        let Some(intent_bytes) = read_preflight_intent(
2925            &self.root,
2926            "configuration.intent",
2927            MAX_TRANSITION_INTENT_BYTES,
2928        )?
2929        else {
2930            return Ok(());
2931        };
2932        let (slot, slot_bytes, configuration_bytes) = decode_transition_intent(&intent_bytes)?;
2933        let configuration = decode_configuration_state(&configuration_bytes)?;
2934        let slot_state = decode_recorder_state(&slot_bytes)?;
2935        let head = self.head_after_slot_state(&configuration, &slot_state)?;
2936        atomic_write(&self.path(slot), &slot_bytes)?;
2937        atomic_write(&self.configuration_path(), &configuration_bytes)?;
2938        atomic_write(
2939            &self.recorded_head_path(),
2940            &encode_recorded_head(
2941                &self.cluster_id,
2942                self.epoch,
2943                &configuration,
2944                &head,
2945                &[],
2946                self.wal_checkpoint()?,
2947            )?,
2948        )?;
2949        fs::remove_file(path).map_err(|err| Error::Io(err.to_string()))?;
2950        fs::File::open(&self.root)
2951            .and_then(|directory| directory.sync_all())
2952            .map_err(|err| Error::Io(err.to_string()))?;
2953        *self
2954            .configuration
2955            .lock()
2956            .map_err(|_| Error::Io("configuration lock poisoned".into()))? = configuration;
2957        *self
2958            .recorded_head
2959            .lock()
2960            .map_err(|_| Error::Io("recorder head lock poisoned".into()))? = head;
2961        self.recent_slots
2962            .lock()
2963            .map_err(|_| Error::Io("recorder recent-slot lock poisoned".into()))?
2964            .clear();
2965        Ok(())
2966    }
2967
2968    fn recover_configuration_head_intent(&self) -> Result<()> {
2969        let path = self.configuration_head_intent_path();
2970        let Some(intent_bytes) = read_preflight_intent(
2971            &self.root,
2972            "configuration-head.intent",
2973            MAX_CONFIGURATION_HEAD_INTENT_BYTES,
2974        )?
2975        else {
2976            return Ok(());
2977        };
2978        let (configuration_bytes, head_bytes) = decode_configuration_head_intent(&intent_bytes)?;
2979        atomic_write(&self.configuration_path(), configuration_bytes)?;
2980        atomic_write(&self.recorded_head_path(), head_bytes)?;
2981        fs::remove_file(path).map_err(|err| Error::Io(err.to_string()))?;
2982        self.sync_root()
2983    }
2984
2985    fn commit_configuration_head_unlocked(
2986        &self,
2987        configuration: &ConfigurationState,
2988        head: &RecordedHeadProvenance,
2989    ) -> Result<()> {
2990        let configuration_bytes = encode_configuration_state(configuration)?;
2991        let head_bytes = encode_recorded_head(
2992            &self.cluster_id,
2993            self.epoch,
2994            configuration,
2995            head,
2996            &[],
2997            self.wal_checkpoint()?,
2998        )?;
2999        atomic_write(
3000            &self.configuration_head_intent_path(),
3001            &encode_configuration_head_intent(&configuration_bytes, &head_bytes),
3002        )?;
3003        self.fail_seal_at(SealFaultPoint::AfterHeadIntent)?;
3004        atomic_write(&self.configuration_path(), &configuration_bytes)?;
3005        self.fail_seal_at(SealFaultPoint::AfterHeadConfiguration)?;
3006        atomic_write(&self.recorded_head_path(), &head_bytes)?;
3007        self.fail_seal_at(SealFaultPoint::AfterHead)?;
3008        fs::remove_file(self.configuration_head_intent_path())
3009            .map_err(|err| Error::Io(err.to_string()))?;
3010        self.sync_root()
3011    }
3012
3013    fn persist_state_transition_unlocked(
3014        &self,
3015        slot_state: &RecorderSlotState,
3016        previous: &ConfigurationState,
3017        next: &ConfigurationState,
3018    ) -> Result<()> {
3019        self.persist_state_transition_with_command_unlocked(slot_state, previous, next, None)
3020    }
3021
3022    fn persist_state_transition_with_command_unlocked(
3023        &self,
3024        slot_state: &RecorderSlotState,
3025        previous: &ConfigurationState,
3026        next: &ConfigurationState,
3027        command: Option<(LogHash, &StoredCommand)>,
3028    ) -> Result<()> {
3029        if configuration_structure_changed(previous, next) {
3030            self.checkpoint_wal_unlocked()?;
3031            if let Some((hash, command)) = command {
3032                self.store_command_unlocked(hash, command)?;
3033            }
3034            return self.commit_transition_unlocked(slot_state, next);
3035        }
3036        let head = self.head_after_slot_state(next, slot_state)?;
3037        self.append_wal_unlocked(slot_state, next, &head, command)?;
3038        *self
3039            .configuration
3040            .lock()
3041            .map_err(|_| Error::Io("configuration lock poisoned".into()))? = next.clone();
3042        *self
3043            .recorded_head
3044            .lock()
3045            .map_err(|_| Error::Io("recorder head lock poisoned".into()))? = head;
3046        self.recent_slots
3047            .lock()
3048            .map_err(|_| Error::Io("recorder recent-slot lock poisoned".into()))?
3049            .clear();
3050        Ok(())
3051    }
3052
3053    fn append_wal_unlocked(
3054        &self,
3055        slot_state: &RecorderSlotState,
3056        configuration: &ConfigurationState,
3057        head: &RecordedHeadProvenance,
3058        command: Option<(LogHash, &StoredCommand)>,
3059    ) -> Result<()> {
3060        let should_checkpoint = {
3061            let wal = self
3062                .wal
3063                .lock()
3064                .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
3065            if wal.failed {
3066                return Err(Error::Io(
3067                    "recorder WAL is unavailable after an I/O failure".into(),
3068                ));
3069            }
3070            wal.frame_count >= RECORDER_WAL_HARD_FRAME_LIMIT
3071                || wal.byte_count >= RECORDER_WAL_SOFT_BYTE_LIMIT
3072        };
3073        if should_checkpoint {
3074            self.checkpoint_wal_unlocked()?;
3075        }
3076        let (generation, sequence, prev_digest) = {
3077            let wal = self
3078                .wal
3079                .lock()
3080                .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
3081            (
3082                wal.checkpoint.generation,
3083                wal.next_sequence,
3084                wal.last_digest,
3085            )
3086        };
3087        let (frame, digest, slot_bytes) = encode_wal_frame(
3088            generation,
3089            sequence,
3090            prev_digest,
3091            slot_state,
3092            configuration,
3093            head,
3094            command,
3095        )?;
3096        let mut wal = self
3097            .wal
3098            .lock()
3099            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
3100        let append_result = (|| {
3101            let file = wal
3102                .file
3103                .as_mut()
3104                .ok_or_else(|| Error::Io("recorder WAL file is not open".into()))?;
3105            file.write_all(&frame)
3106                .map_err(|error| Error::Io(error.to_string()))?;
3107            self.fail_seal_at(SealFaultPoint::AfterWalWrite)?;
3108            sync_wal_append(file).map_err(|error| Error::Io(error.to_string()))?;
3109            self.fail_seal_at(SealFaultPoint::AfterWalSync)
3110        })();
3111        if let Err(error) = append_result {
3112            wal.failed = true;
3113            return Err(error);
3114        }
3115        wal.slots.insert(slot_state.slot(), slot_bytes);
3116        if let Some((hash, command)) = command {
3117            wal.commands.entry(hash).or_insert_with(|| command.clone());
3118        }
3119        wal.next_sequence = sequence
3120            .checked_add(1)
3121            .ok_or_else(|| Error::Io("recorder WAL sequence exhausted".into()))?;
3122        wal.last_digest = digest;
3123        wal.frame_count += 1;
3124        wal.byte_count = wal
3125            .byte_count
3126            .checked_add(frame.len() as u64)
3127            .ok_or_else(|| Error::Io("recorder WAL byte count overflow".into()))?;
3128        Ok(())
3129    }
3130
3131    fn checkpoint_wal_unlocked(&self) -> Result<()> {
3132        let (checkpoint, next_sequence, slots, commands) = {
3133            let wal = self
3134                .wal
3135                .lock()
3136                .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
3137            if wal.failed {
3138                return Err(Error::Io(
3139                    "recorder WAL is unavailable after an I/O failure".into(),
3140                ));
3141            }
3142            if wal.frame_count == 0 {
3143                return Ok(());
3144            }
3145            (
3146                wal.checkpoint,
3147                wal.next_sequence,
3148                wal.slots.clone(),
3149                wal.commands.clone(),
3150            )
3151        };
3152        let next_checkpoint = WalCheckpoint {
3153            generation: checkpoint
3154                .generation
3155                .checked_add(1)
3156                .ok_or_else(|| Error::Io("recorder WAL generation exhausted".into()))?,
3157            through_sequence: next_sequence - 1,
3158        };
3159        for (hash, command) in &commands {
3160            atomic_replace(&self.command_path(*hash), &encode_stored_command(command))?;
3161        }
3162        for (slot, bytes) in &slots {
3163            atomic_replace(&self.path(*slot), bytes)?;
3164        }
3165        let configuration = self.configuration_state()?;
3166        let head = self
3167            .recorded_head
3168            .lock()
3169            .map_err(|_| Error::Io("recorder head lock poisoned".into()))?
3170            .clone();
3171        atomic_replace(
3172            &self.configuration_path(),
3173            &encode_configuration_state(&configuration)?,
3174        )?;
3175        atomic_write(
3176            &self.recorded_head_path(),
3177            &encode_recorded_head(
3178                &self.cluster_id,
3179                self.epoch,
3180                &configuration,
3181                &head,
3182                &[],
3183                next_checkpoint,
3184            )?,
3185        )?;
3186        let truncate_result = fs::OpenOptions::new()
3187            .write(true)
3188            .open(self.wal_path())
3189            .and_then(|file| file.set_len(0).and_then(|_| sync_wal_metadata(&file)));
3190        if let Err(error) = truncate_result {
3191            if let Ok(mut wal) = self.wal.lock() {
3192                wal.failed = true;
3193            }
3194            return Err(Error::Io(error.to_string()));
3195        }
3196        let mut wal = self
3197            .wal
3198            .lock()
3199            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?;
3200        wal.checkpoint = next_checkpoint;
3201        wal.last_digest = LogHash::ZERO;
3202        wal.frame_count = 0;
3203        wal.byte_count = 0;
3204        wal.slots.clear();
3205        wal.commands.clear();
3206        self.recent_slots
3207            .lock()
3208            .map_err(|_| Error::Io("recorder recent-slot lock poisoned".into()))?
3209            .clear();
3210        Ok(())
3211    }
3212
3213    fn sync_root(&self) -> Result<()> {
3214        fs::File::open(&self.root)
3215            .and_then(|directory| directory.sync_all())
3216            .map_err(|err| Error::Io(err.to_string()))?;
3217        #[cfg(test)]
3218        record_directory_sync();
3219        Ok(())
3220    }
3221
3222    fn commit_transition_unlocked(
3223        &self,
3224        slot_state: &RecorderSlotState,
3225        configuration: &ConfigurationState,
3226    ) -> Result<()> {
3227        let slot_bytes = encode_recorder_state(slot_state)?;
3228        let configuration_bytes = encode_configuration_state(configuration)?;
3229        let head = self.head_after_slot_state(configuration, slot_state)?;
3230        let head_bytes = encode_recorded_head(
3231            &self.cluster_id,
3232            self.epoch,
3233            configuration,
3234            &head,
3235            &[],
3236            self.wal_checkpoint()?,
3237        )?;
3238        atomic_write(
3239            &self.intent_path(),
3240            &encode_transition_intent(slot_state.slot(), &slot_bytes, &configuration_bytes)?,
3241        )?;
3242        self.fail_seal_at(SealFaultPoint::AfterIntent)?;
3243        atomic_write(&self.path(slot_state.slot()), &slot_bytes)?;
3244        self.fail_seal_at(SealFaultPoint::AfterSlot)?;
3245        atomic_write(&self.configuration_path(), &configuration_bytes)?;
3246        self.fail_seal_at(SealFaultPoint::AfterConfiguration)?;
3247        atomic_write(&self.recorded_head_path(), &head_bytes)?;
3248        fs::remove_file(self.intent_path()).map_err(|err| Error::Io(err.to_string()))?;
3249        fs::File::open(&self.root)
3250            .and_then(|directory| directory.sync_all())
3251            .map_err(|err| Error::Io(err.to_string()))?;
3252        *self
3253            .configuration
3254            .lock()
3255            .map_err(|_| Error::Io("configuration lock poisoned".into()))? = configuration.clone();
3256        *self
3257            .recorded_head
3258            .lock()
3259            .map_err(|_| Error::Io("recorder head lock poisoned".into()))? = head;
3260        self.recent_slots
3261            .lock()
3262            .map_err(|_| Error::Io("recorder recent-slot lock poisoned".into()))?
3263            .clear();
3264        Ok(())
3265    }
3266
3267    fn fail_seal_at(&self, point: SealFaultPoint) -> Result<()> {
3268        let mut fault = self
3269            .seal_fault
3270            .lock()
3271            .map_err(|_| Error::Io("seal fault lock poisoned".into()))?;
3272        if *fault == Some(point) {
3273            *fault = None;
3274            return Err(Error::Io(format!("injected seal fault at {point:?}")));
3275        }
3276        Ok(())
3277    }
3278
3279    fn config_digest(&self) -> LogHash {
3280        self.configuration
3281            .lock()
3282            .map(|state| state.config_digest)
3283            .unwrap_or(self.config_digest)
3284    }
3285
3286    fn current_config_id(&self) -> ConfigId {
3287        self.configuration
3288            .lock()
3289            .map(|state| state.config_id)
3290            .unwrap_or(self.config_id)
3291    }
3292
3293    fn wal_checkpoint(&self) -> Result<WalCheckpoint> {
3294        self.wal
3295            .lock()
3296            .map(|wal| wal.checkpoint)
3297            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))
3298    }
3299
3300    #[cfg(test)]
3301    fn wal_stats(&self) -> Result<(u64, u64, u64)> {
3302        self.wal
3303            .lock()
3304            .map(|wal| {
3305                (
3306                    wal.checkpoint.generation,
3307                    wal.checkpoint.through_sequence,
3308                    wal.frame_count,
3309                )
3310            })
3311            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))
3312    }
3313}
3314
3315fn configuration_structure_changed(
3316    previous: &ConfigurationState,
3317    next: &ConfigurationState,
3318) -> bool {
3319    previous.config_id != next.config_id
3320        || previous.config_digest != next.config_digest
3321        || previous.membership != next.membership
3322        || previous.predecessor != next.predecessor
3323        || previous.seal != next.seal
3324        || previous.activated != next.activated
3325}
3326
3327fn recorder_state_values(state: &RecorderSlotState) -> impl Iterator<Item = &AcceptedValue> {
3328    [
3329        state.accepted.as_ref().map(|accepted| &accepted.value),
3330        state.decided.as_ref().map(|decided| &decided.value),
3331        state
3332            .isr
3333            .first_current
3334            .as_ref()
3335            .and_then(|proposal| proposal.value.as_ref()),
3336        state
3337            .isr
3338            .aggregate_current
3339            .as_ref()
3340            .and_then(|proposal| proposal.value.as_ref()),
3341        state
3342            .isr
3343            .aggregate_prior
3344            .as_ref()
3345            .and_then(|proposal| proposal.value.as_ref()),
3346        state
3347            .decided_proof
3348            .as_ref()
3349            .and_then(|proof| proof.proposal().value.as_ref()),
3350    ]
3351    .into_iter()
3352    .flatten()
3353}
3354
3355pub struct ThreeNodeConsensus {
3356    cluster_id: ClusterId,
3357    proposer_id: NodeId,
3358    epoch: Epoch,
3359    config_id: ConfigId,
3360    config_digest: LogHash,
3361    membership: FixedMembership,
3362    recorders: Vec<Arc<dyn RecorderRpc>>,
3363    record_workers: Vec<RecordWorker>,
3364    control_workers: Vec<ControlWorker>,
3365    // Read fences must not queue behind recovery/control RPCs whose network
3366    // deadline is intentionally longer. A lost majority can otherwise occupy
3367    // two control workers and turn a read-only quorum check into the caller's
3368    // HTTP timeout instead of a prompt Unavailable result.
3369    read_fence_workers: Vec<ControlWorker>,
3370    priority_source: Arc<dyn PrioritySource>,
3371    proposal_sequence: AtomicU64,
3372    legacy_tip: Mutex<SingleNodeState>,
3373}
3374
3375struct RecordJob {
3376    index: usize,
3377    request: RecordRequest,
3378    result: std::sync::mpsc::SyncSender<(usize, Result<RecordSummary>)>,
3379}
3380
3381struct RecordWorker {
3382    sender: Option<std::sync::mpsc::SyncSender<RecordJob>>,
3383    handle: Option<thread::JoinHandle<()>>,
3384    pending: Arc<AtomicUsize>,
3385}
3386
3387impl RecordWorker {
3388    fn spawn(
3389        recorder_id: NodeId,
3390        recorder: Arc<dyn RecorderRpc>,
3391        config_id: ConfigId,
3392        config_digest: LogHash,
3393    ) -> Result<Self> {
3394        let expected_id = recorder_id;
3395        let (sender, receiver) =
3396            std::sync::mpsc::sync_channel::<RecordJob>(RECORD_WORKER_QUEUE_CAPACITY);
3397        let pending = Arc::new(AtomicUsize::new(0));
3398        let worker_pending = Arc::clone(&pending);
3399        let handle = thread::Builder::new()
3400            .spawn(move || {
3401                while let Ok(job) = receiver.recv() {
3402                    let expected_slot = job.request.slot;
3403                    let reply = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3404                        recorder.record(job.request)
3405                    }))
3406                    .unwrap_or(Err(Error::ProposeFailed))
3407                    .and_then(|reply| {
3408                        if reply.recorder_id == expected_id
3409                            && reply.slot == expected_slot
3410                            && reply.config_id == config_id
3411                            && reply.config_digest == config_digest
3412                        {
3413                            Ok(reply)
3414                        } else {
3415                            Err(Error::Rejected(RejectReason::InvalidRequest))
3416                        }
3417                    });
3418                    let _ = job.result.send((job.index, reply));
3419                    worker_pending.fetch_sub(1, Ordering::Release);
3420                }
3421            })
3422            .map_err(|error| Error::Io(error.to_string()))?;
3423        Ok(Self {
3424            sender: Some(sender),
3425            handle: Some(handle),
3426            pending,
3427        })
3428    }
3429
3430    fn dispatch(&self, job: RecordJob) -> bool {
3431        self.pending.fetch_add(1, Ordering::Relaxed);
3432        let (failed, saturated) = match &self.sender {
3433            Some(sender) => match sender.try_send(job) {
3434                Ok(()) => (None, false),
3435                Err(std::sync::mpsc::TrySendError::Full(job)) => (
3436                    Some((
3437                        job,
3438                        Error::Io("recorder worker queue is temporarily full".into()),
3439                    )),
3440                    true,
3441                ),
3442                Err(std::sync::mpsc::TrySendError::Disconnected(job)) => {
3443                    (Some((job, Error::ProposeFailed)), false)
3444                }
3445            },
3446            None => (Some((job, Error::ProposeFailed)), false),
3447        };
3448        if let Some((job, error)) = failed {
3449            self.pending.fetch_sub(1, Ordering::Relaxed);
3450            let _ = job.result.send((job.index, Err(error)));
3451        }
3452        saturated
3453    }
3454
3455    fn is_idle(&self) -> bool {
3456        self.pending.load(Ordering::Acquire) == 0
3457    }
3458
3459    fn shutdown(&mut self) {
3460        self.sender.take();
3461        if let Some(handle) = self.handle.take() {
3462            // Recorder RPCs are deadline-bounded, but Drop must not wait for a
3463            // blocked minority. Idle workers are joined; an in-flight worker
3464            // observes the disconnected queue and exits after its RPC returns.
3465            if self.pending.load(Ordering::Acquire) == 0 || handle.is_finished() {
3466                let _ = handle.join();
3467            }
3468        }
3469    }
3470}
3471
3472impl Drop for RecordWorker {
3473    fn drop(&mut self) {
3474        self.shutdown();
3475    }
3476}
3477
3478enum ControlJob {
3479    InstallProof {
3480        index: usize,
3481        proof: DecisionProof,
3482        membership: Membership,
3483        result: std::sync::mpsc::SyncSender<(usize, Result<()>)>,
3484    },
3485    InspectProof {
3486        index: usize,
3487        slot: Slot,
3488        result: std::sync::mpsc::SyncSender<(usize, Result<Option<DecisionProof>>)>,
3489    },
3490    InspectSummary {
3491        index: usize,
3492        slot: Slot,
3493        result: std::sync::mpsc::SyncSender<(usize, Result<Option<RecordSummary>>)>,
3494    },
3495    ObserveReadFence {
3496        index: usize,
3497        request: ReadFenceRequest,
3498        result: std::sync::mpsc::SyncSender<(usize, Result<ReadFenceObservation>)>,
3499    },
3500    StoreCommand {
3501        index: usize,
3502        cluster_id: ClusterId,
3503        epoch: Epoch,
3504        config_id: ConfigId,
3505        config_digest: LogHash,
3506        command_hash: LogHash,
3507        command: StoredCommand,
3508        result: std::sync::mpsc::SyncSender<(usize, Result<()>)>,
3509    },
3510    FetchCommand {
3511        index: usize,
3512        cluster_id: ClusterId,
3513        epoch: Epoch,
3514        config_id: ConfigId,
3515        config_digest: LogHash,
3516        command_hash: LogHash,
3517        result: std::sync::mpsc::SyncSender<(usize, Result<Option<StoredCommand>>)>,
3518    },
3519}
3520
3521impl ControlJob {
3522    fn run(self, recorder: &dyn RecorderRpc) {
3523        match self {
3524            Self::InstallProof {
3525                index,
3526                proof,
3527                membership,
3528                result,
3529            } => {
3530                let reply = control_rpc(|| recorder.install_decision_proof(proof, &membership));
3531                let _ = result.send((index, reply));
3532            }
3533            Self::InspectProof {
3534                index,
3535                slot,
3536                result,
3537            } => {
3538                let reply = control_rpc(|| recorder.inspect_decision_proof(slot));
3539                let _ = result.send((index, reply));
3540            }
3541            Self::InspectSummary {
3542                index,
3543                slot,
3544                result,
3545            } => {
3546                let reply = control_rpc(|| recorder.inspect_record_summary(slot));
3547                let _ = result.send((index, reply));
3548            }
3549            Self::ObserveReadFence {
3550                index,
3551                request,
3552                result,
3553            } => {
3554                let reply = control_rpc(|| recorder.observe_read_fence(request));
3555                let _ = result.send((index, reply));
3556            }
3557            Self::StoreCommand {
3558                index,
3559                cluster_id,
3560                epoch,
3561                config_id,
3562                config_digest,
3563                command_hash,
3564                command,
3565                result,
3566            } => {
3567                let reply = control_rpc(|| {
3568                    recorder.store_command_for(
3569                        cluster_id,
3570                        epoch,
3571                        config_id,
3572                        config_digest,
3573                        command_hash,
3574                        command,
3575                    )
3576                });
3577                let _ = result.send((index, reply));
3578            }
3579            Self::FetchCommand {
3580                index,
3581                cluster_id,
3582                epoch,
3583                config_id,
3584                config_digest,
3585                command_hash,
3586                result,
3587            } => {
3588                let reply = control_rpc(|| {
3589                    recorder.fetch_command_for(
3590                        cluster_id,
3591                        epoch,
3592                        config_id,
3593                        config_digest,
3594                        command_hash,
3595                    )
3596                });
3597                let _ = result.send((index, reply));
3598            }
3599        }
3600    }
3601
3602    fn fail(self, error: Error) {
3603        match self {
3604            Self::InstallProof { index, result, .. } | Self::StoreCommand { index, result, .. } => {
3605                let _ = result.send((index, Err(error)));
3606            }
3607            Self::InspectProof { index, result, .. } => {
3608                let _ = result.send((index, Err(error)));
3609            }
3610            Self::InspectSummary { index, result, .. } => {
3611                let _ = result.send((index, Err(error)));
3612            }
3613            Self::ObserveReadFence { index, result, .. } => {
3614                let _ = result.send((index, Err(error)));
3615            }
3616            Self::FetchCommand { index, result, .. } => {
3617                let _ = result.send((index, Err(error)));
3618            }
3619        }
3620    }
3621}
3622
3623fn control_rpc<T>(call: impl FnOnce() -> Result<T>) -> Result<T> {
3624    std::panic::catch_unwind(std::panic::AssertUnwindSafe(call))
3625        .unwrap_or(Err(Error::ProposeFailed))
3626}
3627
3628struct QueuedControlJob {
3629    job: ControlJob,
3630    cancelled: Option<Arc<AtomicBool>>,
3631}
3632
3633impl QueuedControlJob {
3634    fn run(self, recorder: &dyn RecorderRpc) {
3635        if !self
3636            .cancelled
3637            .as_ref()
3638            .is_some_and(|cancelled| cancelled.load(Ordering::Acquire))
3639        {
3640            self.job.run(recorder);
3641        }
3642    }
3643
3644    fn fail(self, error: Error) {
3645        self.job.fail(error);
3646    }
3647}
3648
3649struct ControlJobCancellation {
3650    cancelled: Arc<AtomicBool>,
3651}
3652
3653impl ControlJobCancellation {
3654    fn new() -> Self {
3655        Self {
3656            cancelled: Arc::new(AtomicBool::new(false)),
3657        }
3658    }
3659
3660    fn token(&self) -> Arc<AtomicBool> {
3661        Arc::clone(&self.cancelled)
3662    }
3663}
3664
3665impl Drop for ControlJobCancellation {
3666    fn drop(&mut self) {
3667        self.cancelled.store(true, Ordering::Release);
3668    }
3669}
3670
3671struct ControlWorker {
3672    sender: Option<std::sync::mpsc::SyncSender<QueuedControlJob>>,
3673    handle: Option<thread::JoinHandle<()>>,
3674    pending: Arc<AtomicUsize>,
3675}
3676
3677#[derive(Clone, Copy, Eq, PartialEq)]
3678enum ControlDispatch {
3679    Accepted,
3680    Saturated,
3681    Failed,
3682}
3683
3684impl ControlWorker {
3685    fn spawn(recorder: Arc<dyn RecorderRpc>) -> Result<Self> {
3686        let (sender, receiver) =
3687            std::sync::mpsc::sync_channel::<QueuedControlJob>(CONTROL_WORKER_QUEUE_CAPACITY);
3688        let pending = Arc::new(AtomicUsize::new(0));
3689        let worker_pending = Arc::clone(&pending);
3690        let handle = thread::Builder::new()
3691            .spawn(move || {
3692                while let Ok(job) = receiver.recv() {
3693                    job.run(recorder.as_ref());
3694                    worker_pending.fetch_sub(1, Ordering::Release);
3695                }
3696            })
3697            .map_err(|error| Error::Io(error.to_string()))?;
3698        Ok(Self {
3699            sender: Some(sender),
3700            handle: Some(handle),
3701            pending,
3702        })
3703    }
3704
3705    fn dispatch(&self, job: ControlJob) -> ControlDispatch {
3706        self.dispatch_with_cancellation(job, None)
3707    }
3708
3709    fn dispatch_cancellable(
3710        &self,
3711        job: ControlJob,
3712        cancellation: Arc<AtomicBool>,
3713    ) -> ControlDispatch {
3714        self.dispatch_with_cancellation(job, Some(cancellation))
3715    }
3716
3717    fn dispatch_with_cancellation(
3718        &self,
3719        job: ControlJob,
3720        cancelled: Option<Arc<AtomicBool>>,
3721    ) -> ControlDispatch {
3722        self.pending.fetch_add(1, Ordering::Relaxed);
3723        let job = QueuedControlJob { job, cancelled };
3724        let (failed, outcome) = match &self.sender {
3725            Some(sender) => match sender.try_send(job) {
3726                Ok(()) => (None, ControlDispatch::Accepted),
3727                Err(std::sync::mpsc::TrySendError::Full(job)) => (
3728                    Some((
3729                        job,
3730                        Error::Io("recorder control worker queue is temporarily full".into()),
3731                    )),
3732                    ControlDispatch::Saturated,
3733                ),
3734                Err(std::sync::mpsc::TrySendError::Disconnected(job)) => {
3735                    (Some((job, Error::ProposeFailed)), ControlDispatch::Failed)
3736                }
3737            },
3738            None => (Some((job, Error::ProposeFailed)), ControlDispatch::Failed),
3739        };
3740        if let Some((job, error)) = failed {
3741            self.pending.fetch_sub(1, Ordering::Relaxed);
3742            job.fail(error);
3743        }
3744        outcome
3745    }
3746
3747    fn is_idle(&self) -> bool {
3748        self.pending.load(Ordering::Acquire) == 0
3749    }
3750
3751    fn shutdown(&mut self) {
3752        self.sender.take();
3753        if let Some(handle) = self.handle.take() {
3754            if self.pending.load(Ordering::Acquire) == 0 || handle.is_finished() {
3755                let _ = handle.join();
3756            }
3757        }
3758    }
3759}
3760
3761fn control_quorum_reachable(successful: usize, saturated: usize, quorum: usize) -> bool {
3762    successful.saturating_add(saturated) >= quorum
3763}
3764
3765impl Drop for ControlWorker {
3766    fn drop(&mut self) {
3767        self.shutdown();
3768    }
3769}
3770
3771pub trait PrioritySource: Send + Sync {
3772    fn sample(
3773        &self,
3774        slot: Slot,
3775        round: Round,
3776        proposer_id: &str,
3777        recorder_id: &str,
3778    ) -> Result<ProposalPriority>;
3779}
3780
3781#[derive(Debug, Default)]
3782pub struct OsPrioritySource;
3783
3784impl PrioritySource for OsPrioritySource {
3785    fn sample(
3786        &self,
3787        slot: Slot,
3788        round: Round,
3789        proposer_id: &str,
3790        recorder_id: &str,
3791    ) -> Result<ProposalPriority> {
3792        let mut bytes = [0; 32];
3793        let _ = (slot, round, proposer_id, recorder_id);
3794        getrandom::fill(&mut bytes)
3795            .map_err(|error| Error::RandomnessUnavailable(error.to_string()))?;
3796        if bytes == ProposalPriority::ZERO.0 || bytes == ProposalPriority::MAX.0 {
3797            bytes[31] = 1;
3798        }
3799        Ok(ProposalPriority(bytes))
3800    }
3801}
3802
3803#[derive(Clone, Debug, Eq, PartialEq)]
3804pub struct ProposerProgress {
3805    pub slot: Slot,
3806    pub step: Step,
3807    pub proposal: Proposal,
3808    phase_zero_priorities: BTreeMap<(Round, NodeId), ProposalPriority>,
3809    command: Option<StoredCommand>,
3810    command_holders: BTreeSet<NodeId>,
3811    transition_involved: bool,
3812}
3813
3814impl ProposerProgress {
3815    pub fn new(slot: Slot, proposal: Proposal) -> Self {
3816        Self {
3817            slot,
3818            step: 4,
3819            proposal,
3820            phase_zero_priorities: BTreeMap::new(),
3821            command: None,
3822            command_holders: BTreeSet::new(),
3823            transition_involved: false,
3824        }
3825    }
3826
3827    fn with_command(mut self, command: StoredCommand) -> Self {
3828        self.transition_involved = command.entry_type == EntryType::ConfigChange;
3829        self.command = Some(command);
3830        self
3831    }
3832}
3833
3834#[derive(Clone, Debug, Eq, PartialEq)]
3835pub enum DriveOutcome {
3836    Progress(ProposerProgress),
3837    Pending(ProposerProgress),
3838    Decision(DecisionProof),
3839}
3840
3841impl fmt::Debug for ThreeNodeConsensus {
3842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3843        f.debug_struct("ThreeNodeConsensus")
3844            .field("cluster_id", &self.cluster_id)
3845            .field("proposer_id", &self.proposer_id)
3846            .field("epoch", &self.epoch)
3847            .field("config_id", &self.config_id)
3848            .field("recorders", &self.membership.members())
3849            .finish_non_exhaustive()
3850    }
3851}
3852
3853impl Drop for ThreeNodeConsensus {
3854    fn drop(&mut self) {
3855        for worker in &mut self.record_workers {
3856            worker.shutdown();
3857        }
3858        for worker in &mut self.control_workers {
3859            worker.shutdown();
3860        }
3861        for worker in &mut self.read_fence_workers {
3862            worker.shutdown();
3863        }
3864    }
3865}
3866
3867impl ThreeNodeConsensus {
3868    /// Waits up to `timeout` for accepted recorder and control RPC jobs.
3869    /// Callers must first quiesce proposal admission and ensure no proposal
3870    /// can still dispatch jobs; this only drains already accepted jobs.
3871    pub fn finish_pending_rpcs(&self, timeout: Duration) -> bool {
3872        let started = Instant::now();
3873        loop {
3874            if self.record_workers.iter().all(RecordWorker::is_idle)
3875                && self.control_workers.iter().all(ControlWorker::is_idle)
3876                && self.read_fence_workers.iter().all(ControlWorker::is_idle)
3877            {
3878                return true;
3879            }
3880            if started.elapsed() >= timeout {
3881                return false;
3882            }
3883            thread::sleep(Duration::from_millis(1));
3884        }
3885    }
3886
3887    pub const fn config_id(&self) -> ConfigId {
3888        self.config_id
3889    }
3890
3891    pub const fn membership(&self) -> &FixedMembership {
3892        &self.membership
3893    }
3894
3895    pub fn new(
3896        cluster_id: impl Into<ClusterId>,
3897        proposer_id: impl Into<NodeId>,
3898        epoch: Epoch,
3899        config_id: ConfigId,
3900        recorder_roots: [PathBuf; 3],
3901    ) -> Result<Self> {
3902        Self::from_recovered_tip(
3903            cluster_id,
3904            proposer_id,
3905            epoch,
3906            config_id,
3907            recorder_roots,
3908            1,
3909            LogHash::ZERO,
3910        )
3911    }
3912
3913    pub fn from_recovered_tip(
3914        cluster_id: impl Into<ClusterId>,
3915        proposer_id: impl Into<NodeId>,
3916        epoch: Epoch,
3917        config_id: ConfigId,
3918        recorder_roots: [PathBuf; 3],
3919        next_index: LogIndex,
3920        last_hash: LogHash,
3921    ) -> Result<Self> {
3922        let cluster_id = cluster_id.into();
3923        let recorder_roots: Vec<_> = recorder_roots.into_iter().collect();
3924        let recorder_ids: Vec<_> = recorder_roots
3925            .iter()
3926            .map(|root| {
3927                root.file_name()
3928                    .and_then(|name| name.to_str())
3929                    .filter(|name| !name.is_empty())
3930                    .unwrap_or("recorder")
3931                    .to_owned()
3932            })
3933            .collect();
3934        let membership = Membership::from_voters(recorder_ids.iter().cloned())?;
3935        let recorders = recorder_roots
3936            .into_iter()
3937            .zip(recorder_ids)
3938            .map(|(root, recorder_id)| -> Result<Box<dyn RecorderRpc>> {
3939                Ok(Box::new(RecorderFileStore::new_with_membership(
3940                    root,
3941                    recorder_id,
3942                    cluster_id.clone(),
3943                    epoch,
3944                    config_id,
3945                    membership.clone(),
3946                )?) as Box<dyn RecorderRpc>)
3947            })
3948            .collect::<Result<Vec<_>>>()?;
3949        Self::from_recorders_with_recovered_tip(
3950            cluster_id,
3951            proposer_id,
3952            epoch,
3953            config_id,
3954            recorders,
3955            next_index,
3956            last_hash,
3957        )
3958    }
3959
3960    pub fn from_recorders(
3961        cluster_id: impl Into<ClusterId>,
3962        proposer_id: impl Into<NodeId>,
3963        epoch: Epoch,
3964        config_id: ConfigId,
3965        recorders: Vec<Box<dyn RecorderRpc>>,
3966    ) -> Result<Self> {
3967        Self::from_recorders_with_recovered_tip(
3968            cluster_id,
3969            proposer_id,
3970            epoch,
3971            config_id,
3972            recorders,
3973            1,
3974            LogHash::ZERO,
3975        )
3976    }
3977
3978    /// Constructs a proposer from expected recorder identities paired with RPC clients.
3979    ///
3980    /// This path does not issue `Identity` RPCs. Reply identities are still
3981    /// checked against the corresponding expected identity on every call.
3982    pub fn from_recorders_with_ids(
3983        cluster_id: impl Into<ClusterId>,
3984        proposer_id: impl Into<NodeId>,
3985        epoch: Epoch,
3986        config_id: ConfigId,
3987        recorders: Vec<(NodeId, Box<dyn RecorderRpc>)>,
3988    ) -> Result<Self> {
3989        Self::from_recorders_with_ids_and_recovered_tip(
3990            cluster_id,
3991            proposer_id,
3992            epoch,
3993            config_id,
3994            recorders,
3995            1,
3996            LogHash::ZERO,
3997        )
3998    }
3999
4000    pub fn from_recorders_with_recovered_tip(
4001        cluster_id: impl Into<ClusterId>,
4002        proposer_id: impl Into<NodeId>,
4003        epoch: Epoch,
4004        config_id: ConfigId,
4005        recorders: Vec<Box<dyn RecorderRpc>>,
4006        next_index: LogIndex,
4007        last_hash: LogHash,
4008    ) -> Result<Self> {
4009        let recorder_ids = recorders
4010            .iter()
4011            .map(|recorder| recorder.recorder_id())
4012            .collect::<Result<Vec<_>>>()?;
4013        Self::from_recorders_with_ids_and_recovered_tip(
4014            cluster_id,
4015            proposer_id,
4016            epoch,
4017            config_id,
4018            recorder_ids.into_iter().zip(recorders).collect(),
4019            next_index,
4020            last_hash,
4021        )
4022    }
4023
4024    /// Recovered-tip variant of [`Self::from_recorders_with_ids`].
4025    pub fn from_recorders_with_ids_and_recovered_tip(
4026        cluster_id: impl Into<ClusterId>,
4027        proposer_id: impl Into<NodeId>,
4028        epoch: Epoch,
4029        config_id: ConfigId,
4030        mut recorders: Vec<(NodeId, Box<dyn RecorderRpc>)>,
4031        next_index: LogIndex,
4032        last_hash: LogHash,
4033    ) -> Result<Self> {
4034        if next_index == 0 {
4035            return Err(Error::InvalidRecoveredTip);
4036        }
4037        recorders.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
4038        let (recorder_ids, recorders): (Vec<_>, Vec<_>) = recorders.into_iter().unzip();
4039        let recorders: Vec<Arc<dyn RecorderRpc>> = recorders.into_iter().map(Arc::from).collect();
4040        let membership = FixedMembership::from_members(recorder_ids)?;
4041        let config_digest = membership.digest();
4042        let record_workers = membership
4043            .members()
4044            .iter()
4045            .cloned()
4046            .zip(&recorders)
4047            .map(|(recorder_id, recorder)| {
4048                RecordWorker::spawn(recorder_id, Arc::clone(recorder), config_id, config_digest)
4049            })
4050            .collect::<Result<Vec<_>>>()?;
4051        let control_workers = recorders
4052            .iter()
4053            .cloned()
4054            .map(ControlWorker::spawn)
4055            .collect::<Result<Vec<_>>>()?;
4056        let read_fence_workers = recorders
4057            .iter()
4058            .cloned()
4059            .map(ControlWorker::spawn)
4060            .collect::<Result<Vec<_>>>()?;
4061        Ok(Self {
4062            cluster_id: cluster_id.into(),
4063            proposer_id: proposer_id.into(),
4064            epoch,
4065            config_id,
4066            config_digest,
4067            membership,
4068            recorders,
4069            record_workers,
4070            control_workers,
4071            read_fence_workers,
4072            priority_source: Arc::new(OsPrioritySource),
4073            proposal_sequence: AtomicU64::new(1),
4074            legacy_tip: Mutex::new(SingleNodeState {
4075                next_index,
4076                last_hash,
4077            }),
4078        })
4079    }
4080
4081    pub fn with_priority_source(mut self, source: Arc<dyn PrioritySource>) -> Self {
4082        self.priority_source = source;
4083        self
4084    }
4085
4086    /// Stores command bytes on a recorder quorum after verifying their hash.
4087    ///
4088    /// [`Error::NoQuorum`] is retryable, including when bounded control-worker
4089    /// queues are temporarily saturated.
4090    pub fn register_command(&self, command_hash: LogHash, command_bytes: Vec<u8>) -> Result<()> {
4091        let command = StoredCommand::new(EntryType::Command, command_bytes);
4092        if command.hash() != command_hash {
4093            return Err(Error::CommandHashMismatch);
4094        }
4095        self.store_command_on_quorum(command_hash, &command)
4096    }
4097
4098    fn propose_next(&self, command: Command) -> Result<LogEntry> {
4099        let mut tip = self.legacy_tip.lock().map_err(|_| Error::ProposeFailed)?;
4100        let entry = self.propose_at(tip.next_index, tip.last_hash, command)?;
4101        tip.next_index = entry.index + 1;
4102        tip.last_hash = entry.hash;
4103        Ok(entry)
4104    }
4105
4106    pub fn propose_at(&self, slot: Slot, prev_hash: LogHash, command: Command) -> Result<LogEntry> {
4107        self.propose_stored_at(slot, prev_hash, stored_command(command)?)
4108    }
4109
4110    pub fn propose_at_cancellable(
4111        &self,
4112        slot: Slot,
4113        prev_hash: LogHash,
4114        command: Command,
4115        cancelled: &std::sync::atomic::AtomicBool,
4116    ) -> Result<LogEntry> {
4117        self.propose_stored_at_until(slot, prev_hash, stored_command(command)?, || {
4118            cancelled.load(Ordering::Acquire)
4119        })
4120    }
4121
4122    pub fn propose_stop_at(&self, slot: Slot, prev_hash: LogHash) -> Result<LogEntry> {
4123        self.propose_stored_at(
4124            slot,
4125            prev_hash,
4126            ConfigChange::stop(self.config_id, self.config_digest).to_stored_command(),
4127        )
4128    }
4129
4130    pub fn propose_stop_for_successor_at(
4131        &self,
4132        slot: Slot,
4133        prev_hash: LogHash,
4134        successor: &Membership,
4135    ) -> Result<LogEntry> {
4136        let next_config_id = self
4137            .config_id
4138            .checked_add(1)
4139            .ok_or(Error::Rejected(RejectReason::InvalidTransition))?;
4140        let stop = ConfigChange::bound_stop(
4141            self.cluster_id.clone(),
4142            self.config_id,
4143            self.config_digest,
4144            next_config_id,
4145            successor.members().to_vec(),
4146        )
4147        .map_err(|_| Error::Rejected(RejectReason::InvalidTransition))?;
4148        self.propose_stored_at(slot, prev_hash, stop.to_stored_command())
4149    }
4150
4151    pub fn propose_activation_barrier_at(
4152        &self,
4153        stop_slot: Slot,
4154        prefix_hash: LogHash,
4155    ) -> Result<LogEntry> {
4156        self.propose_stored_at(
4157            stop_slot.checked_add(1).ok_or(Error::InvalidRecoveredTip)?,
4158            prefix_hash,
4159            ConfigChange::activation_barrier(
4160                self.config_id,
4161                self.config_digest,
4162                stop_slot,
4163                prefix_hash,
4164            )
4165            .to_stored_command(),
4166        )
4167    }
4168
4169    pub fn propose_activation_for_stop_entry(&self, stop: &LogEntry) -> Result<LogEntry> {
4170        let command = StoredCommand::new(stop.entry_type, stop.payload.clone());
4171        let change = ConfigChange::recognize(&command)
4172            .map_err(|_| Error::Rejected(RejectReason::InvalidTransition))?;
4173        let successor = change
4174            .successor()
4175            .filter(|successor| {
4176                successor.cluster_id() == self.cluster_id
4177                    && successor.config_id() == self.config_id
4178                    && successor.digest() == self.config_digest
4179                    && successor.members() == self.membership.members()
4180            })
4181            .ok_or(Error::Rejected(RejectReason::InvalidTransition))?
4182            .clone();
4183        self.propose_stored_at(
4184            stop.index
4185                .checked_add(1)
4186                .ok_or(Error::InvalidRecoveredTip)?,
4187            stop.hash,
4188            ConfigChange::bound_activation_barrier(
4189                successor,
4190                stop.index,
4191                stop.hash,
4192                command.hash(),
4193            )
4194            .to_stored_command(),
4195        )
4196    }
4197
4198    pub fn propose_activation_for_stop_at(&self, stop_proof: &DecisionProof) -> Result<LogEntry> {
4199        if proof_cluster_id(stop_proof) != self.cluster_id {
4200            return Err(Error::Rejected(RejectReason::WrongCluster));
4201        }
4202        let (stop_slot, epoch, predecessor_config_id, _) = proof_context(stop_proof);
4203        if epoch != self.epoch || predecessor_config_id.checked_add(1) != Some(self.config_id) {
4204            return Err(Error::Rejected(RejectReason::InvalidTransition));
4205        }
4206        let value = stop_proof
4207            .proposal()
4208            .value
4209            .as_ref()
4210            .ok_or(Error::Rejected(RejectReason::InvalidCertificate))?;
4211        let bound_stop = ConfigChange::bound_stop(
4212            self.cluster_id.clone(),
4213            predecessor_config_id,
4214            proof_context(stop_proof).3,
4215            self.config_id,
4216            self.membership.members().to_vec(),
4217        )
4218        .map_err(|_| Error::Rejected(RejectReason::InvalidTransition))?;
4219        let stop_command = bound_stop.to_stored_command();
4220        let expected = AcceptedValue::from_command(
4221            &self.cluster_id,
4222            stop_slot,
4223            self.epoch,
4224            predecessor_config_id,
4225            value.prev_hash,
4226            &stop_command,
4227        );
4228        if &expected != value {
4229            return Err(Error::Rejected(RejectReason::InvalidTransition));
4230        }
4231        let successor = bound_stop
4232            .successor()
4233            .expect("bound stop has successor")
4234            .clone();
4235        self.propose_stored_at(
4236            stop_slot.checked_add(1).ok_or(Error::InvalidRecoveredTip)?,
4237            value.entry_hash,
4238            ConfigChange::bound_activation_barrier(
4239                successor,
4240                stop_slot,
4241                value.entry_hash,
4242                value.command_hash,
4243            )
4244            .to_stored_command(),
4245        )
4246    }
4247
4248    pub fn propose_stored_at(
4249        &self,
4250        slot: Slot,
4251        prev_hash: LogHash,
4252        command: StoredCommand,
4253    ) -> Result<LogEntry> {
4254        self.propose_stored_at_until(slot, prev_hash, command, || false)
4255    }
4256
4257    fn propose_stored_at_until<F>(
4258        &self,
4259        slot: Slot,
4260        prev_hash: LogHash,
4261        offered_command: StoredCommand,
4262        cancelled: F,
4263    ) -> Result<LogEntry>
4264    where
4265        F: Fn() -> bool,
4266    {
4267        if cancelled() {
4268            return Err(Error::Cancelled);
4269        }
4270        let offered_value = AcceptedValue::from_command(
4271            &self.cluster_id,
4272            slot,
4273            self.epoch,
4274            self.config_id,
4275            prev_hash,
4276            &offered_command,
4277        );
4278        let proposal_id = self.proposal_sequence.fetch_add(1, Ordering::Relaxed);
4279        let mut progress = ProposerProgress::new(
4280            slot,
4281            Proposal::new(
4282                ProposalPriority::MAX,
4283                self.proposer_id.clone(),
4284                proposal_id,
4285                offered_value,
4286            ),
4287        )
4288        .with_command(offered_command.clone());
4289        loop {
4290            if cancelled() {
4291                return Err(Error::Cancelled);
4292            }
4293            match self.drive(progress)? {
4294                DriveOutcome::Progress(next) => progress = next,
4295                DriveOutcome::Pending(next) => {
4296                    progress = next;
4297                    thread::sleep(std::time::Duration::from_millis(10));
4298                }
4299                DriveOutcome::Decision(proof) => {
4300                    let value = proof
4301                        .proposal()
4302                        .value
4303                        .as_ref()
4304                        .ok_or(Error::Rejected(RejectReason::InvalidCertificate))?;
4305                    self.ensure_predecessor(slot, prev_hash, value.prev_hash)?;
4306                    let command = if self.command_matches_value(slot, value, &offered_command) {
4307                        offered_command.clone()
4308                    } else {
4309                        self.fetch_verified_value(slot, value)?
4310                            .ok_or(Error::CommandUnavailable)?
4311                    };
4312                    return self.log_entry_from_value(slot, command, value);
4313                }
4314            }
4315        }
4316    }
4317
4318    pub fn drive(&self, mut progress: ProposerProgress) -> Result<DriveOutcome> {
4319        self.ensure_progress_command(&mut progress)?;
4320        let round = progress.step / 4;
4321        let phase = progress.step % 4;
4322        if phase == 0 {
4323            progress
4324                .phase_zero_priorities
4325                .retain(|(cached_round, _), _| *cached_round == round);
4326        } else {
4327            progress.phase_zero_priorities.clear();
4328        }
4329        let command_targets: BTreeSet<_> = self
4330            .membership
4331            .members()
4332            .iter()
4333            .filter(|recorder_id| !progress.command_holders.contains(*recorder_id))
4334            .cloned()
4335            .collect();
4336        let requests: Vec<_> = self
4337            .membership
4338            .members()
4339            .iter()
4340            .map(|recorder_id| -> Result<RecordRequest> {
4341                let mut proposal = progress.proposal.clone();
4342                if phase == 0 {
4343                    proposal.priority =
4344                        if progress.step == 4 && self.proposer_id == self.membership.members()[0] {
4345                            ProposalPriority::MAX
4346                        } else {
4347                            match progress
4348                                .phase_zero_priorities
4349                                .entry((round, recorder_id.clone()))
4350                            {
4351                                std::collections::btree_map::Entry::Occupied(entry) => *entry.get(),
4352                                std::collections::btree_map::Entry::Vacant(entry) => {
4353                                    *entry.insert(self.priority_source.sample(
4354                                        progress.slot,
4355                                        round,
4356                                        &self.proposer_id,
4357                                        recorder_id,
4358                                    )?)
4359                                }
4360                            }
4361                        };
4362                }
4363                Ok(RecordRequest {
4364                    cluster_id: self.cluster_id.clone(),
4365                    epoch: self.epoch,
4366                    config_id: self.config_id,
4367                    config_digest: self.config_digest,
4368                    slot: progress.slot,
4369                    step: progress.step,
4370                    proposal,
4371                    command: command_targets
4372                        .contains(recorder_id)
4373                        .then(|| progress.command.clone())
4374                        .flatten(),
4375                })
4376            })
4377            .collect::<Result<_>>()?;
4378        let mut replies = self.record_broadcast(requests)?;
4379        progress.command_holders.extend(
4380            replies
4381                .iter()
4382                .filter(|reply| command_targets.contains(&reply.recorder_id))
4383                .map(|reply| reply.recorder_id.clone()),
4384        );
4385        for reply in &replies {
4386            if let Some(proof) = &reply.decided {
4387                if proof_cluster_id(proof) != self.cluster_id {
4388                    return Err(Error::Rejected(RejectReason::WrongCluster));
4389                }
4390                proof
4391                    .validate_for_cluster(
4392                        &self.cluster_id,
4393                        progress.slot,
4394                        self.epoch,
4395                        self.config_id,
4396                        &self.membership,
4397                    )
4398                    .map_err(Error::Rejected)?;
4399                return self.finish_decision(
4400                    proof.clone(),
4401                    progress.command.as_ref(),
4402                    progress.transition_involved,
4403                );
4404            }
4405        }
4406        if let Some(highest) = replies.iter().map(|reply| reply.step).max() {
4407            if highest > progress.step {
4408                let caught_up = replies
4409                    .iter()
4410                    .filter(|reply| reply.step == highest)
4411                    .min_by(|left, right| left.recorder_id.cmp(&right.recorder_id))
4412                    .expect("highest reply exists");
4413                progress.step = highest;
4414                if let Some(proposal) = &caught_up.first_current {
4415                    progress.proposal = proposal.clone();
4416                }
4417                self.ensure_progress_command(&mut progress)?;
4418                progress.phase_zero_priorities.clear();
4419                return Ok(DriveOutcome::Progress(progress));
4420            }
4421        }
4422        replies.retain(|reply| reply.step == progress.step);
4423        replies.sort_by(|left, right| left.recorder_id.cmp(&right.recorder_id));
4424        replies.dedup_by(|left, right| left.recorder_id == right.recorder_id);
4425        if replies.len() < self.membership.quorum_size() {
4426            return Ok(DriveOutcome::Pending(progress));
4427        }
4428        replies.truncate(self.membership.quorum_size());
4429        let summaries: Vec<_> = replies
4430            .iter()
4431            .map(|reply| RecorderSummary {
4432                recorder_id: reply.recorder_id.clone(),
4433                slot: reply.slot,
4434                step: reply.step,
4435                first_current: reply.first_current.clone(),
4436                aggregate_prior: reply.aggregate_prior.clone(),
4437            })
4438            .collect();
4439        match phase {
4440            0 => {
4441                let fast_proposal = summaries
4442                    .first()
4443                    .and_then(|summary| summary.first_current.as_ref())
4444                    .filter(|proposal| proposal.priority == ProposalPriority::MAX)
4445                    .filter(|proposal| {
4446                        progress.step == 4
4447                            && summaries.iter().all(|summary| {
4448                                summary
4449                                    .first_current
4450                                    .as_ref()
4451                                    .is_some_and(|candidate| proposal_exact(candidate, proposal))
4452                            })
4453                    })
4454                    .cloned();
4455                if let Some(proposal) = fast_proposal {
4456                    let proof = DecisionProof::FastPath {
4457                        cluster_id: self.cluster_id.clone(),
4458                        slot: progress.slot,
4459                        epoch: self.epoch,
4460                        config_id: self.config_id,
4461                        config_digest: self.config_digest,
4462                        proposal,
4463                        summaries,
4464                    };
4465                    return self.finish_decision(
4466                        proof,
4467                        progress.command.as_ref(),
4468                        progress.transition_involved,
4469                    );
4470                }
4471                progress.proposal = replies
4472                    .iter()
4473                    .filter_map(|reply| reply.first_current.clone())
4474                    .max()
4475                    .ok_or(Error::Rejected(RejectReason::InvalidRequest))?;
4476            }
4477            1 => {}
4478            2 => {
4479                let maximum = replies
4480                    .iter()
4481                    .filter_map(|reply| reply.aggregate_prior.clone())
4482                    .max();
4483                if maximum.as_ref() == Some(&progress.proposal) {
4484                    let proof = DecisionProof::Phase2 {
4485                        cluster_id: self.cluster_id.clone(),
4486                        slot: progress.slot,
4487                        epoch: self.epoch,
4488                        config_id: self.config_id,
4489                        config_digest: self.config_digest,
4490                        step: progress.step,
4491                        proposal: progress.proposal.clone(),
4492                        summaries,
4493                    };
4494                    return self.finish_decision(
4495                        proof,
4496                        progress.command.as_ref(),
4497                        progress.transition_involved,
4498                    );
4499                }
4500            }
4501            3 => {
4502                progress.proposal = replies
4503                    .iter()
4504                    .filter_map(|reply| reply.aggregate_prior.clone())
4505                    .max()
4506                    .ok_or(Error::Rejected(RejectReason::InvalidRequest))?;
4507            }
4508            _ => unreachable!("phase is step modulo four"),
4509        }
4510        self.ensure_progress_command(&mut progress)?;
4511        progress.step = progress.step.checked_add(1).ok_or(Error::ProposeFailed)?;
4512        progress.phase_zero_priorities.clear();
4513        Ok(DriveOutcome::Progress(progress))
4514    }
4515
4516    fn finish_decision(
4517        &self,
4518        proof: DecisionProof,
4519        known_command: Option<&StoredCommand>,
4520        transition_involved: bool,
4521    ) -> Result<DriveOutcome> {
4522        proof
4523            .validate_for_cluster(
4524                &self.cluster_id,
4525                proof_context(&proof).0,
4526                self.epoch,
4527                self.config_id,
4528                &self.membership,
4529            )
4530            .map_err(Error::Rejected)?;
4531        let value = proof
4532            .proposal()
4533            .value
4534            .as_ref()
4535            .ok_or(Error::Rejected(RejectReason::InvalidCertificate))?;
4536        let command = match known_command {
4537            Some(command)
4538                if self.command_matches_value(proof_context(&proof).0, value, command) =>
4539            {
4540                command.clone()
4541            }
4542            _ => self
4543                .fetch_verified_value(proof_context(&proof).0, value)?
4544                .ok_or(Error::CommandUnavailable)?,
4545        };
4546        if command.entry_type != EntryType::ConfigChange && !transition_involved {
4547            return Ok(DriveOutcome::Decision(proof));
4548        }
4549        self.install_decision_proof_quorum(proof.clone())?;
4550        Ok(DriveOutcome::Decision(proof))
4551    }
4552
4553    fn install_decision_proof_quorum(&self, proof: DecisionProof) -> Result<()> {
4554        let membership = self.membership.clone();
4555        let quorum = membership.quorum_size();
4556        let total = self.control_workers.len();
4557        let (sender, receiver) = std::sync::mpsc::sync_channel(total.max(1));
4558        let mut saturated = 0;
4559        for (index, worker) in self.control_workers.iter().enumerate() {
4560            if worker.dispatch(ControlJob::InstallProof {
4561                index,
4562                proof: proof.clone(),
4563                membership: membership.clone(),
4564                result: sender.clone(),
4565            }) == ControlDispatch::Saturated
4566            {
4567                saturated += 1;
4568            }
4569        }
4570        drop(sender);
4571        let mut installed = 0;
4572        let mut worker_failed = false;
4573        for (_, result) in receiver {
4574            match result {
4575                Ok(()) => installed += 1,
4576                Err(Error::ProposeFailed) => worker_failed = true,
4577                Err(_) => {}
4578            }
4579            if installed >= quorum {
4580                break;
4581            }
4582        }
4583        if installed < quorum {
4584            return Err(
4585                if worker_failed && !control_quorum_reachable(installed, saturated, quorum) {
4586                    Error::ProposeFailed
4587                } else {
4588                    Error::NoQuorum
4589                },
4590            );
4591        }
4592        Ok(())
4593    }
4594
4595    fn record_broadcast(&self, requests: Vec<RecordRequest>) -> Result<Vec<RecordSummary>> {
4596        let quorum = self.membership.quorum_size();
4597        let total = self.record_workers.len().min(requests.len());
4598        let (sender, receiver) = std::sync::mpsc::sync_channel(total.max(1));
4599        let mut saturated_workers = vec![false; total];
4600        for (index, (worker, request)) in self.record_workers.iter().zip(requests).enumerate() {
4601            saturated_workers[index] = worker.dispatch(RecordJob {
4602                index,
4603                request,
4604                result: sender.clone(),
4605            });
4606        }
4607        drop(sender);
4608        let saturated = saturated_workers
4609            .iter()
4610            .filter(|saturated| **saturated)
4611            .count();
4612        let accepted = total.saturating_sub(saturated);
4613        let mut accepted_completed = 0;
4614        let mut typed_errors = vec![None; total];
4615        let mut worker_failed = false;
4616        let mut replies = Vec::with_capacity(quorum);
4617        for (index, result) in receiver {
4618            if !saturated_workers[index] {
4619                accepted_completed += 1;
4620            }
4621            match result {
4622                Ok(reply) => {
4623                    if !replies
4624                        .iter()
4625                        .any(|seen: &RecordSummary| seen.recorder_id == reply.recorder_id)
4626                    {
4627                        replies.push(reply);
4628                    }
4629                    if replies.len() >= quorum {
4630                        return Ok(replies);
4631                    }
4632                }
4633                Err(error @ Error::Rejected(_)) | Err(error @ Error::TypedRecordRequired) => {
4634                    typed_errors[index] = Some(error);
4635                }
4636                Err(Error::ProposeFailed) => worker_failed = true,
4637                Err(_) => {}
4638            }
4639            let accepted_remaining = accepted.saturating_sub(accepted_completed);
4640            if replies.len() + saturated + accepted_remaining < quorum {
4641                return match typed_errors.into_iter().flatten().next() {
4642                    Some(error) => Err(error),
4643                    None if worker_failed => Err(Error::ProposeFailed),
4644                    None => Ok(replies),
4645                };
4646            }
4647        }
4648        if replies.len() + saturated >= quorum {
4649            return Ok(replies);
4650        }
4651        match typed_errors.into_iter().flatten().next() {
4652            Some(error) => Err(error),
4653            None if worker_failed => Err(Error::ProposeFailed),
4654            None => Ok(replies),
4655        }
4656    }
4657
4658    pub fn inspect_decision_at(
4659        &self,
4660        slot: Slot,
4661        prev_hash: LogHash,
4662    ) -> Result<DecisionInspection> {
4663        Ok(match self.inspect_certified_decision_at(slot, prev_hash)? {
4664            CertifiedDecisionInspection::Committed(certified) => {
4665                DecisionInspection::Committed(certified.entry)
4666            }
4667            CertifiedDecisionInspection::Empty => DecisionInspection::Empty,
4668            CertifiedDecisionInspection::Pending => DecisionInspection::Pending,
4669            CertifiedDecisionInspection::Unavailable => DecisionInspection::Unavailable,
4670        })
4671    }
4672
4673    pub fn inspect_decision_proof_at(&self, slot: Slot) -> Result<Option<DecisionProof>> {
4674        let quorum = self.membership.quorum_size();
4675        let total = self.control_workers.len();
4676        let (sender, receiver) = std::sync::mpsc::sync_channel(total.max(1));
4677        let mut saturated = 0;
4678        for (index, worker) in self.control_workers.iter().enumerate() {
4679            if worker.dispatch(ControlJob::InspectProof {
4680                index,
4681                slot,
4682                result: sender.clone(),
4683            }) == ControlDispatch::Saturated
4684            {
4685                saturated += 1;
4686            }
4687        }
4688        drop(sender);
4689        let mut successful = BTreeSet::new();
4690        let mut proofs = Vec::new();
4691        let mut worker_failed = false;
4692        for (index, result) in receiver {
4693            match result {
4694                Ok(proof) => {
4695                    successful.insert(self.membership.members()[index].clone());
4696                    proofs.extend(proof);
4697                }
4698                Err(Error::ProposeFailed) => worker_failed = true,
4699                Err(_) => {}
4700            }
4701            if successful.len() >= quorum {
4702                break;
4703            }
4704        }
4705        if successful.len() < quorum {
4706            return Err(
4707                if worker_failed && !control_quorum_reachable(successful.len(), saturated, quorum) {
4708                    Error::ProposeFailed
4709                } else {
4710                    Error::NoQuorum
4711                },
4712            );
4713        }
4714        self.select_decision_proof(slot, proofs)
4715    }
4716
4717    fn select_decision_proof(
4718        &self,
4719        slot: Slot,
4720        mut proofs: Vec<DecisionProof>,
4721    ) -> Result<Option<DecisionProof>> {
4722        for proof in &proofs {
4723            if proof_cluster_id(proof) != self.cluster_id {
4724                return Err(Error::Rejected(RejectReason::WrongCluster));
4725            }
4726            proof
4727                .validate_for_cluster(
4728                    &self.cluster_id,
4729                    slot,
4730                    self.epoch,
4731                    self.config_id,
4732                    &self.membership,
4733                )
4734                .map_err(Error::Rejected)?;
4735        }
4736        let Some(first) = proofs.first() else {
4737            return Ok(None);
4738        };
4739        if proofs
4740            .iter()
4741            .skip(1)
4742            .any(|proof| proof.proposal().value != first.proposal().value)
4743        {
4744            return Err(Error::ConflictingCertificates);
4745        }
4746        proofs.sort_by_key(|proof| match proof {
4747            DecisionProof::FastPath { .. } => 4,
4748            DecisionProof::Phase2 { step, .. } => *step,
4749        });
4750        Ok(proofs.pop())
4751    }
4752
4753    fn certified_inspection_from_proof(
4754        &self,
4755        slot: Slot,
4756        prev_hash: LogHash,
4757        proof: DecisionProof,
4758    ) -> Result<CertifiedDecisionInspection> {
4759        let decision = certificate_from_proof(&proof)?;
4760        self.ensure_predecessor(slot, prev_hash, decision.value.prev_hash)?;
4761        let Some(command) = self.fetch_verified_value(slot, &decision.value)? else {
4762            return Ok(CertifiedDecisionInspection::Unavailable);
4763        };
4764        if command.entry_type == EntryType::ConfigChange {
4765            self.install_decision_proof_quorum(proof.clone())?;
4766        }
4767        let entry = self.log_entry_from_value(slot, command, &decision.value)?;
4768        Ok(CertifiedDecisionInspection::Committed(Box::new(
4769            CertifiedDecision {
4770                entry,
4771                certificate: decision,
4772                proof,
4773            },
4774        )))
4775    }
4776
4777    pub fn inspect_certified_decision_at(
4778        &self,
4779        slot: Slot,
4780        prev_hash: LogHash,
4781    ) -> Result<CertifiedDecisionInspection> {
4782        self.inspect_typed_record_summaries(slot, prev_hash)
4783    }
4784
4785    pub fn supports_context_read_fence(&self) -> bool {
4786        self.recorders
4787            .iter()
4788            .all(|recorder| recorder.supports_context_read_fence())
4789    }
4790
4791    /// Observes whether `slot` is still empty at a quorum of recorders without
4792    /// mutating durable state. Any occupied or ambiguous quorum is delegated to
4793    /// the existing certified inspection path and can never become Empty.
4794    pub fn inspect_context_read_fence_at(
4795        &self,
4796        slot: Slot,
4797        prev_hash: LogHash,
4798    ) -> Result<CertifiedDecisionInspection> {
4799        if !self.supports_context_read_fence() {
4800            return Err(Error::ReadFenceUnsupported);
4801        }
4802        let quorum = self.membership.quorum_size();
4803        let total = self.read_fence_workers.len();
4804        let request = ReadFenceRequest {
4805            cluster_id: self.cluster_id.clone(),
4806            epoch: self.epoch,
4807            config_id: self.config_id,
4808            config_digest: self.config_digest,
4809            slot,
4810        };
4811        let (sender, receiver) = std::sync::mpsc::sync_channel(total.max(1));
4812        let mut saturated = 0;
4813        for (index, worker) in self.read_fence_workers.iter().enumerate() {
4814            if worker.dispatch(ControlJob::ObserveReadFence {
4815                index,
4816                request: request.clone(),
4817                result: sender.clone(),
4818            }) == ControlDispatch::Saturated
4819            {
4820                saturated += 1;
4821            }
4822        }
4823        drop(sender);
4824        let mut successful = 0_usize;
4825        let mut empty = 0_usize;
4826        let mut worker_failed = false;
4827        let mut received = 0_usize;
4828        for (index, result) in receiver {
4829            received += 1;
4830            match result {
4831                Ok(observation)
4832                    if valid_read_fence_observation(
4833                        &observation,
4834                        &self.membership.members()[index],
4835                        &request,
4836                    ) =>
4837                {
4838                    successful += 1;
4839                    if observation.slot_state == ReadFenceSlotState::Empty {
4840                        empty += 1;
4841                        if empty >= quorum {
4842                            return Ok(CertifiedDecisionInspection::Empty);
4843                        }
4844                    }
4845                }
4846                Err(Error::ProposeFailed) => worker_failed = true,
4847                Ok(_) | Err(_) => {}
4848            }
4849            let remaining = total.saturating_sub(received);
4850            if successful.saturating_add(remaining) < quorum {
4851                return Ok(CertifiedDecisionInspection::Unavailable);
4852            }
4853        }
4854        if successful < quorum {
4855            if worker_failed && !control_quorum_reachable(successful, saturated, quorum) {
4856                return Err(Error::ProposeFailed);
4857            }
4858            return Ok(CertifiedDecisionInspection::Unavailable);
4859        }
4860        Ok(match self.inspect_certified_decision_at(slot, prev_hash)? {
4861            // An occupied fence quorum cannot be weakened by the legacy typed
4862            // summary path's context-free absence classification.
4863            CertifiedDecisionInspection::Empty => CertifiedDecisionInspection::Pending,
4864            inspection => inspection,
4865        })
4866    }
4867
4868    fn inspect_typed_record_summaries(
4869        &self,
4870        slot: Slot,
4871        prev_hash: LogHash,
4872    ) -> Result<CertifiedDecisionInspection> {
4873        let quorum = self.membership.quorum_size();
4874        let config_id = self.config_id;
4875        let config_digest = self.config_digest;
4876        let total = self.control_workers.len();
4877        let (sender, receiver) = std::sync::mpsc::sync_channel(total.max(1));
4878        let cancellation = ControlJobCancellation::new();
4879        let mut saturated = 0;
4880        for (index, worker) in self.control_workers.iter().enumerate() {
4881            if worker.dispatch_cancellable(
4882                ControlJob::InspectSummary {
4883                    index,
4884                    slot,
4885                    result: sender.clone(),
4886                },
4887                cancellation.token(),
4888            ) == ControlDispatch::Saturated
4889            {
4890                saturated += 1;
4891            }
4892        }
4893        drop(sender);
4894        let mut successful = 0;
4895        let mut summaries = Vec::new();
4896        let mut worker_failed = false;
4897        for (index, result) in receiver {
4898            match result {
4899                Ok(summary)
4900                    if summary.as_ref().is_none_or(|summary| {
4901                        summary.recorder_id == self.membership.members()[index]
4902                            && summary.slot == slot
4903                            && summary.config_id == config_id
4904                            && summary.config_digest == config_digest
4905                    }) =>
4906                {
4907                    successful += 1;
4908                    summaries.extend(summary);
4909                }
4910                Err(Error::ProposeFailed) => worker_failed = true,
4911                Ok(_) | Err(_) => {}
4912            }
4913            if successful >= quorum {
4914                if let Some(proof) = self.proof_from_record_summaries(slot, &summaries)? {
4915                    return self.certified_inspection_from_proof(slot, prev_hash, proof);
4916                }
4917            }
4918        }
4919        if successful < quorum {
4920            if worker_failed && !control_quorum_reachable(successful, saturated, quorum) {
4921                return Err(Error::ProposeFailed);
4922            }
4923            return Ok(CertifiedDecisionInspection::Unavailable);
4924        }
4925        if summaries.is_empty() {
4926            return Ok(CertifiedDecisionInspection::Empty);
4927        }
4928        if summaries.len() < quorum {
4929            return Ok(CertifiedDecisionInspection::Unavailable);
4930        }
4931        if let Some(proof) = self.proof_from_record_summaries(slot, &summaries)? {
4932            return self.certified_inspection_from_proof(slot, prev_hash, proof);
4933        }
4934        Ok(CertifiedDecisionInspection::Pending)
4935    }
4936
4937    fn proof_from_record_summaries(
4938        &self,
4939        slot: Slot,
4940        summaries: &[RecordSummary],
4941    ) -> Result<Option<DecisionProof>> {
4942        let quorum = self.membership.quorum_size();
4943        let mut summaries = summaries.to_vec();
4944        summaries.sort_by(|left, right| left.recorder_id.cmp(&right.recorder_id));
4945        summaries.dedup_by(|left, right| left.recorder_id == right.recorder_id);
4946        let installed_proofs = summaries
4947            .iter()
4948            .filter_map(|summary| summary.decided.clone())
4949            .collect();
4950        if let Some(proof) = self.select_decision_proof(slot, installed_proofs)? {
4951            return Ok(Some(proof));
4952        }
4953        for step in summaries
4954            .iter()
4955            .map(|summary| summary.step)
4956            .collect::<BTreeSet<_>>()
4957            .into_iter()
4958            .rev()
4959        {
4960            let mut step_summaries: Vec<_> = summaries
4961                .iter()
4962                .filter(|summary| summary.step == step)
4963                .cloned()
4964                .collect();
4965            if step_summaries.len() < quorum {
4966                continue;
4967            }
4968            step_summaries.truncate(quorum);
4969            let proof_summaries: Vec<_> = step_summaries
4970                .iter()
4971                .map(|summary| RecorderSummary {
4972                    recorder_id: summary.recorder_id.clone(),
4973                    slot: summary.slot,
4974                    step: summary.step,
4975                    first_current: summary.first_current.clone(),
4976                    aggregate_prior: summary.aggregate_prior.clone(),
4977                })
4978                .collect();
4979            let proof = if step == 4 {
4980                step_summaries
4981                    .first()
4982                    .and_then(|summary| summary.first_current.clone())
4983                    .filter(|proposal| proposal.priority == ProposalPriority::MAX)
4984                    .filter(|proposal| {
4985                        step_summaries.iter().all(|summary| {
4986                            summary
4987                                .first_current
4988                                .as_ref()
4989                                .is_some_and(|candidate| proposal_exact(candidate, proposal))
4990                        })
4991                    })
4992                    .map(|proposal| DecisionProof::FastPath {
4993                        cluster_id: self.cluster_id.clone(),
4994                        slot,
4995                        epoch: self.epoch,
4996                        config_id: self.config_id,
4997                        config_digest: self.config_digest,
4998                        proposal,
4999                        summaries: proof_summaries.clone(),
5000                    })
5001            } else if step % 4 == 2 {
5002                step_summaries
5003                    .iter()
5004                    .filter_map(|summary| summary.aggregate_prior.clone())
5005                    .max()
5006                    .map(|proposal| DecisionProof::Phase2 {
5007                        cluster_id: self.cluster_id.clone(),
5008                        slot,
5009                        epoch: self.epoch,
5010                        config_id: self.config_id,
5011                        config_digest: self.config_digest,
5012                        step,
5013                        proposal,
5014                        summaries: proof_summaries.clone(),
5015                    })
5016            } else {
5017                None
5018            };
5019            let Some(proof) = proof else {
5020                continue;
5021            };
5022            let Ok(Some(proof)) = self.select_decision_proof(slot, vec![proof]) else {
5023                continue;
5024            };
5025            return Ok(Some(proof));
5026        }
5027        Ok(None)
5028    }
5029
5030    pub fn recover_decision_at(
5031        &self,
5032        slot: Slot,
5033        prev_hash: LogHash,
5034    ) -> Result<DecisionInspection> {
5035        match self.inspect_decision_at(slot, prev_hash)? {
5036            DecisionInspection::Pending => self
5037                .propose_stored_at(
5038                    slot,
5039                    prev_hash,
5040                    StoredCommand::new(EntryType::Noop, Vec::new()),
5041                )
5042                .map(DecisionInspection::Committed),
5043            inspection => Ok(inspection),
5044        }
5045    }
5046
5047    pub fn recover_decided_at(&self, slot: Slot, prev_hash: LogHash) -> Result<Option<LogEntry>> {
5048        match self.inspect_decision_at(slot, prev_hash)? {
5049            DecisionInspection::Committed(entry) => Ok(Some(entry)),
5050            DecisionInspection::Empty | DecisionInspection::Pending => Ok(None),
5051            DecisionInspection::Unavailable => Err(Error::CommandUnavailable),
5052        }
5053    }
5054
5055    pub fn recover_decided_next(&self) -> Result<Option<LogEntry>> {
5056        let mut tip = self.legacy_tip.lock().map_err(|_| Error::ProposeFailed)?;
5057        let Some(entry) = self.recover_decided_at(tip.next_index, tip.last_hash)? else {
5058            return Ok(None);
5059        };
5060        tip.next_index = entry
5061            .index
5062            .checked_add(1)
5063            .ok_or(Error::InvalidRecoveredTip)?;
5064        tip.last_hash = entry.hash;
5065        Ok(Some(entry))
5066    }
5067
5068    fn ensure_predecessor(
5069        &self,
5070        slot: Slot,
5071        actual_prev_hash: LogHash,
5072        expected_prev_hash: LogHash,
5073    ) -> Result<()> {
5074        if actual_prev_hash != expected_prev_hash {
5075            return Err(Error::ChainConflict {
5076                slot,
5077                expected_prev_hash,
5078                actual_prev_hash,
5079            });
5080        }
5081        Ok(())
5082    }
5083
5084    fn store_command_on_quorum(
5085        &self,
5086        command_hash: LogHash,
5087        command: &StoredCommand,
5088    ) -> Result<()> {
5089        let quorum = quorum_size(self.control_workers.len());
5090        let total = self.control_workers.len();
5091        let (sender, receiver) = std::sync::mpsc::sync_channel(total.max(1));
5092        let mut saturated = 0;
5093        for (index, worker) in self.control_workers.iter().enumerate() {
5094            if worker.dispatch(ControlJob::StoreCommand {
5095                index,
5096                cluster_id: self.cluster_id.clone(),
5097                epoch: self.epoch,
5098                config_id: self.config_id,
5099                config_digest: self.config_digest,
5100                command_hash,
5101                command: command.clone(),
5102                result: sender.clone(),
5103            }) == ControlDispatch::Saturated
5104            {
5105                saturated += 1;
5106            }
5107        }
5108        drop(sender);
5109        let mut stored = 0;
5110        let mut worker_failed = false;
5111        for (_, result) in receiver {
5112            match result {
5113                Ok(()) => stored += 1,
5114                Err(Error::ProposeFailed) => worker_failed = true,
5115                Err(_) => {}
5116            }
5117            if stored >= quorum {
5118                break;
5119            }
5120        }
5121        if stored < quorum {
5122            return Err(
5123                if worker_failed && !control_quorum_reachable(stored, saturated, quorum) {
5124                    Error::ProposeFailed
5125                } else {
5126                    Error::NoQuorum
5127                },
5128            );
5129        }
5130        Ok(())
5131    }
5132
5133    fn fetch_verified_value(
5134        &self,
5135        slot: Slot,
5136        value: &AcceptedValue,
5137    ) -> Result<Option<StoredCommand>> {
5138        let quorum = quorum_size(self.control_workers.len());
5139        let total = self.control_workers.len();
5140        let (sender, receiver) = std::sync::mpsc::sync_channel(total.max(1));
5141        let cancellation = ControlJobCancellation::new();
5142        let mut saturated = 0;
5143        for (index, worker) in self.control_workers.iter().enumerate() {
5144            if worker.dispatch_cancellable(
5145                ControlJob::FetchCommand {
5146                    index,
5147                    cluster_id: self.cluster_id.clone(),
5148                    epoch: self.epoch,
5149                    config_id: self.config_id,
5150                    config_digest: self.config_digest,
5151                    command_hash: value.command_hash,
5152                    result: sender.clone(),
5153                },
5154                cancellation.token(),
5155            ) == ControlDispatch::Saturated
5156            {
5157                saturated += 1;
5158            }
5159        }
5160        drop(sender);
5161        let mut mismatch = false;
5162        let mut successful = 0;
5163        let mut worker_failed = false;
5164        for (_, result) in receiver {
5165            match result {
5166                Ok(command) => {
5167                    successful += 1;
5168                    if let Some(command) = command {
5169                        if command.hash() != value.command_hash {
5170                            continue;
5171                        }
5172                        let expected = AcceptedValue::from_command(
5173                            &self.cluster_id,
5174                            slot,
5175                            self.epoch,
5176                            self.config_id,
5177                            value.prev_hash,
5178                            &command,
5179                        );
5180                        if expected == *value {
5181                            return Ok(Some(command));
5182                        }
5183                        mismatch = true;
5184                    }
5185                }
5186                Err(Error::ProposeFailed) => worker_failed = true,
5187                Err(_) => {}
5188            }
5189        }
5190        if mismatch {
5191            Err(Error::Rejected(RejectReason::InvalidValue))
5192        } else if successful < quorum && saturated > 0 {
5193            Err(Error::NoQuorum)
5194        } else if worker_failed && !control_quorum_reachable(successful, saturated, quorum) {
5195            Err(Error::ProposeFailed)
5196        } else {
5197            Ok(None)
5198        }
5199    }
5200
5201    fn ensure_progress_command(&self, progress: &mut ProposerProgress) -> Result<()> {
5202        let value = progress
5203            .proposal
5204            .value
5205            .as_ref()
5206            .ok_or(Error::Rejected(RejectReason::InvalidRequest))?;
5207        if progress
5208            .command
5209            .as_ref()
5210            .is_some_and(|command| self.command_matches_value(progress.slot, value, command))
5211        {
5212            return Ok(());
5213        }
5214        progress.command_holders.clear();
5215        progress.command = self.fetch_verified_value(progress.slot, value)?;
5216        if let Some(command) = &progress.command {
5217            progress.transition_involved |= command.entry_type == EntryType::ConfigChange;
5218            Ok(())
5219        } else {
5220            Err(Error::CommandUnavailable)
5221        }
5222    }
5223
5224    fn command_matches_value(
5225        &self,
5226        slot: Slot,
5227        value: &AcceptedValue,
5228        command: &StoredCommand,
5229    ) -> bool {
5230        AcceptedValue::from_command(
5231            &self.cluster_id,
5232            slot,
5233            self.epoch,
5234            self.config_id,
5235            value.prev_hash,
5236            command,
5237        ) == *value
5238    }
5239
5240    fn log_entry_from_value(
5241        &self,
5242        slot: Slot,
5243        command: StoredCommand,
5244        value: &AcceptedValue,
5245    ) -> Result<LogEntry> {
5246        let entry = LogEntry {
5247            cluster_id: self.cluster_id.clone(),
5248            epoch: self.epoch,
5249            config_id: self.config_id,
5250            index: slot,
5251            entry_type: command.entry_type,
5252            payload: command.payload,
5253            prev_hash: value.prev_hash,
5254            hash: value.entry_hash,
5255        };
5256        if entry.recompute_hash() != entry.hash {
5257            return Err(Error::Rejected(RejectReason::InvalidValue));
5258        }
5259        Ok(entry)
5260    }
5261}
5262
5263#[derive(Clone, Debug, Eq, PartialEq)]
5264pub enum DecisionInspection {
5265    Committed(LogEntry),
5266    Empty,
5267    Pending,
5268    Unavailable,
5269}
5270
5271#[derive(Clone, Debug, Eq, PartialEq)]
5272pub struct CertifiedDecision {
5273    pub entry: LogEntry,
5274    pub certificate: DecisionCertificate,
5275    pub proof: DecisionProof,
5276}
5277
5278#[derive(Clone, Debug, Eq, PartialEq)]
5279pub enum CertifiedDecisionInspection {
5280    Empty,
5281    Pending,
5282    Committed(Box<CertifiedDecision>),
5283    Unavailable,
5284}
5285
5286impl RecorderRpc for RecorderFileStore {
5287    fn recorder_id(&self) -> Result<NodeId> {
5288        Ok(self.recorder_id.clone())
5289    }
5290
5291    fn record(&self, request: RecordRequest) -> Result<RecordSummary> {
5292        self.record_proposal(request)
5293    }
5294
5295    fn install_decision_proof(&self, proof: DecisionProof, membership: &Membership) -> Result<()> {
5296        self.install_decision_proof_record(proof, membership)
5297    }
5298
5299    fn inspect_decision_proof(&self, slot: Slot) -> Result<Option<DecisionProof>> {
5300        Ok(self.load(slot)?.decision_proof().cloned())
5301    }
5302
5303    fn inspect_record_summary(&self, slot: Slot) -> Result<Option<RecordSummary>> {
5304        let _guard = self
5305            .sync
5306            .lock()
5307            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
5308        self.recover_intent()?;
5309        let configuration = self.configuration_state()?;
5310        let exists_in_wal = self
5311            .wal
5312            .lock()
5313            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?
5314            .slots
5315            .contains_key(&slot);
5316        if !exists_in_wal && !self.path(slot).exists() {
5317            return Ok(None);
5318        }
5319        let state = self.load_unlocked(slot, configuration.config_digest)?;
5320        Ok(Some(record_summary(
5321            &self.recorder_id,
5322            &state,
5323            state.decision_proof().cloned(),
5324        )))
5325    }
5326
5327    fn supports_context_read_fence(&self) -> bool {
5328        true
5329    }
5330
5331    fn observe_read_fence(&self, request: ReadFenceRequest) -> Result<ReadFenceObservation> {
5332        let _guard = self
5333            .sync
5334            .lock()
5335            .map_err(|_| Error::Io("recorder lock poisoned".into()))?;
5336        self.recover_intent()?;
5337        if request.cluster_id != self.cluster_id {
5338            return Err(Error::Rejected(RejectReason::WrongCluster));
5339        }
5340        if request.epoch != self.epoch {
5341            return Err(Error::Rejected(if request.epoch < self.epoch {
5342                RejectReason::StaleEpoch
5343            } else {
5344                RejectReason::FutureEpoch
5345            }));
5346        }
5347        let configuration = self.configuration_state()?;
5348        if request.config_id != configuration.config_id
5349            || request.config_digest != configuration.config_digest
5350        {
5351            return Err(Error::Rejected(RejectReason::WrongConfig));
5352        }
5353        let max_head = configuration.max_accepted_or_decided_slot;
5354        let exists_in_wal = self
5355            .wal
5356            .lock()
5357            .map_err(|_| Error::Io("recorder WAL lock poisoned".into()))?
5358            .slots
5359            .contains_key(&request.slot);
5360        let exact_exists = exists_in_wal || self.path(request.slot).exists();
5361        let summary = if exact_exists {
5362            let state = self.load_unlocked(request.slot, configuration.config_digest)?;
5363            Some(Box::new(record_summary(
5364                &self.recorder_id,
5365                &state,
5366                state.decision_proof().cloned(),
5367            )))
5368        } else {
5369            None
5370        };
5371        let slot_state =
5372            if summary.is_none() && max_head.is_none_or(|max_head| max_head < request.slot) {
5373                ReadFenceSlotState::Empty
5374            } else {
5375                ReadFenceSlotState::Occupied { summary }
5376            };
5377        Ok(ReadFenceObservation {
5378            recorder_id: self.recorder_id.clone(),
5379            cluster_id: request.cluster_id,
5380            epoch: request.epoch,
5381            config_id: request.config_id,
5382            config_digest: request.config_digest,
5383            slot: request.slot,
5384            max_head,
5385            slot_state,
5386        })
5387    }
5388
5389    fn store_command(&self, command_hash: LogHash, command: StoredCommand) -> Result<()> {
5390        RecorderFileStore::store_command(self, command_hash, command)
5391    }
5392
5393    fn store_command_for(
5394        &self,
5395        cluster_id: ClusterId,
5396        epoch: Epoch,
5397        config_id: ConfigId,
5398        config_digest: LogHash,
5399        command_hash: LogHash,
5400        command: StoredCommand,
5401    ) -> Result<()> {
5402        self.apply(RecorderRequest::StoreCommand {
5403            cluster_id,
5404            epoch,
5405            config_id,
5406            config_digest,
5407            command_hash,
5408            command,
5409        })?;
5410        Ok(())
5411    }
5412
5413    fn fetch_command(&self, command_hash: LogHash) -> Result<Option<StoredCommand>> {
5414        RecorderFileStore::fetch_command(self, command_hash)
5415    }
5416
5417    fn fetch_command_for(
5418        &self,
5419        cluster_id: ClusterId,
5420        epoch: Epoch,
5421        config_id: ConfigId,
5422        config_digest: LogHash,
5423        command_hash: LogHash,
5424    ) -> Result<Option<StoredCommand>> {
5425        Ok(self
5426            .apply(RecorderRequest::FetchCommand {
5427                cluster_id,
5428                epoch,
5429                config_id,
5430                config_digest,
5431                command_hash,
5432            })?
5433            .command)
5434    }
5435}
5436
5437fn proposal_ballot(proposal: &Proposal) -> Option<Ballot> {
5438    proposal.value.as_ref()?;
5439    Some(Ballot::new(
5440        proposal.proposal_id,
5441        proposal.priority.legacy_u128(),
5442        proposal.proposer_id.clone(),
5443    ))
5444}
5445
5446fn record_summary(
5447    recorder_id: &str,
5448    state: &RecorderSlotState,
5449    decided: Option<DecisionProof>,
5450) -> RecordSummary {
5451    RecordSummary {
5452        recorder_id: recorder_id.to_string(),
5453        slot: state.slot,
5454        config_id: state.config_id,
5455        config_digest: state.config_digest,
5456        step: state.isr.step(),
5457        first_current: state.isr.first_current().cloned(),
5458        aggregate_prior: state.isr.aggregate_prior().cloned(),
5459        decided,
5460    }
5461}
5462
5463fn proof_context(proof: &DecisionProof) -> (Slot, Epoch, ConfigId, LogHash) {
5464    match proof {
5465        DecisionProof::FastPath {
5466            slot,
5467            epoch,
5468            config_id,
5469            config_digest,
5470            ..
5471        }
5472        | DecisionProof::Phase2 {
5473            slot,
5474            epoch,
5475            config_id,
5476            config_digest,
5477            ..
5478        } => (*slot, *epoch, *config_id, *config_digest),
5479    }
5480}
5481
5482fn proof_cluster_id(proof: &DecisionProof) -> &str {
5483    match proof {
5484        DecisionProof::FastPath { cluster_id, .. } | DecisionProof::Phase2 { cluster_id, .. } => {
5485            cluster_id
5486        }
5487    }
5488}
5489
5490fn certificate_from_proof(proof: &DecisionProof) -> Result<DecisionCertificate> {
5491    let (slot, epoch, config_id, config_digest) = proof_context(proof);
5492    let proposal = proof.proposal();
5493    let value = proposal
5494        .value
5495        .clone()
5496        .ok_or(Error::Rejected(RejectReason::InvalidCertificate))?;
5497    let recorder_ids = match proof {
5498        DecisionProof::FastPath { summaries, .. } | DecisionProof::Phase2 { summaries, .. } => {
5499            summaries
5500                .iter()
5501                .map(|summary| summary.recorder_id.clone())
5502                .collect()
5503        }
5504    };
5505    Ok(DecisionCertificate {
5506        slot,
5507        epoch,
5508        config_id,
5509        config_digest,
5510        ballot: Ballot::new(
5511            proposal.proposal_id,
5512            proposal.priority.legacy_u128(),
5513            encode_certificate_proposer(proof_cluster_id(proof), &proposal.proposer_id),
5514        ),
5515        value,
5516        recorder_ids,
5517    })
5518}
5519
5520const CERTIFICATE_PROPOSER_PREFIX: &str = "QDC1:";
5521
5522fn encode_certificate_proposer(cluster_id: &str, proposer_id: &str) -> String {
5523    format!(
5524        "{CERTIFICATE_PROPOSER_PREFIX}{}:{cluster_id}{proposer_id}",
5525        cluster_id.len()
5526    )
5527}
5528
5529fn decode_certificate_proposer(encoded: &str) -> Option<(&str, &str)> {
5530    let rest = encoded.strip_prefix(CERTIFICATE_PROPOSER_PREFIX)?;
5531    let (length, joined) = rest.split_once(':')?;
5532    let length: usize = length.parse().ok()?;
5533    Some((joined.get(..length)?, joined.get(length..)?))
5534}
5535
5536impl Consensus for ThreeNodeConsensus {
5537    fn propose(&self, command: Command) -> Result<LogEntry> {
5538        self.propose_next(command)
5539    }
5540}
5541
5542fn request_slot(request: &RecorderRequest) -> Option<Slot> {
5543    match request {
5544        RecorderRequest::Inspect { slot, .. }
5545        | RecorderRequest::Observe { slot, .. }
5546        | RecorderRequest::Converge { slot, .. }
5547        | RecorderRequest::Decide { slot, .. } => Some(*slot),
5548        RecorderRequest::Identity
5549        | RecorderRequest::StoreCommand { .. }
5550        | RecorderRequest::FetchCommand { .. } => None,
5551    }
5552}
5553
5554fn request_context(request: &RecorderRequest) -> Option<(&ClusterId, Epoch, ConfigId, LogHash)> {
5555    match request {
5556        RecorderRequest::StoreCommand {
5557            cluster_id,
5558            epoch,
5559            config_id,
5560            config_digest,
5561            ..
5562        }
5563        | RecorderRequest::FetchCommand {
5564            cluster_id,
5565            epoch,
5566            config_id,
5567            config_digest,
5568            ..
5569        }
5570        | RecorderRequest::Inspect {
5571            cluster_id,
5572            epoch,
5573            config_id,
5574            config_digest,
5575            ..
5576        }
5577        | RecorderRequest::Observe {
5578            cluster_id,
5579            epoch,
5580            config_id,
5581            config_digest,
5582            ..
5583        }
5584        | RecorderRequest::Converge {
5585            cluster_id,
5586            epoch,
5587            config_id,
5588            config_digest,
5589            ..
5590        }
5591        | RecorderRequest::Decide {
5592            cluster_id,
5593            epoch,
5594            config_id,
5595            config_digest,
5596            ..
5597        } => Some((cluster_id, *epoch, *config_id, *config_digest)),
5598        _ => None,
5599    }
5600}
5601
5602fn stored_command(command: Command) -> Result<StoredCommand> {
5603    let entry_type = match command.kind() {
5604        CommandKind::Deterministic => EntryType::Command,
5605        CommandKind::ReadBarrier => EntryType::Noop,
5606    };
5607    Ok(StoredCommand::new(entry_type, command.payload().to_vec()))
5608}
5609
5610fn encode_configuration_state(state: &ConfigurationState) -> Result<Vec<u8>> {
5611    let mut out = Vec::new();
5612    out.extend_from_slice(b"QCON");
5613    put_u16(&mut out, CONFIGURATION_STATE_VERSION);
5614    put_u64(&mut out, state.config_id);
5615    out.extend_from_slice(state.config_digest.as_bytes());
5616    out.push(u8::from(state.activated));
5617    match state.max_accepted_or_decided_slot {
5618        Some(slot) => {
5619            out.push(1);
5620            put_u64(&mut out, slot);
5621        }
5622        None => out.push(0),
5623    }
5624    match &state.membership {
5625        Some(membership) => {
5626            out.push(membership.members().len() as u8);
5627            for member in membership.members() {
5628                put_bytes(&mut out, member.as_bytes())?;
5629            }
5630        }
5631        None => out.push(0),
5632    }
5633    encode_optional_seal(&mut out, state.predecessor.as_ref());
5634    encode_optional_seal(&mut out, state.seal.as_ref());
5635    let digest = LogHash::digest(&[&out]);
5636    out.extend_from_slice(digest.as_bytes());
5637    Ok(out)
5638}
5639
5640fn decode_configuration_state(bytes: &[u8]) -> Result<ConfigurationState> {
5641    if bytes.len() < 4 + 2 + 32 || bytes.get(..4) != Some(b"QCON") {
5642        return Err(Error::Decode("invalid configuration state".into()));
5643    }
5644    let (body, digest) = bytes.split_at(bytes.len() - 32);
5645    if LogHash::digest(&[body]).as_bytes() != digest {
5646        return Err(Error::Decode("configuration digest mismatch".into()));
5647    }
5648    let mut cursor = 4;
5649    let version = read_u16(body, &mut cursor)?;
5650    if version != CONFIGURATION_STATE_VERSION {
5651        return Err(Error::MigrationRequired {
5652            format: "QCON",
5653            version,
5654        });
5655    }
5656    let config_id = read_u64(body, &mut cursor)?;
5657    let config_digest = read_hash(body, &mut cursor)?;
5658    let activated = match read_u8(body, &mut cursor)? {
5659        0 => false,
5660        1 => true,
5661        _ => return Err(Error::Decode("invalid activation flag".into())),
5662    };
5663    let max_accepted_or_decided_slot = match read_u8(body, &mut cursor)? {
5664        0 => None,
5665        1 => Some(read_u64(body, &mut cursor)?),
5666        _ => return Err(Error::Decode("invalid accepted-slot flag".into())),
5667    };
5668    let member_count = read_u8(body, &mut cursor)? as usize;
5669    let membership = if member_count == 0 {
5670        None
5671    } else {
5672        let members = (0..member_count)
5673            .map(|_| {
5674                String::from_utf8(read_bytes(body, &mut cursor)?)
5675                    .map_err(|err| Error::Decode(err.to_string()))
5676            })
5677            .collect::<Result<Vec<_>>>()?;
5678        let membership = Membership::from_voters(members)?;
5679        if membership.digest() != config_digest {
5680            return Err(Error::Decode("membership digest mismatch".into()));
5681        }
5682        Some(membership)
5683    };
5684    let predecessor = decode_optional_seal(body, &mut cursor)?;
5685    let seal = decode_optional_seal(body, &mut cursor)?;
5686    if cursor != body.len() {
5687        return Err(Error::Decode("trailing configuration bytes".into()));
5688    }
5689    Ok(ConfigurationState {
5690        config_id,
5691        config_digest,
5692        membership,
5693        predecessor,
5694        seal,
5695        max_accepted_or_decided_slot,
5696        activated,
5697    })
5698}
5699
5700fn encode_optional_seal(out: &mut Vec<u8>, seal: Option<&ConfigurationSeal>) {
5701    match seal {
5702        Some(seal) => {
5703            out.push(1);
5704            put_u64(out, seal.stop_slot);
5705            out.extend_from_slice(seal.command_hash.as_bytes());
5706            out.extend_from_slice(seal.prefix_hash.as_bytes());
5707        }
5708        None => out.push(0),
5709    }
5710}
5711
5712fn decode_optional_seal(bytes: &[u8], cursor: &mut usize) -> Result<Option<ConfigurationSeal>> {
5713    match read_u8(bytes, cursor)? {
5714        0 => Ok(None),
5715        1 => Ok(Some(ConfigurationSeal {
5716            stop_slot: read_u64(bytes, cursor)?,
5717            command_hash: read_hash(bytes, cursor)?,
5718            prefix_hash: read_hash(bytes, cursor)?,
5719        })),
5720        _ => Err(Error::Decode("invalid configuration seal flag".into())),
5721    }
5722}
5723
5724fn encode_transition_intent(
5725    slot: Slot,
5726    slot_bytes: &[u8],
5727    configuration_bytes: &[u8],
5728) -> Result<Vec<u8>> {
5729    let mut out = Vec::new();
5730    out.extend_from_slice(b"QINT");
5731    put_u16(&mut out, 1);
5732    put_u64(&mut out, slot);
5733    put_bytes(&mut out, slot_bytes)?;
5734    put_bytes(&mut out, configuration_bytes)?;
5735    let digest = LogHash::digest(&[&out]);
5736    out.extend_from_slice(digest.as_bytes());
5737    Ok(out)
5738}
5739
5740fn decode_transition_intent(bytes: &[u8]) -> Result<(Slot, Vec<u8>, Vec<u8>)> {
5741    if bytes.len() < 4 + 2 + 32 || bytes.get(..4) != Some(b"QINT") {
5742        return Err(Error::Decode("invalid transition intent".into()));
5743    }
5744    let (body, digest) = bytes.split_at(bytes.len() - 32);
5745    if LogHash::digest(&[body]).as_bytes() != digest {
5746        return Err(Error::Decode("transition intent digest mismatch".into()));
5747    }
5748    let mut cursor = 4;
5749    if read_u16(body, &mut cursor)? != 1 {
5750        return Err(Error::Decode(
5751            "unsupported transition intent version".into(),
5752        ));
5753    }
5754    let slot = read_u64(body, &mut cursor)?;
5755    let slot_bytes = read_bytes(body, &mut cursor)?;
5756    let configuration_bytes = read_bytes(body, &mut cursor)?;
5757    if cursor != body.len() {
5758        return Err(Error::Decode("trailing transition intent bytes".into()));
5759    }
5760    Ok((slot, slot_bytes, configuration_bytes))
5761}
5762
5763fn encode_recorder_state(state: &RecorderSlotState) -> Result<Vec<u8>> {
5764    let mut out = Vec::new();
5765    out.extend_from_slice(b"QREC");
5766    put_u16(&mut out, RECORDER_STATE_VERSION);
5767    put_u64(&mut out, state.slot);
5768    put_u64(&mut out, state.epoch);
5769    put_u64(&mut out, state.config_id);
5770    out.extend_from_slice(state.config_digest.as_bytes());
5771    put_bytes(&mut out, state.cluster_id.as_bytes())?;
5772    encode_optional_ballot(&mut out, state.highest_promised.as_ref())?;
5773    match &state.accepted {
5774        Some(accepted) => {
5775            out.push(1);
5776            encode_ballot(&mut out, &accepted.ballot)?;
5777            encode_value(&mut out, &accepted.value);
5778        }
5779        None => out.push(0),
5780    }
5781    match &state.decided {
5782        Some(decided) => {
5783            out.push(1);
5784            encode_certificate(&mut out, decided)?;
5785        }
5786        None => out.push(0),
5787    }
5788    put_u64(&mut out, state.isr.step);
5789    encode_optional_proposal(&mut out, state.isr.first_current.as_ref())?;
5790    encode_optional_proposal(&mut out, state.isr.aggregate_current.as_ref())?;
5791    encode_optional_proposal(&mut out, state.isr.aggregate_prior.as_ref())?;
5792    encode_optional_proof(&mut out, state.decided_proof.as_ref())?;
5793    let digest = LogHash::digest(&[&out]);
5794    out.extend_from_slice(digest.as_bytes());
5795    Ok(out)
5796}
5797
5798fn decode_recorder_state(bytes: &[u8]) -> Result<RecorderSlotState> {
5799    if bytes.len() < 4 + 2 + 32 || &bytes[..4] != b"QREC" {
5800        return Err(Error::Decode("invalid recorder magic".into()));
5801    }
5802    let (body, digest) = bytes.split_at(bytes.len() - 32);
5803    if LogHash::digest(&[body]).as_bytes() != digest {
5804        return Err(Error::Decode("recorder digest mismatch".into()));
5805    }
5806    let mut cursor = 4;
5807    let version = read_u16(body, &mut cursor)?;
5808    if version != RECORDER_STATE_VERSION {
5809        return Err(Error::MigrationRequired {
5810            format: "QREC",
5811            version,
5812        });
5813    }
5814    let slot = read_u64(body, &mut cursor)?;
5815    let epoch = read_u64(body, &mut cursor)?;
5816    let config_id = read_u64(body, &mut cursor)?;
5817    let config_digest = read_hash(body, &mut cursor)?;
5818    let cluster_id = String::from_utf8(read_bytes(body, &mut cursor)?)
5819        .map_err(|err| Error::Decode(err.to_string()))?;
5820    let highest_promised = decode_optional_ballot(body, &mut cursor)?;
5821    let accepted = match read_u8(body, &mut cursor)? {
5822        0 => None,
5823        1 => Some(AcceptedSummary {
5824            ballot: decode_ballot(body, &mut cursor)?,
5825            value: decode_value(body, &mut cursor)?,
5826        }),
5827        _ => return Err(Error::Decode("invalid accepted flag".into())),
5828    };
5829    let decided = match read_u8(body, &mut cursor)? {
5830        0 => None,
5831        1 => Some(decode_certificate(body, &mut cursor)?),
5832        _ => return Err(Error::Decode("invalid decided flag".into())),
5833    };
5834    let isr = IsrState {
5835        step: read_u64(body, &mut cursor)?,
5836        first_current: decode_optional_proposal(body, &mut cursor)?,
5837        aggregate_current: decode_optional_proposal(body, &mut cursor)?,
5838        aggregate_prior: decode_optional_proposal(body, &mut cursor)?,
5839    };
5840    let decided_proof = decode_optional_proof(body, &mut cursor)?;
5841    if cursor != body.len() {
5842        return Err(Error::Decode("trailing recorder bytes".into()));
5843    }
5844    if let Some(accepted) = &accepted {
5845        if highest_promised.as_ref() < Some(&accepted.ballot) {
5846            return Err(Error::Decode("accepted ballot exceeds promise".into()));
5847        }
5848    }
5849    if let Some(decided) = &decided {
5850        decided
5851            .validate_context(slot, epoch, config_id, config_digest)
5852            .map_err(|_| Error::Decode("invalid decision certificate".into()))?;
5853    }
5854    if let Some(proof) = &decided_proof {
5855        let proof_value = proof.proposal().value.as_ref();
5856        if proof_context(proof) != (slot, epoch, config_id, config_digest)
5857            || proof_cluster_id(proof) != cluster_id
5858            || !matches!((decided.as_ref(), proof_value), (Some(certificate), Some(value)) if &certificate.value == value)
5859        {
5860            return Err(Error::Decode("invalid persisted decision proof".into()));
5861        }
5862    }
5863    Ok(RecorderSlotState {
5864        slot,
5865        cluster_id,
5866        epoch,
5867        config_id,
5868        config_digest,
5869        highest_promised,
5870        accepted,
5871        decided,
5872        isr,
5873        decided_proof,
5874    })
5875}
5876
5877fn encode_optional_proposal(out: &mut Vec<u8>, proposal: Option<&Proposal>) -> Result<()> {
5878    match proposal {
5879        None => out.push(0),
5880        Some(proposal) => {
5881            out.push(1);
5882            out.extend_from_slice(&proposal.priority.0);
5883            put_bytes(out, proposal.proposer_id.as_bytes())?;
5884            put_u64(out, proposal.proposal_id);
5885            match &proposal.value {
5886                None => out.push(0),
5887                Some(value) => {
5888                    out.push(1);
5889                    encode_value(out, value);
5890                }
5891            }
5892        }
5893    }
5894    Ok(())
5895}
5896
5897fn decode_optional_proposal(bytes: &[u8], cursor: &mut usize) -> Result<Option<Proposal>> {
5898    match read_u8(bytes, cursor)? {
5899        0 => Ok(None),
5900        1 => {
5901            let end = cursor
5902                .checked_add(32)
5903                .ok_or_else(|| Error::Decode("priority overflow".into()))?;
5904            let priority = ProposalPriority(
5905                bytes
5906                    .get(*cursor..end)
5907                    .ok_or_else(|| Error::Decode("truncated priority".into()))?
5908                    .try_into()
5909                    .expect("checked priority length"),
5910            );
5911            *cursor = end;
5912            let proposer_id = String::from_utf8(read_bytes(bytes, cursor)?)
5913                .map_err(|error| Error::Decode(error.to_string()))?;
5914            let proposal_id = read_u64(bytes, cursor)?;
5915            let value = match read_u8(bytes, cursor)? {
5916                0 => None,
5917                1 => Some(decode_value(bytes, cursor)?),
5918                _ => return Err(Error::Decode("invalid proposal value flag".into())),
5919            };
5920            Ok(Some(Proposal {
5921                priority,
5922                proposer_id,
5923                proposal_id,
5924                value,
5925            }))
5926        }
5927        _ => Err(Error::Decode("invalid proposal flag".into())),
5928    }
5929}
5930
5931fn encode_summary(out: &mut Vec<u8>, summary: &RecorderSummary) -> Result<()> {
5932    put_bytes(out, summary.recorder_id.as_bytes())?;
5933    put_u64(out, summary.slot);
5934    put_u64(out, summary.step);
5935    encode_optional_proposal(out, summary.first_current.as_ref())?;
5936    encode_optional_proposal(out, summary.aggregate_prior.as_ref())
5937}
5938
5939fn decode_summary(bytes: &[u8], cursor: &mut usize) -> Result<RecorderSummary> {
5940    Ok(RecorderSummary {
5941        recorder_id: String::from_utf8(read_bytes(bytes, cursor)?)
5942            .map_err(|error| Error::Decode(error.to_string()))?,
5943        slot: read_u64(bytes, cursor)?,
5944        step: read_u64(bytes, cursor)?,
5945        first_current: decode_optional_proposal(bytes, cursor)?,
5946        aggregate_prior: decode_optional_proposal(bytes, cursor)?,
5947    })
5948}
5949
5950fn encode_optional_proof(out: &mut Vec<u8>, proof: Option<&DecisionProof>) -> Result<()> {
5951    let Some(proof) = proof else {
5952        out.push(0);
5953        return Ok(());
5954    };
5955    let (tag, cluster_id, slot, epoch, config_id, digest, step, proposal, summaries) = match proof {
5956        DecisionProof::FastPath {
5957            cluster_id,
5958            slot,
5959            epoch,
5960            config_id,
5961            config_digest,
5962            proposal,
5963            summaries,
5964        } => (
5965            1,
5966            cluster_id,
5967            *slot,
5968            *epoch,
5969            *config_id,
5970            *config_digest,
5971            4,
5972            proposal,
5973            summaries,
5974        ),
5975        DecisionProof::Phase2 {
5976            cluster_id,
5977            slot,
5978            epoch,
5979            config_id,
5980            config_digest,
5981            step,
5982            proposal,
5983            summaries,
5984        } => (
5985            2,
5986            cluster_id,
5987            *slot,
5988            *epoch,
5989            *config_id,
5990            *config_digest,
5991            *step,
5992            proposal,
5993            summaries,
5994        ),
5995    };
5996    out.push(tag);
5997    put_bytes(out, cluster_id.as_bytes())?;
5998    put_u64(out, slot);
5999    put_u64(out, epoch);
6000    put_u64(out, config_id);
6001    out.extend_from_slice(digest.as_bytes());
6002    put_u64(out, step);
6003    encode_optional_proposal(out, Some(proposal))?;
6004    put_u16(
6005        out,
6006        u16::try_from(summaries.len())
6007            .map_err(|_| Error::Decode("too many proof summaries".into()))?,
6008    );
6009    for summary in summaries {
6010        encode_summary(out, summary)?;
6011    }
6012    Ok(())
6013}
6014
6015fn decode_optional_proof(bytes: &[u8], cursor: &mut usize) -> Result<Option<DecisionProof>> {
6016    let tag = read_u8(bytes, cursor)?;
6017    if tag == 0 {
6018        return Ok(None);
6019    }
6020    let cluster_id = String::from_utf8(read_bytes(bytes, cursor)?)
6021        .map_err(|error| Error::Decode(error.to_string()))?;
6022    let slot = read_u64(bytes, cursor)?;
6023    let epoch = read_u64(bytes, cursor)?;
6024    let config_id = read_u64(bytes, cursor)?;
6025    let config_digest = read_hash(bytes, cursor)?;
6026    let step = read_u64(bytes, cursor)?;
6027    let proposal = decode_optional_proposal(bytes, cursor)?
6028        .ok_or_else(|| Error::Decode("nil decision proposal".into()))?;
6029    let summaries = (0..read_u16(bytes, cursor)? as usize)
6030        .map(|_| decode_summary(bytes, cursor))
6031        .collect::<Result<Vec<_>>>()?;
6032    match tag {
6033        1 if step == 4 => Ok(Some(DecisionProof::FastPath {
6034            cluster_id,
6035            slot,
6036            epoch,
6037            config_id,
6038            config_digest,
6039            proposal,
6040            summaries,
6041        })),
6042        2 => Ok(Some(DecisionProof::Phase2 {
6043            cluster_id,
6044            slot,
6045            epoch,
6046            config_id,
6047            config_digest,
6048            step,
6049            proposal,
6050            summaries,
6051        })),
6052        _ => Err(Error::Decode("invalid decision proof tag".into())),
6053    }
6054}
6055
6056fn encode_optional_ballot(out: &mut Vec<u8>, ballot: Option<&Ballot>) -> Result<()> {
6057    match ballot {
6058        Some(ballot) => {
6059            out.push(1);
6060            encode_ballot(out, ballot)
6061        }
6062        None => {
6063            out.push(0);
6064            Ok(())
6065        }
6066    }
6067}
6068
6069fn decode_optional_ballot(bytes: &[u8], cursor: &mut usize) -> Result<Option<Ballot>> {
6070    match read_u8(bytes, cursor)? {
6071        0 => Ok(None),
6072        1 => Ok(Some(decode_ballot(bytes, cursor)?)),
6073        _ => Err(Error::Decode("invalid ballot flag".into())),
6074    }
6075}
6076
6077fn encode_ballot(out: &mut Vec<u8>, ballot: &Ballot) -> Result<()> {
6078    put_u64(out, ballot.round);
6079    put_u128(out, ballot.priority);
6080    put_bytes(out, ballot.proposer_id.as_bytes())
6081}
6082
6083fn decode_ballot(bytes: &[u8], cursor: &mut usize) -> Result<Ballot> {
6084    Ok(Ballot {
6085        round: read_u64(bytes, cursor)?,
6086        priority: read_u128(bytes, cursor)?,
6087        proposer_id: String::from_utf8(read_bytes(bytes, cursor)?)
6088            .map_err(|err| Error::Decode(err.to_string()))?,
6089    })
6090}
6091
6092fn encode_value(out: &mut Vec<u8>, value: &AcceptedValue) {
6093    out.extend_from_slice(value.command_hash.as_bytes());
6094    out.extend_from_slice(value.prev_hash.as_bytes());
6095    out.extend_from_slice(value.entry_hash.as_bytes());
6096}
6097
6098fn decode_value(bytes: &[u8], cursor: &mut usize) -> Result<AcceptedValue> {
6099    Ok(AcceptedValue {
6100        command_hash: read_hash(bytes, cursor)?,
6101        prev_hash: read_hash(bytes, cursor)?,
6102        entry_hash: read_hash(bytes, cursor)?,
6103    })
6104}
6105
6106fn encode_certificate(out: &mut Vec<u8>, decision: &DecisionCertificate) -> Result<()> {
6107    put_u64(out, decision.slot);
6108    put_u64(out, decision.epoch);
6109    put_u64(out, decision.config_id);
6110    out.extend_from_slice(decision.config_digest.as_bytes());
6111    encode_ballot(out, &decision.ballot)?;
6112    encode_value(out, &decision.value);
6113    put_u16(
6114        out,
6115        u16::try_from(decision.recorder_ids.len())
6116            .map_err(|_| Error::Decode("too many certificate recorders".into()))?,
6117    );
6118    for recorder_id in &decision.recorder_ids {
6119        put_bytes(out, recorder_id.as_bytes())?;
6120    }
6121    Ok(())
6122}
6123
6124fn decode_certificate(bytes: &[u8], cursor: &mut usize) -> Result<DecisionCertificate> {
6125    let slot = read_u64(bytes, cursor)?;
6126    let epoch = read_u64(bytes, cursor)?;
6127    let config_id = read_u64(bytes, cursor)?;
6128    let config_digest = read_hash(bytes, cursor)?;
6129    let ballot = decode_ballot(bytes, cursor)?;
6130    let value = decode_value(bytes, cursor)?;
6131    let recorder_count = read_u16(bytes, cursor)? as usize;
6132    let recorder_ids = (0..recorder_count)
6133        .map(|_| {
6134            String::from_utf8(read_bytes(bytes, cursor)?)
6135                .map_err(|err| Error::Decode(err.to_string()))
6136        })
6137        .collect::<Result<Vec<_>>>()?;
6138    Ok(DecisionCertificate {
6139        slot,
6140        epoch,
6141        config_id,
6142        config_digest,
6143        ballot,
6144        value,
6145        recorder_ids,
6146    })
6147}
6148
6149fn encode_stored_command(command: &StoredCommand) -> Vec<u8> {
6150    let mut out = Vec::new();
6151    out.extend_from_slice(b"QCMD");
6152    put_u16(&mut out, 1);
6153    out.push(command.entry_type.as_u8());
6154    put_u64(&mut out, command.payload.len() as u64);
6155    out.extend_from_slice(&command.payload);
6156    let digest = LogHash::digest(&[&out]);
6157    out.extend_from_slice(digest.as_bytes());
6158    out
6159}
6160
6161fn decode_stored_command(bytes: &[u8]) -> Result<StoredCommand> {
6162    if bytes.len() < 4 + 2 + 1 + 8 + 32 || &bytes[..4] != b"QCMD" {
6163        return Err(Error::Decode("invalid command magic".into()));
6164    }
6165    let (body, digest) = bytes.split_at(bytes.len() - 32);
6166    if LogHash::digest(&[body]).as_bytes() != digest {
6167        return Err(Error::Decode("command digest mismatch".into()));
6168    }
6169    let mut cursor = 4;
6170    if read_u16(body, &mut cursor)? != 1 {
6171        return Err(Error::Decode("unsupported command version".into()));
6172    }
6173    let entry_type = EntryType::from_u8(read_u8(body, &mut cursor)?)
6174        .ok_or_else(|| Error::Decode("invalid command entry type".into()))?;
6175    let payload_len = usize::try_from(read_u64(body, &mut cursor)?)
6176        .map_err(|_| Error::Decode("command payload too large".into()))?;
6177    let end = cursor
6178        .checked_add(payload_len)
6179        .ok_or_else(|| Error::Decode("command payload length overflow".into()))?;
6180    let payload = body
6181        .get(cursor..end)
6182        .ok_or_else(|| Error::Decode("short command payload".into()))?
6183        .to_vec();
6184    cursor = end;
6185    if cursor != body.len() {
6186        return Err(Error::Decode("trailing command bytes".into()));
6187    }
6188    Ok(StoredCommand::new(entry_type, payload))
6189}
6190
6191static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
6192
6193#[cfg(test)]
6194std::thread_local! {
6195    static SYNC_COUNTS: std::cell::Cell<(usize, usize)> = const {
6196        std::cell::Cell::new((0, 0))
6197    };
6198    static LAST_FILE_SYNC_KIND: std::cell::Cell<Option<FileSyncKind>> = const {
6199        std::cell::Cell::new(None)
6200    };
6201    static COMMAND_FILE_READS: std::cell::Cell<usize> = const {
6202        std::cell::Cell::new(0)
6203    };
6204}
6205
6206#[cfg(test)]
6207#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6208enum FileSyncKind {
6209    #[cfg(target_os = "linux")]
6210    Data,
6211    All,
6212}
6213
6214#[cfg(target_os = "linux")]
6215fn sync_wal_append(file: &fs::File) -> io::Result<()> {
6216    // Linux fdatasync (File::sync_data) also flushes metadata required for later data retrieval,
6217    // including the file size extended by this append, so a complete WAL frame remains replayable.
6218    file.sync_data()?;
6219    #[cfg(test)]
6220    record_file_sync_kind(FileSyncKind::Data);
6221    Ok(())
6222}
6223
6224#[cfg(not(target_os = "linux"))]
6225fn sync_wal_append(file: &fs::File) -> io::Result<()> {
6226    // Keep non-Linux durability conservative because sync_data semantics vary by platform.
6227    file.sync_all()?;
6228    #[cfg(test)]
6229    record_file_sync_kind(FileSyncKind::All);
6230    Ok(())
6231}
6232
6233fn sync_wal_metadata(file: &fs::File) -> io::Result<()> {
6234    file.sync_all()?;
6235    #[cfg(test)]
6236    record_file_sync_kind(FileSyncKind::All);
6237    Ok(())
6238}
6239
6240#[cfg(test)]
6241fn record_file_sync() {
6242    SYNC_COUNTS.with(|counts| {
6243        let (file, directory) = counts.get();
6244        counts.set((file + 1, directory));
6245    });
6246}
6247
6248#[cfg(test)]
6249fn record_file_sync_kind(kind: FileSyncKind) {
6250    record_file_sync();
6251    LAST_FILE_SYNC_KIND.with(|last| last.set(Some(kind)));
6252}
6253
6254#[cfg(test)]
6255fn record_directory_sync() {
6256    SYNC_COUNTS.with(|counts| {
6257        let (file, directory) = counts.get();
6258        counts.set((file, directory + 1));
6259    });
6260}
6261
6262#[cfg(test)]
6263fn reset_sync_counts() {
6264    SYNC_COUNTS.with(|counts| counts.set((0, 0)));
6265    LAST_FILE_SYNC_KIND.with(|last| last.set(None));
6266}
6267
6268#[cfg(test)]
6269fn sync_counts() -> (usize, usize) {
6270    SYNC_COUNTS.with(std::cell::Cell::get)
6271}
6272
6273#[cfg(test)]
6274fn last_file_sync_kind() -> Option<FileSyncKind> {
6275    LAST_FILE_SYNC_KIND.with(std::cell::Cell::get)
6276}
6277
6278#[cfg(test)]
6279fn reset_command_file_reads() {
6280    COMMAND_FILE_READS.with(|reads| reads.set(0));
6281}
6282
6283#[cfg(test)]
6284fn command_file_reads() -> usize {
6285    COMMAND_FILE_READS.with(std::cell::Cell::get)
6286}
6287
6288const CONFIGURATION_HEAD_INTENT_MAGIC: &[u8; 4] = b"QCHI";
6289
6290fn encode_configuration_head_intent(configuration: &[u8], head: &[u8]) -> Vec<u8> {
6291    let mut encoded = Vec::new();
6292    encoded.extend_from_slice(CONFIGURATION_HEAD_INTENT_MAGIC);
6293    put_u16(&mut encoded, 1);
6294    put_u64(&mut encoded, configuration.len() as u64);
6295    encoded.extend_from_slice(configuration);
6296    put_u64(&mut encoded, head.len() as u64);
6297    encoded.extend_from_slice(head);
6298    encoded
6299}
6300
6301fn decode_configuration_head_intent(bytes: &[u8]) -> Result<(&[u8], &[u8])> {
6302    let mut cursor = 0;
6303    if bytes.get(..CONFIGURATION_HEAD_INTENT_MAGIC.len()) != Some(CONFIGURATION_HEAD_INTENT_MAGIC) {
6304        return Err(Error::Decode(
6305            "invalid configuration-head intent magic".into(),
6306        ));
6307    }
6308    cursor += CONFIGURATION_HEAD_INTENT_MAGIC.len();
6309    if read_u16(bytes, &mut cursor)? != 1 {
6310        return Err(Error::Decode(
6311            "unsupported configuration-head intent version".into(),
6312        ));
6313    }
6314    let configuration_len = usize::try_from(read_u64(bytes, &mut cursor)?)
6315        .map_err(|_| Error::Decode("configuration-head intent length overflow".into()))?;
6316    let configuration_end = cursor
6317        .checked_add(configuration_len)
6318        .ok_or_else(|| Error::Decode("configuration-head intent length overflow".into()))?;
6319    let configuration = bytes
6320        .get(cursor..configuration_end)
6321        .ok_or_else(|| Error::Decode("truncated configuration-head intent".into()))?;
6322    cursor = configuration_end;
6323    let head_len = usize::try_from(read_u64(bytes, &mut cursor)?)
6324        .map_err(|_| Error::Decode("configuration-head intent length overflow".into()))?;
6325    let head_end = cursor
6326        .checked_add(head_len)
6327        .ok_or_else(|| Error::Decode("configuration-head intent length overflow".into()))?;
6328    let head = bytes
6329        .get(cursor..head_end)
6330        .ok_or_else(|| Error::Decode("truncated configuration-head intent".into()))?;
6331    if head_end != bytes.len() {
6332        return Err(Error::Decode(
6333            "trailing configuration-head intent bytes".into(),
6334        ));
6335    }
6336    Ok((configuration, head))
6337}
6338
6339fn encode_wal_frame(
6340    generation: u64,
6341    sequence: u64,
6342    prev_digest: LogHash,
6343    slot_state: &RecorderSlotState,
6344    configuration: &ConfigurationState,
6345    head: &RecordedHeadProvenance,
6346    command: Option<(LogHash, &StoredCommand)>,
6347) -> Result<(Vec<u8>, LogHash, Vec<u8>)> {
6348    let slot_bytes = encode_recorder_state(slot_state)?;
6349    let configuration_bytes = encode_configuration_state(configuration)?;
6350    let mut payload = Vec::new();
6351    put_u64(&mut payload, generation);
6352    put_u64(&mut payload, sequence);
6353    payload.extend_from_slice(prev_digest.as_bytes());
6354    put_u64(&mut payload, slot_state.slot());
6355    put_blob(&mut payload, &slot_bytes)?;
6356    put_blob(&mut payload, &configuration_bytes)?;
6357    encode_head_provenance(&mut payload, head);
6358    match command {
6359        Some((hash, command)) => {
6360            payload.push(1);
6361            payload.extend_from_slice(hash.as_bytes());
6362            put_blob(&mut payload, &encode_stored_command(command))?;
6363        }
6364        None => payload.push(0),
6365    }
6366    let total_len = 4usize
6367        .checked_add(2)
6368        .and_then(|len| len.checked_add(8))
6369        .and_then(|len| len.checked_add(payload.len()))
6370        .and_then(|len| len.checked_add(32))
6371        .ok_or_else(|| Error::Io("recorder WAL frame length overflow".into()))?;
6372    let mut frame = Vec::with_capacity(total_len);
6373    frame.extend_from_slice(RECORDER_WAL_MAGIC);
6374    put_u16(&mut frame, RECORDER_WAL_VERSION);
6375    put_u64(&mut frame, total_len as u64);
6376    frame.extend_from_slice(&payload);
6377    let digest = LogHash::digest(&[&frame]);
6378    frame.extend_from_slice(digest.as_bytes());
6379    Ok((frame, digest, slot_bytes))
6380}
6381
6382fn decode_wal_frame(bytes: &[u8], offset: usize) -> Result<Option<(WalFrame, usize)>> {
6383    const PREFIX_LEN: usize = 4 + 2 + 8;
6384    let remaining = bytes
6385        .get(offset..)
6386        .ok_or_else(|| Error::Decode("recorder WAL offset overflow".into()))?;
6387    if remaining.len() < PREFIX_LEN {
6388        return Ok(None);
6389    }
6390    if remaining.get(..4) != Some(RECORDER_WAL_MAGIC) {
6391        return Err(Error::Decode("recorder WAL frame magic mismatch".into()));
6392    }
6393    let mut cursor = offset + 4;
6394    if read_u16(bytes, &mut cursor)? != RECORDER_WAL_VERSION {
6395        return Err(Error::Decode("recorder WAL frame version mismatch".into()));
6396    }
6397    let frame_len = usize::try_from(read_u64(bytes, &mut cursor)?)
6398        .map_err(|_| Error::Decode("recorder WAL frame length overflow".into()))?;
6399    if frame_len < PREFIX_LEN + 32 {
6400        return Err(Error::Decode("recorder WAL frame length is invalid".into()));
6401    }
6402    let end = offset
6403        .checked_add(frame_len)
6404        .ok_or_else(|| Error::Decode("recorder WAL frame length overflow".into()))?;
6405    if end > bytes.len() {
6406        return Ok(None);
6407    }
6408    let digest_offset = end - 32;
6409    let digest = read_hash(bytes, &mut { digest_offset })?;
6410    let expected = LogHash::digest(&[&bytes[offset..digest_offset]]);
6411    if digest != expected {
6412        return Err(Error::Decode("recorder WAL frame checksum mismatch".into()));
6413    }
6414    let generation = read_u64(bytes, &mut cursor)?;
6415    let sequence = read_u64(bytes, &mut cursor)?;
6416    let prev_digest = read_hash(bytes, &mut cursor)?;
6417    let slot = read_u64(bytes, &mut cursor)?;
6418    let slot_bytes = read_blob(bytes, &mut cursor)?;
6419    let configuration_bytes = read_blob(bytes, &mut cursor)?;
6420    let head = decode_head_provenance(bytes, &mut cursor)?;
6421    let command = match read_u8(bytes, &mut cursor)? {
6422        0 => None,
6423        1 => {
6424            let hash = read_hash(bytes, &mut cursor)?;
6425            let command = decode_stored_command(&read_blob(bytes, &mut cursor)?)?;
6426            Some((hash, command))
6427        }
6428        _ => return Err(Error::Decode("recorder WAL command flag is invalid".into())),
6429    };
6430    if cursor != digest_offset {
6431        return Err(Error::Decode(
6432            "recorder WAL frame has trailing bytes".into(),
6433        ));
6434    }
6435    Ok(Some((
6436        WalFrame {
6437            generation,
6438            sequence,
6439            prev_digest,
6440            digest,
6441            slot,
6442            slot_bytes,
6443            configuration_bytes,
6444            head,
6445            command,
6446        },
6447        end,
6448    )))
6449}
6450
6451fn read_regular_file_bounded(path: &Path, maximum: usize, name: &str) -> Result<Vec<u8>> {
6452    let before = match fs::symlink_metadata(path) {
6453        Ok(metadata) => metadata,
6454        Err(error) if error.kind() == io::ErrorKind::NotFound => {
6455            return Err(Error::Decode(format!("recorder is missing {name}")))
6456        }
6457        Err(error) => return Err(Error::Io(error.to_string())),
6458    };
6459    if before.file_type().is_symlink() || !before.is_file() || before.len() > maximum as u64 {
6460        return Err(Error::Decode(format!(
6461            "recorder {name} must be a bounded regular file"
6462        )));
6463    }
6464    let mut file = open_regular_file_no_follow(path)?;
6465    let opened = file
6466        .metadata()
6467        .map_err(|error| Error::Io(error.to_string()))?;
6468    if !opened.is_file()
6469        || !same_opened_file(&before, &opened)
6470        || opened.len() != before.len()
6471        || opened.len() > maximum as u64
6472    {
6473        return Err(Error::Decode(format!(
6474            "recorder {name} changed before no-follow open"
6475        )));
6476    }
6477    let mut bytes = Vec::with_capacity(opened.len() as usize);
6478    Read::by_ref(&mut file)
6479        .take(maximum as u64 + 1)
6480        .read_to_end(&mut bytes)
6481        .map_err(|error| Error::Io(error.to_string()))?;
6482    let after_opened = file
6483        .metadata()
6484        .map_err(|error| Error::Io(error.to_string()))?;
6485    let after_path = fs::symlink_metadata(path).map_err(|error| Error::Io(error.to_string()))?;
6486    if after_path.file_type().is_symlink()
6487        || !after_path.is_file()
6488        || !after_opened.is_file()
6489        || !same_opened_file(&opened, &after_opened)
6490        || !same_opened_file(&opened, &after_path)
6491        || after_opened.len() != opened.len()
6492        || after_path.len() != opened.len()
6493    {
6494        return Err(Error::Decode(format!(
6495            "recorder {name} changed during bounded read"
6496        )));
6497    }
6498    if bytes.len() > maximum || bytes.len() as u64 != opened.len() {
6499        return Err(Error::Decode(format!(
6500            "recorder {name} exceeds its size limit"
6501        )));
6502    }
6503    Ok(bytes)
6504}
6505
6506fn open_regular_file_no_follow(path: &Path) -> Result<fs::File> {
6507    let mut options = fs::OpenOptions::new();
6508    options.read(true);
6509    #[cfg(any(
6510        target_os = "linux",
6511        target_os = "android",
6512        target_os = "macos",
6513        target_os = "ios",
6514        target_os = "freebsd",
6515        target_os = "openbsd",
6516        target_os = "netbsd",
6517        target_os = "dragonfly"
6518    ))]
6519    options.custom_flags(O_NOFOLLOW_FLAG);
6520    options
6521        .open(path)
6522        .map_err(|error| Error::Io(error.to_string()))
6523}
6524
6525fn open_existing_root_lock(path: &Path) -> Result<fs::File> {
6526    let before = match fs::symlink_metadata(path) {
6527        Ok(metadata) => metadata,
6528        Err(error) if error.kind() == io::ErrorKind::NotFound => {
6529            return Err(Error::Decode("recorder root lock does not exist".into()))
6530        }
6531        Err(error) => return Err(Error::Io(error.to_string())),
6532    };
6533    if before.file_type().is_symlink() || !before.is_file() {
6534        return Err(Error::Decode(
6535            "recorder root lock must be an existing regular file".into(),
6536        ));
6537    }
6538    let mut options = fs::OpenOptions::new();
6539    options.read(true).write(true);
6540    #[cfg(any(
6541        target_os = "linux",
6542        target_os = "android",
6543        target_os = "macos",
6544        target_os = "ios",
6545        target_os = "freebsd",
6546        target_os = "openbsd",
6547        target_os = "netbsd",
6548        target_os = "dragonfly"
6549    ))]
6550    options.custom_flags(O_NOFOLLOW_FLAG);
6551    let file = options
6552        .open(path)
6553        .map_err(|error| Error::Io(error.to_string()))?;
6554    let opened = file
6555        .metadata()
6556        .map_err(|error| Error::Io(error.to_string()))?;
6557    let after = fs::symlink_metadata(path).map_err(|error| Error::Io(error.to_string()))?;
6558    if after.file_type().is_symlink()
6559        || !after.is_file()
6560        || !opened.is_file()
6561        || !same_opened_file(&before, &opened)
6562        || !same_opened_file(&opened, &after)
6563        || before.len() != opened.len()
6564        || opened.len() != after.len()
6565    {
6566        return Err(Error::Decode(
6567            "recorder root lock changed before open".into(),
6568        ));
6569    }
6570    Ok(file)
6571}
6572
6573#[cfg(unix)]
6574fn same_opened_file(opened: &fs::Metadata, linked: &fs::Metadata) -> bool {
6575    opened.dev() == linked.dev() && opened.ino() == linked.ino()
6576}
6577
6578#[cfg(not(unix))]
6579fn same_opened_file(opened: &fs::Metadata, linked: &fs::Metadata) -> bool {
6580    opened.file_type() == linked.file_type()
6581        && opened.len() == linked.len()
6582        && opened.modified().ok() == linked.modified().ok()
6583}
6584
6585fn read_preflight_intent(root: &Path, name: &str, maximum: usize) -> Result<Option<Vec<u8>>> {
6586    let path = root.join(name);
6587    match read_regular_file_bounded(&path, maximum, name) {
6588        Ok(bytes) => Ok(Some(bytes)),
6589        Err(Error::Decode(message)) if message == format!("recorder is missing {name}") => Ok(None),
6590        Err(error) => Err(error),
6591    }
6592}
6593
6594fn validate_preflight_configuration(
6595    configuration: &ConfigurationState,
6596    config_id: ConfigId,
6597    membership: &Membership,
6598) -> Result<()> {
6599    if configuration.config_id != config_id
6600        || configuration.config_digest != membership.digest()
6601        || configuration.membership.as_ref() != Some(membership)
6602    {
6603        return Err(Error::Decode(
6604            "recorder configuration does not exactly match expected membership".into(),
6605        ));
6606    }
6607    Ok(())
6608}
6609
6610fn validate_existing_snapshots(
6611    root: &Path,
6612    snapshots: &[DurableSlotSnapshot],
6613    cluster_id: &str,
6614    epoch: Epoch,
6615    configuration: &ConfigurationState,
6616) -> Result<()> {
6617    let no_inline_commands = HashMap::new();
6618    for snapshot in snapshots {
6619        let state = decode_recorder_state(&snapshot.bytes)?;
6620        if state.slot() != snapshot.slot
6621            || state.cluster_id != cluster_id
6622            || state.epoch != epoch
6623            || state.config_id != configuration.config_id
6624            || state.config_digest != configuration.config_digest
6625        {
6626            return Err(Error::Decode(
6627                "durable recorder snapshot identity mismatch".into(),
6628            ));
6629        }
6630        for value in recorder_state_values(&state) {
6631            validate_existing_value(
6632                root,
6633                cluster_id,
6634                snapshot.slot,
6635                epoch,
6636                configuration.config_id,
6637                value,
6638                &no_inline_commands,
6639            )?;
6640        }
6641    }
6642    Ok(())
6643}
6644
6645fn validate_recoverable_transition_intent(
6646    root: &Path,
6647    bytes: &[u8],
6648    cluster_id: &str,
6649    epoch: Epoch,
6650    config_id: ConfigId,
6651    membership: &Membership,
6652) -> Result<()> {
6653    let (slot, slot_bytes, configuration_bytes) = decode_transition_intent(bytes)?;
6654    let configuration = decode_configuration_state(&configuration_bytes)?;
6655    validate_preflight_configuration(&configuration, config_id, membership)?;
6656    let state = decode_recorder_state(&slot_bytes)?;
6657    if state.slot() != slot
6658        || state.cluster_id != cluster_id
6659        || state.epoch != epoch
6660        || state.config_id != configuration.config_id
6661        || state.config_digest != configuration.config_digest
6662    {
6663        return Err(Error::Decode(
6664            "recorder transition intent state identity mismatch".into(),
6665        ));
6666    }
6667    let no_inline_commands = HashMap::new();
6668    for value in recorder_state_values(&state) {
6669        validate_existing_value(
6670            root,
6671            cluster_id,
6672            slot,
6673            epoch,
6674            configuration.config_id,
6675            value,
6676            &no_inline_commands,
6677        )?;
6678    }
6679    Ok(())
6680}
6681
6682fn validate_recoverable_configuration_head_intent(
6683    root: &Path,
6684    bytes: &[u8],
6685    cluster_id: &str,
6686    epoch: Epoch,
6687    config_id: ConfigId,
6688    membership: &Membership,
6689) -> Result<()> {
6690    let (configuration_bytes, head_bytes) = decode_configuration_head_intent(bytes)?;
6691    let configuration = decode_configuration_state(configuration_bytes)?;
6692    validate_preflight_configuration(&configuration, config_id, membership)?;
6693    let (_, snapshots, _) = decode_recorded_head(head_bytes, cluster_id, epoch, &configuration)?;
6694    validate_existing_snapshots(root, &snapshots, cluster_id, epoch, &configuration)
6695}
6696
6697fn validate_empty_recovery_wal(root: &Path) -> Result<()> {
6698    let path = root.join("recorder.wal");
6699    let metadata = match fs::symlink_metadata(&path) {
6700        Ok(metadata) => metadata,
6701        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
6702        Err(error) => return Err(Error::Io(error.to_string())),
6703    };
6704    if metadata.file_type().is_symlink() || !metadata.is_file() {
6705        return Err(Error::Decode(
6706            "recorder WAL must be a regular file during recovery".into(),
6707        ));
6708    }
6709    if metadata.len() != 0 {
6710        return Err(Error::Decode(
6711            "recorder recovery intent requires an empty WAL".into(),
6712        ));
6713    }
6714    Ok(())
6715}
6716
6717fn validate_existing_wal(
6718    root: &Path,
6719    bytes: &[u8],
6720    cluster_id: &str,
6721    epoch: Epoch,
6722    initial_configuration: &ConfigurationState,
6723    initial_head: &RecordedHeadProvenance,
6724    checkpoint: WalCheckpoint,
6725) -> Result<bool> {
6726    let mut commands = HashMap::new();
6727    let mut next_sequence = checkpoint
6728        .through_sequence
6729        .checked_add(1)
6730        .ok_or_else(|| Error::Decode("recorder WAL sequence exhausted".into()))?;
6731    let mut last_digest = LogHash::ZERO;
6732    let mut configuration = initial_configuration.clone();
6733    let mut head = initial_head.clone();
6734    let mut offset = 0usize;
6735    while offset < bytes.len() {
6736        let Some((frame, end)) = decode_wal_frame(bytes, offset)? else {
6737            return Ok(true);
6738        };
6739        if frame.generation < checkpoint.generation {
6740            offset = end;
6741            continue;
6742        }
6743        if frame.generation != checkpoint.generation
6744            || frame.sequence != next_sequence
6745            || frame.prev_digest != last_digest
6746        {
6747            return Err(Error::Decode(
6748                "recorder WAL sequence or digest chain mismatch".into(),
6749            ));
6750        }
6751        let state = decode_recorder_state(&frame.slot_bytes)?;
6752        let next_configuration = decode_configuration_state(&frame.configuration_bytes)?;
6753        if state.slot() != frame.slot
6754            || state.cluster_id != cluster_id
6755            || state.epoch != epoch
6756            || state.config_id != next_configuration.config_id
6757            || state.config_digest != next_configuration.config_digest
6758            || configuration_structure_changed(&configuration, &next_configuration)
6759        {
6760            return Err(Error::Decode("recorder WAL state identity mismatch".into()));
6761        }
6762        if let Some((hash, command)) = &frame.command {
6763            if command.hash() != *hash {
6764                return Err(Error::Decode(
6765                    "recorder WAL inline command hash mismatch".into(),
6766                ));
6767            }
6768            upsert_wal_command(&mut commands, *hash, command)?;
6769        }
6770        for value in recorder_state_values(&state) {
6771            validate_existing_value(
6772                root,
6773                cluster_id,
6774                frame.slot,
6775                epoch,
6776                next_configuration.config_id,
6777                value,
6778                &commands,
6779            )?;
6780        }
6781        let expected_head = if next_configuration.max_accepted_or_decided_slot == Some(frame.slot)
6782            && recorder_state_values(&state).next().is_some()
6783        {
6784            RecordedHeadProvenance::SlotBacked { slot: frame.slot }
6785        } else {
6786            head.clone()
6787        };
6788        if frame.head != expected_head {
6789            return Err(Error::Decode("recorder WAL head mismatch".into()));
6790        }
6791        next_sequence = next_sequence
6792            .checked_add(1)
6793            .ok_or_else(|| Error::Decode("recorder WAL sequence exhausted".into()))?;
6794        last_digest = frame.digest;
6795        configuration = next_configuration;
6796        head = frame.head;
6797        offset = end;
6798    }
6799    Ok(false)
6800}
6801
6802fn validate_existing_value(
6803    root: &Path,
6804    cluster_id: &str,
6805    slot: Slot,
6806    epoch: Epoch,
6807    config_id: ConfigId,
6808    value: &AcceptedValue,
6809    inline_commands: &HashMap<LogHash, StoredCommand>,
6810) -> Result<()> {
6811    let cached_command;
6812    let command = match inline_commands.get(&value.command_hash) {
6813        Some(command) => command,
6814        None => {
6815            cached_command = read_existing_command_cache(root, value.command_hash)?;
6816            &cached_command
6817        }
6818    };
6819    if AcceptedValue::from_command(cluster_id, slot, epoch, config_id, value.prev_hash, command)
6820        != *value
6821    {
6822        return Err(Error::Decode("recorder WAL value mismatch".into()));
6823    }
6824    Ok(())
6825}
6826
6827fn read_existing_command_cache(root: &Path, command_hash: LogHash) -> Result<StoredCommand> {
6828    let path = root.join(format!("command-{}.cmd", command_hash.to_hex()));
6829    let bytes =
6830        match read_regular_file_bounded(&path, MAX_COMMAND_CACHE_BYTES, "command cache entry") {
6831            Err(Error::Decode(message)) if message == "recorder is missing command cache entry" => {
6832                return Err(Error::CommandUnavailable)
6833            }
6834            result => result?,
6835        };
6836    let command = decode_stored_command(&bytes)?;
6837    if command.hash() != command_hash {
6838        return Err(Error::CommandHashMismatch);
6839    }
6840    Ok(command)
6841}
6842
6843fn encode_head_provenance(out: &mut Vec<u8>, head: &RecordedHeadProvenance) {
6844    match head {
6845        RecordedHeadProvenance::Empty => out.push(0),
6846        RecordedHeadProvenance::SlotBacked { slot } => {
6847            out.push(1);
6848            put_u64(out, *slot);
6849        }
6850        RecordedHeadProvenance::CheckpointBacked {
6851            stop_slot,
6852            prefix_hash,
6853            recovered_tip,
6854            recovered_hash,
6855        } => {
6856            out.push(2);
6857            put_u64(out, *stop_slot);
6858            out.extend_from_slice(prefix_hash.as_bytes());
6859            put_u64(out, *recovered_tip);
6860            out.extend_from_slice(recovered_hash.as_bytes());
6861        }
6862    }
6863}
6864
6865fn decode_head_provenance(bytes: &[u8], cursor: &mut usize) -> Result<RecordedHeadProvenance> {
6866    match read_u8(bytes, cursor)? {
6867        0 => Ok(RecordedHeadProvenance::Empty),
6868        1 => Ok(RecordedHeadProvenance::SlotBacked {
6869            slot: read_u64(bytes, cursor)?,
6870        }),
6871        2 => Ok(RecordedHeadProvenance::CheckpointBacked {
6872            stop_slot: read_u64(bytes, cursor)?,
6873            prefix_hash: read_hash(bytes, cursor)?,
6874            recovered_tip: read_u64(bytes, cursor)?,
6875            recovered_hash: read_hash(bytes, cursor)?,
6876        }),
6877        _ => Err(Error::Decode(
6878            "recorder WAL head provenance is invalid".into(),
6879        )),
6880    }
6881}
6882
6883fn upsert_wal_command(
6884    commands: &mut HashMap<LogHash, StoredCommand>,
6885    hash: LogHash,
6886    command: &StoredCommand,
6887) -> Result<()> {
6888    match commands.entry(hash) {
6889        hash_map::Entry::Occupied(existing) if existing.get() != command => {
6890            Err(Error::CommandHashMismatch)
6891        }
6892        hash_map::Entry::Occupied(_) => Ok(()),
6893        hash_map::Entry::Vacant(vacant) => {
6894            vacant.insert(command.clone());
6895            Ok(())
6896        }
6897    }
6898}
6899
6900fn encode_recorded_head(
6901    cluster_id: &str,
6902    epoch: Epoch,
6903    configuration: &ConfigurationState,
6904    provenance: &RecordedHeadProvenance,
6905    recent_slots: &[DurableSlotSnapshot],
6906    wal_checkpoint: WalCheckpoint,
6907) -> Result<Vec<u8>> {
6908    if recent_slots.len() > 2 {
6909        return Err(Error::Io(
6910            "recorder manifest can retain at most two slot snapshots".into(),
6911        ));
6912    }
6913    let mut encoded = Vec::new();
6914    encoded.extend_from_slice(RECORDED_HEAD_MAGIC);
6915    put_u16(&mut encoded, RECORDED_HEAD_VERSION);
6916    put_bytes(&mut encoded, cluster_id.as_bytes())?;
6917    put_u64(&mut encoded, epoch);
6918    put_u64(&mut encoded, configuration.config_id);
6919    encoded.extend_from_slice(configuration.config_digest.as_bytes());
6920    match provenance {
6921        RecordedHeadProvenance::Empty => encoded.push(0),
6922        RecordedHeadProvenance::SlotBacked { slot } => {
6923            encoded.push(1);
6924            put_u64(&mut encoded, *slot);
6925        }
6926        RecordedHeadProvenance::CheckpointBacked {
6927            stop_slot,
6928            prefix_hash,
6929            recovered_tip,
6930            recovered_hash,
6931        } => {
6932            encoded.push(2);
6933            put_u64(&mut encoded, *stop_slot);
6934            encoded.extend_from_slice(prefix_hash.as_bytes());
6935            put_u64(&mut encoded, *recovered_tip);
6936            encoded.extend_from_slice(recovered_hash.as_bytes());
6937        }
6938    }
6939    put_u64(&mut encoded, wal_checkpoint.generation);
6940    put_u64(&mut encoded, wal_checkpoint.through_sequence);
6941    put_u16(&mut encoded, recent_slots.len() as u16);
6942    for snapshot in recent_slots {
6943        put_u64(&mut encoded, snapshot.slot);
6944        put_bytes(&mut encoded, &snapshot.bytes)?;
6945    }
6946    let digest = LogHash::digest(&[&encoded]);
6947    encoded.extend_from_slice(digest.as_bytes());
6948    Ok(encoded)
6949}
6950
6951fn decode_recorded_head(
6952    bytes: &[u8],
6953    expected_cluster_id: &str,
6954    expected_epoch: Epoch,
6955    configuration: &ConfigurationState,
6956) -> Result<(
6957    RecordedHeadProvenance,
6958    Vec<DurableSlotSnapshot>,
6959    WalCheckpoint,
6960)> {
6961    if bytes.get(..RECORDED_HEAD_MAGIC.len()) != Some(RECORDED_HEAD_MAGIC) {
6962        return Err(Error::Decode("invalid recorder durable head magic".into()));
6963    }
6964    let mut version_cursor = RECORDED_HEAD_MAGIC.len();
6965    if read_u16(bytes, &mut version_cursor)? != RECORDED_HEAD_VERSION {
6966        return Err(Error::MigrationRequired {
6967            format: "recorder durable head",
6968            version: RECORDED_HEAD_VERSION,
6969        });
6970    }
6971    if bytes.len() < 32 {
6972        return Err(Error::Decode("truncated recorder durable head".into()));
6973    }
6974    let (body, digest) = bytes.split_at(bytes.len() - 32);
6975    if LogHash::digest(&[body]).as_bytes() != digest {
6976        return Err(Error::Decode(
6977            "recorder durable head digest mismatch".into(),
6978        ));
6979    }
6980    let mut cursor = 0;
6981    cursor += RECORDED_HEAD_MAGIC.len();
6982    let _version = read_u16(body, &mut cursor)?;
6983    let cluster_id = String::from_utf8(read_bytes(body, &mut cursor)?)
6984        .map_err(|error| Error::Decode(error.to_string()))?;
6985    let epoch = read_u64(body, &mut cursor)?;
6986    let config_id = read_u64(body, &mut cursor)?;
6987    let config_digest = read_hash(body, &mut cursor)?;
6988    if cluster_id != expected_cluster_id
6989        || epoch != expected_epoch
6990        || config_id != configuration.config_id
6991        || config_digest != configuration.config_digest
6992    {
6993        return Err(Error::Decode(
6994            "recorder durable head identity mismatch".into(),
6995        ));
6996    }
6997    let provenance = match read_u8(body, &mut cursor)? {
6998        0 => RecordedHeadProvenance::Empty,
6999        1 => RecordedHeadProvenance::SlotBacked {
7000            slot: read_u64(body, &mut cursor)?,
7001        },
7002        2 => RecordedHeadProvenance::CheckpointBacked {
7003            stop_slot: read_u64(body, &mut cursor)?,
7004            prefix_hash: read_hash(body, &mut cursor)?,
7005            recovered_tip: read_u64(body, &mut cursor)?,
7006            recovered_hash: read_hash(body, &mut cursor)?,
7007        },
7008        value => {
7009            return Err(Error::Decode(format!(
7010                "invalid recorder durable head provenance {value}"
7011            )));
7012        }
7013    };
7014    let wal_checkpoint = WalCheckpoint {
7015        generation: read_u64(body, &mut cursor)?,
7016        through_sequence: read_u64(body, &mut cursor)?,
7017    };
7018    if wal_checkpoint.generation == 0 {
7019        return Err(Error::Decode(
7020            "recorder durable head has zero WAL generation".into(),
7021        ));
7022    }
7023    let recent_count = usize::from(read_u16(body, &mut cursor)?);
7024    if recent_count > 2 {
7025        return Err(Error::Decode(
7026            "recorder manifest contains too many slot snapshots".into(),
7027        ));
7028    }
7029    let mut recent_slots = Vec::with_capacity(recent_count);
7030    for _ in 0..recent_count {
7031        let slot = read_u64(body, &mut cursor)?;
7032        if recent_slots
7033            .iter()
7034            .any(|snapshot: &DurableSlotSnapshot| snapshot.slot == slot)
7035        {
7036            return Err(Error::Decode(
7037                "recorder manifest contains duplicate slot snapshots".into(),
7038            ));
7039        }
7040        recent_slots.push(DurableSlotSnapshot {
7041            slot,
7042            bytes: read_bytes(body, &mut cursor)?,
7043        });
7044    }
7045    if cursor != body.len() {
7046        return Err(Error::Decode("trailing recorder durable head bytes".into()));
7047    }
7048    Ok((provenance, recent_slots, wal_checkpoint))
7049}
7050
7051fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
7052    atomic_replace(path, bytes)?;
7053    let parent = path
7054        .parent()
7055        .ok_or_else(|| Error::Io("atomic write path has no parent".into()))?;
7056    fs::File::open(parent)
7057        .and_then(|directory| directory.sync_all())
7058        .map_err(|err| Error::Io(err.to_string()))?;
7059    #[cfg(test)]
7060    record_directory_sync();
7061    Ok(())
7062}
7063
7064fn atomic_replace(path: &Path, bytes: &[u8]) -> Result<()> {
7065    let parent = path
7066        .parent()
7067        .ok_or_else(|| Error::Io("atomic write path has no parent".into()))?;
7068    fs::create_dir_all(parent).map_err(|err| Error::Io(err.to_string()))?;
7069    let file_name = path
7070        .file_name()
7071        .ok_or_else(|| Error::Io("atomic write path has no file name".into()))?
7072        .to_string_lossy();
7073    let (temp_path, mut file) = loop {
7074        let counter = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
7075        let temp_path = parent.join(format!(".{file_name}.tmp-{}-{counter}", std::process::id()));
7076        match fs::OpenOptions::new()
7077            .write(true)
7078            .create_new(true)
7079            .open(&temp_path)
7080        {
7081            Ok(file) => break (temp_path, file),
7082            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => continue,
7083            Err(err) => return Err(Error::Io(err.to_string())),
7084        }
7085    };
7086    let result = (|| -> io::Result<()> {
7087        file.write_all(bytes)?;
7088        file.sync_all()?;
7089        #[cfg(test)]
7090        record_file_sync();
7091        drop(file);
7092        fs::rename(&temp_path, path)?;
7093        Ok(())
7094    })();
7095    if let Err(err) = result {
7096        let _ = fs::remove_file(&temp_path);
7097        return Err(Error::Io(err.to_string()));
7098    }
7099    Ok(())
7100}
7101
7102fn put_u16(out: &mut Vec<u8>, value: u16) {
7103    out.extend_from_slice(&value.to_be_bytes());
7104}
7105
7106fn put_u64(out: &mut Vec<u8>, value: u64) {
7107    out.extend_from_slice(&value.to_be_bytes());
7108}
7109
7110fn put_u128(out: &mut Vec<u8>, value: u128) {
7111    out.extend_from_slice(&value.to_be_bytes());
7112}
7113
7114fn put_bytes(out: &mut Vec<u8>, value: &[u8]) -> Result<()> {
7115    let len = u16::try_from(value.len())
7116        .map_err(|_| Error::Decode("recorder string is too long".into()))?;
7117    put_u16(out, len);
7118    out.extend_from_slice(value);
7119    Ok(())
7120}
7121
7122fn put_blob(out: &mut Vec<u8>, value: &[u8]) -> Result<()> {
7123    let len = u64::try_from(value.len())
7124        .map_err(|_| Error::Decode("recorder blob is too long".into()))?;
7125    put_u64(out, len);
7126    out.extend_from_slice(value);
7127    Ok(())
7128}
7129
7130fn read_u8(bytes: &[u8], cursor: &mut usize) -> Result<u8> {
7131    let value = *bytes
7132        .get(*cursor)
7133        .ok_or_else(|| Error::Decode("short recorder u8".into()))?;
7134    *cursor += 1;
7135    Ok(value)
7136}
7137
7138fn read_u16(bytes: &[u8], cursor: &mut usize) -> Result<u16> {
7139    let end = cursor
7140        .checked_add(2)
7141        .ok_or_else(|| Error::Decode("recorder cursor overflow".into()))?;
7142    let slice = bytes
7143        .get(*cursor..end)
7144        .ok_or_else(|| Error::Decode("short recorder u16".into()))?;
7145    *cursor = end;
7146    Ok(u16::from_be_bytes(slice.try_into().expect("u16 slice")))
7147}
7148
7149fn read_u64(bytes: &[u8], cursor: &mut usize) -> Result<u64> {
7150    let end = cursor
7151        .checked_add(8)
7152        .ok_or_else(|| Error::Decode("recorder cursor overflow".into()))?;
7153    let slice = bytes
7154        .get(*cursor..end)
7155        .ok_or_else(|| Error::Decode("short recorder u64".into()))?;
7156    *cursor = end;
7157    Ok(u64::from_be_bytes(slice.try_into().expect("u64 slice")))
7158}
7159
7160fn read_u128(bytes: &[u8], cursor: &mut usize) -> Result<u128> {
7161    let end = cursor
7162        .checked_add(16)
7163        .ok_or_else(|| Error::Decode("recorder cursor overflow".into()))?;
7164    let slice = bytes
7165        .get(*cursor..end)
7166        .ok_or_else(|| Error::Decode("short recorder u128".into()))?;
7167    *cursor = end;
7168    Ok(u128::from_be_bytes(slice.try_into().expect("u128 slice")))
7169}
7170
7171fn read_hash(bytes: &[u8], cursor: &mut usize) -> Result<LogHash> {
7172    let end = cursor
7173        .checked_add(32)
7174        .ok_or_else(|| Error::Decode("recorder cursor overflow".into()))?;
7175    let slice = bytes
7176        .get(*cursor..end)
7177        .ok_or_else(|| Error::Decode("short recorder hash".into()))?;
7178    *cursor = end;
7179    let mut out = [0; 32];
7180    out.copy_from_slice(slice);
7181    Ok(LogHash::from_bytes(out))
7182}
7183
7184fn read_bytes(bytes: &[u8], cursor: &mut usize) -> Result<Vec<u8>> {
7185    let len = read_u16(bytes, cursor)? as usize;
7186    let end = cursor
7187        .checked_add(len)
7188        .ok_or_else(|| Error::Decode("recorder cursor overflow".into()))?;
7189    let slice = bytes
7190        .get(*cursor..end)
7191        .ok_or_else(|| Error::Decode("short recorder bytes".into()))?;
7192    *cursor = end;
7193    Ok(slice.to_vec())
7194}
7195
7196fn read_blob(bytes: &[u8], cursor: &mut usize) -> Result<Vec<u8>> {
7197    let len = usize::try_from(read_u64(bytes, cursor)?)
7198        .map_err(|_| Error::Decode("recorder blob length overflow".into()))?;
7199    let end = cursor
7200        .checked_add(len)
7201        .ok_or_else(|| Error::Decode("recorder blob length overflow".into()))?;
7202    let value = bytes
7203        .get(*cursor..end)
7204        .ok_or_else(|| Error::Decode("short recorder blob".into()))?
7205        .to_vec();
7206    *cursor = end;
7207    Ok(value)
7208}
7209
7210#[derive(Debug)]
7211pub struct SingleNodeConsensus {
7212    cluster_id: String,
7213    epoch: Epoch,
7214    config_id: ConfigId,
7215    state: Mutex<SingleNodeState>,
7216}
7217
7218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7219struct SingleNodeState {
7220    next_index: LogIndex,
7221    last_hash: LogHash,
7222}
7223
7224impl SingleNodeConsensus {
7225    pub fn new(cluster_id: impl Into<String>, epoch: Epoch, config_id: ConfigId) -> Self {
7226        Self {
7227            cluster_id: cluster_id.into(),
7228            epoch,
7229            config_id,
7230            state: Mutex::new(SingleNodeState {
7231                next_index: 1,
7232                last_hash: LogHash::ZERO,
7233            }),
7234        }
7235    }
7236}
7237
7238impl Consensus for SingleNodeConsensus {
7239    fn propose(&self, command: Command) -> Result<LogEntry> {
7240        let mut state = self.state.lock().map_err(|_| Error::ProposeFailed)?;
7241        let command = stored_command(command)?;
7242        let prev_hash = state.last_hash;
7243        let hash = LogEntry::calculate_hash(
7244            &self.cluster_id,
7245            state.next_index,
7246            self.epoch,
7247            self.config_id,
7248            command.entry_type,
7249            prev_hash,
7250            &command.payload,
7251        );
7252        let entry = LogEntry {
7253            cluster_id: self.cluster_id.clone(),
7254            epoch: self.epoch,
7255            config_id: self.config_id,
7256            index: state.next_index,
7257            entry_type: command.entry_type,
7258            payload: command.payload,
7259            prev_hash,
7260            hash,
7261        };
7262        state.next_index += 1;
7263        state.last_hash = hash;
7264        Ok(entry)
7265    }
7266}
7267
7268#[cfg(test)]
7269mod tests {
7270    use super::{
7271        command_file_reads, decode_configuration_state, decode_recorder_state, decode_wal_frame,
7272        encode_stored_command, encode_wal_frame, last_file_sync_kind, reset_command_file_reads,
7273        reset_sync_counts, sync_counts, sync_wal_append, sync_wal_metadata, upsert_wal_command,
7274        AcceptedValue, CertifiedDecisionInspection, ConfigChange, ConfigurationState, Consensus,
7275        ControlDispatch, ControlJob, DecisionInspection, DecisionProof, DriveOutcome, Error,
7276        FileSyncKind, Membership, PrioritySource, Proposal, ProposalPriority, ProposerProgress,
7277        ReadFenceObservation, ReadFenceRequest, ReadFenceSlotState, RecordRequest, RecordSummary,
7278        RecordedHeadProvenance, RecorderFileStore, RecorderPreflight, RecorderRequest, RecorderRpc,
7279        RecorderSlotState, RecorderSummary, RejectReason, SealFaultPoint, SingleNodeConsensus,
7280        ThreeNodeConsensus,
7281    };
7282    use proptest::prelude::*;
7283    use rhiza_core::{Command, CommandKind, EntryType, LogHash, StoredCommand};
7284    use std::{
7285        collections::{BTreeMap, BTreeSet, HashMap, HashSet},
7286        path::{Path, PathBuf},
7287        sync::{
7288            atomic::{AtomicUsize, Ordering},
7289            mpsc, Arc, Condvar, Mutex,
7290        },
7291        thread,
7292        time::{Duration, Instant},
7293    };
7294
7295    fn directory_files(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
7296        fn visit(base: &Path, current: &Path, files: &mut BTreeMap<PathBuf, Vec<u8>>) {
7297            if !current.exists() {
7298                return;
7299            }
7300            for entry in std::fs::read_dir(current).unwrap() {
7301                let entry = entry.unwrap();
7302                if entry.file_type().unwrap().is_dir() {
7303                    visit(base, &entry.path(), files);
7304                } else {
7305                    files.insert(
7306                        entry.path().strip_prefix(base).unwrap().to_path_buf(),
7307                        std::fs::read(entry.path()).unwrap(),
7308                    );
7309                }
7310            }
7311        }
7312        let mut files = BTreeMap::new();
7313        visit(root, root, &mut files);
7314        files
7315    }
7316
7317    fn cache_backed_recorder_for_preflight(
7318        root: &Path,
7319        membership: Membership,
7320    ) -> (LogHash, StoredCommand) {
7321        let store =
7322            RecorderFileStore::new_with_membership(root, "n1", "cluster", 1, 1, membership.clone())
7323                .unwrap();
7324        let command = StoredCommand::new(EntryType::Command, b"preflight-cache".to_vec());
7325        store
7326            .store_command(command.hash(), command.clone())
7327            .unwrap();
7328        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
7329        store
7330            .record_proposal(RecordRequest {
7331                cluster_id: "cluster".into(),
7332                epoch: 1,
7333                config_id: 1,
7334                config_digest: membership.digest(),
7335                slot: 8,
7336                step: 4,
7337                proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
7338                command: None,
7339            })
7340            .unwrap();
7341        (command.hash(), command)
7342    }
7343
7344    fn record_requests(consensus: &ThreeNodeConsensus, slot: u64) -> Vec<RecordRequest> {
7345        let proposal = Proposal::new(
7346            ProposalPriority::MAX,
7347            "n1",
7348            slot,
7349            AcceptedValue {
7350                command_hash: LogHash::ZERO,
7351                prev_hash: LogHash::ZERO,
7352                entry_hash: LogHash::ZERO,
7353            },
7354        );
7355        consensus
7356            .membership()
7357            .members()
7358            .iter()
7359            .map(|_| RecordRequest {
7360                cluster_id: "cluster".into(),
7361                epoch: 1,
7362                config_id: 1,
7363                config_digest: consensus.membership().digest(),
7364                slot,
7365                step: 4,
7366                proposal: proposal.clone(),
7367                command: None,
7368            })
7369            .collect()
7370    }
7371
7372    fn record_summary(recorder_id: &str, request: RecordRequest) -> RecordSummary {
7373        RecordSummary {
7374            recorder_id: recorder_id.into(),
7375            slot: request.slot,
7376            config_id: request.config_id,
7377            config_digest: request.config_digest,
7378            step: request.step,
7379            first_current: Some(request.proposal),
7380            aggregate_prior: None,
7381            decided: None,
7382        }
7383    }
7384
7385    struct ThreadRecordingRecorder {
7386        recorder_id: &'static str,
7387        threads: Arc<Mutex<HashSet<thread::ThreadId>>>,
7388    }
7389
7390    impl RecorderRpc for ThreadRecordingRecorder {
7391        fn record(&self, request: RecordRequest) -> super::Result<RecordSummary> {
7392            self.threads.lock().unwrap().insert(thread::current().id());
7393            Ok(record_summary(self.recorder_id, request))
7394        }
7395    }
7396
7397    struct ThreadRecordingControlRecorder {
7398        threads: Arc<Mutex<HashSet<thread::ThreadId>>>,
7399    }
7400
7401    impl RecorderRpc for ThreadRecordingControlRecorder {
7402        fn inspect_decision_proof(
7403            &self,
7404            _slot: u64,
7405        ) -> super::Result<Option<super::DecisionProof>> {
7406            self.threads.lock().unwrap().insert(thread::current().id());
7407            Ok(None)
7408        }
7409    }
7410
7411    struct BlockingControlRecorder {
7412        recorder_id: &'static str,
7413        started: mpsc::SyncSender<u64>,
7414        release_first: Mutex<mpsc::Receiver<()>>,
7415    }
7416
7417    struct BlockingInspectionReadFenceRecorder {
7418        recorder_id: &'static str,
7419        block_inspection: bool,
7420        started: mpsc::SyncSender<&'static str>,
7421        release: Arc<(Mutex<bool>, Condvar)>,
7422    }
7423
7424    struct BlockingCommandStoreRecorder {
7425        started: mpsc::SyncSender<()>,
7426        release: Arc<(Mutex<bool>, Condvar)>,
7427    }
7428
7429    struct SuccessfulCommandStoreRecorder;
7430
7431    impl RecorderRpc for SuccessfulCommandStoreRecorder {
7432        fn store_command_for(
7433            &self,
7434            _cluster_id: String,
7435            _epoch: u64,
7436            _config_id: u64,
7437            _config_digest: LogHash,
7438            _command_hash: LogHash,
7439            _command: StoredCommand,
7440        ) -> super::Result<()> {
7441            Ok(())
7442        }
7443    }
7444
7445    struct FailingCommandStoreRecorder;
7446
7447    impl RecorderRpc for FailingCommandStoreRecorder {
7448        fn store_command_for(
7449            &self,
7450            _cluster_id: String,
7451            _epoch: u64,
7452            _config_id: u64,
7453            _config_digest: LogHash,
7454            _command_hash: LogHash,
7455            _command: StoredCommand,
7456        ) -> super::Result<()> {
7457            Err(Error::ProposeFailed)
7458        }
7459    }
7460
7461    impl RecorderRpc for BlockingCommandStoreRecorder {
7462        fn store_command_for(
7463            &self,
7464            _cluster_id: String,
7465            _epoch: u64,
7466            _config_id: u64,
7467            _config_digest: LogHash,
7468            _command_hash: LogHash,
7469            _command: StoredCommand,
7470        ) -> super::Result<()> {
7471            self.started.send(()).unwrap();
7472            let (released, condition) = &*self.release;
7473            let mut released = released.lock().unwrap();
7474            while !*released {
7475                released = condition.wait(released).unwrap();
7476            }
7477            Ok(())
7478        }
7479    }
7480
7481    impl RecorderRpc for BlockingControlRecorder {
7482        fn record(&self, request: RecordRequest) -> super::Result<RecordSummary> {
7483            Ok(record_summary(self.recorder_id, request))
7484        }
7485
7486        fn inspect_decision_proof(&self, slot: u64) -> super::Result<Option<super::DecisionProof>> {
7487            self.started.send(slot).unwrap();
7488            if slot == 1 {
7489                self.release_first.lock().unwrap().recv().unwrap();
7490            }
7491            Ok(None)
7492        }
7493    }
7494
7495    impl RecorderRpc for BlockingInspectionReadFenceRecorder {
7496        fn inspect_record_summary(&self, _slot: u64) -> super::Result<Option<RecordSummary>> {
7497            if self.block_inspection {
7498                self.started.send(self.recorder_id).unwrap();
7499                let (released, condition) = &*self.release;
7500                let mut released = released.lock().unwrap();
7501                while !*released {
7502                    released = condition.wait(released).unwrap();
7503                }
7504            }
7505            Ok(None)
7506        }
7507
7508        fn supports_context_read_fence(&self) -> bool {
7509            true
7510        }
7511
7512        fn observe_read_fence(
7513            &self,
7514            request: ReadFenceRequest,
7515        ) -> super::Result<ReadFenceObservation> {
7516            Ok(ReadFenceObservation {
7517                recorder_id: self.recorder_id.into(),
7518                cluster_id: request.cluster_id,
7519                epoch: request.epoch,
7520                config_id: request.config_id,
7521                config_digest: request.config_digest,
7522                slot: request.slot,
7523                max_head: None,
7524                slot_state: ReadFenceSlotState::Empty,
7525            })
7526        }
7527    }
7528
7529    struct BlockingRecorder {
7530        recorder_id: &'static str,
7531        started: mpsc::SyncSender<u64>,
7532        release_first: Mutex<mpsc::Receiver<()>>,
7533    }
7534
7535    impl RecorderRpc for BlockingRecorder {
7536        fn record(&self, request: RecordRequest) -> super::Result<RecordSummary> {
7537            self.started.send(request.slot).unwrap();
7538            if request.slot == 1 {
7539                self.release_first.lock().unwrap().recv().unwrap();
7540            }
7541            Ok(record_summary(self.recorder_id, request))
7542        }
7543    }
7544
7545    struct SlotRecorder {
7546        recorder_id: &'static str,
7547        reject_slot: Option<u64>,
7548        observed: Option<mpsc::SyncSender<u64>>,
7549    }
7550
7551    impl RecorderRpc for SlotRecorder {
7552        fn record(&self, request: RecordRequest) -> super::Result<RecordSummary> {
7553            if let Some(observed) = &self.observed {
7554                observed.send(request.slot).unwrap();
7555            }
7556            if self.reject_slot == Some(request.slot) {
7557                Err(Error::Rejected(RejectReason::InvalidRequest))
7558            } else {
7559                Ok(record_summary(self.recorder_id, request))
7560            }
7561        }
7562    }
7563
7564    struct RejectFromSlotRecorder {
7565        recorder_id: &'static str,
7566        reject_from: u64,
7567    }
7568
7569    impl RecorderRpc for RejectFromSlotRecorder {
7570        fn record(&self, request: RecordRequest) -> super::Result<RecordSummary> {
7571            if request.slot >= self.reject_from {
7572                Err(Error::Rejected(RejectReason::InvalidRequest))
7573            } else {
7574                Ok(record_summary(self.recorder_id, request))
7575            }
7576        }
7577    }
7578
7579    struct PanickingRecorder;
7580
7581    impl RecorderRpc for PanickingRecorder {
7582        fn record(&self, _request: RecordRequest) -> super::Result<RecordSummary> {
7583            panic!("injected recorder panic")
7584        }
7585    }
7586
7587    struct FailingFromSlotRecorder {
7588        recorder_id: &'static str,
7589        fail_from: u64,
7590    }
7591
7592    impl RecorderRpc for FailingFromSlotRecorder {
7593        fn record(&self, request: RecordRequest) -> super::Result<RecordSummary> {
7594            if request.slot >= self.fail_from {
7595                Err(Error::ProposeFailed)
7596            } else {
7597                Ok(record_summary(self.recorder_id, request))
7598            }
7599        }
7600    }
7601
7602    struct AlwaysIoRecorder;
7603
7604    impl RecorderRpc for AlwaysIoRecorder {
7605        fn record(&self, _request: RecordRequest) -> super::Result<RecordSummary> {
7606            Err(Error::Io("injected recorder unavailable".into()))
7607        }
7608    }
7609
7610    struct MissingCommandRecorder {
7611        observed: mpsc::SyncSender<()>,
7612    }
7613
7614    impl RecorderRpc for MissingCommandRecorder {
7615        fn fetch_command_for(
7616            &self,
7617            _cluster_id: String,
7618            _epoch: u64,
7619            _config_id: u64,
7620            _config_digest: LogHash,
7621            _command_hash: LogHash,
7622        ) -> super::Result<Option<StoredCommand>> {
7623            self.observed.send(()).unwrap();
7624            Ok(None)
7625        }
7626    }
7627
7628    struct BlockingCommandRecorder {
7629        started: mpsc::SyncSender<()>,
7630        release: Mutex<mpsc::Receiver<()>>,
7631        command: StoredCommand,
7632    }
7633
7634    struct AvailableCommandRecorder {
7635        command: StoredCommand,
7636    }
7637
7638    impl RecorderRpc for AvailableCommandRecorder {
7639        fn fetch_command_for(
7640            &self,
7641            _cluster_id: String,
7642            _epoch: u64,
7643            _config_id: u64,
7644            _config_digest: LogHash,
7645            _command_hash: LogHash,
7646        ) -> super::Result<Option<StoredCommand>> {
7647            Ok(Some(self.command.clone()))
7648        }
7649    }
7650
7651    struct FailingCommandFetchRecorder;
7652
7653    impl RecorderRpc for FailingCommandFetchRecorder {
7654        fn fetch_command_for(
7655            &self,
7656            _cluster_id: String,
7657            _epoch: u64,
7658            _config_id: u64,
7659            _config_digest: LogHash,
7660            _command_hash: LogHash,
7661        ) -> super::Result<Option<StoredCommand>> {
7662            Err(Error::ProposeFailed)
7663        }
7664    }
7665
7666    impl RecorderRpc for BlockingCommandRecorder {
7667        fn fetch_command_for(
7668            &self,
7669            _cluster_id: String,
7670            _epoch: u64,
7671            _config_id: u64,
7672            _config_digest: LogHash,
7673            _command_hash: LogHash,
7674        ) -> super::Result<Option<StoredCommand>> {
7675            self.started.send(()).unwrap();
7676            let _ = self.release.lock().unwrap().recv();
7677            Ok(Some(self.command.clone()))
7678        }
7679    }
7680
7681    struct FailingPrioritySource;
7682
7683    impl PrioritySource for FailingPrioritySource {
7684        fn sample(
7685            &self,
7686            _slot: u64,
7687            _round: u64,
7688            _proposer_id: &str,
7689            _recorder_id: &str,
7690        ) -> super::Result<ProposalPriority> {
7691            Err(Error::RandomnessUnavailable("unexpected sample".into()))
7692        }
7693    }
7694
7695    #[derive(Default)]
7696    struct CountingPrioritySource {
7697        samples: AtomicUsize,
7698    }
7699
7700    impl PrioritySource for CountingPrioritySource {
7701        fn sample(
7702            &self,
7703            _slot: u64,
7704            _round: u64,
7705            _proposer_id: &str,
7706            _recorder_id: &str,
7707        ) -> super::Result<ProposalPriority> {
7708            let sample = self.samples.fetch_add(1, Ordering::Relaxed) + 1;
7709            Ok(ProposalPriority::from_u64(sample as u64))
7710        }
7711    }
7712
7713    struct CatchUpRecorder {
7714        recorder_id: &'static str,
7715        step: u64,
7716    }
7717
7718    impl RecorderRpc for CatchUpRecorder {
7719        fn record(&self, request: RecordRequest) -> super::Result<RecordSummary> {
7720            let mut summary = record_summary(self.recorder_id, request);
7721            summary.step = self.step;
7722            Ok(summary)
7723        }
7724    }
7725
7726    #[test]
7727    fn single_node_consensus_commits_contiguous_hash_chain() {
7728        let consensus = SingleNodeConsensus::new("cluster-a", 1, 1);
7729        let first = consensus
7730            .propose(Command::new(CommandKind::Deterministic, b"first".to_vec()))
7731            .unwrap();
7732        let second = consensus
7733            .propose(Command::new(CommandKind::Deterministic, b"second".to_vec()))
7734            .unwrap();
7735
7736        assert_eq!(first.index, 1);
7737        assert_eq!(first.prev_hash, LogHash::ZERO);
7738        assert_eq!(first.hash, first.recompute_hash());
7739        assert_eq!(second.index, 2);
7740        assert_eq!(second.prev_hash, first.hash);
7741        assert_eq!(second.hash, second.recompute_hash());
7742    }
7743
7744    #[test]
7745    fn cached_phase_zero_priorities_do_not_resample_randomness() {
7746        let consensus = ThreeNodeConsensus::from_recorders_with_ids(
7747            "cluster",
7748            "writer",
7749            1,
7750            1,
7751            ["n1", "n2", "n3"]
7752                .into_iter()
7753                .map(|recorder_id| {
7754                    (
7755                        recorder_id.into(),
7756                        Box::new(SlotRecorder {
7757                            recorder_id,
7758                            reject_slot: None,
7759                            observed: None,
7760                        }) as Box<dyn RecorderRpc>,
7761                    )
7762                })
7763                .collect(),
7764        )
7765        .unwrap()
7766        .with_priority_source(Arc::new(FailingPrioritySource));
7767        let command = StoredCommand::new(EntryType::Command, b"cached-priority".to_vec());
7768        let value = AcceptedValue::from_command("cluster", 1, 1, 1, LogHash::ZERO, &command);
7769        let mut progress =
7770            ProposerProgress::new(1, Proposal::new(ProposalPriority::MAX, "writer", 1, value))
7771                .with_command(command);
7772        progress.step = 0;
7773        for recorder_id in ["n1", "n2", "n3"] {
7774            progress
7775                .phase_zero_priorities
7776                .insert((0, recorder_id.into()), ProposalPriority::from_u64(1));
7777        }
7778
7779        let DriveOutcome::Progress(progress) = consensus.drive(progress).unwrap() else {
7780            panic!("phase zero quorum should advance progress");
7781        };
7782        assert!(progress.phase_zero_priorities.is_empty());
7783    }
7784
7785    #[test]
7786    fn phase_zero_priorities_are_stable_only_for_pending_retries_in_the_current_round() {
7787        let source = Arc::new(CountingPrioritySource::default());
7788        let consensus = ThreeNodeConsensus::from_recorders_with_ids(
7789            "cluster",
7790            "writer",
7791            1,
7792            1,
7793            vec![
7794                (
7795                    "n1".into(),
7796                    Box::new(SlotRecorder {
7797                        recorder_id: "n1",
7798                        reject_slot: None,
7799                        observed: None,
7800                    }) as Box<dyn RecorderRpc>,
7801                ),
7802                ("n2".into(), Box::new(AlwaysIoRecorder)),
7803                ("n3".into(), Box::new(AlwaysIoRecorder)),
7804            ],
7805        )
7806        .unwrap()
7807        .with_priority_source(source.clone());
7808        let command = StoredCommand::new(EntryType::Command, b"bounded-priorities".to_vec());
7809        let value = AcceptedValue::from_command("cluster", 1, 1, 1, LogHash::ZERO, &command);
7810        let mut progress =
7811            ProposerProgress::new(1, Proposal::new(ProposalPriority::MAX, "writer", 1, value))
7812                .with_command(command);
7813        progress.step = 0;
7814
7815        let DriveOutcome::Pending(mut progress) = consensus.drive(progress).unwrap() else {
7816            panic!("one recorder reply should leave progress pending");
7817        };
7818        assert_eq!(progress.phase_zero_priorities.len(), 3);
7819        assert_eq!(source.samples.load(Ordering::Relaxed), 3);
7820
7821        let DriveOutcome::Pending(retry) = consensus.drive(progress.clone()).unwrap() else {
7822            panic!("same-round retry should remain pending");
7823        };
7824        assert_eq!(retry.phase_zero_priorities, progress.phase_zero_priorities);
7825        assert_eq!(source.samples.load(Ordering::Relaxed), 3);
7826
7827        for round in 1..=64 {
7828            progress.step = round * 4;
7829            let DriveOutcome::Pending(next) = consensus.drive(progress).unwrap() else {
7830                panic!("one recorder reply should leave progress pending");
7831            };
7832            assert_eq!(next.phase_zero_priorities.len(), 3);
7833            assert!(next
7834                .phase_zero_priorities
7835                .keys()
7836                .all(|(cached_round, _)| *cached_round == round));
7837            progress = next;
7838        }
7839        assert_eq!(source.samples.load(Ordering::Relaxed), 3 * 65);
7840    }
7841
7842    #[test]
7843    fn phase_zero_priorities_are_cleared_when_progress_catches_up() {
7844        let consensus = ThreeNodeConsensus::from_recorders_with_ids(
7845            "cluster",
7846            "writer",
7847            1,
7848            1,
7849            ["n1", "n2", "n3"]
7850                .into_iter()
7851                .map(|recorder_id| {
7852                    (
7853                        recorder_id.into(),
7854                        Box::new(CatchUpRecorder {
7855                            recorder_id,
7856                            step: 8,
7857                        }) as Box<dyn RecorderRpc>,
7858                    )
7859                })
7860                .collect(),
7861        )
7862        .unwrap()
7863        .with_priority_source(Arc::new(FailingPrioritySource));
7864        let command = StoredCommand::new(EntryType::Command, b"catch-up".to_vec());
7865        let value = AcceptedValue::from_command("cluster", 1, 1, 1, LogHash::ZERO, &command);
7866        let mut progress =
7867            ProposerProgress::new(1, Proposal::new(ProposalPriority::MAX, "writer", 1, value))
7868                .with_command(command);
7869        progress.step = 0;
7870        for recorder_id in ["n1", "n2", "n3"] {
7871            progress
7872                .phase_zero_priorities
7873                .insert((0, recorder_id.into()), ProposalPriority::from_u64(1));
7874        }
7875
7876        let DriveOutcome::Progress(progress) = consensus.drive(progress).unwrap() else {
7877            panic!("higher recorder steps should catch progress up");
7878        };
7879        assert_eq!(progress.step, 8);
7880        assert!(progress.phase_zero_priorities.is_empty());
7881    }
7882
7883    #[test]
7884    fn wal_command_cache_rejects_same_hash_with_different_payload() {
7885        let hash = LogHash::digest(&[b"forced-cache-key"]);
7886        let first = StoredCommand::new(EntryType::Command, b"first".to_vec());
7887        let second = StoredCommand::new(EntryType::Command, b"second".to_vec());
7888        let mut commands = HashMap::new();
7889
7890        upsert_wal_command(&mut commands, hash, &first).unwrap();
7891        upsert_wal_command(&mut commands, hash, &first).unwrap();
7892        assert_eq!(
7893            upsert_wal_command(&mut commands, hash, &second),
7894            Err(Error::CommandHashMismatch)
7895        );
7896        assert_eq!(commands.len(), 1);
7897    }
7898
7899    #[test]
7900    fn normal_record_uses_one_file_sync_and_no_directory_barrier() {
7901        let root = tempfile::tempdir().unwrap();
7902        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
7903        let store = RecorderFileStore::new_with_membership(
7904            root.path(),
7905            "n1",
7906            "cluster",
7907            1,
7908            1,
7909            membership.clone(),
7910        )
7911        .unwrap();
7912        let command = StoredCommand::new(EntryType::Command, b"barrier-count".to_vec());
7913        store
7914            .store_command(command.hash(), command.clone())
7915            .unwrap();
7916        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
7917        reset_sync_counts();
7918
7919        store
7920            .record_proposal(RecordRequest {
7921                cluster_id: "cluster".into(),
7922                epoch: 1,
7923                config_id: 1,
7924                config_digest: membership.digest(),
7925                slot: 8,
7926                step: 4,
7927                proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
7928                command: None,
7929            })
7930            .unwrap();
7931
7932        assert_eq!(sync_counts(), (1, 0));
7933        assert!(!root.path().join("slot-head.intent").exists());
7934
7935        let inline = StoredCommand::new(EntryType::Command, b"inline-command".to_vec());
7936        let inline_value = AcceptedValue::from_command("cluster", 9, 1, 1, LogHash::ZERO, &inline);
7937        reset_sync_counts();
7938        store
7939            .record_proposal(RecordRequest {
7940                cluster_id: "cluster".into(),
7941                epoch: 1,
7942                config_id: 1,
7943                config_digest: membership.digest(),
7944                slot: 9,
7945                step: 4,
7946                proposal: Proposal::new(ProposalPriority::MAX, "writer", 2, inline_value),
7947                command: Some(inline),
7948            })
7949            .unwrap();
7950
7951        assert_eq!(sync_counts(), (1, 0));
7952    }
7953
7954    #[test]
7955    fn wal_durable_command_store_adds_no_sync_and_survives_proof_checkpoint_recovery() {
7956        let root = tempfile::tempdir().unwrap();
7957        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
7958        let command = StoredCommand::new(EntryType::Command, b"proof-worker-command".to_vec());
7959        let command_hash = command.hash();
7960        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
7961        let proposal = Proposal::new(ProposalPriority::MAX, "writer", 1, value);
7962        let proof = DecisionProof::FastPath {
7963            cluster_id: "cluster".into(),
7964            slot: 8,
7965            epoch: 1,
7966            config_id: 1,
7967            config_digest: membership.digest(),
7968            proposal: proposal.clone(),
7969            summaries: ["n1", "n2"]
7970                .into_iter()
7971                .map(|recorder_id| RecorderSummary {
7972                    recorder_id: recorder_id.into(),
7973                    slot: 8,
7974                    step: 4,
7975                    first_current: Some(proposal.clone()),
7976                    aggregate_prior: None,
7977                })
7978                .collect(),
7979        };
7980        let store = RecorderFileStore::new_with_membership(
7981            root.path(),
7982            "n1",
7983            "cluster",
7984            1,
7985            1,
7986            membership.clone(),
7987        )
7988        .unwrap();
7989        store
7990            .record_proposal(RecordRequest {
7991                cluster_id: "cluster".into(),
7992                epoch: 1,
7993                config_id: 1,
7994                config_digest: membership.digest(),
7995                slot: 8,
7996                step: 4,
7997                proposal,
7998                command: Some(command.clone()),
7999            })
8000            .unwrap();
8001
8002        reset_sync_counts();
8003        RecorderRpc::store_command_for(
8004            &store,
8005            "cluster".into(),
8006            1,
8007            1,
8008            membership.digest(),
8009            command_hash,
8010            command.clone(),
8011        )
8012        .unwrap();
8013        assert_eq!(sync_counts(), (0, 0));
8014        assert!(!store.command_path(command_hash).exists());
8015
8016        store
8017            .install_decision_proof_record(proof.clone(), &membership)
8018            .unwrap();
8019        drop(store);
8020
8021        let reopened = RecorderFileStore::new_with_membership(
8022            root.path(),
8023            "n1",
8024            "cluster",
8025            1,
8026            1,
8027            membership.clone(),
8028        )
8029        .unwrap();
8030        assert_eq!(
8031            reopened.fetch_command(command_hash).unwrap(),
8032            Some(command.clone())
8033        );
8034        assert_eq!(reopened.load(8).unwrap().decision_proof(), Some(&proof));
8035        reopened.checkpoint_wal_unlocked().unwrap();
8036        assert!(reopened.command_path(command_hash).exists());
8037        drop(reopened);
8038
8039        let checkpointed =
8040            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8041                .unwrap();
8042        assert_eq!(
8043            checkpointed.fetch_command(command_hash).unwrap(),
8044            Some(command)
8045        );
8046        assert_eq!(checkpointed.load(8).unwrap().decision_proof(), Some(&proof));
8047    }
8048
8049    #[test]
8050    fn duplicate_command_file_store_keeps_the_root_directory_barrier() {
8051        let root = tempfile::tempdir().unwrap();
8052        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8053        let store =
8054            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8055                .unwrap();
8056        let command = StoredCommand::new(EntryType::Command, b"durable-command".to_vec());
8057        store
8058            .store_command(command.hash(), command.clone())
8059            .unwrap();
8060        reset_sync_counts();
8061
8062        store.store_command(command.hash(), command).unwrap();
8063
8064        assert_eq!(sync_counts(), (0, 1));
8065    }
8066
8067    #[test]
8068    fn direct_store_command_rejects_a_claimed_hash_without_creating_a_file() {
8069        let root = tempfile::tempdir().unwrap();
8070        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8071        let store = RecorderFileStore::new_with_membership(
8072            root.path(),
8073            "n1",
8074            "cluster",
8075            1,
8076            1,
8077            membership.clone(),
8078        )
8079        .unwrap();
8080        let command = StoredCommand::new(EntryType::Command, b"mismatched-hash".to_vec());
8081        let claimed_hash = LogHash::digest(&[b"claimed-hash"]);
8082
8083        assert_eq!(
8084            store.apply(RecorderRequest::StoreCommand {
8085                cluster_id: "cluster".into(),
8086                epoch: 1,
8087                config_id: 1,
8088                config_digest: membership.digest(),
8089                command_hash: claimed_hash,
8090                command,
8091            }),
8092            Err(Error::CommandHashMismatch)
8093        );
8094        assert!(!store.command_path(claimed_hash).exists());
8095    }
8096
8097    #[test]
8098    fn wal_command_store_rejects_conflicting_bytes_without_syncing() {
8099        let root = tempfile::tempdir().unwrap();
8100        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8101        let store =
8102            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8103                .unwrap();
8104        let command = StoredCommand::new(EntryType::Command, b"expected".to_vec());
8105        let conflicting = StoredCommand::new(EntryType::Command, b"conflicting".to_vec());
8106        store
8107            .wal
8108            .lock()
8109            .unwrap()
8110            .commands
8111            .insert(command.hash(), conflicting);
8112        reset_sync_counts();
8113
8114        assert_eq!(
8115            store.store_command(command.hash(), command.clone()),
8116            Err(Error::CommandHashMismatch)
8117        );
8118        assert_eq!(sync_counts(), (0, 0));
8119        assert!(!store.command_path(command.hash()).exists());
8120    }
8121
8122    #[test]
8123    fn prestored_command_resolution_revalidates_the_durable_file() {
8124        let root = tempfile::tempdir().unwrap();
8125        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8126        let store = RecorderFileStore::new_with_membership(
8127            root.path(),
8128            "n1",
8129            "cluster",
8130            1,
8131            1,
8132            membership.clone(),
8133        )
8134        .unwrap();
8135        let command = StoredCommand::new(EntryType::Command, b"pre-stored-command".to_vec());
8136        store
8137            .store_command(command.hash(), command.clone())
8138            .unwrap();
8139        reset_command_file_reads();
8140
8141        for slot in [8, 9] {
8142            let value = AcceptedValue::from_command("cluster", slot, 1, 1, LogHash::ZERO, &command);
8143            store
8144                .record_proposal(RecordRequest {
8145                    cluster_id: "cluster".into(),
8146                    epoch: 1,
8147                    config_id: 1,
8148                    config_digest: membership.digest(),
8149                    slot,
8150                    step: 4,
8151                    proposal: Proposal::new(ProposalPriority::MAX, "writer", slot, value),
8152                    command: None,
8153                })
8154                .unwrap();
8155        }
8156
8157        assert_eq!(command_file_reads(), 2);
8158    }
8159
8160    #[test]
8161    fn commandless_record_rejects_a_prestored_command_replaced_while_open() {
8162        let root = tempfile::tempdir().unwrap();
8163        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8164        let store = RecorderFileStore::new_with_membership(
8165            root.path(),
8166            "n1",
8167            "cluster",
8168            1,
8169            1,
8170            membership.clone(),
8171        )
8172        .unwrap();
8173        let command = StoredCommand::new(EntryType::Command, b"pre-stored".to_vec());
8174        let replacement = StoredCommand::new(EntryType::Command, b"replacement".to_vec());
8175        let command_hash = command.hash();
8176        store.store_command(command_hash, command.clone()).unwrap();
8177        std::fs::write(
8178            store.command_path(command_hash),
8179            encode_stored_command(&replacement),
8180        )
8181        .unwrap();
8182        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
8183
8184        assert_eq!(
8185            store.record_proposal(RecordRequest {
8186                cluster_id: "cluster".into(),
8187                epoch: 1,
8188                config_id: 1,
8189                config_digest: membership.digest(),
8190                slot: 8,
8191                step: 4,
8192                proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value.clone()),
8193                command: None,
8194            }),
8195            Err(Error::CommandHashMismatch)
8196        );
8197        assert_eq!(store.load(8).unwrap().isr.step(), 0);
8198        drop(store);
8199
8200        let reopened = RecorderFileStore::new_with_membership(
8201            root.path(),
8202            "n1",
8203            "cluster",
8204            1,
8205            1,
8206            membership.clone(),
8207        )
8208        .unwrap();
8209        assert_eq!(
8210            reopened.record_proposal(RecordRequest {
8211                cluster_id: "cluster".into(),
8212                epoch: 1,
8213                config_id: 1,
8214                config_digest: membership.digest(),
8215                slot: 8,
8216                step: 4,
8217                proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
8218                command: None,
8219            }),
8220            Err(Error::CommandHashMismatch)
8221        );
8222        assert_eq!(reopened.load(8).unwrap().isr.step(), 0);
8223    }
8224
8225    #[test]
8226    fn duplicate_store_rejects_a_prestored_command_replaced_while_open() {
8227        let root = tempfile::tempdir().unwrap();
8228        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8229        let store =
8230            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8231                .unwrap();
8232        let command = StoredCommand::new(EntryType::Command, b"pre-stored".to_vec());
8233        let replacement = StoredCommand::new(EntryType::Command, b"replacement".to_vec());
8234        let command_hash = command.hash();
8235        store.store_command(command_hash, command.clone()).unwrap();
8236        std::fs::write(
8237            store.command_path(command_hash),
8238            encode_stored_command(&replacement),
8239        )
8240        .unwrap();
8241
8242        assert_eq!(
8243            store.store_command(command_hash, command),
8244            Err(Error::CommandHashMismatch)
8245        );
8246    }
8247
8248    #[test]
8249    fn record_rejects_mismatched_inline_command_independent_of_prestored_command_state() {
8250        for prestored in [false, true] {
8251            let root = tempfile::tempdir().unwrap();
8252            let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8253            let store = RecorderFileStore::new_with_membership(
8254                root.path(),
8255                "n1",
8256                "cluster",
8257                1,
8258                1,
8259                membership.clone(),
8260            )
8261            .unwrap();
8262            let expected = StoredCommand::new(EntryType::Command, b"expected".to_vec());
8263            if prestored {
8264                store
8265                    .store_command(expected.hash(), expected.clone())
8266                    .unwrap();
8267            }
8268            let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &expected);
8269            let mismatched = StoredCommand::new(EntryType::Command, b"mismatched".to_vec());
8270
8271            assert_eq!(
8272                store.record_proposal(RecordRequest {
8273                    cluster_id: "cluster".into(),
8274                    epoch: 1,
8275                    config_id: 1,
8276                    config_digest: membership.digest(),
8277                    slot: 8,
8278                    step: 4,
8279                    proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
8280                    command: Some(mismatched),
8281                }),
8282                Err(Error::Rejected(RejectReason::InvalidValue)),
8283                "prestored={prestored}"
8284            );
8285        }
8286    }
8287
8288    #[test]
8289    fn inline_record_uses_the_bound_command_without_reading_the_durable_command_file() {
8290        let root = tempfile::tempdir().unwrap();
8291        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8292        let command = StoredCommand::new(EntryType::Command, b"inline-hot-path".to_vec());
8293        let command_hash = command.hash();
8294        let store = RecorderFileStore::new_with_membership(
8295            root.path(),
8296            "n1",
8297            "cluster",
8298            1,
8299            1,
8300            membership.clone(),
8301        )
8302        .unwrap();
8303        std::fs::write(store.command_path(command_hash), b"corrupt cache entry").unwrap();
8304        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
8305        reset_command_file_reads();
8306
8307        store
8308            .record_proposal(RecordRequest {
8309                cluster_id: "cluster".into(),
8310                epoch: 1,
8311                config_id: 1,
8312                config_digest: membership.digest(),
8313                slot: 8,
8314                step: 4,
8315                proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
8316                command: Some(command.clone()),
8317            })
8318            .unwrap();
8319
8320        assert_eq!(command_file_reads(), 0);
8321        assert_eq!(
8322            store.fetch_command(command_hash).unwrap(),
8323            Some(command.clone())
8324        );
8325        drop(store);
8326
8327        reset_command_file_reads();
8328        let reopened =
8329            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8330                .unwrap();
8331        assert_eq!(command_file_reads(), 0);
8332        assert_eq!(reopened.fetch_command(command_hash).unwrap(), Some(command));
8333    }
8334
8335    #[test]
8336    fn record_rejects_malformed_config_change_piggyback_as_invalid_value_before_parsing() {
8337        let root = tempfile::tempdir().unwrap();
8338        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8339        let store = RecorderFileStore::new_with_membership(
8340            root.path(),
8341            "n1",
8342            "cluster",
8343            1,
8344            1,
8345            membership.clone(),
8346        )
8347        .unwrap();
8348        let expected = StoredCommand::new(EntryType::Command, b"expected".to_vec());
8349        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &expected);
8350        let malformed = StoredCommand::new(EntryType::ConfigChange, b"malformed".to_vec());
8351
8352        assert_eq!(
8353            store.record_proposal(RecordRequest {
8354                cluster_id: "cluster".into(),
8355                epoch: 1,
8356                config_id: 1,
8357                config_digest: membership.digest(),
8358                slot: 8,
8359                step: 4,
8360                proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
8361                command: Some(malformed),
8362            }),
8363            Err(Error::Rejected(RejectReason::InvalidValue))
8364        );
8365    }
8366
8367    #[test]
8368    fn wal_append_uses_the_platform_safe_file_sync() {
8369        let file = tempfile::tempfile().unwrap();
8370        reset_sync_counts();
8371
8372        sync_wal_append(&file).unwrap();
8373
8374        #[cfg(target_os = "linux")]
8375        assert_eq!(last_file_sync_kind(), Some(FileSyncKind::Data));
8376        #[cfg(not(target_os = "linux"))]
8377        assert_eq!(last_file_sync_kind(), Some(FileSyncKind::All));
8378    }
8379
8380    #[test]
8381    fn wal_metadata_changes_keep_full_file_sync() {
8382        let file = tempfile::tempfile().unwrap();
8383        reset_sync_counts();
8384
8385        sync_wal_metadata(&file).unwrap();
8386
8387        assert_eq!(last_file_sync_kind(), Some(FileSyncKind::All));
8388    }
8389
8390    #[test]
8391    fn wal_replays_acknowledged_records_after_reopen() {
8392        let root = tempfile::tempdir().unwrap();
8393        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8394        let command = StoredCommand::new(EntryType::Command, b"wal-reopen".to_vec());
8395        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
8396        {
8397            let store = RecorderFileStore::new_with_membership(
8398                root.path(),
8399                "n1",
8400                "cluster",
8401                1,
8402                1,
8403                membership.clone(),
8404            )
8405            .unwrap();
8406            store
8407                .record_proposal(RecordRequest {
8408                    cluster_id: "cluster".into(),
8409                    epoch: 1,
8410                    config_id: 1,
8411                    config_digest: membership.digest(),
8412                    slot: 8,
8413                    step: 4,
8414                    proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
8415                    command: Some(command.clone()),
8416                })
8417                .unwrap();
8418        }
8419
8420        let reopened =
8421            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8422                .unwrap();
8423        assert_eq!(
8424            reopened.fetch_command(command.hash()).unwrap(),
8425            Some(command)
8426        );
8427        assert_eq!(reopened.load(8).unwrap().isr.step(), 4);
8428    }
8429
8430    #[test]
8431    fn wal_sync_fault_never_acknowledges_before_the_durable_frame_is_replayable() {
8432        let root = tempfile::tempdir().unwrap();
8433        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8434        let command = StoredCommand::new(EntryType::Command, b"wal-sync-fault".to_vec());
8435        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
8436        {
8437            let store = RecorderFileStore::new_with_membership(
8438                root.path(),
8439                "n1",
8440                "cluster",
8441                1,
8442                1,
8443                membership.clone(),
8444            )
8445            .unwrap();
8446            store
8447                .set_seal_fault(Some(SealFaultPoint::AfterWalSync))
8448                .unwrap();
8449            assert!(matches!(
8450                store.record_proposal(RecordRequest {
8451                    cluster_id: "cluster".into(),
8452                    epoch: 1,
8453                    config_id: 1,
8454                    config_digest: membership.digest(),
8455                    slot: 8,
8456                    step: 4,
8457                    proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
8458                    command: Some(command.clone()),
8459                }),
8460                Err(Error::Io(message)) if message.contains("AfterWalSync")
8461            ));
8462            assert_eq!(
8463                store
8464                    .configuration_state()
8465                    .unwrap()
8466                    .max_accepted_or_decided_slot(),
8467                None
8468            );
8469            assert_eq!(store.load(8).unwrap().isr.step(), 0);
8470        }
8471
8472        let reopened =
8473            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8474                .unwrap();
8475        assert_eq!(
8476            reopened.fetch_command(command.hash()).unwrap(),
8477            Some(command)
8478        );
8479        assert_eq!(reopened.load(8).unwrap().isr.step(), 4);
8480    }
8481
8482    #[test]
8483    fn wal_ignores_a_torn_final_frame_but_replays_the_committed_prefix() {
8484        let root = tempfile::tempdir().unwrap();
8485        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8486        let first = StoredCommand::new(EntryType::Command, b"wal-first".to_vec());
8487        let second = StoredCommand::new(EntryType::Command, b"wal-second".to_vec());
8488        {
8489            let store = RecorderFileStore::new_with_membership(
8490                root.path(),
8491                "n1",
8492                "cluster",
8493                1,
8494                1,
8495                membership.clone(),
8496            )
8497            .unwrap();
8498            for (slot, command) in [(8, first.clone()), (9, second.clone())] {
8499                let value =
8500                    AcceptedValue::from_command("cluster", slot, 1, 1, LogHash::ZERO, &command);
8501                store
8502                    .record_proposal(RecordRequest {
8503                        cluster_id: "cluster".into(),
8504                        epoch: 1,
8505                        config_id: 1,
8506                        config_digest: membership.digest(),
8507                        slot,
8508                        step: 4,
8509                        proposal: Proposal::new(ProposalPriority::MAX, "writer", slot, value),
8510                        command: Some(command),
8511                    })
8512                    .unwrap();
8513            }
8514        }
8515        let wal = root.path().join("recorder.wal");
8516        let len = std::fs::metadata(&wal).unwrap().len();
8517        std::fs::OpenOptions::new()
8518            .write(true)
8519            .open(&wal)
8520            .unwrap()
8521            .set_len(len - 7)
8522            .unwrap();
8523
8524        let reopened =
8525            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8526                .unwrap();
8527        assert_eq!(reopened.fetch_command(first.hash()).unwrap(), Some(first));
8528        assert_eq!(reopened.load(8).unwrap().isr.step(), 4);
8529        assert_eq!(reopened.fetch_command(second.hash()).unwrap(), None);
8530        assert_eq!(reopened.load(9).unwrap().isr.step(), 0);
8531    }
8532
8533    #[test]
8534    fn recorder_preflight_distinguishes_absent_from_valid_without_mutation() {
8535        let root = tempfile::tempdir().unwrap();
8536        let absent = root.path().join("absent");
8537        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8538        assert_eq!(
8539            RecorderFileStore::preflight_existing_with_membership_outcome(
8540                &absent,
8541                "cluster",
8542                1,
8543                1,
8544                &membership,
8545            )
8546            .unwrap(),
8547            RecorderPreflight::Missing
8548        );
8549        assert!(!absent.exists());
8550
8551        let valid = root.path().join("valid");
8552        drop(
8553            RecorderFileStore::new_with_membership(
8554                &valid,
8555                "n1",
8556                "cluster",
8557                1,
8558                1,
8559                membership.clone(),
8560            )
8561            .unwrap(),
8562        );
8563        let before = directory_files(&valid);
8564        assert_eq!(
8565            RecorderFileStore::preflight_existing_with_membership_outcome(
8566                &valid,
8567                "cluster",
8568                1,
8569                1,
8570                &membership,
8571            )
8572            .unwrap(),
8573            RecorderPreflight::Valid
8574        );
8575        assert_eq!(directory_files(&valid), before);
8576    }
8577
8578    #[test]
8579    fn recorder_preflight_rejects_partial_or_foreign_state_without_mutation() {
8580        let root = tempfile::tempdir().unwrap();
8581        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8582        let partial = root.path().join("partial");
8583        std::fs::create_dir(&partial).unwrap();
8584        std::fs::write(partial.join("recorded-head.rec"), b"partial").unwrap();
8585        let partial_before = directory_files(&partial);
8586        assert!(
8587            RecorderFileStore::preflight_existing_with_membership_outcome(
8588                &partial,
8589                "cluster",
8590                1,
8591                1,
8592                &membership,
8593            )
8594            .is_err()
8595        );
8596        assert_eq!(directory_files(&partial), partial_before);
8597
8598        let valid = root.path().join("foreign");
8599        drop(
8600            RecorderFileStore::new_with_membership(
8601                &valid,
8602                "n1",
8603                "cluster",
8604                1,
8605                1,
8606                membership.clone(),
8607            )
8608            .unwrap(),
8609        );
8610        let before = directory_files(&valid);
8611        let other_membership = Membership::new(["n1", "n2", "n4"]).unwrap();
8612        assert!(
8613            RecorderFileStore::preflight_existing_with_membership_outcome(
8614                &valid,
8615                "cluster",
8616                1,
8617                2,
8618                &membership,
8619            )
8620            .is_err()
8621        );
8622        assert!(
8623            RecorderFileStore::preflight_existing_with_membership_outcome(
8624                &valid,
8625                "cluster",
8626                1,
8627                1,
8628                &other_membership,
8629            )
8630            .is_err()
8631        );
8632        assert_eq!(directory_files(&valid), before);
8633    }
8634
8635    #[test]
8636    fn recorder_preflight_validates_command_cache_backed_values_without_mutation() {
8637        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8638        for case in ["valid", "missing", "corrupt", "foreign"] {
8639            let temp = tempfile::tempdir().unwrap();
8640            let root = temp.path().join(case);
8641            let (command_hash, command) =
8642                cache_backed_recorder_for_preflight(&root, membership.clone());
8643            let command_path = root.join(format!("command-{}.cmd", command_hash.to_hex()));
8644            match case {
8645                "valid" => {}
8646                "missing" => std::fs::remove_file(&command_path).unwrap(),
8647                "corrupt" => std::fs::write(&command_path, b"corrupt command cache").unwrap(),
8648                "foreign" => std::fs::write(
8649                    &command_path,
8650                    encode_stored_command(&StoredCommand::new(
8651                        EntryType::Command,
8652                        b"different command".to_vec(),
8653                    )),
8654                )
8655                .unwrap(),
8656                _ => unreachable!(),
8657            }
8658            let before = directory_files(&root);
8659            let result = RecorderFileStore::preflight_existing_with_membership_outcome(
8660                &root,
8661                "cluster",
8662                1,
8663                1,
8664                &membership,
8665            );
8666            if case == "valid" {
8667                assert_eq!(result.unwrap(), RecorderPreflight::Valid, "{case}");
8668                assert_eq!(
8669                    std::fs::read(&command_path).unwrap(),
8670                    encode_stored_command(&command),
8671                    "{case}"
8672                );
8673            } else {
8674                assert!(result.is_err(), "{case}");
8675            }
8676            assert_eq!(directory_files(&root), before, "{case}");
8677        }
8678    }
8679
8680    #[test]
8681    fn recorder_preflight_rejects_nonregular_command_cache_without_mutation() {
8682        let temp = tempfile::tempdir().unwrap();
8683        let root = temp.path().join("recorder");
8684        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8685        let (command_hash, _) = cache_backed_recorder_for_preflight(&root, membership.clone());
8686        let command_path = root.join(format!("command-{}.cmd", command_hash.to_hex()));
8687        std::fs::remove_file(&command_path).unwrap();
8688        std::fs::create_dir(&command_path).unwrap();
8689        let before = directory_files(&root);
8690
8691        assert!(
8692            RecorderFileStore::preflight_existing_with_membership_outcome(
8693                &root,
8694                "cluster",
8695                1,
8696                1,
8697                &membership,
8698            )
8699            .is_err()
8700        );
8701        assert_eq!(directory_files(&root), before);
8702    }
8703
8704    #[cfg(unix)]
8705    #[test]
8706    fn recorder_preflight_rejects_symlinked_command_cache_without_mutation() {
8707        use std::os::unix::fs::symlink;
8708
8709        let temp = tempfile::tempdir().unwrap();
8710        let root = temp.path().join("recorder");
8711        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8712        let (command_hash, command) =
8713            cache_backed_recorder_for_preflight(&root, membership.clone());
8714        let command_path = root.join(format!("command-{}.cmd", command_hash.to_hex()));
8715        let outside = temp.path().join("outside-command.cmd");
8716        std::fs::write(&outside, encode_stored_command(&command)).unwrap();
8717        std::fs::remove_file(&command_path).unwrap();
8718        symlink(&outside, &command_path).unwrap();
8719
8720        assert!(
8721            RecorderFileStore::preflight_existing_with_membership_outcome(
8722                &root,
8723                "cluster",
8724                1,
8725                1,
8726                &membership,
8727            )
8728            .is_err()
8729        );
8730        assert!(std::fs::symlink_metadata(&command_path)
8731            .unwrap()
8732            .file_type()
8733            .is_symlink());
8734        assert_eq!(
8735            std::fs::read(&outside).unwrap(),
8736            encode_stored_command(&command)
8737        );
8738    }
8739
8740    #[test]
8741    fn recorder_preflight_rejects_command_cache_backed_value_mismatch_without_mutation() {
8742        let temp = tempfile::tempdir().unwrap();
8743        let root = temp.path().join("recorder");
8744        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8745        cache_backed_recorder_for_preflight(&root, membership.clone());
8746        let wal_path = root.join("recorder.wal");
8747        let original = std::fs::read(&wal_path).unwrap();
8748        let (frame, end) = decode_wal_frame(&original, 0).unwrap().unwrap();
8749        assert_eq!(end, original.len());
8750        let mut state = decode_recorder_state(&frame.slot_bytes).unwrap();
8751        state
8752            .isr
8753            .first_current
8754            .as_mut()
8755            .unwrap()
8756            .value
8757            .as_mut()
8758            .unwrap()
8759            .entry_hash = LogHash::ZERO;
8760        let configuration = decode_configuration_state(&frame.configuration_bytes).unwrap();
8761        let (tampered, _, _) = encode_wal_frame(
8762            frame.generation,
8763            frame.sequence,
8764            frame.prev_digest,
8765            &state,
8766            &configuration,
8767            &frame.head,
8768            None,
8769        )
8770        .unwrap();
8771        std::fs::write(&wal_path, tampered).unwrap();
8772        let before = directory_files(&root);
8773
8774        assert!(
8775            RecorderFileStore::preflight_existing_with_membership_outcome(
8776                &root,
8777                "cluster",
8778                1,
8779                1,
8780                &membership,
8781            )
8782            .is_err()
8783        );
8784        assert_eq!(directory_files(&root), before);
8785    }
8786
8787    #[test]
8788    fn recorder_preflight_allows_normal_configuration_intent_recovery_before_open() {
8789        let root = tempfile::tempdir().unwrap();
8790        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8791        let store = RecorderFileStore::new_with_membership(
8792            root.path(),
8793            "n1",
8794            "cluster",
8795            1,
8796            1,
8797            membership.clone(),
8798        )
8799        .unwrap();
8800        let configuration = store.configuration_state().unwrap();
8801        let state = RecorderSlotState::new_with_digest(8, "cluster", 1, 1, membership.digest());
8802        store
8803            .set_seal_fault(Some(SealFaultPoint::AfterIntent))
8804            .unwrap();
8805        assert!(store
8806            .commit_transition_unlocked(&state, &configuration)
8807            .is_err());
8808        drop(store);
8809        let before = directory_files(root.path());
8810
8811        assert_eq!(
8812            RecorderFileStore::preflight_existing_with_membership_outcome(
8813                root.path(),
8814                "cluster",
8815                1,
8816                1,
8817                &membership,
8818            )
8819            .unwrap(),
8820            RecorderPreflight::Recoverable,
8821        );
8822        assert_eq!(directory_files(root.path()), before);
8823        RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8824            .unwrap();
8825        assert!(!root.path().join("configuration.intent").exists());
8826    }
8827
8828    #[test]
8829    fn recorder_preflight_allows_torn_final_wal_recovery_before_open() {
8830        let root = tempfile::tempdir().unwrap();
8831        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8832        let command = StoredCommand::new(EntryType::Command, b"torn-preflight".to_vec());
8833        {
8834            let store = RecorderFileStore::new_with_membership(
8835                root.path(),
8836                "n1",
8837                "cluster",
8838                1,
8839                1,
8840                membership.clone(),
8841            )
8842            .unwrap();
8843            let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
8844            store
8845                .record_proposal(RecordRequest {
8846                    cluster_id: "cluster".into(),
8847                    epoch: 1,
8848                    config_id: 1,
8849                    config_digest: membership.digest(),
8850                    slot: 8,
8851                    step: 4,
8852                    proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
8853                    command: Some(command.clone()),
8854                })
8855                .unwrap();
8856        }
8857        let wal = root.path().join("recorder.wal");
8858        let len = std::fs::metadata(&wal).unwrap().len();
8859        std::fs::OpenOptions::new()
8860            .write(true)
8861            .open(&wal)
8862            .unwrap()
8863            .set_len(len - 7)
8864            .unwrap();
8865        let before = directory_files(root.path());
8866
8867        assert_eq!(
8868            RecorderFileStore::preflight_existing_with_membership_outcome(
8869                root.path(),
8870                "cluster",
8871                1,
8872                1,
8873                &membership,
8874            )
8875            .unwrap(),
8876            RecorderPreflight::Recoverable,
8877        );
8878        assert_eq!(directory_files(root.path()), before);
8879        let reopened =
8880            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
8881                .unwrap();
8882        assert_eq!(reopened.load(8).unwrap().isr.step(), 0);
8883    }
8884
8885    #[test]
8886    fn recorder_preflight_keeps_interior_wal_corruption_invalid_without_mutation() {
8887        let root = tempfile::tempdir().unwrap();
8888        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8889        let store = RecorderFileStore::new_with_membership(
8890            root.path(),
8891            "n1",
8892            "cluster",
8893            1,
8894            1,
8895            membership.clone(),
8896        )
8897        .unwrap();
8898        let command = StoredCommand::new(EntryType::Command, b"interior-preflight".to_vec());
8899        let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
8900        store
8901            .record_proposal(RecordRequest {
8902                cluster_id: "cluster".into(),
8903                epoch: 1,
8904                config_id: 1,
8905                config_digest: membership.digest(),
8906                slot: 8,
8907                step: 4,
8908                proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
8909                command: Some(command),
8910            })
8911            .unwrap();
8912        drop(store);
8913        let wal = root.path().join("recorder.wal");
8914        let mut bytes = std::fs::read(&wal).unwrap();
8915        bytes[100] ^= 0x80;
8916        std::fs::write(&wal, bytes).unwrap();
8917        let before = directory_files(root.path());
8918
8919        assert!(
8920            RecorderFileStore::preflight_existing_with_membership_outcome(
8921                root.path(),
8922                "cluster",
8923                1,
8924                1,
8925                &membership,
8926            )
8927            .is_err()
8928        );
8929        assert_eq!(directory_files(root.path()), before);
8930    }
8931
8932    #[test]
8933    fn recorder_preflight_rejects_oversized_wal_cache_and_intent_without_mutation() {
8934        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
8935        for (name, limit) in [
8936            ("recorder.wal", super::MAX_RECORDER_WAL_BYTES),
8937            ("configuration.intent", super::MAX_TRANSITION_INTENT_BYTES),
8938        ] {
8939            let root = tempfile::tempdir().unwrap();
8940            drop(
8941                RecorderFileStore::new_with_membership(
8942                    root.path(),
8943                    "n1",
8944                    "cluster",
8945                    1,
8946                    1,
8947                    membership.clone(),
8948                )
8949                .unwrap(),
8950            );
8951            let path = root.path().join(name);
8952            std::fs::File::create(&path)
8953                .unwrap()
8954                .set_len(limit as u64 + 1)
8955                .unwrap();
8956            let before_len = std::fs::symlink_metadata(&path).unwrap().len();
8957            assert!(
8958                RecorderFileStore::preflight_existing_with_membership_outcome(
8959                    root.path(),
8960                    "cluster",
8961                    1,
8962                    1,
8963                    &membership,
8964                )
8965                .is_err()
8966            );
8967            assert_eq!(std::fs::symlink_metadata(&path).unwrap().len(), before_len);
8968        }
8969
8970        let root = tempfile::tempdir().unwrap();
8971        let (command_hash, _) =
8972            cache_backed_recorder_for_preflight(root.path(), membership.clone());
8973        let path = root
8974            .path()
8975            .join(format!("command-{}.cmd", command_hash.to_hex()));
8976        std::fs::OpenOptions::new()
8977            .write(true)
8978            .open(&path)
8979            .unwrap()
8980            .set_len(super::MAX_COMMAND_CACHE_BYTES as u64 + 1)
8981            .unwrap();
8982        let before_len = std::fs::symlink_metadata(&path).unwrap().len();
8983        assert!(
8984            RecorderFileStore::preflight_existing_with_membership_outcome(
8985                root.path(),
8986                "cluster",
8987                1,
8988                1,
8989                &membership,
8990            )
8991            .is_err()
8992        );
8993        assert_eq!(std::fs::symlink_metadata(&path).unwrap().len(), before_len);
8994    }
8995
8996    #[cfg(unix)]
8997    #[test]
8998    fn recorder_preflight_rejects_symlinked_authority_files_without_mutation() {
8999        use std::os::unix::fs::symlink;
9000
9001        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9002        for name in [
9003            "configuration.rec",
9004            "recorded-head.rec",
9005            "recorder.wal",
9006            "configuration.intent",
9007            "configuration-head.intent",
9008        ] {
9009            let root = tempfile::tempdir().unwrap();
9010            drop(
9011                RecorderFileStore::new_with_membership(
9012                    root.path(),
9013                    "n1",
9014                    "cluster",
9015                    1,
9016                    1,
9017                    membership.clone(),
9018                )
9019                .unwrap(),
9020            );
9021            let path = root.path().join(name);
9022            let outside = root.path().join(format!("outside-{name}"));
9023            if path.exists() {
9024                std::fs::rename(&path, &outside).unwrap();
9025            } else {
9026                std::fs::write(&outside, b"intent").unwrap();
9027            }
9028            symlink(&outside, &path).unwrap();
9029
9030            assert!(
9031                RecorderFileStore::preflight_existing_with_membership_outcome(
9032                    root.path(),
9033                    "cluster",
9034                    1,
9035                    1,
9036                    &membership,
9037                )
9038                .is_err(),
9039                "{name}"
9040            );
9041            assert!(
9042                std::fs::symlink_metadata(&path)
9043                    .unwrap()
9044                    .file_type()
9045                    .is_symlink(),
9046                "{name}"
9047            );
9048        }
9049    }
9050
9051    #[test]
9052    fn recorder_open_revalidates_after_preflight_before_recovery_mutation() {
9053        let root = tempfile::tempdir().unwrap();
9054        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9055        drop(
9056            RecorderFileStore::new_with_membership(
9057                root.path(),
9058                "n1",
9059                "cluster",
9060                1,
9061                1,
9062                membership.clone(),
9063            )
9064            .unwrap(),
9065        );
9066        assert_eq!(
9067            RecorderFileStore::preflight_existing_with_membership_outcome(
9068                root.path(),
9069                "cluster",
9070                1,
9071                1,
9072                &membership,
9073            )
9074            .unwrap(),
9075            RecorderPreflight::Valid,
9076        );
9077        let configuration = root.path().join("configuration.rec");
9078        std::fs::write(&configuration, b"replaced after preflight").unwrap();
9079        let before = directory_files(root.path());
9080
9081        assert!(RecorderFileStore::new_with_membership(
9082            root.path(),
9083            "n1",
9084            "cluster",
9085            1,
9086            1,
9087            membership,
9088        )
9089        .is_err());
9090        assert_eq!(directory_files(root.path()), before);
9091    }
9092
9093    #[test]
9094    fn existing_open_never_recreates_a_deleted_or_replaced_valid_recorder_root() {
9095        let parent = tempfile::tempdir().unwrap();
9096        let root = parent.path().join("recorder");
9097        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9098        drop(
9099            RecorderFileStore::new_with_membership(
9100                &root,
9101                "n1",
9102                "cluster",
9103                1,
9104                1,
9105                membership.clone(),
9106            )
9107            .unwrap(),
9108        );
9109        assert_eq!(
9110            RecorderFileStore::preflight_existing_with_membership_outcome(
9111                &root,
9112                "cluster",
9113                1,
9114                1,
9115                &membership,
9116            )
9117            .unwrap(),
9118            RecorderPreflight::Valid,
9119        );
9120        std::fs::remove_dir_all(&root).unwrap();
9121        let parent_before = directory_files(parent.path());
9122        assert!(RecorderFileStore::open_existing_with_membership(
9123            &root,
9124            "n1",
9125            "cluster",
9126            1,
9127            1,
9128            membership.clone(),
9129        )
9130        .is_err());
9131        assert!(!root.exists());
9132        assert_eq!(directory_files(parent.path()), parent_before);
9133
9134        std::fs::create_dir(&root).unwrap();
9135        std::fs::write(root.join("replacement"), b"foreign").unwrap();
9136        let parent_before = directory_files(parent.path());
9137        assert!(RecorderFileStore::open_existing_with_membership(
9138            &root, "n1", "cluster", 1, 1, membership,
9139        )
9140        .is_err());
9141        assert!(!root.join(".recorder.lock").exists());
9142        assert_eq!(directory_files(parent.path()), parent_before);
9143    }
9144
9145    #[test]
9146    fn existing_open_never_recreates_a_deleted_or_replaced_recoverable_recorder_root() {
9147        let parent = tempfile::tempdir().unwrap();
9148        let root = parent.path().join("recorder");
9149        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9150        let store = RecorderFileStore::new_with_membership(
9151            &root,
9152            "n1",
9153            "cluster",
9154            1,
9155            1,
9156            membership.clone(),
9157        )
9158        .unwrap();
9159        let configuration = store.configuration_state().unwrap();
9160        let state = RecorderSlotState::new_with_digest(8, "cluster", 1, 1, membership.digest());
9161        store
9162            .set_seal_fault(Some(SealFaultPoint::AfterIntent))
9163            .unwrap();
9164        assert!(store
9165            .commit_transition_unlocked(&state, &configuration)
9166            .is_err());
9167        drop(store);
9168        assert_eq!(
9169            RecorderFileStore::preflight_existing_with_membership_outcome(
9170                &root,
9171                "cluster",
9172                1,
9173                1,
9174                &membership,
9175            )
9176            .unwrap(),
9177            RecorderPreflight::Recoverable,
9178        );
9179        std::fs::remove_dir_all(&root).unwrap();
9180        let parent_before = directory_files(parent.path());
9181        assert!(RecorderFileStore::open_existing_with_membership(
9182            &root,
9183            "n1",
9184            "cluster",
9185            1,
9186            1,
9187            membership.clone(),
9188        )
9189        .is_err());
9190        assert!(!root.exists());
9191        assert_eq!(directory_files(parent.path()), parent_before);
9192
9193        std::fs::create_dir(&root).unwrap();
9194        std::fs::write(root.join("replacement"), b"foreign").unwrap();
9195        let parent_before = directory_files(parent.path());
9196        assert!(RecorderFileStore::open_existing_with_membership(
9197            &root, "n1", "cluster", 1, 1, membership,
9198        )
9199        .is_err());
9200        assert!(!root.join(".recorder.lock").exists());
9201        assert_eq!(directory_files(parent.path()), parent_before);
9202    }
9203
9204    #[cfg(unix)]
9205    #[test]
9206    fn existing_open_rejects_a_recorder_root_symlink_without_touching_its_target() {
9207        use std::os::unix::fs::symlink;
9208
9209        let parent = tempfile::tempdir().unwrap();
9210        let target = tempfile::tempdir().unwrap();
9211        let root = parent.path().join("recorder");
9212        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9213        std::fs::write(target.path().join("sentinel"), b"untouched").unwrap();
9214        let before = directory_files(target.path());
9215        symlink(target.path(), &root).unwrap();
9216
9217        assert!(RecorderFileStore::open_existing_with_membership(
9218            &root, "n1", "cluster", 1, 1, membership,
9219        )
9220        .is_err());
9221        assert!(std::fs::symlink_metadata(&root)
9222            .unwrap()
9223            .file_type()
9224            .is_symlink());
9225        assert_eq!(directory_files(target.path()), before);
9226    }
9227
9228    #[test]
9229    fn wal_fails_closed_on_interior_corruption() {
9230        let root = tempfile::tempdir().unwrap();
9231        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9232        {
9233            let store = RecorderFileStore::new_with_membership(
9234                root.path(),
9235                "n1",
9236                "cluster",
9237                1,
9238                1,
9239                membership.clone(),
9240            )
9241            .unwrap();
9242            for slot in [8, 9] {
9243                let command = StoredCommand::new(
9244                    EntryType::Command,
9245                    format!("wal-corrupt-{slot}").into_bytes(),
9246                );
9247                let value =
9248                    AcceptedValue::from_command("cluster", slot, 1, 1, LogHash::ZERO, &command);
9249                store
9250                    .record_proposal(RecordRequest {
9251                        cluster_id: "cluster".into(),
9252                        epoch: 1,
9253                        config_id: 1,
9254                        config_digest: membership.digest(),
9255                        slot,
9256                        step: 4,
9257                        proposal: Proposal::new(ProposalPriority::MAX, "writer", slot, value),
9258                        command: Some(command),
9259                    })
9260                    .unwrap();
9261            }
9262        }
9263        let wal = root.path().join("recorder.wal");
9264        let mut bytes = std::fs::read(&wal).unwrap();
9265        bytes[100] ^= 0x80;
9266        std::fs::write(&wal, bytes).unwrap();
9267
9268        assert!(matches!(
9269            RecorderFileStore::new_with_membership(
9270                root.path(),
9271                "n1",
9272                "cluster",
9273                1,
9274                1,
9275                membership,
9276            ),
9277            Err(Error::Decode(message)) if message.contains("WAL")
9278        ));
9279    }
9280
9281    #[test]
9282    fn wal_fails_closed_on_full_length_final_frame_corruption() {
9283        let root = tempfile::tempdir().unwrap();
9284        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9285        {
9286            let store = RecorderFileStore::new_with_membership(
9287                root.path(),
9288                "n1",
9289                "cluster",
9290                1,
9291                1,
9292                membership.clone(),
9293            )
9294            .unwrap();
9295            let command = StoredCommand::new(EntryType::Command, b"wal-final-corrupt".to_vec());
9296            let value = AcceptedValue::from_command("cluster", 8, 1, 1, LogHash::ZERO, &command);
9297            store
9298                .record_proposal(RecordRequest {
9299                    cluster_id: "cluster".into(),
9300                    epoch: 1,
9301                    config_id: 1,
9302                    config_digest: membership.digest(),
9303                    slot: 8,
9304                    step: 4,
9305                    proposal: Proposal::new(ProposalPriority::MAX, "writer", 1, value),
9306                    command: Some(command),
9307                })
9308                .unwrap();
9309        }
9310        let wal = root.path().join("recorder.wal");
9311        let mut bytes = std::fs::read(&wal).unwrap();
9312        let last = bytes.len() - 1;
9313        bytes[last] ^= 0x80;
9314        std::fs::write(&wal, bytes).unwrap();
9315
9316        assert!(matches!(
9317            RecorderFileStore::new_with_membership(
9318                root.path(),
9319                "n1",
9320                "cluster",
9321                1,
9322                1,
9323                membership,
9324            ),
9325            Err(Error::Decode(message)) if message.contains("WAL frame checksum")
9326        ));
9327    }
9328
9329    #[test]
9330    fn wal_rotation_checkpoints_before_reusing_the_stable_file() {
9331        let root = tempfile::tempdir().unwrap();
9332        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9333        let store = RecorderFileStore::new_with_membership(
9334            root.path(),
9335            "n1",
9336            "cluster",
9337            1,
9338            1,
9339            membership.clone(),
9340        )
9341        .unwrap();
9342        for slot in 1..=super::RECORDER_WAL_HARD_FRAME_LIMIT + 1 {
9343            let command = StoredCommand::new(
9344                EntryType::Command,
9345                format!("wal-rotate-{slot}").into_bytes(),
9346            );
9347            let value = AcceptedValue::from_command("cluster", slot, 1, 1, LogHash::ZERO, &command);
9348            store
9349                .record_proposal(RecordRequest {
9350                    cluster_id: "cluster".into(),
9351                    epoch: 1,
9352                    config_id: 1,
9353                    config_digest: membership.digest(),
9354                    slot,
9355                    step: 4,
9356                    proposal: Proposal::new(ProposalPriority::MAX, "writer", slot, value),
9357                    command: Some(command),
9358                })
9359                .unwrap();
9360        }
9361        let (generation, through_sequence, frames) = store.wal_stats().unwrap();
9362        assert!(generation > 1);
9363        assert!(through_sequence >= super::RECORDER_WAL_HARD_FRAME_LIMIT);
9364        assert_eq!(frames, 1);
9365        drop(store);
9366
9367        let reopened =
9368            RecorderFileStore::new_with_membership(root.path(), "n1", "cluster", 1, 1, membership)
9369                .unwrap();
9370        assert_eq!(
9371            reopened
9372                .load(super::RECORDER_WAL_HARD_FRAME_LIMIT + 1)
9373                .unwrap()
9374                .isr
9375                .step(),
9376            4
9377        );
9378    }
9379
9380    proptest! {
9381        #[test]
9382        fn wal_frame_round_trips_arbitrary_inline_commands(
9383            sequence in 1u64..u64::MAX,
9384            payload in proptest::collection::vec(any::<u8>(), 0..2048),
9385        ) {
9386            let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9387            let configuration = ConfigurationState::initial(
9388                1,
9389                membership.digest(),
9390                Some(membership),
9391            );
9392            let state = RecorderSlotState::new_with_digest(
9393                8,
9394                "cluster",
9395                1,
9396                1,
9397                configuration.config_digest(),
9398            );
9399            let command = StoredCommand::new(EntryType::Command, payload);
9400            let (encoded, digest, slot_bytes) = encode_wal_frame(
9401                3,
9402                sequence,
9403                LogHash::ZERO,
9404                &state,
9405                &configuration,
9406                &RecordedHeadProvenance::Empty,
9407                Some((command.hash(), &command)),
9408            ).unwrap();
9409            let (decoded, end) = decode_wal_frame(&encoded, 0).unwrap().unwrap();
9410            prop_assert_eq!(end, encoded.len());
9411            prop_assert_eq!(decoded.generation, 3);
9412            prop_assert_eq!(decoded.sequence, sequence);
9413            prop_assert_eq!(decoded.digest, digest);
9414            prop_assert_eq!(decoded.slot_bytes, slot_bytes);
9415            prop_assert_eq!(decoded.command, Some((command.hash(), command)));
9416        }
9417    }
9418
9419    #[test]
9420    fn fresh_initialization_recovers_when_configuration_was_published_before_head() {
9421        let root = tempfile::tempdir().unwrap();
9422        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9423        let (store, existing_format) =
9424            RecorderFileStore::open_root(root.path(), "n1", "cluster", 1, 1).unwrap();
9425        assert!(!existing_format);
9426        let configuration =
9427            ConfigurationState::initial(1, membership.digest(), Some(membership.clone()));
9428        store
9429            .set_seal_fault(Some(SealFaultPoint::AfterHeadConfiguration))
9430            .unwrap();
9431
9432        assert!(matches!(
9433            store.commit_configuration_head_unlocked(
9434                &configuration,
9435                &RecordedHeadProvenance::Empty,
9436            ),
9437            Err(Error::Io(message))
9438                if message.contains("AfterHeadConfiguration")
9439        ));
9440        assert!(root.path().join("configuration.rec").exists());
9441        assert!(!root.path().join("recorded-head.rec").exists());
9442        assert!(root.path().join("configuration-head.intent").exists());
9443        drop(store);
9444
9445        let reopened = RecorderFileStore::new_with_membership(
9446            root.path(),
9447            "n1",
9448            "cluster",
9449            1,
9450            1,
9451            membership.clone(),
9452        )
9453        .unwrap();
9454        assert_eq!(
9455            reopened.configuration_state().unwrap().membership(),
9456            Some(&membership)
9457        );
9458        assert!(root.path().join("recorded-head.rec").exists());
9459        assert!(!root.path().join("configuration-head.intent").exists());
9460    }
9461
9462    #[test]
9463    fn progress_remembers_config_change_after_later_normal_adoption() {
9464        let root = tempfile::tempdir().unwrap();
9465        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
9466        let stores: Vec<_> = membership
9467            .members()
9468            .iter()
9469            .map(|id| {
9470                RecorderFileStore::new_with_membership(
9471                    root.path().join(id),
9472                    id.clone(),
9473                    "cluster",
9474                    1,
9475                    1,
9476                    membership.clone(),
9477                )
9478                .unwrap()
9479            })
9480            .collect();
9481        let offered = StoredCommand::new(EntryType::Command, b"offered".to_vec());
9482        let transition = ConfigChange::stop(1, membership.digest()).to_stored_command();
9483        let adopted = StoredCommand::new(EntryType::Command, b"adopted".to_vec());
9484        for store in &stores {
9485            for command in [&transition, &adopted] {
9486                store
9487                    .store_command(command.hash(), command.clone())
9488                    .unwrap();
9489            }
9490        }
9491        let recorders = membership
9492            .members()
9493            .iter()
9494            .zip(&stores)
9495            .map(|(id, store)| (id.clone(), Box::new(store.clone()) as Box<dyn RecorderRpc>))
9496            .collect();
9497        let consensus =
9498            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
9499        let proposal = |command: &StoredCommand| {
9500            Proposal::new(
9501                ProposalPriority::from_u64(1),
9502                "other",
9503                1,
9504                AcceptedValue::from_command("cluster", 1, 1, 1, LogHash::ZERO, command),
9505            )
9506        };
9507        let mut progress = ProposerProgress::new(1, proposal(&offered)).with_command(offered);
9508
9509        progress.proposal = proposal(&transition);
9510        consensus.ensure_progress_command(&mut progress).unwrap();
9511        progress.proposal = proposal(&adopted);
9512        consensus.ensure_progress_command(&mut progress).unwrap();
9513
9514        assert_eq!(progress.command, Some(adopted));
9515        assert!(progress.transition_involved);
9516    }
9517
9518    #[test]
9519    fn record_broadcast_reuses_one_worker_thread_per_recorder() {
9520        let seen: Vec<_> = (0..3)
9521            .map(|_| Arc::new(Mutex::new(HashSet::new())))
9522            .collect();
9523        let recorders = ["n1", "n2", "n3"]
9524            .into_iter()
9525            .zip(&seen)
9526            .map(|(recorder_id, threads)| {
9527                (
9528                    recorder_id.into(),
9529                    Box::new(ThreadRecordingRecorder {
9530                        recorder_id,
9531                        threads: Arc::clone(threads),
9532                    }) as Box<dyn RecorderRpc>,
9533                )
9534            })
9535            .collect();
9536        let consensus =
9537            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
9538
9539        for slot in 1..=16 {
9540            assert_eq!(
9541                consensus
9542                    .record_broadcast(record_requests(&consensus, slot))
9543                    .unwrap()
9544                    .len(),
9545                2
9546            );
9547        }
9548        drop(consensus);
9549
9550        assert!(seen
9551            .iter()
9552            .all(|threads| threads.lock().unwrap().len() == 1));
9553    }
9554
9555    #[test]
9556    fn unsorted_explicit_recorder_ids_preserve_rpc_pairing_across_worker_paths() {
9557        let recorders = ["n3", "n1", "n2"]
9558            .into_iter()
9559            .map(|recorder_id| {
9560                (
9561                    recorder_id.into(),
9562                    Box::new(SlotRecorder {
9563                        recorder_id,
9564                        reject_slot: None,
9565                        observed: None,
9566                    }) as Box<dyn RecorderRpc>,
9567                )
9568            })
9569            .collect();
9570        let consensus =
9571            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
9572
9573        assert_eq!(
9574            consensus
9575                .record_broadcast(record_requests(&consensus, 1))
9576                .unwrap()
9577                .len(),
9578            2
9579        );
9580        assert_eq!(consensus.inspect_decision_proof_at(1).unwrap(), None);
9581        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
9582    }
9583
9584    #[test]
9585    fn repeated_control_operations_reuse_one_worker_thread_per_recorder() {
9586        let seen: Vec<_> = (0..3)
9587            .map(|_| Arc::new(Mutex::new(HashSet::new())))
9588            .collect();
9589        let recorders = ["n1", "n2", "n3"]
9590            .into_iter()
9591            .zip(&seen)
9592            .map(|(recorder_id, threads)| {
9593                (
9594                    recorder_id.into(),
9595                    Box::new(ThreadRecordingControlRecorder {
9596                        threads: Arc::clone(threads),
9597                    }) as Box<dyn RecorderRpc>,
9598                )
9599            })
9600            .collect();
9601        let consensus =
9602            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
9603
9604        for slot in 1..=16 {
9605            assert_eq!(consensus.inspect_decision_proof_at(slot).unwrap(), None);
9606        }
9607        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
9608
9609        assert!(seen
9610            .iter()
9611            .all(|threads| threads.lock().unwrap().len() == 1));
9612    }
9613
9614    #[test]
9615    fn blocked_control_minority_does_not_delay_record_quorum() {
9616        let (started_tx, started_rx) = mpsc::sync_channel(1);
9617        let (release_tx, release_rx) = mpsc::sync_channel(0);
9618        let recorders = vec![
9619            (
9620                "n1".into(),
9621                Box::new(SlotRecorder {
9622                    recorder_id: "n1",
9623                    reject_slot: None,
9624                    observed: None,
9625                }) as Box<dyn RecorderRpc>,
9626            ),
9627            (
9628                "n2".into(),
9629                Box::new(BlockingControlRecorder {
9630                    recorder_id: "n2",
9631                    started: started_tx,
9632                    release_first: Mutex::new(release_rx),
9633                }) as Box<dyn RecorderRpc>,
9634            ),
9635            (
9636                "n3".into(),
9637                Box::new(SlotRecorder {
9638                    recorder_id: "n3",
9639                    reject_slot: None,
9640                    observed: None,
9641                }) as Box<dyn RecorderRpc>,
9642            ),
9643        ];
9644        let consensus =
9645            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
9646
9647        assert_eq!(consensus.inspect_decision_proof_at(1).unwrap(), None);
9648        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
9649        let replies = consensus
9650            .record_broadcast(record_requests(&consensus, 1))
9651            .unwrap();
9652        assert_eq!(replies.len(), 2);
9653
9654        release_tx.send(()).unwrap();
9655        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
9656    }
9657
9658    #[test]
9659    fn blocked_control_majority_does_not_head_of_line_block_read_fence() {
9660        let (started_tx, started_rx) = mpsc::sync_channel(2);
9661        let release = Arc::new((Mutex::new(false), Condvar::new()));
9662        let recorders = ["n1", "n2", "n3"]
9663            .into_iter()
9664            .map(|recorder_id| {
9665                (
9666                    recorder_id.into(),
9667                    Box::new(BlockingInspectionReadFenceRecorder {
9668                        recorder_id,
9669                        block_inspection: recorder_id != "n3",
9670                        started: started_tx.clone(),
9671                        release: Arc::clone(&release),
9672                    }) as Box<dyn RecorderRpc>,
9673                )
9674            })
9675            .collect();
9676        let consensus = Arc::new(
9677            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap(),
9678        );
9679        let inspecting = Arc::clone(&consensus);
9680        let inspection = thread::spawn(move || inspecting.inspect_decision_at(1, LogHash::ZERO));
9681        let mut started = BTreeSet::new();
9682        started.insert(started_rx.recv_timeout(Duration::from_secs(1)).unwrap());
9683        started.insert(started_rx.recv_timeout(Duration::from_secs(1)).unwrap());
9684        assert_eq!(started, BTreeSet::from(["n1", "n2"]));
9685
9686        let before = Instant::now();
9687        assert_eq!(
9688            consensus
9689                .inspect_context_read_fence_at(1, LogHash::ZERO)
9690                .unwrap(),
9691            CertifiedDecisionInspection::Empty
9692        );
9693        assert!(before.elapsed() < Duration::from_millis(250));
9694
9695        let (released, condition) = &*release;
9696        *released.lock().unwrap() = true;
9697        condition.notify_all();
9698        assert_eq!(
9699            inspection.join().unwrap().unwrap(),
9700            DecisionInspection::Empty
9701        );
9702        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
9703    }
9704
9705    #[test]
9706    fn saturated_control_queue_does_not_contaminate_later_requests() {
9707        let (started_tx, started_rx) = mpsc::sync_channel(4);
9708        let (release_tx, release_rx) = mpsc::sync_channel(0);
9709        let recorders = vec![
9710            (
9711                "n1".into(),
9712                Box::new(SlotRecorder {
9713                    recorder_id: "n1",
9714                    reject_slot: None,
9715                    observed: None,
9716                }) as Box<dyn RecorderRpc>,
9717            ),
9718            (
9719                "n2".into(),
9720                Box::new(BlockingControlRecorder {
9721                    recorder_id: "n2",
9722                    started: started_tx,
9723                    release_first: Mutex::new(release_rx),
9724                }) as Box<dyn RecorderRpc>,
9725            ),
9726            (
9727                "n3".into(),
9728                Box::new(SlotRecorder {
9729                    recorder_id: "n3",
9730                    reject_slot: None,
9731                    observed: None,
9732                }) as Box<dyn RecorderRpc>,
9733            ),
9734        ];
9735        let consensus =
9736            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
9737
9738        assert_eq!(consensus.inspect_decision_proof_at(1).unwrap(), None);
9739        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
9740        assert_eq!(consensus.inspect_decision_proof_at(2).unwrap(), None);
9741        assert_eq!(consensus.inspect_decision_proof_at(3).unwrap(), None);
9742
9743        release_tx.send(()).unwrap();
9744        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
9745        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(2));
9746        assert!(matches!(
9747            started_rx.try_recv(),
9748            Err(mpsc::TryRecvError::Empty)
9749        ));
9750
9751        assert_eq!(consensus.inspect_decision_proof_at(4).unwrap(), None);
9752        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(4));
9753    }
9754
9755    #[test]
9756    fn command_registration_returns_no_quorum_when_all_control_queues_are_full() {
9757        let (started_tx, started_rx) = mpsc::sync_channel(3);
9758        let release = Arc::new((Mutex::new(false), Condvar::new()));
9759        let recorders = ["n1", "n2", "n3"]
9760            .into_iter()
9761            .map(|recorder_id| {
9762                (
9763                    recorder_id.into(),
9764                    Box::new(BlockingCommandStoreRecorder {
9765                        started: started_tx.clone(),
9766                        release: Arc::clone(&release),
9767                    }) as Box<dyn RecorderRpc>,
9768                )
9769            })
9770            .collect();
9771        let consensus = Arc::new(
9772            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap(),
9773        );
9774        let first = StoredCommand::new(EntryType::Command, b"first".to_vec());
9775        let registering = Arc::clone(&consensus);
9776        let first_hash = first.hash();
9777        let first_payload = first.payload.clone();
9778        let registration =
9779            thread::spawn(move || registering.register_command(first_hash, first_payload));
9780
9781        for _ in 0..3 {
9782            started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
9783        }
9784
9785        let queued = StoredCommand::new(EntryType::Command, b"queued".to_vec());
9786        let (queued_tx, _queued_rx) = mpsc::sync_channel(3);
9787        for (index, worker) in consensus.control_workers.iter().enumerate() {
9788            worker.dispatch(ControlJob::StoreCommand {
9789                index,
9790                cluster_id: "cluster".into(),
9791                epoch: 1,
9792                config_id: 1,
9793                config_digest: consensus.membership().digest(),
9794                command_hash: queued.hash(),
9795                command: queued.clone(),
9796                result: queued_tx.clone(),
9797            });
9798        }
9799
9800        let saturated = StoredCommand::new(EntryType::Command, b"saturated".to_vec());
9801        assert_eq!(
9802            consensus.register_command(saturated.hash(), saturated.payload),
9803            Err(Error::NoQuorum)
9804        );
9805
9806        let (released, condition) = &*release;
9807        *released.lock().unwrap() = true;
9808        condition.notify_all();
9809        assert_eq!(registration.join().unwrap(), Ok(()));
9810        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
9811    }
9812
9813    #[test]
9814    fn saturated_control_worker_keeps_command_quorum_retryable_after_worker_failure() {
9815        let (started_tx, started_rx) = mpsc::sync_channel(1);
9816        let release = Arc::new((Mutex::new(false), Condvar::new()));
9817        let recorders = vec![
9818            (
9819                "n1".into(),
9820                Box::new(BlockingCommandStoreRecorder {
9821                    started: started_tx,
9822                    release: Arc::clone(&release),
9823                }) as Box<dyn RecorderRpc>,
9824            ),
9825            (
9826                "n2".into(),
9827                Box::new(SuccessfulCommandStoreRecorder) as Box<dyn RecorderRpc>,
9828            ),
9829            (
9830                "n3".into(),
9831                Box::new(FailingCommandStoreRecorder) as Box<dyn RecorderRpc>,
9832            ),
9833        ];
9834        let consensus = Arc::new(
9835            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap(),
9836        );
9837        let first = StoredCommand::new(EntryType::Command, b"first".to_vec());
9838        let registering = Arc::clone(&consensus);
9839        let registration =
9840            thread::spawn(move || registering.register_command(first.hash(), first.payload));
9841        started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
9842
9843        let queued = StoredCommand::new(EntryType::Command, b"queued".to_vec());
9844        let (queued_tx, _queued_rx) = mpsc::sync_channel(1);
9845        consensus.control_workers[0].dispatch(ControlJob::StoreCommand {
9846            index: 0,
9847            cluster_id: "cluster".into(),
9848            epoch: 1,
9849            config_id: 1,
9850            config_digest: consensus.membership().digest(),
9851            command_hash: queued.hash(),
9852            command: queued,
9853            result: queued_tx,
9854        });
9855
9856        let retry = StoredCommand::new(EntryType::Command, b"retry".to_vec());
9857        assert_eq!(
9858            consensus.register_command(retry.hash(), retry.payload),
9859            Err(Error::NoQuorum)
9860        );
9861
9862        let (released, condition) = &*release;
9863        *released.lock().unwrap() = true;
9864        condition.notify_all();
9865        assert_eq!(registration.join().unwrap(), Ok(()));
9866        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
9867    }
9868
9869    #[test]
9870    fn control_worker_finish_and_drop_are_bounded() {
9871        let new_consensus = || {
9872            let (started_tx, started_rx) = mpsc::sync_channel(1);
9873            let (release_tx, release_rx) = mpsc::sync_channel(0);
9874            let recorders = vec![
9875                (
9876                    "n1".into(),
9877                    Box::new(SlotRecorder {
9878                        recorder_id: "n1",
9879                        reject_slot: None,
9880                        observed: None,
9881                    }) as Box<dyn RecorderRpc>,
9882                ),
9883                (
9884                    "n2".into(),
9885                    Box::new(BlockingControlRecorder {
9886                        recorder_id: "n2",
9887                        started: started_tx,
9888                        release_first: Mutex::new(release_rx),
9889                    }) as Box<dyn RecorderRpc>,
9890                ),
9891                (
9892                    "n3".into(),
9893                    Box::new(SlotRecorder {
9894                        recorder_id: "n3",
9895                        reject_slot: None,
9896                        observed: None,
9897                    }) as Box<dyn RecorderRpc>,
9898                ),
9899            ];
9900            (
9901                ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders)
9902                    .unwrap(),
9903                started_rx,
9904                release_tx,
9905            )
9906        };
9907
9908        let (consensus, started_rx, release_tx) = new_consensus();
9909        let consensus = Arc::new(consensus);
9910        assert_eq!(consensus.inspect_decision_proof_at(1).unwrap(), None);
9911        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
9912        let (finished_tx, finished_rx) = mpsc::sync_channel(1);
9913        let finishing = Arc::clone(&consensus);
9914        let finisher = thread::spawn(move || {
9915            finished_tx
9916                .send(finishing.finish_pending_rpcs(Duration::from_millis(10)))
9917                .unwrap();
9918        });
9919        assert_eq!(finished_rx.recv_timeout(Duration::from_secs(1)), Ok(false));
9920        finisher.join().unwrap();
9921        release_tx.send(()).unwrap();
9922        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
9923
9924        let (consensus, started_rx, release_tx) = new_consensus();
9925        assert_eq!(consensus.inspect_decision_proof_at(1).unwrap(), None);
9926        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
9927        let (dropped_tx, dropped_rx) = mpsc::sync_channel(1);
9928        let dropper = thread::spawn(move || {
9929            drop(consensus);
9930            dropped_tx.send(()).unwrap();
9931        });
9932        let dropped = dropped_rx.recv_timeout(Duration::from_secs(1));
9933        release_tx.send(()).unwrap();
9934        dropper.join().unwrap();
9935        assert_eq!(dropped, Ok(()));
9936    }
9937
9938    #[test]
9939    fn saturated_minority_queue_does_not_delay_or_contaminate_later_broadcasts() {
9940        let (started_tx, started_rx) = mpsc::sync_channel(2);
9941        let (release_tx, release_rx) = mpsc::sync_channel(0);
9942        let (n1_seen_tx, n1_seen_rx) = mpsc::sync_channel(1);
9943        let (reject_seen_tx, reject_seen_rx) = mpsc::sync_channel(1);
9944        let recorders = vec![
9945            (
9946                "n1".into(),
9947                Box::new(SlotRecorder {
9948                    recorder_id: "n1",
9949                    reject_slot: None,
9950                    observed: Some(n1_seen_tx),
9951                }) as Box<dyn RecorderRpc>,
9952            ),
9953            (
9954                "n2".into(),
9955                Box::new(BlockingRecorder {
9956                    recorder_id: "n2",
9957                    started: started_tx,
9958                    release_first: Mutex::new(release_rx),
9959                }) as Box<dyn RecorderRpc>,
9960            ),
9961            (
9962                "n3".into(),
9963                Box::new(SlotRecorder {
9964                    recorder_id: "n3",
9965                    reject_slot: Some(2),
9966                    observed: Some(reject_seen_tx),
9967                }) as Box<dyn RecorderRpc>,
9968            ),
9969        ];
9970        let consensus = Arc::new(
9971            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap(),
9972        );
9973
9974        let first = consensus
9975            .record_broadcast(record_requests(&consensus, 1))
9976            .unwrap();
9977        assert_eq!(first.len(), 2);
9978        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
9979        assert_eq!(n1_seen_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
9980        assert_eq!(reject_seen_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
9981
9982        let (done_tx, done_rx) = mpsc::sync_channel(1);
9983        let second_consensus = Arc::clone(&consensus);
9984        let second = thread::spawn(move || {
9985            done_tx
9986                .send(second_consensus.record_broadcast(record_requests(&second_consensus, 2)))
9987                .unwrap();
9988        });
9989        assert_eq!(reject_seen_rx.recv_timeout(Duration::from_secs(1)), Ok(2));
9990        assert_eq!(n1_seen_rx.recv_timeout(Duration::from_secs(1)), Ok(2));
9991        assert!(matches!(done_rx.try_recv(), Err(mpsc::TryRecvError::Empty)));
9992
9993        let (third_done_tx, third_done_rx) = mpsc::sync_channel(1);
9994        let third_consensus = Arc::clone(&consensus);
9995        let third = thread::spawn(move || {
9996            third_done_tx
9997                .send(third_consensus.record_broadcast(record_requests(&third_consensus, 3)))
9998                .unwrap();
9999        });
10000        let third_replies = third_done_rx
10001            .recv_timeout(Duration::from_secs(1))
10002            .unwrap()
10003            .unwrap();
10004        assert_eq!(third_replies.len(), 2);
10005        assert!(third_replies.iter().all(|reply| reply.slot == 3));
10006
10007        release_tx.send(()).unwrap();
10008
10009        let replies = done_rx
10010            .recv_timeout(Duration::from_secs(1))
10011            .unwrap()
10012            .unwrap();
10013        assert_eq!(replies.len(), 2);
10014        assert!(replies.iter().all(|reply| reply.slot == 2));
10015        assert!(replies.iter().any(|reply| reply.recorder_id == "n2"));
10016        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(2));
10017        second.join().unwrap();
10018        third.join().unwrap();
10019    }
10020
10021    #[test]
10022    fn saturated_recorder_keeps_a_minority_rejection_retryable() {
10023        let (started_tx, started_rx) = mpsc::sync_channel(2);
10024        let (release_tx, release_rx) = mpsc::sync_channel(0);
10025        let recorders = vec![
10026            (
10027                "n1".into(),
10028                Box::new(SlotRecorder {
10029                    recorder_id: "n1",
10030                    reject_slot: None,
10031                    observed: None,
10032                }) as Box<dyn RecorderRpc>,
10033            ),
10034            (
10035                "n2".into(),
10036                Box::new(BlockingRecorder {
10037                    recorder_id: "n2",
10038                    started: started_tx,
10039                    release_first: Mutex::new(release_rx),
10040                }) as Box<dyn RecorderRpc>,
10041            ),
10042            (
10043                "n3".into(),
10044                Box::new(RejectFromSlotRecorder {
10045                    recorder_id: "n3",
10046                    reject_from: 3,
10047                }) as Box<dyn RecorderRpc>,
10048            ),
10049        ];
10050        let consensus =
10051            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
10052
10053        assert_eq!(
10054            consensus
10055                .record_broadcast(record_requests(&consensus, 1))
10056                .unwrap()
10057                .len(),
10058            2
10059        );
10060        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
10061        assert_eq!(
10062            consensus
10063                .record_broadcast(record_requests(&consensus, 2))
10064                .unwrap()
10065                .len(),
10066            2
10067        );
10068
10069        let failure = (3..=514).find_map(|slot| {
10070            let result = consensus.record_broadcast(record_requests(&consensus, slot));
10071            match &result {
10072                Ok(replies)
10073                    if replies.len() == 1
10074                        && replies[0].recorder_id == "n1"
10075                        && replies[0].slot == slot =>
10076                {
10077                    None
10078                }
10079                _ => Some((slot, result)),
10080            }
10081        });
10082        release_tx.send(()).unwrap();
10083        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
10084        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(2));
10085        assert!(
10086            failure.is_none(),
10087            "a saturated recorder must keep minority rejection retryable: {failure:?}"
10088        );
10089    }
10090
10091    #[test]
10092    fn saturated_recorder_keeps_quorum_reachable_when_another_worker_fails() {
10093        let (started_tx, started_rx) = mpsc::sync_channel(2);
10094        let (release_tx, release_rx) = mpsc::sync_channel(0);
10095        let recorders = vec![
10096            (
10097                "n1".into(),
10098                Box::new(BlockingRecorder {
10099                    recorder_id: "n1",
10100                    started: started_tx,
10101                    release_first: Mutex::new(release_rx),
10102                }) as Box<dyn RecorderRpc>,
10103            ),
10104            (
10105                "n2".into(),
10106                Box::new(FailingFromSlotRecorder {
10107                    recorder_id: "n2",
10108                    fail_from: 3,
10109                }) as Box<dyn RecorderRpc>,
10110            ),
10111            (
10112                "n3".into(),
10113                Box::new(SlotRecorder {
10114                    recorder_id: "n3",
10115                    reject_slot: None,
10116                    observed: None,
10117                }) as Box<dyn RecorderRpc>,
10118            ),
10119        ];
10120        let consensus =
10121            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
10122
10123        assert_eq!(
10124            consensus
10125                .record_broadcast(record_requests(&consensus, 1))
10126                .unwrap()
10127                .len(),
10128            2
10129        );
10130        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
10131        assert_eq!(
10132            consensus
10133                .record_broadcast(record_requests(&consensus, 2))
10134                .unwrap()
10135                .len(),
10136            2
10137        );
10138
10139        let third = consensus.record_broadcast(record_requests(&consensus, 3));
10140        assert!(
10141            matches!(third, Ok(ref replies) if replies.len() == 1 && replies[0].recorder_id == "n3"),
10142            "a saturated healthy voter must keep the quorum retryable after a worker failure: {third:?}"
10143        );
10144
10145        release_tx.send(()).unwrap();
10146        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
10147        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(2));
10148    }
10149
10150    #[test]
10151    fn command_lookup_waits_for_valid_reply_after_quorum_reports_missing() {
10152        let command = StoredCommand::new(EntryType::Command, b"available".to_vec());
10153        let value = AcceptedValue::from_command("cluster", 7, 1, 1, LogHash::ZERO, &command);
10154        let (observed_tx, observed_rx) = mpsc::sync_channel(2);
10155        let (started_tx, started_rx) = mpsc::sync_channel(1);
10156        let (release_tx, release_rx) = mpsc::sync_channel(0);
10157        let recorders = vec![
10158            (
10159                "n1".into(),
10160                Box::new(MissingCommandRecorder {
10161                    observed: observed_tx.clone(),
10162                }) as Box<dyn RecorderRpc>,
10163            ),
10164            (
10165                "n2".into(),
10166                Box::new(MissingCommandRecorder {
10167                    observed: observed_tx,
10168                }) as Box<dyn RecorderRpc>,
10169            ),
10170            (
10171                "n3".into(),
10172                Box::new(BlockingCommandRecorder {
10173                    started: started_tx,
10174                    release: Mutex::new(release_rx),
10175                    command: command.clone(),
10176                }) as Box<dyn RecorderRpc>,
10177            ),
10178        ];
10179        let consensus =
10180            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
10181        let (done_tx, done_rx) = mpsc::sync_channel(1);
10182        let fetch = thread::spawn(move || {
10183            done_tx
10184                .send(consensus.fetch_verified_value(7, &value))
10185                .unwrap();
10186        });
10187
10188        assert_eq!(observed_rx.recv_timeout(Duration::from_secs(1)), Ok(()));
10189        assert_eq!(observed_rx.recv_timeout(Duration::from_secs(1)), Ok(()));
10190        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(()));
10191        assert!(matches!(done_rx.try_recv(), Err(mpsc::TryRecvError::Empty)));
10192
10193        release_tx.send(()).unwrap();
10194        assert_eq!(
10195            done_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
10196            Ok(Some(command))
10197        );
10198        fetch.join().unwrap();
10199    }
10200
10201    #[test]
10202    fn command_lookup_rejects_cryptographic_mismatch_despite_reachable_quorum() {
10203        let command = StoredCommand::new(EntryType::Command, b"mismatched".to_vec());
10204        let mut value = AcceptedValue::from_command("cluster", 7, 1, 1, LogHash::ZERO, &command);
10205        value.entry_hash = LogHash::ZERO;
10206        let (observed_tx, observed_rx) = mpsc::sync_channel(1);
10207        let recorders = vec![
10208            (
10209                "n1".into(),
10210                Box::new(AvailableCommandRecorder {
10211                    command: command.clone(),
10212                }) as Box<dyn RecorderRpc>,
10213            ),
10214            (
10215                "n2".into(),
10216                Box::new(MissingCommandRecorder {
10217                    observed: observed_tx,
10218                }) as Box<dyn RecorderRpc>,
10219            ),
10220            (
10221                "n3".into(),
10222                Box::new(FailingCommandFetchRecorder) as Box<dyn RecorderRpc>,
10223            ),
10224        ];
10225        let consensus =
10226            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
10227
10228        assert_eq!(
10229            consensus.fetch_verified_value(7, &value),
10230            Err(Error::Rejected(RejectReason::InvalidValue))
10231        );
10232        assert_eq!(observed_rx.recv_timeout(Duration::from_secs(1)), Ok(()));
10233    }
10234
10235    #[test]
10236    fn command_lookup_returns_no_quorum_when_a_control_worker_queue_is_full() {
10237        let (started_tx, started_rx) = mpsc::sync_channel(1);
10238        let release = Arc::new((Mutex::new(false), Condvar::new()));
10239        let (observed_tx, observed_rx) = mpsc::sync_channel(1);
10240        let recorders = vec![
10241            (
10242                "n1".into(),
10243                Box::new(BlockingCommandStoreRecorder {
10244                    started: started_tx,
10245                    release: Arc::clone(&release),
10246                }) as Box<dyn RecorderRpc>,
10247            ),
10248            (
10249                "n2".into(),
10250                Box::new(MissingCommandRecorder {
10251                    observed: observed_tx,
10252                }) as Box<dyn RecorderRpc>,
10253            ),
10254            (
10255                "n3".into(),
10256                Box::new(FailingCommandFetchRecorder) as Box<dyn RecorderRpc>,
10257            ),
10258        ];
10259        let consensus =
10260            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
10261        let blocking = StoredCommand::new(EntryType::Command, b"blocking".to_vec());
10262        let (blocking_tx, _blocking_rx) = mpsc::sync_channel(1);
10263        assert!(matches!(
10264            consensus.control_workers[0].dispatch(ControlJob::StoreCommand {
10265                index: 0,
10266                cluster_id: "cluster".into(),
10267                epoch: 1,
10268                config_id: 1,
10269                config_digest: consensus.membership().digest(),
10270                command_hash: blocking.hash(),
10271                command: blocking,
10272                result: blocking_tx,
10273            }),
10274            ControlDispatch::Accepted
10275        ));
10276        started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
10277
10278        let queued = StoredCommand::new(EntryType::Command, b"queued".to_vec());
10279        let (queued_tx, _queued_rx) = mpsc::sync_channel(1);
10280        assert!(matches!(
10281            consensus.control_workers[0].dispatch(ControlJob::StoreCommand {
10282                index: 0,
10283                cluster_id: "cluster".into(),
10284                epoch: 1,
10285                config_id: 1,
10286                config_digest: consensus.membership().digest(),
10287                command_hash: queued.hash(),
10288                command: queued,
10289                result: queued_tx,
10290            }),
10291            ControlDispatch::Accepted
10292        ));
10293
10294        let command = StoredCommand::new(EntryType::Command, b"missing".to_vec());
10295        let value = AcceptedValue::from_command("cluster", 7, 1, 1, LogHash::ZERO, &command);
10296        assert_eq!(
10297            consensus.fetch_verified_value(7, &value),
10298            Err(Error::NoQuorum)
10299        );
10300        assert_eq!(observed_rx.recv_timeout(Duration::from_secs(1)), Ok(()));
10301
10302        let (released, condition) = &*release;
10303        *released.lock().unwrap() = true;
10304        condition.notify_all();
10305        assert!(consensus.finish_pending_rpcs(Duration::from_secs(1)));
10306    }
10307
10308    #[test]
10309    fn full_record_worker_queue_is_transient_unavailable_not_fatal() {
10310        let (started_tx, started_rx) = mpsc::sync_channel(2);
10311        let (release_tx, release_rx) = mpsc::sync_channel(0);
10312        let recorders = vec![
10313            (
10314                "n1".into(),
10315                Box::new(BlockingRecorder {
10316                    recorder_id: "n1",
10317                    started: started_tx,
10318                    release_first: Mutex::new(release_rx),
10319                }) as Box<dyn RecorderRpc>,
10320            ),
10321            (
10322                "n2".into(),
10323                Box::new(AlwaysIoRecorder) as Box<dyn RecorderRpc>,
10324            ),
10325            (
10326                "n3".into(),
10327                Box::new(AlwaysIoRecorder) as Box<dyn RecorderRpc>,
10328            ),
10329        ];
10330        let consensus =
10331            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
10332
10333        assert!(consensus
10334            .record_broadcast(record_requests(&consensus, 1))
10335            .unwrap()
10336            .is_empty());
10337        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(1));
10338
10339        assert!(consensus
10340            .record_broadcast(record_requests(&consensus, 2))
10341            .unwrap()
10342            .is_empty());
10343
10344        let third = consensus.record_broadcast(record_requests(&consensus, 3));
10345        assert!(
10346            matches!(third, Ok(ref replies) if replies.is_empty()),
10347            "a full worker queue must remain retryable, got {third:?}"
10348        );
10349
10350        release_tx.send(()).unwrap();
10351        assert_eq!(started_rx.recv_timeout(Duration::from_secs(1)), Ok(2));
10352    }
10353
10354    #[test]
10355    fn disconnected_record_worker_is_fatal() {
10356        let membership = Membership::new(["n1", "n2", "n3"]).unwrap();
10357        let proposal = Proposal::new(
10358            ProposalPriority::MAX,
10359            "n1",
10360            1,
10361            AcceptedValue {
10362                command_hash: LogHash::ZERO,
10363                prev_hash: LogHash::ZERO,
10364                entry_hash: LogHash::ZERO,
10365            },
10366        );
10367        let request = RecordRequest {
10368            cluster_id: "cluster".into(),
10369            epoch: 1,
10370            config_id: 1,
10371            config_digest: membership.digest(),
10372            slot: 1,
10373            step: 4,
10374            proposal,
10375            command: None,
10376        };
10377        let (job_tx, job_rx) = mpsc::sync_channel(1);
10378        drop(job_rx);
10379        let pending = Arc::new(std::sync::atomic::AtomicUsize::new(0));
10380        let worker = super::RecordWorker {
10381            sender: Some(job_tx),
10382            handle: None,
10383            pending: Arc::clone(&pending),
10384        };
10385        let (result_tx, result_rx) = mpsc::sync_channel(1);
10386
10387        worker.dispatch(super::RecordJob {
10388            index: 0,
10389            request,
10390            result: result_tx,
10391        });
10392
10393        assert_eq!(result_rx.recv().unwrap().1, Err(Error::ProposeFailed));
10394        assert_eq!(pending.load(std::sync::atomic::Ordering::Acquire), 0);
10395    }
10396
10397    #[test]
10398    fn recorder_panics_are_reported_without_panicking_the_proposer() {
10399        let recorders = vec![
10400            (
10401                "n1".into(),
10402                Box::new(SlotRecorder {
10403                    recorder_id: "n1",
10404                    reject_slot: None,
10405                    observed: None,
10406                }) as Box<dyn RecorderRpc>,
10407            ),
10408            (
10409                "n2".into(),
10410                Box::new(PanickingRecorder) as Box<dyn RecorderRpc>,
10411            ),
10412            (
10413                "n3".into(),
10414                Box::new(PanickingRecorder) as Box<dyn RecorderRpc>,
10415            ),
10416        ];
10417        let consensus =
10418            ThreeNodeConsensus::from_recorders_with_ids("cluster", "n1", 1, 1, recorders).unwrap();
10419
10420        assert_eq!(
10421            consensus.record_broadcast(record_requests(&consensus, 1)),
10422            Err(Error::ProposeFailed)
10423        );
10424    }
10425}