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