Skip to main content

krabka_verified/audit/
spool_append_decision.rs

1use {
2    super::{AuditCheckpointAdmission, AuditLossMarkerAdmission, AuditLosses, SpoolAppendDecision},
3    creusot_std::prelude::*,
4};
5
6/// Bind a verified checkpoint to the exact nonempty chain position and head.
7#[ensures((result == AuditCheckpointAdmission::Admit) == (
8    signature_valid && head_matches && expected_seq@ > 0
9        && checkpoint_seq_high@ + 1 == expected_seq@))]
10#[ensures((result == AuditCheckpointAdmission::RejectSignature) == !signature_valid)]
11#[ensures((result == AuditCheckpointAdmission::RejectHead)
12    == (signature_valid && !head_matches))]
13#[ensures((result == AuditCheckpointAdmission::RejectSequence) == (
14    signature_valid && head_matches
15        && (expected_seq@ == 0 || checkpoint_seq_high@ + 1 != expected_seq@)))]
16#[must_use]
17pub fn audit_checkpoint_admission(
18    signature_valid: bool,
19    head_matches: bool,
20    expected_seq: u64,
21    checkpoint_seq_high: u64,
22) -> AuditCheckpointAdmission {
23    if !signature_valid {
24        AuditCheckpointAdmission::RejectSignature
25    } else if !head_matches {
26        AuditCheckpointAdmission::RejectHead
27    } else if expected_seq == 0 || checkpoint_seq_high != expected_seq - 1 {
28        AuditCheckpointAdmission::RejectSequence
29    } else {
30        AuditCheckpointAdmission::Admit
31    }
32}
33
34/// Admit exactly a marker whose body is the two-field
35/// `{records_lost, loss_generation}` shape with a positive count and a
36/// generation strictly newer than the last accepted one.
37#[ensures(match result {
38    AuditLossMarkerAdmission::Admit { generation: admitted } => header_matches
39        && field_count@ == 2
40        && count@ > 0
41        && generation == Some(admitted)
42        && admitted@ > previous_generation@,
43    AuditLossMarkerAdmission::Reject => !(header_matches && field_count@ == 2 && count@ > 0
44        && match generation {
45            Some(generation) => generation@ > previous_generation@,
46            None => false,
47        }),
48})]
49#[must_use]
50pub fn audit_loss_marker_admission(
51    header_matches: bool,
52    field_count: u64,
53    count: u64,
54    generation: Option<u64>,
55    previous_generation: u64,
56) -> AuditLossMarkerAdmission {
57    match generation {
58        Some(generation)
59            if header_matches
60                && field_count == 2
61                && count > 0
62                && generation > previous_generation =>
63        {
64            AuditLossMarkerAdmission::Admit { generation }
65        }
66        _ => AuditLossMarkerAdmission::Reject,
67    }
68}
69
70/// Settle the pending losses `state` against a durable marker that reports
71/// `batch`, at the writer's commit and at open-time reconciliation alike.
72///
73/// A marker settles only its own generation, and only the count it reports.
74/// Losses that `AuditHandle::emit` added after the writer's snapshot stay
75/// pending, and move to the next generation. The host only ever moves a
76/// generation forward, so no marker already in the spool names that one: no
77/// later reconciliation settles them a second time, and the next marker names
78/// a generation newer than the last, as [`audit_loss_marker_admission`]
79/// requires. A marker of another generation settles nothing.
80///
81/// A batch larger than the pending count settles it to zero. The host never
82/// produces one -- a batch is a snapshot of the pending count, which only grows
83/// within a generation until settled -- so that case is clamped, not
84/// conserved. At `u64::MAX` the generation saturates rather than bumps; that
85/// takes 2^64 settlements and is the host's to rule out.
86#[ensures(state.generation != batch.generation
87    ==> result.generation == state.generation && result.count == state.count)]
88#[ensures(state.generation == batch.generation && batch.count@ <= state.count@
89    ==> result.count@ + batch.count@ == state.count@)]
90#[ensures(state.generation == batch.generation && batch.count@ > state.count@
91    ==> result.count@ == 0)]
92#[ensures(state.generation == batch.generation
93    ==> (result.generation != state.generation)
94        == (result.count@ > 0 && state.generation@ < u64::MAX@))]
95#[ensures(result.generation != state.generation
96    ==> result.generation@ == state.generation@ + 1)]
97#[must_use]
98pub fn settle_loss_batch(state: AuditLosses, batch: AuditLosses) -> AuditLosses {
99    if state.generation != batch.generation {
100        return state;
101    }
102    if batch.count >= state.count {
103        return AuditLosses {
104            generation: state.generation,
105            count: 0,
106        };
107    }
108    AuditLosses {
109        generation: if state.generation < u64::MAX {
110            state.generation + 1
111        } else {
112            state.generation
113        },
114        count: state.count - batch.count,
115    }
116}
117
118/// Admit one frame and advance the successful-append sync cadence.
119#[requires(sync_every@ > 0)]
120#[requires(unsynced@ < sync_every@)]
121#[ensures(result.accepted
122    == (current_bytes@ <= max_bytes@ && frame_bytes@ <= max_bytes@ - current_bytes@))]
123#[ensures(result.accepted ==> result.new_bytes@ == current_bytes@ + frame_bytes@)]
124#[ensures(result.accepted ==> result.new_bytes@ <= max_bytes@)]
125#[ensures(!result.accepted ==> result.new_bytes@ == current_bytes@)]
126#[ensures(result.sync
127    == (result.accepted && unsynced@ + 1 >= sync_every@))]
128#[ensures(result.next_unsynced@ < sync_every@)]
129#[ensures(result.next_unsynced@ == if result.accepted {
130    if unsynced@ + 1 >= sync_every@ { 0 } else { unsynced@ + 1 }
131} else {
132    unsynced@
133})]
134#[must_use]
135pub fn spool_append_decision(
136    current_bytes: u64,
137    frame_bytes: u64,
138    max_bytes: u64,
139    unsynced: u64,
140    sync_every: u64,
141) -> SpoolAppendDecision {
142    if current_bytes > max_bytes || frame_bytes > max_bytes - current_bytes {
143        return SpoolAppendDecision {
144            accepted: false,
145            new_bytes: current_bytes,
146            sync: false,
147            next_unsynced: unsynced,
148        };
149    }
150
151    let next_unsynced = unsynced + 1;
152    let sync = next_unsynced >= sync_every;
153    SpoolAppendDecision {
154        accepted: true,
155        new_bytes: current_bytes + frame_bytes,
156        sync,
157        next_unsynced: if sync { 0 } else { next_unsynced },
158    }
159}