krabka_verified/stretch/min_insync_is_site_loss_safe.rs
1use creusot_std::prelude::*;
2
3/// The replicas that round-robin placement puts on `site`: the placement
4/// visits sites `0, 1, ..., sites - 1, 0, 1, ...` in turn, so replica `r`
5/// lands on site `r % sites`, and this counts the `r < rf` that land on
6/// `site`.
7// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
8#[cfg(creusot)]
9#[cfg_attr(test, mutants::skip)]
10#[logic]
11#[variant(rf)]
12pub fn round_robin_load(rf: Int, sites: Int, site: Int) -> Int {
13 pearlite! {
14 if rf <= 0 {
15 0
16 } else {
17 round_robin_load(rf - 1, sites, site)
18 + if (rf - 1) % sites == site { 1 } else { 0 }
19 }
20 }
21}
22
23/// Integer division by a positive divisor is monotone on nonnegative
24/// numerators.
25// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
26#[cfg(creusot)]
27#[cfg_attr(test, mutants::skip)]
28#[logic]
29#[requires(0 <= low && low <= high && divisor >= 1)]
30#[ensures(low / divisor <= high / divisor)]
31pub fn lemma_div_monotone(low: Int, high: Int, divisor: Int) {
32 // Both numerators split into quotient and remainder.
33 proof_assert!(low == divisor * (low / divisor) + low % divisor && 0 <= low % divisor);
34 proof_assert!(high == divisor * (high / divisor) + high % divisor && high % divisor < divisor);
35 // A larger quotient for `low` would put `low` a whole divisor past `high`.
36 proof_assert!(low / divisor > high / divisor
37 ==> (low / divisor - high / divisor - 1) * divisor >= 0);
38}
39
40/// A numerator written as `divisor * quotient + remainder`, with the
41/// remainder in `[0, divisor)`, divides to exactly that quotient.
42// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
43#[cfg(creusot)]
44#[cfg_attr(test, mutants::skip)]
45#[logic]
46#[requires(divisor >= 1 && quotient >= 0 && 0 <= remainder && remainder < divisor)]
47#[ensures((divisor * quotient + remainder) / divisor == quotient)]
48pub fn lemma_div_exact(divisor: Int, quotient: Int, remainder: Int) {
49 // Why3's `Div_mult` peels the whole multiples off, and `Div_inf` makes the
50 // remainder's own quotient 0.
51 proof_assert!((divisor * quotient + remainder) / divisor == quotient + remainder / divisor);
52 proof_assert!(remainder / divisor == 0);
53}
54
55#[krabka_macros::round_robin_site_lemma(1)]
56/// One more replica raises the closed form `(rf - site + sites - 1) / sites`
57/// by one exactly when that replica lands on `site`.
58#[ensures((rf - site + sites - 1) / sites
59 == (rf - 1 - site + sites - 1) / sites + if (rf - 1) % sites == site { 1 } else { 0 })]
60pub fn lemma_round_robin_step(rf: Int, sites: Int, site: Int) {
61 let q = (rf - 1) / sites;
62 let r = (rf - 1) % sites;
63 // Division of the nonnegative `rf - 1`: quotient `q`, remainder `r`.
64 proof_assert!(rf - 1 == sites * q + r && 0 <= r && r < sites && q >= 0);
65 // Each case writes both numerators as `sites * quotient + remainder` with
66 // the remainder in `[0, sites)`, which fixes the quotient.
67 if r == site {
68 proof_assert!(rf - site + sites - 1 == sites * (q + 1) + 0);
69 proof_assert!(rf - 1 - site + sites - 1 == sites * q + (sites - 1));
70 lemma_div_exact(sites, q + 1, 0);
71 lemma_div_exact(sites, q, sites - 1);
72 } else if r > site {
73 proof_assert!(rf - site + sites - 1 == sites * (q + 1) + (r - site));
74 proof_assert!(rf - 1 - site + sites - 1 == sites * (q + 1) + (r - site - 1));
75 lemma_div_exact(sites, q + 1, r - site);
76 lemma_div_exact(sites, q + 1, r - site - 1);
77 } else {
78 proof_assert!(rf - site + sites - 1 == sites * q + (sites + r - site));
79 proof_assert!(rf - 1 - site + sites - 1 == sites * q + (sites + r - site - 1));
80 lemma_div_exact(sites, q, sites + r - site);
81 lemma_div_exact(sites, q, sites + r - site - 1);
82 }
83}
84
85#[krabka_macros::round_robin_site_lemma(0)]
86/// The closed form of `round_robin_load`: site `site` holds
87/// `(rf - site + sites - 1) / sites` replicas.
88#[ensures(round_robin_load(rf, sites, site) == (rf - site + sites - 1) / sites)]
89#[variant(rf)]
90pub fn lemma_round_robin_load(rf: Int, sites: Int, site: Int) {
91 if rf > 0 {
92 lemma_round_robin_load(rf - 1, sites, site);
93 lemma_round_robin_step(rf, sites, site);
94 }
95}
96
97#[krabka_macros::round_robin_site_lemma(0)]
98/// No site holds more than site 0 under round-robin placement, so site 0 is
99/// a fullest site.
100#[ensures(round_robin_load(rf, sites, site) <= round_robin_load(rf, sites, 0))]
101pub fn lemma_site_load_at_most_first(rf: Int, sites: Int, site: Int) {
102 // Both loads in closed form; site 0 has the largest numerator.
103 lemma_round_robin_load(rf, sites, site);
104 lemma_round_robin_load(rf, sites, 0);
105 lemma_div_monotone(rf - site + sites - 1, rf + sites - 1, sites);
106}
107
108/// The replica count that survives the loss of any one site.
109///
110/// Under the round-robin placement, `round_robin_load`, site 0 is a fullest
111/// site. The contract proves that the result is what the loss of site 0
112/// leaves, and that the loss of any other site leaves at least as many, so it
113/// is the fewest replicas any single site loss leaves. The body computes it as
114/// `rf - ceil(rf / sites)`; the proof derives that ceiling from the model
115/// rather than taking it as the definition.
116///
117/// An operator reads this number to pick `min.insync.replicas`. Three replicas
118/// on three sites leave 2, because each site holds one replica. Three replicas
119/// on two sites leave 1, because two of the three replicas share a site. One
120/// site leaves 0, because the loss of that site is the loss of the partition.
121///
122/// The proof assumes both `rf` and `sites` are in `1..=1024`. Normal Rust
123/// builds do not check that proof precondition.
124///
125/// # Panics
126///
127/// Panics when `sites` is zero. Arithmetic outside the proof bounds can panic
128/// when overflow checks are enabled.
129#[requires(rf@ >= 1 && rf@ <= 1024)]
130#[requires(sites@ >= 1 && sites@ <= 1024)]
131#[ensures(result@ == rf@ - round_robin_load(rf@, sites@, 0))]
132#[ensures(forall<site: Int> 0 <= site && site < sites@
133 ==> rf@ - round_robin_load(rf@, sites@, site) >= result@)]
134#[ensures(result@ >= 0)]
135#[ensures(result@ < rf@)]
136#[ensures(sites@ == 1 ==> result@ == 0)]
137#[must_use]
138pub fn site_loss_survivors(rf: i64, sites: i64) -> i64 {
139 // `(rf - 1) * (sites - 1) >= 0` gives `rf + sites - 1 <= rf * sites`, which
140 // is what bounds the ceiling by `rf` and keeps the result at 0 or above.
141 proof_assert!((rf@ - 1) * (sites@ - 1) >= 0);
142 proof_assert!(rf@ + sites@ - 1 <= sites@ * rf@ + 0);
143 proof_assert!({
144 lemma_div_monotone(rf@ + sites@ - 1, sites@ * rf@ + 0, sites@);
145 lemma_div_exact(sites@, rf@, 0);
146 (rf@ + sites@ - 1) / sites@ <= rf@
147 });
148 // The placement model: site 0 holds the ceiling, and no site holds more.
149 proof_assert!({
150 lemma_round_robin_load(rf@, sites@, 0);
151 round_robin_load(rf@, sites@, 0) == (rf@ + sites@ - 1) / sites@
152 });
153 proof_assert!(forall<site: Int> 0 <= site && site < sites@ ==> {
154 lemma_site_load_at_most_first(rf@, sites@, site);
155 round_robin_load(rf@, sites@, site) <= round_robin_load(rf@, sites@, 0)
156 });
157 rf - (rf + sites - 1) / sites
158}
159
160/// `true` if `min_insync` keeps `acks=all` writes durable and available through
161/// the loss of any one site.
162///
163/// Two bounds define the safe range, and the contract states both against
164/// every site of the round-robin placement, `round_robin_load`.
165///
166/// The **lower** bound is that no single site holds `min_insync` replicas.
167/// `min.insync.replicas` is a count and not a placement: the leader accepts an
168/// `acks=all` write as soon as that many in-sync replicas hold it, wherever
169/// they sit. If one site can hold `min_insync` of them, then the in-sync set
170/// can shrink to that one site, and the leader then acknowledges a write that
171/// only that site holds. The loss of that site loses an acknowledged write,
172/// and the surviving sites still hold the voter majority, so they elect a
173/// leader that never saw it. The shrink needs no second site loss to get
174/// there: one site down and one lagging replica is enough, and a witness on a
175/// cheap link is the replica most likely to lag.
176///
177/// The **upper** bound is that the loss of any one site leaves at least
178/// `min_insync` replicas, so the leader keeps accepting `acks=all` writes. The
179/// fewest that a site loss leaves is [`site_loss_survivors`].
180///
181/// The lower bound implies `min_insync >= 2`, because one site always holds at
182/// least one replica. It is therefore the whole of the durability condition,
183/// and 2 is not stated separately.
184///
185/// For three replicas on three sites each site holds one, so the bounds are
186/// `min_insync > 1` and `min_insync <= 2`: they meet, and 2 is the only safe
187/// value. Four replicas on three sites have no safe value at all, because one
188/// site holds two of them: the bounds become `min_insync > 2` and
189/// `min_insync <= 2`. Three replicas on two sites have none either, for the
190/// same reason. A witness site closes that gap. It holds a replica of its own,
191/// so three replicas spread one per site over three sites again.
192#[requires(rf@ >= 1 && rf@ <= 1024)]
193#[requires(sites@ >= 1 && sites@ <= 1024)]
194#[ensures(result == (
195 (forall<site: Int> 0 <= site && site < sites@
196 ==> round_robin_load(rf@, sites@, site) < min_insync@)
197 && (forall<site: Int> 0 <= site && site < sites@
198 ==> rf@ - round_robin_load(rf@, sites@, site) >= min_insync@)
199))]
200#[must_use]
201pub fn min_insync_is_site_loss_safe(rf: i64, sites: i64, min_insync: i64) -> bool {
202 let survivors = site_loss_survivors(rf, sites);
203 // What the fullest site holds. `site_loss_survivors` is `rf` less that
204 // count, so this recovers it without restating the ceiling.
205 let largest_site = rf - survivors;
206 min_insync > largest_site && min_insync <= survivors
207}