Skip to main content

planarian_txn/
chain.rs

1//! The journal's hash chain as a decision (I18), apart from the I/O in
2//! `journal.rs`. Hashing stays outside: SHA-256 is trusted (P5), so each
3//! record enters as a `Link` carrying the head it claims to follow (`prev`)
4//! and the head after it (`next`, from `journal::chain`). What is proved here
5//! is everything else: a record follows exactly when its seq is the next one
6//! and its `prev` is the current head; a segment is accepted exactly when
7//! every record follows, and otherwise the first that does not is reported;
8//! and what the writer appends always follows.
9
10use planarian_schema::Hash;
11use vstd::prelude::*;
12
13verus! {
14
15/// One record as the chain sees it.
16#[derive(Clone, Copy)]
17pub struct Link {
18    pub seq: u64,
19    /// The chain head this record says it follows.
20    pub prev: Hash,
21    /// The chain head after it: SHA-256(`prev` ‖ `seq` ‖ payload).
22    pub next: Hash,
23}
24
25/// Whether a record follows the chain, and if not, why.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum Follows {
28    Yes,
29    /// Its seq is not the next one: a torn tail, or a leftover.
30    OutOfSequence,
31    /// The next seq, but not chained to the head: edited or damaged (I18).
32    Unchained,
33}
34
35/// Two hashes are the same bytes.
36pub open spec fn same(a: Hash, b: Hash) -> bool {
37    a.0@ == b.0@
38}
39
40pub open spec fn follows_spec(last: u64, head: Hash, seq: u64, prev: Hash) -> Follows {
41    if last == u64::MAX || seq != last + 1 {
42        Follows::OutOfSequence
43    } else if !same(prev, head) {
44        Follows::Unchained
45    } else {
46        Follows::Yes
47    }
48}
49
50/// The (last seq, head) a segment's `i`-th record must follow.
51pub open spec fn cursor_at(last: u64, head: Hash, links: Seq<Link>, i: int) -> (u64, Hash) {
52    if i == 0 {
53        (last, head)
54    } else {
55        (links[i - 1].seq, links[i - 1].next)
56    }
57}
58
59pub open spec fn link_ok(last: u64, head: Hash, links: Seq<Link>, i: int) -> bool {
60    follows_spec(cursor_at(last, head, links, i).0, cursor_at(last, head, links, i).1, links[i].seq, links[i].prev)
61        == Follows::Yes
62}
63
64/// The first `n` records of the segment follow the chain.
65pub open spec fn chained_upto(last: u64, head: Hash, links: Seq<Link>, n: int) -> bool {
66    forall|i: int| 0 <= i < n ==> #[trigger] link_ok(last, head, links, i)
67}
68
69/// I18 for a segment starting after (`last`, `head`): every record follows.
70pub open spec fn chained(last: u64, head: Hash, links: Seq<Link>) -> bool {
71    chained_upto(last, head, links, links.len() as int)
72}
73
74pub fn hash_eq(a: &Hash, b: &Hash) -> (r: bool)
75    ensures
76        r == same(*a, *b),
77{
78    let mut i: usize = 0;
79    while i < 32
80        invariant
81            0 <= i <= 32,
82            a.0@.len() == 32,
83            b.0@.len() == 32,
84            forall|j: int| 0 <= j < i ==> a.0@[j] == b.0@[j],
85        decreases 32 - i,
86    {
87        if a.0[i] != b.0[i] {
88            return false;
89        }
90        i += 1;
91    }
92    assert(a.0@ =~= b.0@);
93    true
94}
95
96/// The check every read path applies to each record (`journal::scan`).
97pub fn follows(last: u64, head: &Hash, seq: u64, prev: &Hash) -> (r: Follows)
98    ensures
99        r == follows_spec(last, *head, seq, *prev),
100{
101    if last == u64::MAX || seq != last + 1 {
102        Follows::OutOfSequence
103    } else if !hash_eq(prev, head) {
104        Follows::Unchained
105    } else {
106        Follows::Yes
107    }
108}
109
110/// How a whole segment checked out.
111pub enum Verdict {
112    /// Every record follows; the segment ends at this seq and head.
113    Chained { last: u64, head: Hash },
114    /// The record at `at` is the first that does not follow.
115    Break { at: usize, why: Follows },
116}
117
118/// Check a segment (`journal::verify_chain`): accepts exactly a chained
119/// segment, and otherwise reports its first break.
120pub fn verify(last: u64, head: Hash, links: &Vec<Link>) -> (r: Verdict)
121    ensures
122        (r is Chained) == chained(last, head, links@),
123        r matches Verdict::Chained { last: l, head: h } ==> (l, h) == cursor_at(last, head, links@, links@.len() as int),
124        r matches Verdict::Break { at, why } ==> {
125            &&& at < links@.len()
126            &&& chained_upto(last, head, links@, at as int)
127            &&& !link_ok(last, head, links@, at as int)
128            &&& why != Follows::Yes
129        },
130{
131    let mut i: usize = 0;
132    let mut cur_last = last;
133    let mut cur_head = head;
134    while i < links.len()
135        invariant
136            0 <= i <= links@.len(),
137            chained_upto(last, head, links@, i as int),
138            (cur_last, cur_head) == cursor_at(last, head, links@, i as int),
139        decreases links@.len() - i,
140    {
141        let l = links[i];
142        assert(l == links@[i as int]);
143        let f = follows(cur_last, &cur_head, l.seq, &l.prev);
144        match f {
145            Follows::Yes => {},
146            _ => {
147                assert(!link_ok(last, head, links@, i as int));
148                return Verdict::Break { at: i, why: f };
149            },
150        }
151        assert(link_ok(last, head, links@, i as int));
152        cur_last = l.seq;
153        cur_head = l.next;
154        i += 1;
155        assert(chained_upto(last, head, links@, i as int)) by {
156            assert forall|j: int| 0 <= j < i implies #[trigger] link_ok(last, head, links@, j) by {
157                if j < i - 1 {
158                    assert(chained_upto(last, head, links@, (i - 1) as int));
159                }
160            }
161        }
162    }
163    Verdict::Chained { last: cur_last, head: cur_head }
164}
165
166/// The seq and `prev` the writer gives the next record (`journal::append`):
167/// the next seq, and the current head.
168pub fn next_link(last: u64, head: &Hash) -> (r: (u64, Hash))
169    requires
170        last < u64::MAX,
171    ensures
172        follows_spec(last, *head, r.0, r.1) == Follows::Yes,
173        r.0 == last + 1,
174        same(r.1, *head),
175{
176    (last + 1, *head)
177}
178
179/// I18's writing half: a chained segment with a record appended the way
180/// `next_link` makes it is still chained. By induction, every journal the
181/// writer produces is chained, so a break on read is damage or an edit.
182pub proof fn appending_keeps_the_chain(last: u64, head: Hash, links: Seq<Link>, l: Link)
183    requires
184        chained(last, head, links),
185        follows_spec(
186            cursor_at(last, head, links, links.len() as int).0,
187            cursor_at(last, head, links, links.len() as int).1,
188            l.seq,
189            l.prev,
190        ) == Follows::Yes,
191    ensures
192        chained(last, head, links.push(l)),
193{
194    let s = links.push(l);
195    assert forall|i: int| 0 <= i < s.len() implies #[trigger] link_ok(last, head, s, i) by {
196        if i < links.len() {
197            assert(link_ok(last, head, links, i));
198            if i > 0 {
199                assert(s[i - 1] == links[i - 1]);
200            }
201            assert(s[i] == links[i]);
202        } else {
203            assert(s[i] == l);
204            if i > 0 {
205                assert(s[i - 1] == links[i - 1]);
206            }
207        }
208    }
209}
210
211/// An edit is caught: if a record's `next` changes (its payload was edited,
212/// so its hash changed, P5) while the record after it is untouched, the
213/// segment no longer verifies.
214pub proof fn an_edited_record_breaks_the_chain(last: u64, head: Hash, links: Seq<Link>, i: int, edited: Hash)
215    requires
216        chained(last, head, links),
217        0 <= i < links.len() - 1,
218        !same(edited, links[i].next),
219    ensures
220        !chained(last, head, links.update(i, Link { next: edited, ..links[i] })),
221{
222    let s = links.update(i, Link { next: edited, ..links[i] });
223    assert(link_ok(last, head, links, i + 1));
224    assert(s[i + 1] == links[i + 1]);
225    assert(!link_ok(last, head, s, i + 1));
226}
227
228} // verus!