Skip to main content

automation_structures/primitives/
audit_sink.rs

1// Shared append-only chain owner for AuditSink and its named compositions.
2//
3// The chain operation is a verified parameter. BoundedHash is the public bounded model instance;
4// AdditiveChain is the ReductionStream instance proved by ReductionStreamFromAuditSink.tla.
5
6use vstd::prelude::*;
7
8verus! {
9
10/// Operation used to extend an AuditSink chain.
11pub trait ChainOperation: Copy {
12    /// Whether the executable operation is defined for this input pair.
13    spec fn enabled(&self, previous: u64, operation: u64) -> bool;
14
15    /// Mathematical result of extending the chain.
16    spec fn combine_spec(&self, previous: u64, operation: u64) -> u64;
17
18    /// Executable form of `enabled`.
19    fn enabled_exec(&self, previous: u64, operation: u64) -> (enabled: bool)
20        ensures enabled == self.enabled(previous, operation);
21
22    /// Extend the chain.
23    fn combine(&self, previous: u64, operation: u64) -> (result: u64)
24        requires self.enabled(previous, operation),
25        ensures result == self.combine_spec(previous, operation);
26}
27
28/// The bounded recomputation operation used by the public audit sink.
29///
30/// Its modulo-100 output intentionally permits collisions. It supplies neither cryptographic
31/// collision resistance nor evidence of durable custody or external tamper detection.
32#[derive(Clone, Copy)]
33pub struct BoundedHash;
34
35impl ChainOperation for BoundedHash {
36    open spec fn enabled(&self, _previous: u64, _operation: u64) -> bool {
37        true
38    }
39
40    open spec fn combine_spec(&self, previous: u64, operation: u64) -> u64 {
41        AuditSink::<BoundedHash>::hash_spec(previous, operation) as u64
42    }
43
44    fn enabled_exec(&self, _previous: u64, _operation: u64) -> (enabled: bool) {
45        true
46    }
47
48    fn combine(&self, previous: u64, operation: u64) -> (result: u64) {
49        AuditSink::<BoundedHash>::hash_exec(previous, operation)
50    }
51}
52
53/// Exact additive chain operation used by ReductionStream.
54#[derive(Clone, Copy)]
55pub struct AdditiveChain;
56
57impl ChainOperation for AdditiveChain {
58    open spec fn enabled(&self, previous: u64, operation: u64) -> bool {
59        previous as int + operation as int <= u64::MAX as int
60    }
61
62    open spec fn combine_spec(&self, previous: u64, operation: u64) -> u64 {
63        (previous + operation) as u64
64    }
65
66    fn enabled_exec(&self, previous: u64, operation: u64) -> (enabled: bool) {
67        operation <= u64::MAX - previous
68    }
69
70    fn combine(&self, previous: u64, operation: u64) -> (result: u64) {
71        previous + operation
72    }
73}
74
75/// One audit record: the operation, predecessor chain value, and resulting chain value.
76pub struct AuditEntry {
77    /// Operation committed by this entry.
78    pub operation: u64,
79    /// Chain value immediately before the operation.
80    pub prev_hash: u64,
81    /// Chain value produced by the operation.
82    pub hash: u64,
83}
84
85/// An append-only chained log. The operation parameter defaults to the public bounded hash.
86pub struct AuditSink<O: ChainOperation = BoundedHash> {
87    /// Chain operation instance.
88    pub operator: O,
89    /// Maximum retained entry count.
90    pub max_log_len: usize,
91    /// Append-only audit entries.
92    pub log: Vec<AuditEntry>,
93    /// Chain value after the latest entry, or zero for an empty log.
94    pub last_hash: u64,
95}
96
97impl AuditSink<BoundedHash> {
98    /// Chain hash in the original AuditSink model's integer form.
99    pub open spec fn hash_spec(previous: u64, operation: u64) -> int {
100        ((previous as int) * 3 + ((operation as int) % 100) + 1) % 100
101    }
102
103    /// Execute the public bounded hash instance.
104    pub fn hash_exec(previous: u64, operation: u64) -> (result: u64)
105        ensures
106            result as int == Self::hash_spec(previous, operation),
107            result < 100,
108    {
109        ((previous % 100) * 3 + (operation % 100) + 1) % 100
110    }
111
112    /// Construct the public bounded-hash AuditSink.
113    pub fn new(max_log_len: usize) -> (sink: AuditSink<BoundedHash>)
114        ensures
115            sink.max_log_len == max_log_len,
116            sink.log@.len() == 0,
117            sink.last_hash == 0,
118            sink.inv(),
119    {
120        AuditSink::with_operator(max_log_len, BoundedHash)
121    }
122}
123
124impl<O: ChainOperation> AuditSink<O> {
125    /// Construct an empty chain for a verified operation instance.
126    pub fn with_operator(max_log_len: usize, operator: O) -> (sink: AuditSink<O>)
127        ensures
128            sink.operator == operator,
129            sink.max_log_len == max_log_len,
130            sink.log@.len() == 0,
131            sink.last_hash == 0,
132            sink.inv(),
133    {
134        AuditSink { operator, max_log_len, log: Vec::new(), last_hash: 0 }
135    }
136
137    /// Whether the retained log fits within its configured capacity.
138    pub open spec fn type_invariant(&self) -> bool {
139        self.log.len() <= self.max_log_len
140    }
141
142    /// Whether every non-genesis record links to its immediate predecessor.
143    pub open spec fn chain_integrity(&self) -> bool {
144        forall|index: int|
145            #![trigger self.log@[index]]
146            1 <= index < self.log.len() ==>
147                self.log@[index].prev_hash == self.log@[index - 1].hash
148    }
149
150    /// Whether the retained head agrees with the last record or the empty-chain value.
151    pub open spec fn hash_consistency(&self) -> bool {
152        if self.log.len() > 0 {
153            self.last_hash == self.log@[self.log.len() - 1].hash
154        } else {
155            self.last_hash == 0
156        }
157    }
158
159    /// Every entry is the configured chain operation recomputed from its stored content.
160    ///
161    /// For `BoundedHash`, this is an arithmetic consistency check, not a cryptographic binding.
162    pub open spec fn hash_binds_content(&self) -> bool {
163        forall|index: int|
164            #![trigger self.log@[index]]
165            0 <= index < self.log.len() ==>
166                self.log@[index].hash
167                    == self.operator.combine_spec(
168                        self.log@[index].prev_hash,
169                        self.log@[index].operation,
170                    )
171    }
172
173    /// Every stored operation was in the configured operation's executable domain.
174    pub open spec fn operations_enabled(&self) -> bool {
175        forall|index: int|
176            #![trigger self.log@[index]]
177            0 <= index < self.log.len() ==>
178                self.operator.enabled(
179                    self.log@[index].prev_hash,
180                    self.log@[index].operation,
181                )
182    }
183
184    /// Whether the first retained record links to the genesis hash.
185    pub open spec fn genesis_consistency(&self) -> bool {
186        self.log.len() > 0 ==> self.log@[0].prev_hash == 0
187    }
188
189    /// Whether all append-only chain contract clauses hold.
190    pub open spec fn inv(&self) -> bool {
191        &&& self.type_invariant()
192        &&& self.chain_integrity()
193        &&& self.hash_consistency()
194        &&& self.hash_binds_content()
195        &&& self.operations_enabled()
196        &&& self.genesis_consistency()
197    }
198
199    /// Append one operation through the chain owner.
200    pub fn record(&mut self, operation: u64) -> (accepted: bool)
201        requires
202            old(self).inv(),
203            old(self).operator.enabled(old(self).last_hash, operation),
204        ensures
205            final(self).inv(),
206            final(self).operator == old(self).operator,
207            final(self).max_log_len == old(self).max_log_len,
208            accepted == (old(self).log.len() < old(self).max_log_len),
209            accepted ==> {
210                &&& final(self).log@.len() == old(self).log@.len() + 1
211                &&& final(self).last_hash
212                    == old(self).operator.combine_spec(old(self).last_hash, operation)
213                &&& final(self).log@[old(self).log@.len() as int].operation == operation
214                &&& final(self).log@[old(self).log@.len() as int].prev_hash
215                    == old(self).last_hash
216                &&& forall|index: int|
217                    #![trigger final(self).log@[index]]
218                    0 <= index < old(self).log@.len() ==>
219                        final(self).log@[index] == old(self).log@[index]
220            },
221            !accepted ==>
222                final(self).log@ == old(self).log@
223                    && final(self).last_hash == old(self).last_hash,
224    {
225        if self.log.len() < self.max_log_len {
226            let new_hash = self.operator.combine(self.last_hash, operation);
227            let entry = AuditEntry {
228                operation,
229                prev_hash: self.last_hash,
230                hash: new_hash,
231            };
232            assert(self.log@.len() > 0 ==>
233                self.last_hash == self.log@[self.log@.len() - 1].hash);
234            self.log.push(entry);
235            self.last_hash = new_hash;
236            assert(self.chain_integrity()) by {
237                assert forall|index: int| #![trigger self.log@[index]]
238                    1 <= index < self.log.len() implies
239                        self.log@[index].prev_hash == self.log@[index - 1].hash by {
240                    if index < self.log.len() - 1 {
241                    }
242                }
243            }
244            assert(self.hash_binds_content()) by {
245                assert forall|index: int| #![trigger self.log@[index]]
246                    0 <= index < self.log.len() implies
247                        self.log@[index].hash == self.operator.combine_spec(
248                            self.log@[index].prev_hash,
249                            self.log@[index].operation,
250                        ) by {
251                    if index < self.log.len() - 1 {
252                    }
253                }
254            }
255            assert(self.operations_enabled()) by {
256                assert forall|index: int| #![trigger self.log@[index]]
257                    0 <= index < self.log.len() implies
258                        self.operator.enabled(
259                            self.log@[index].prev_hash,
260                            self.log@[index].operation,
261                        ) by {
262                    if index < self.log.len() - 1 {
263                    }
264                }
265            }
266            assert(self.genesis_consistency());
267            true
268        } else {
269            false
270        }
271    }
272
273    /// Recompute the entire configured chain from the zero genesis.
274    pub fn validate(&self) -> (valid: bool)
275        ensures valid == self.inv(),
276    {
277        if self.log.len() > self.max_log_len {
278            return false;
279        }
280
281        let length = self.log.len();
282        let mut index: usize = 0;
283        let mut expected_previous: u64 = 0;
284        while index < length
285            invariant
286                index <= length,
287                length == self.log.len(),
288                self.log.len() <= self.max_log_len,
289                index == 0 ==> expected_previous == 0,
290                index > 0 ==> expected_previous == self.log@[index as int - 1].hash,
291                forall|entry: int| 0 <= entry < index ==>
292                    #[trigger] self.log@[entry].hash == self.operator.combine_spec(
293                        self.log@[entry].prev_hash,
294                        self.log@[entry].operation,
295                    ),
296                forall|entry: int| 0 <= entry < index ==>
297                    #[trigger] self.operator.enabled(
298                        self.log@[entry].prev_hash,
299                        self.log@[entry].operation,
300                    ),
301                forall|entry: int| 1 <= entry < index ==>
302                    #[trigger] self.log@[entry].prev_hash == self.log@[entry - 1].hash,
303                index > 0 ==> self.log@[0].prev_hash == 0,
304            decreases length - index,
305        {
306            if self.log[index].prev_hash != expected_previous {
307                return false;
308            }
309            if !self.operator.enabled_exec(self.log[index].prev_hash, self.log[index].operation) {
310                return false;
311            }
312            let expected_hash = self.operator.combine(
313                self.log[index].prev_hash,
314                self.log[index].operation,
315            );
316            if self.log[index].hash != expected_hash {
317                return false;
318            }
319            expected_previous = self.log[index].hash;
320            index = index + 1;
321        }
322
323        if self.last_hash != expected_previous {
324            return false;
325        }
326        true
327    }
328}
329
330}