Skip to main content

rhiza_quepaxa/
lib.rs

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