use creusot_std::prelude::*;
#[cfg(creusot)]
#[logic]
pub fn hwm_member_at(log_end: Int, s: Seq<i64>, k: Int) -> Int {
pearlite! {
if k == 0 { log_end } else { s[k - 1]@ }
}
}
#[cfg(creusot)]
#[logic]
#[variant(limit)]
pub fn count_ge_prefix(log_end: Int, s: Seq<i64>, v: Int, limit: Int) -> Int {
pearlite! {
if limit <= 0 {
0
} else {
count_ge_prefix(log_end, s, v, limit - 1)
+ (if hwm_member_at(log_end, s, limit - 1) >= v { 1 } else { 0 })
}
}
}
#[cfg(creusot)]
#[logic]
#[variant(s.len())]
pub fn count_ge(log_end: Int, s: Seq<i64>, v: Int) -> Int {
pearlite! { count_ge_prefix(log_end, s, v, s.len() + 1) }
}
#[cfg(creusot)]
#[logic]
#[requires(0 <= limit && limit <= s.len() + 1)]
#[requires(low <= high)]
#[ensures(count_ge_prefix(log_end, s, low, limit) >= count_ge_prefix(log_end, s, high, limit))]
#[variant(limit)]
pub fn lemma_count_ge_prefix_monotone(log_end: Int, s: Seq<i64>, low: Int, high: Int, limit: Int) {
if limit > 0 {
lemma_count_ge_prefix_monotone(log_end, s, low, high, limit - 1);
}
}
#[cfg(creusot)]
#[logic]
#[requires(0 <= limit && limit <= s.len() + 1)]
#[ensures(count_ge_prefix(log_end, s, v, limit) >= 0)]
#[variant(limit)]
pub fn lemma_count_ge_prefix_nonnegative(log_end: Int, s: Seq<i64>, v: Int, limit: Int) {
if limit > 0 {
lemma_count_ge_prefix_nonnegative(log_end, s, v, limit - 1);
}
}
#[cfg(creusot)]
#[logic]
#[requires(0 < limit && limit <= s.len() + 1)]
#[requires(count_ge_prefix(log_end, s, v, limit) >= 1)]
#[ensures(0 <= result && result < limit)]
#[ensures(hwm_member_at(log_end, s, result) >= v)]
#[ensures(count_ge_prefix(log_end, s, hwm_member_at(log_end, s, result), limit)
>= count_ge_prefix(log_end, s, v, limit))]
#[variant(limit)]
pub fn least_hwm_member_ge_index(log_end: Int, s: Seq<i64>, v: Int, limit: Int) -> Int {
if limit <= 1 {
0
} else {
let last_index = limit - 1;
let last_member = hwm_member_at(log_end, s, last_index);
let previous_count = count_ge_prefix(log_end, s, v, last_index);
if last_member >= v {
if previous_count >= 1 {
let previous_index = least_hwm_member_ge_index(log_end, s, v, last_index);
let previous_member = hwm_member_at(log_end, s, previous_index);
if last_member <= previous_member {
lemma_count_ge_prefix_monotone(
log_end,
s,
last_member,
previous_member,
last_index,
);
proof_assert!(
count_ge_prefix(log_end, s, last_member, last_index)
>= count_ge_prefix(log_end, s, v, last_index)
);
proof_assert!(
count_ge_prefix(log_end, s, last_member, limit)
== count_ge_prefix(log_end, s, last_member, last_index) + 1
);
proof_assert!(
count_ge_prefix(log_end, s, v, limit)
== count_ge_prefix(log_end, s, v, last_index) + 1
);
proof_assert!(
count_ge_prefix(log_end, s, last_member, limit)
>= count_ge_prefix(log_end, s, v, limit)
);
last_index
} else {
proof_assert!(previous_member <= last_member);
proof_assert!(
count_ge_prefix(log_end, s, previous_member, limit)
== count_ge_prefix(log_end, s, previous_member, last_index) + 1
);
proof_assert!(
count_ge_prefix(log_end, s, v, limit)
== count_ge_prefix(log_end, s, v, last_index) + 1
);
proof_assert!(
count_ge_prefix(log_end, s, previous_member, limit)
>= count_ge_prefix(log_end, s, v, limit)
);
previous_index
}
} else {
lemma_count_ge_prefix_nonnegative(log_end, s, v, last_index);
lemma_count_ge_prefix_nonnegative(log_end, s, last_member, last_index);
proof_assert!(previous_count <= 0);
proof_assert!(previous_count == 0);
proof_assert!(
count_ge_prefix(log_end, s, last_member, limit)
== count_ge_prefix(log_end, s, last_member, last_index) + 1
);
proof_assert!(count_ge_prefix(log_end, s, last_member, limit) >= 1);
proof_assert!(
count_ge_prefix(log_end, s, v, limit)
== count_ge_prefix(log_end, s, v, last_index) + 1
);
proof_assert!(count_ge_prefix(log_end, s, v, limit) <= 1);
proof_assert!(count_ge_prefix(log_end, s, last_member, limit) >= 1
&& count_ge_prefix(log_end, s, v, limit) <= 1
==> count_ge_prefix(log_end, s, last_member, limit)
>= count_ge_prefix(log_end, s, v, limit));
proof_assert!(
count_ge_prefix(log_end, s, last_member, limit)
>= count_ge_prefix(log_end, s, v, limit)
);
last_index
}
} else {
proof_assert!(count_ge_prefix(log_end, s, v, last_index) >= 1);
least_hwm_member_ge_index(log_end, s, v, last_index)
}
}
}
#[cfg(creusot)]
#[requires(1 <= majority@ && majority@ <= s@.len() + 1)]
#[ensures(forall<v: Int> count_ge(log_end@, s@, v) >= majority@
==> exists<k: Int> 0 <= k && k < s@.len() + 1
&& hwm_member_at(log_end@, s@, k) >= v
&& count_ge(log_end@, s@, hwm_member_at(log_end@, s@, k)) >= majority@)]
pub fn lemma_hwm_threshold_has_member(log_end: i64, s: &[i64], majority: usize) {
proof_assert!(forall<v: Int> count_ge(log_end@, s@, v) >= majority@ ==>
exists<k: Int> k == least_hwm_member_ge_index(log_end@, s@, v, s@.len() + 1)
&& 0 <= k && k < s@.len() + 1
&& hwm_member_at(log_end@, s@, k) >= v
&& count_ge(log_end@, s@, hwm_member_at(log_end@, s@, k)) >= majority@);
}
#[cfg(creusot)]
#[requires(1 <= majority@ && majority@ <= s@.len() + 1)]
#[requires(forall<k: Int> 0 <= k && k < s@.len() + 1
&& count_ge(log_end@, s@, hwm_member_at(log_end@, s@, k)) >= majority@
==> hwm_member_at(log_end@, s@, k) <= best@)]
#[ensures(forall<v: Int> count_ge(log_end@, s@, v) >= majority@ ==> v <= best@)]
pub fn lemma_hwm_member_maximal(log_end: i64, s: &[i64], majority: usize, best: i64) {
lemma_hwm_threshold_has_member(log_end, s, majority);
}
#[ensures(base_ms@ == 0 ==> result@ == 0)]
#[ensures(base_ms@ > 0 ==> result@ < base_ms@)]
#[must_use]
pub fn election_jitter_ms(me: u64, epoch: u32, base_ms: u64) -> u64 {
if base_ms == 0 {
return 0;
}
let mix = me.wrapping_mul(0x9E37_79B9_7F4A_7C15)
^ u64::from(epoch).wrapping_mul(0xD1B5_4A32_D192_ED03);
mix % base_ms
}
#[ensures(result == (cand_epoch@ > my_epoch@
|| (cand_epoch@ == my_epoch@ && cand_offset@ >= my_end@)))]
#[must_use]
pub const fn log_is_up_to_date(
my_epoch: u32,
my_end: i64,
cand_epoch: u32,
cand_offset: i64,
) -> bool {
cand_epoch > my_epoch || (cand_epoch == my_epoch && cand_offset >= my_end)
}
#[requires(1 <= majority@ && majority@ <= follower_offsets@.len() + 1)]
#[ensures(result == (count_ge(log_end@, follower_offsets@, cand@) >= majority@))]
fn candidate_has_majority(
log_end: i64,
follower_offsets: &[i64],
cand: i64,
majority: usize,
) -> bool {
let mut count: usize = 0;
if log_end >= cand && count < majority {
count += 1;
}
let n = follower_offsets.len();
let mut j = 0;
#[invariant(j@ <= n@)]
#[invariant({
let seen = count_ge_prefix(log_end@, follower_offsets@, cand@, j@ + 1);
count@ == if seen < majority@ { seen } else { majority@ }
})]
#[invariant(count@ <= majority@)]
#[variant(n@ - j@)]
while j < n {
let x = follower_offsets[j];
if x >= cand && count < majority {
count += 1;
}
j += 1;
}
count >= majority
}
#[requires(1 <= majority@ && majority@ <= follower_offsets@.len() + 1)]
#[requires(current_hwm@ <= log_end@)]
#[requires(forall<k: Int> 0 <= k && k < follower_offsets@.len()
==> follower_offsets@[k]@ <= log_end@)]
#[ensures(result@ >= current_hwm@)]
#[ensures(result@ <= log_end@)]
#[ensures(forall<v: Int> v > epoch_start_offset@
&& count_ge(log_end@, follower_offsets@, v) >= majority@
==> v <= result@)]
#[ensures(result@ > current_hwm@
==> result@ > epoch_start_offset@
&& count_ge(log_end@, follower_offsets@, result@) >= majority@)]
#[must_use]
pub fn recompute_high_watermark(
log_end: i64,
follower_offsets: &[i64],
majority: usize,
epoch_start_offset: i64,
current_hwm: i64,
) -> i64 {
let n = follower_offsets.len();
let mut majority_offset = i64::MIN;
if candidate_has_majority(log_end, follower_offsets, log_end, majority) {
majority_offset = log_end;
}
let mut i = 0;
#[invariant(i@ <= n@)]
#[invariant(majority_offset@ <= log_end@)]
#[invariant(majority_offset@ == -9223372036854775807 - 1
|| count_ge(log_end@, follower_offsets@, majority_offset@) >= majority@)]
#[invariant(forall<k: Int> 0 <= k && k < i@ + 1
&& count_ge(log_end@, follower_offsets@, hwm_member_at(log_end@, follower_offsets@, k)) >= majority@
==> hwm_member_at(log_end@, follower_offsets@, k) <= majority_offset@)]
#[variant(n@ - i@)]
while i < n {
let cand = follower_offsets[i];
if cand > majority_offset
&& candidate_has_majority(log_end, follower_offsets, cand, majority)
{
majority_offset = cand;
}
i += 1;
}
#[cfg(creusot)]
lemma_hwm_member_maximal(log_end, follower_offsets, majority, majority_offset);
let gated = if majority_offset > epoch_start_offset {
majority_offset
} else {
current_hwm
};
gated.max(current_hwm)
}
#[cfg(test)]
mod tests {
use assert2::{assert, check};
use proptest::prelude::*;
use super::*;
fn hwm_sort_oracle(
log_end: i64,
follower_offsets: &[i64],
majority: usize,
epoch_start_offset: i64,
current_hwm: i64,
) -> i64 {
let mut match_offsets: Vec<i64> = Vec::with_capacity(follower_offsets.len() + 1);
match_offsets.push(log_end);
match_offsets.extend_from_slice(follower_offsets);
match_offsets.sort_unstable_by(|a, b| b.cmp(a));
let majority_offset = match_offsets[majority - 1];
let gated = if majority_offset > epoch_start_offset {
majority_offset
} else {
current_hwm
};
gated.max(current_hwm)
}
proptest! {
#[test]
fn hwm_matches_sort_oracle(
log_end in 0i64..1_000,
followers in proptest::collection::vec(0i64..1_000, 0..7),
majority_seed in 0usize..8,
epoch_start_offset in 0i64..1_000,
current_hwm in 0i64..1_000,
) {
let majority = 1 + majority_seed % (followers.len() + 1);
let followers: Vec<i64> = followers.iter().map(|o| (*o).min(log_end)).collect();
let current_hwm = current_hwm.min(log_end);
prop_assert_eq!(
recompute_high_watermark(log_end, &followers, majority, epoch_start_offset, current_hwm),
hwm_sort_oracle(log_end, &followers, majority, epoch_start_offset, current_hwm)
);
}
#[test]
fn jitter_in_range(me in any::<u64>(), epoch in any::<u32>(), base in 1u64..10_000) {
prop_assert!(election_jitter_ms(me, epoch, base) < base);
}
}
#[test]
fn jitter_zero_base_is_zero() {
assert!(election_jitter_ms(7, 3, 0) == 0);
}
#[test]
fn jitter_uses_node_and_epoch_hash_inputs() {
assert!(election_jitter_ms(1, 0, 1000) == 485);
assert!(election_jitter_ms(2, 0, 1000) == 354);
assert!(election_jitter_ms(1, 1, 1000) == 446);
}
#[test]
fn up_to_date_is_the_kip595_rule() {
check!(log_is_up_to_date(5, 100, 6, 0));
check!(log_is_up_to_date(5, 100, 5, 100));
check!(!log_is_up_to_date(5, 100, 5, 99));
check!(!log_is_up_to_date(5, 0, 4, i64::MAX));
}
#[test]
fn hwm_never_regresses_and_gates_on_epoch_start() {
check!(recompute_high_watermark(10, &[3, 9], 2, 9, 5) == 5);
check!(recompute_high_watermark(10, &[3, 9], 2, 8, 5) == 9);
check!(recompute_high_watermark(10, &[1, 1], 2, 0, 7) == 7);
}
#[test]
fn hwm_counts_leader_and_followers_until_majority() {
check!(recompute_high_watermark(10, &[9, 8], 2, 0, 0) == 9);
check!(recompute_high_watermark(10, &[10, 10], 3, 0, 0) == 10);
check!(recompute_high_watermark(10, &[4, 4], 3, 0, 0) == 4);
}
}