krabka_verified/consensus.rs
1//! KIP-595 consensus decision kernels, extracted from `krabka-kraft-core` so
2//! Creusot can verify them (the host crate's `Instant`/async surface is
3//! untranslatable).
4//!
5//! The functions carry their Creusot preconditions, postconditions, invariants,
6//! variants, and supporting lemmas directly beside the executable bodies.
7
8use creusot_std::prelude::*;
9
10/// Offset-aware recovery mode resolved for one partition.
11#[krabka_macros::model_type]
12pub enum FailoverRecovery {
13 None,
14 Balanced,
15 Aggressive,
16}
17
18/// Safety action selected after the host classifies live ISR and replicas.
19#[krabka_macros::model_type]
20pub enum FailoverAction {
21 ElectClean,
22 ElectFromElr,
23 /// The partition's single last-known leader comes back as an unclean
24 /// leader (Kafka's `canElectLastKnownLeader`).
25 ElectLastKnown,
26 Recover(FailoverRecovery),
27 ElectUnclean,
28 Unavailable,
29 ShrinkIsr,
30 NoChange,
31}
32
33/// What the live members of the partition's ISR can do once the leader is
34/// gone. Every live ISR member holds every committed record; a witness is a
35/// live member that never leads.
36#[krabka_macros::model_type]
37pub enum LiveIsr {
38 /// No ISR member is live.
39 Empty,
40 /// Every live ISR member is a witness, so none can lead.
41 WitnessesOnly,
42 /// A live ISR member that is not a witness can lead.
43 Electable,
44}
45
46/// The out-of-ISR options, consulted only when no ISR member is live.
47#[krabka_macros::model_type]
48pub struct OutOfIsrFacts {
49 /// A live KIP-966 eligible leader replica can lead.
50 pub has_electable_elr: bool,
51 /// Kafka's `canElectLastKnownLeader`, less the empty-ISR test that
52 /// [`LiveIsr::Empty`] already carries: the target ELR is empty, the
53 /// partition's `lastKnownElr` holds exactly one replica, and that replica
54 /// is live and can lead.
55 pub last_known_leader_electable: bool,
56 /// The topic's resolved offset-aware recovery strategy.
57 pub recovery: FailoverRecovery,
58 /// The KIP-841 out-of-ISR election is both permitted by
59 /// `unclean.leader.election.enable` and has a live replica to elect.
60 pub unclean_election_available: bool,
61}
62
63/// What the host established about one partition before the failover
64/// decision. The kernel does not see replica sets; every field is the host's
65/// classification of them.
66#[krabka_macros::model_type]
67pub struct FailoverFacts {
68 /// The partition's leader is the replica that went away.
69 pub leader_dead: bool,
70 /// The live ISR is smaller than the recorded ISR.
71 pub isr_shrunk: bool,
72 /// What the live ISR members can do.
73 pub live_isr: LiveIsr,
74 /// The options left when no ISR member is live.
75 pub out_of_isr: OutOfIsrFacts,
76}
77
78/// One surviving replica's log as KIP-966 unclean recovery ranks it, from its
79/// `GetReplicaLogInfo` answer.
80#[krabka_macros::model_type]
81pub struct RecoveryCandidate {
82 /// The leader epoch of the last record the replica wrote.
83 pub last_epoch: i32,
84 /// The offset one past the replica's last record.
85 pub log_end_offset: i64,
86 /// The replica's broker id, which breaks a tie deterministically.
87 pub broker_id: u64,
88}
89
90mod failover;
91#[cfg(creusot)]
92pub use failover::{
93 count_ge, count_ge_prefix, hwm_member_at, lemma_explicit_vote_count_equal, ranks_ge,
94};
95pub use failover::{
96 election_has_quorum, failover_action, majority_size, select_best_recovery_replica,
97};
98
99mod log_is_up_to_date;
100pub use log_is_up_to_date::{election_jitter_ms, log_is_up_to_date};
101#[cfg(creusot)]
102pub use log_is_up_to_date::{
103 least_hwm_member_ge_index, lemma_count_ge_prefix_monotone, lemma_count_ge_prefix_nonnegative,
104 lemma_hwm_member_maximal, lemma_hwm_threshold_has_member,
105};
106
107mod watermark;
108#[cfg(any(creusot, test))]
109use watermark::candidate_has_majority;
110pub use watermark::{majority_watermark, recompute_high_watermark};
111
112// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
113#[cfg(creusot)]
114#[cfg_attr(test, mutants::skip)]
115#[logic(open)]
116/// Every concrete supporting report belongs to a different node.
117pub(crate) fn supporting_nodes_distinct(supporters: Seq<(u64, i64)>) -> bool {
118 pearlite! { forall<i: Int, j: Int> 0 <= i && i < j && j < supporters.len() ==> supporters[i].0 != supporters[j].0 }
119}
120
121// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
122#[cfg(creusot)]
123#[cfg_attr(test, mutants::skip)]
124#[logic(open)]
125/// The majority bound includes the leader's own slot.
126pub(crate) fn valid_hwm_majority(majority: Int, members: Int) -> bool {
127 pearlite! { 1 <= majority && majority <= members + 1 }
128}
129
130// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
131#[cfg(creusot)]
132#[cfg_attr(test, mutants::skip)]
133#[logic(open)]
134/// Each reported supporting frontier reaches the limit and names a voter with concrete evidence.
135pub(crate) fn supporting_voter_witnesses(
136 supporters: Seq<(u64, i64)>,
137 voters: Seq<u64>,
138 limit: Int,
139 evidence: creusot_std::logic::Mapping<(Int, Int), bool>,
140) -> bool {
141 pearlite! { forall<i: Int> 0 <= i && i < supporters.len() ==> supporters[i].1@ >= limit
142 && exists<j: Int> 0 <= j && j < voters.len() && supporters[i].0 == voters[j]
143 && evidence.get((i, j)) }
144}
145
146#[cfg(test)]
147mod tests;