Skip to main content

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}