1use creusot_std::prelude::*;
4
5#[krabka_macros::model_type]
8pub enum VoteWireDecision {
9 Reject,
10 Accept,
11}
12
13#[krabka_macros::model_type]
15pub enum VoteEncodeDecision {
16 Reject,
17 Accept,
18}
19
20#[cfg(creusot)]
25#[cfg_attr(test, mutants::skip)]
26#[logic]
27pub fn vote_fields_fit_int32(
28 voter_id: u64,
29 candidate_id: u64,
30 candidate_epoch: u32,
31 last_epoch: u32,
32) -> bool {
33 pearlite! {
34 voter_id@ <= i32::MAX@
35 && candidate_id@ <= i32::MAX@
36 && candidate_epoch@ <= i32::MAX@
37 && last_epoch@ <= i32::MAX@
38 }
39}
40
41#[ensures((result == VoteEncodeDecision::Accept)
47 == vote_fields_fit_int32(voter_id, candidate_id, candidate_epoch, last_epoch))]
48#[must_use]
49pub fn vote_encode_decision(
50 voter_id: u64,
51 candidate_id: u64,
52 candidate_epoch: u32,
53 last_epoch: u32,
54) -> VoteEncodeDecision {
55 if voter_id > 2_147_483_647
56 || candidate_id > 2_147_483_647
57 || candidate_epoch > 2_147_483_647
58 || last_epoch > 2_147_483_647
59 {
60 VoteEncodeDecision::Reject
61 } else {
62 VoteEncodeDecision::Accept
63 }
64}
65
66#[cfg(creusot)]
70#[cfg_attr(test, mutants::skip)]
71#[logic]
72pub fn vote_fields_nonnegative(
73 voter_id: i32,
74 candidate_id: i32,
75 candidate_epoch: i32,
76 last_epoch: i32,
77) -> bool {
78 pearlite! {
79 voter_id@ >= 0 && candidate_id@ >= 0 && candidate_epoch@ >= 0 && last_epoch@ >= 0
80 }
81}
82
83#[ensures((result == VoteWireDecision::Accept)
87 == vote_fields_nonnegative(voter_id, candidate_id, candidate_epoch, last_epoch))]
88#[must_use]
89pub fn vote_wire_decision(
90 voter_id: i32,
91 candidate_id: i32,
92 candidate_epoch: i32,
93 last_epoch: i32,
94) -> VoteWireDecision {
95 if voter_id < 0 || candidate_id < 0 || candidate_epoch < 0 || last_epoch < 0 {
96 VoteWireDecision::Reject
97 } else {
98 VoteWireDecision::Accept
99 }
100}
101
102#[krabka_macros::model_type]
104pub enum VoteAdmissionDecision {
105 IgnoreWrongTarget,
107 Deny,
109 Consider,
111}
112
113#[krabka_macros::model_type]
115pub struct VoteTarget {
116 pub voter_id: u64,
118 pub local_id: u64,
120 pub directory_matches: bool,
122}
123
124#[krabka_macros::model_type]
126pub struct VoteMembership {
127 pub cluster_matches: bool,
129 pub local_is_voter: bool,
131 pub candidate_is_voter: bool,
133}
134
135#[ensures((result == VoteAdmissionDecision::IgnoreWrongTarget)
139 == (target.voter_id@ != target.local_id@ || !target.directory_matches))]
140#[ensures((result == VoteAdmissionDecision::Deny) == (
141 target.voter_id@ == target.local_id@ && target.directory_matches
142 && (!membership.cluster_matches
143 || !membership.local_is_voter
144 || !membership.candidate_is_voter)
145))]
146#[ensures((result == VoteAdmissionDecision::Consider) == (
147 target.voter_id@ == target.local_id@ && target.directory_matches
148 && membership.cluster_matches
149 && membership.local_is_voter
150 && membership.candidate_is_voter
151))]
152#[must_use]
153pub fn vote_admission_decision(
154 target: VoteTarget,
155 membership: VoteMembership,
156) -> VoteAdmissionDecision {
157 if target.voter_id != target.local_id || !target.directory_matches {
158 VoteAdmissionDecision::IgnoreWrongTarget
159 } else if !membership.cluster_matches
160 || !membership.local_is_voter
161 || !membership.candidate_is_voter
162 {
163 VoteAdmissionDecision::Deny
164 } else {
165 VoteAdmissionDecision::Consider
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use {super::*, assert2::check};
172
173 #[yare::parameterized(
174 zeroes = { 0, 0, 0, 0, VoteWireDecision::Accept },
175 negative_voter = { -1, 0, 0, 0, VoteWireDecision::Reject },
176 negative_candidate = { 0, -1, 0, 0, VoteWireDecision::Reject },
177 negative_epoch = { 0, 0, -1, 0, VoteWireDecision::Reject },
178 negative_last_epoch = { 0, 0, 0, -1, VoteWireDecision::Reject },
179 )]
180 fn wire_fields_reject_negatives_without_rejecting_zero(
181 voter: i32,
182 candidate: i32,
183 epoch: i32,
184 last_epoch: i32,
185 expected: VoteWireDecision,
186 ) {
187 check!(vote_wire_decision(voter, candidate, epoch, last_epoch) == expected);
188 }
189
190 #[yare::parameterized(
191 signed_maximum = {
192 i32::MAX as u64,
193 i32::MAX as u64,
194 i32::MAX as u32,
195 i32::MAX as u32,
196 VoteEncodeDecision::Accept,
197 },
198 voter_overflow = { i32::MAX as u64 + 1, 0, 0, 0, VoteEncodeDecision::Reject },
199 candidate_overflow = { 0, i32::MAX as u64 + 1, 0, 0, VoteEncodeDecision::Reject },
200 epoch_overflow = { 0, 0, i32::MAX as u32 + 1, 0, VoteEncodeDecision::Reject },
201 last_epoch_overflow = { 0, 0, 0, i32::MAX as u32 + 1, VoteEncodeDecision::Reject },
202 )]
203 fn encode_fields_reject_values_above_signed_wire_maximum(
204 voter: u64,
205 candidate: u64,
206 epoch: u32,
207 last_epoch: u32,
208 expected: VoteEncodeDecision,
209 ) {
210 check!(vote_encode_decision(voter, candidate, epoch, last_epoch) == expected);
211 }
212
213 #[yare::parameterized(
214 other_voter = {
215 VoteTarget { voter_id: 1, local_id: 2, directory_matches: true },
216 VoteMembership { cluster_matches: true, local_is_voter: true, candidate_is_voter: true },
217 VoteAdmissionDecision::IgnoreWrongTarget,
218 },
219 zero_other_voter = {
220 VoteTarget { voter_id: 0, local_id: 1, directory_matches: true },
221 VoteMembership { cluster_matches: true, local_is_voter: true, candidate_is_voter: true },
222 VoteAdmissionDecision::IgnoreWrongTarget,
223 },
224 directory_mismatch = {
225 VoteTarget { voter_id: 1, local_id: 1, directory_matches: false },
226 VoteMembership { cluster_matches: true, local_is_voter: true, candidate_is_voter: true },
227 VoteAdmissionDecision::IgnoreWrongTarget,
228 },
229 exact_target = {
230 VoteTarget { voter_id: 0, local_id: 0, directory_matches: true },
231 VoteMembership { cluster_matches: true, local_is_voter: true, candidate_is_voter: true },
232 VoteAdmissionDecision::Consider,
233 },
234 cluster_mismatch = {
235 VoteTarget { voter_id: 1, local_id: 1, directory_matches: true },
236 VoteMembership { cluster_matches: false, local_is_voter: true, candidate_is_voter: true },
237 VoteAdmissionDecision::Deny,
238 },
239 local_not_voter = {
240 VoteTarget { voter_id: 1, local_id: 1, directory_matches: true },
241 VoteMembership { cluster_matches: true, local_is_voter: false, candidate_is_voter: true },
242 VoteAdmissionDecision::Deny,
243 },
244 candidate_not_voter = {
245 VoteTarget { voter_id: 1, local_id: 1, directory_matches: true },
246 VoteMembership { cluster_matches: true, local_is_voter: true, candidate_is_voter: false },
247 VoteAdmissionDecision::Deny,
248 },
249 valid_nonzero_target = {
250 VoteTarget { voter_id: 1, local_id: 1, directory_matches: true },
251 VoteMembership { cluster_matches: true, local_is_voter: true, candidate_is_voter: true },
252 VoteAdmissionDecision::Consider,
253 },
254 )]
255 fn admission_requires_the_exact_target_and_both_memberships(
256 target: VoteTarget,
257 membership: VoteMembership,
258 expected: VoteAdmissionDecision,
259 ) {
260 check!(
261 vote_admission_decision(target, membership) == expected,
262 "{target:?} {membership:?}"
263 );
264 }
265}