|
| 1 | +-- SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +/-! |
| 3 | +# Raft Consensus Safety — Commit Invariants |
| 4 | +
|
| 5 | +**Proof obligation V4** — companion to |
| 6 | +`nextgen-databases/verisimdb/src/registry/MetadataLog.res` and |
| 7 | +`KRaftCluster.res` |
| 8 | +
|
| 9 | +Lean 4 only — no Mathlib. |
| 10 | +
|
| 11 | +## Scope |
| 12 | +
|
| 13 | +Single-node commit safety invariants of the KRaft-style Raft implementation |
| 14 | +used in VeriSimDB's federation registry: |
| 15 | +
|
| 16 | +1. **V4-A Commit monotonicity** — `commitIndex` never decreases |
| 17 | +2. **V4-B Append isolation** — appending never changes entries at `< commitIndex` |
| 18 | +3. **V4-C WF preservation** — well-formedness is maintained by valid appends |
| 19 | +4. **V4-D Log Matching (single node)** — sequential index uniquely locates entries |
| 20 | +
|
| 21 | +The distributed Log Matching invariant (no divergence across replicas) is proven |
| 22 | +in the companion TLA+ model (`verification/tla+/RaftConsensus.tla`). |
| 23 | +-/ |
| 24 | + |
| 25 | +-- ============================================================================ |
| 26 | +-- § 1. Data types |
| 27 | +-- ============================================================================ |
| 28 | + |
| 29 | +/-- A single Raft log entry. -/ |
| 30 | +structure RaftEntry where |
| 31 | + term : Nat -- Raft term (epoch) |
| 32 | + idx : Nat -- 1-based sequential log index |
| 33 | + cmdId : Nat -- abstract command identifier |
| 34 | + deriving DecidableEq, Repr |
| 35 | + |
| 36 | +/-- Raft node log state (`commitIndex = 0` means nothing committed). -/ |
| 37 | +structure RaftLog where |
| 38 | + entries : List RaftEntry |
| 39 | + commitIndex : Nat |
| 40 | + deriving Repr |
| 41 | + |
| 42 | +-- ============================================================================ |
| 43 | +-- § 2. Well-formedness |
| 44 | +-- ============================================================================ |
| 45 | + |
| 46 | +/-- A log is well-formed: sequential indices, bounded commit pointer, and |
| 47 | + monotone terms. -/ |
| 48 | +structure RaftLog.WF (rl : RaftLog) : Prop where |
| 49 | + idxSeq : ∀ i (e : RaftEntry), rl.entries[i]? = some e → e.idx = i + 1 |
| 50 | + commitBnd : rl.commitIndex ≤ rl.entries.length |
| 51 | + termMono : ∀ i j (ei ej : RaftEntry), |
| 52 | + i ≤ j → j < rl.entries.length → |
| 53 | + rl.entries[i]? = some ei → |
| 54 | + rl.entries[j]? = some ej → |
| 55 | + ei.term ≤ ej.term |
| 56 | + |
| 57 | +-- ============================================================================ |
| 58 | +-- § 3. Transitions |
| 59 | +-- ============================================================================ |
| 60 | + |
| 61 | +/-- Append one entry to the log. -/ |
| 62 | +def RaftLog.appendEntry (rl : RaftLog) (e : RaftEntry) : RaftLog := |
| 63 | + { rl with entries := rl.entries ++ [e] } |
| 64 | + |
| 65 | +/-- Advance `commitIndex` monotonically. -/ |
| 66 | +def RaftLog.advanceCommit (rl : RaftLog) (n : Nat) : RaftLog := |
| 67 | + { rl with commitIndex := Nat.max rl.commitIndex n } |
| 68 | + |
| 69 | +-- ============================================================================ |
| 70 | +-- § 4. V4-A Commit monotonicity |
| 71 | +-- ============================================================================ |
| 72 | + |
| 73 | +theorem commitIndex_monotone (rl : RaftLog) (n : Nat) : |
| 74 | + rl.commitIndex ≤ (rl.advanceCommit n).commitIndex := |
| 75 | + Nat.le_max_left .. |
| 76 | + |
| 77 | +-- ============================================================================ |
| 78 | +-- § 5. V4-B Append isolation |
| 79 | +-- ============================================================================ |
| 80 | + |
| 81 | +/-- Appending a new entry does not change entries at positions below |
| 82 | + `commitIndex`. -/ |
| 83 | +theorem append_preserves_committed (rl : RaftLog) (e : RaftEntry) (i : Nat) |
| 84 | + (hwf : rl.WF) (hi : i < rl.commitIndex) : |
| 85 | + (rl.appendEntry e).entries[i]? = rl.entries[i]? := |
| 86 | + List.getElem?_append_left (Nat.lt_of_lt_of_le hi hwf.commitBnd) |
| 87 | + |
| 88 | +/-- Committed entry is still present after any valid append. -/ |
| 89 | +theorem committed_entry_stable (rl : RaftLog) (e_new : RaftEntry) |
| 90 | + (hwf : rl.WF) (i : Nat) (hi : i < rl.commitIndex) |
| 91 | + (x : RaftEntry) (hx : rl.entries[i]? = some x) : |
| 92 | + (rl.appendEntry e_new).entries[i]? = some x := by |
| 93 | + rw [append_preserves_committed rl e_new i hwf hi]; exact hx |
| 94 | + |
| 95 | +-- ============================================================================ |
| 96 | +-- § 6. V4-C WF preservation under valid append |
| 97 | +-- ============================================================================ |
| 98 | + |
| 99 | +/-! |
| 100 | +A **valid** append carries: |
| 101 | +- `e.idx = rl.entries.length + 1` (next sequential index) |
| 102 | +- `∀ last, getLast? = some last → last.term ≤ e.term` (term monotonicity) |
| 103 | +-/ |
| 104 | + |
| 105 | +/-- `getElem?` of the last element in a singleton-appended list. -/ |
| 106 | +private theorem getElem?_snoc_last {α} (l : List α) (x : α) : |
| 107 | + (l ++ [x])[l.length]? = some x := |
| 108 | + List.getElem?_concat_length |
| 109 | + |
| 110 | +/-- In bounds `getElem?` of appended list equals original. -/ |
| 111 | +private theorem getElem?_snoc_left {α} (l : List α) (x : α) (i : Nat) |
| 112 | + (h : i < l.length) : (l ++ [x])[i]? = l[i]? := |
| 113 | + List.getElem?_append_left h |
| 114 | + |
| 115 | +/-- The only in-bounds position past `l.length` in `l ++ [x]` is `l.length`. -/ |
| 116 | +private theorem snoc_out_implies_eq {α} (l : List α) (x : α) (i : Nat) |
| 117 | + (hge : ¬ i < l.length) |
| 118 | + (hlt : i < (l ++ [x]).length) : i = l.length := by |
| 119 | + simp [List.length_append] at hlt; omega |
| 120 | + |
| 121 | +theorem appendEntry_WF (rl : RaftLog) (e : RaftEntry) |
| 122 | + (hwf : rl.WF) |
| 123 | + (hidx : e.idx = rl.entries.length + 1) |
| 124 | + (hterm : ∀ last, rl.entries.getLast? = some last → last.term ≤ e.term) : |
| 125 | + (rl.appendEntry e).WF := by |
| 126 | + unfold RaftLog.appendEntry |
| 127 | + constructor |
| 128 | + · -- idxSeq |
| 129 | + intro i x hget |
| 130 | + by_cases hi : i < rl.entries.length |
| 131 | + · rw [getElem?_snoc_left _ _ _ hi] at hget |
| 132 | + exact hwf.idxSeq i x hget |
| 133 | + · -- new element |
| 134 | + have hlt : i < (rl.entries ++ [e]).length := by |
| 135 | + cases Nat.lt_or_ge i (rl.entries ++ [e]).length with |
| 136 | + | inl h => exact h |
| 137 | + | inr h => |
| 138 | + exfalso |
| 139 | + have : (rl.entries ++ [e])[i]? = none := |
| 140 | + List.getElem?_eq_none_iff.mpr h |
| 141 | + simp [this] at hget |
| 142 | + have heq : i = rl.entries.length := snoc_out_implies_eq _ _ _ hi hlt |
| 143 | + subst heq |
| 144 | + rw [getElem?_snoc_last] at hget |
| 145 | + exact Option.some.inj hget ▸ hidx |
| 146 | + · -- commitBnd |
| 147 | + simp only [List.length_append, List.length_singleton] |
| 148 | + exact Nat.le_add_right_of_le hwf.commitBnd |
| 149 | + · -- termMono |
| 150 | + intro i j ei ej hij hjlt hgi hgj |
| 151 | + simp only [List.length_append, List.length_singleton] at hjlt |
| 152 | + by_cases hj : j < rl.entries.length |
| 153 | + · -- j in old log → i also in old log |
| 154 | + have hi_lt : i < rl.entries.length := Nat.lt_of_le_of_lt hij hj |
| 155 | + rw [getElem?_snoc_left _ _ _ hi_lt] at hgi |
| 156 | + rw [getElem?_snoc_left _ _ _ hj] at hgj |
| 157 | + exact hwf.termMono i j ei ej hij hj hgi hgj |
| 158 | + · -- j = length (the new element) |
| 159 | + have hjeq : j = rl.entries.length := by omega |
| 160 | + subst hjeq |
| 161 | + rw [getElem?_snoc_last] at hgj |
| 162 | + -- hgj : some e = some ej |
| 163 | + obtain rfl : ej = e := (Option.some.inj hgj).symm |
| 164 | + -- goal: ei.term ≤ e.term |
| 165 | + by_cases hi_lt : i < rl.entries.length |
| 166 | + · -- i in old log: chain ei.term ≤ last.term ≤ e.term |
| 167 | + rw [getElem?_snoc_left _ _ _ hi_lt] at hgi |
| 168 | + have hlen_pos : 0 < rl.entries.length := |
| 169 | + Nat.lt_of_le_of_lt (Nat.zero_le i) hi_lt |
| 170 | + have hne : rl.entries ≠ [] := List.length_pos_iff.mp hlen_pos |
| 171 | + have hbound : rl.entries.length - 1 < rl.entries.length := by omega |
| 172 | + have hlast_get : rl.entries.getLast? = some (rl.entries.getLast hne) := |
| 173 | + List.getLast?_eq_some_getLast hne |
| 174 | + have hlast_idx : rl.entries[rl.entries.length - 1]? = some (rl.entries.getLast hne) := by |
| 175 | + rw [List.getElem?_eq_getElem hbound] |
| 176 | + exact (congrArg some (List.getLast_eq_getElem hne)).symm |
| 177 | + have hterm_e := hterm (rl.entries.getLast hne) hlast_get |
| 178 | + have hei_last : ei.term ≤ (rl.entries.getLast hne).term := |
| 179 | + hwf.termMono i (rl.entries.length - 1) ei (rl.entries.getLast hne) |
| 180 | + (by omega) hbound hgi hlast_idx |
| 181 | + exact Nat.le_trans hei_last hterm_e |
| 182 | + · -- i = rl.entries.length: ei occupies same slot as ej (= e) |
| 183 | + have heq : i = rl.entries.length := by omega |
| 184 | + subst heq |
| 185 | + rw [getElem?_snoc_last] at hgi |
| 186 | + -- hgi : some e = some ei → e = ei |
| 187 | + exact Nat.le_of_eq (congrArg RaftEntry.term (Option.some.inj hgi).symm) |
| 188 | + |
| 189 | +-- ============================================================================ |
| 190 | +-- § 7. V4-D Log Matching (single-node) |
| 191 | +-- ============================================================================ |
| 192 | + |
| 193 | +/-! |
| 194 | +In a well-formed log, the `idx` field uniquely determines the list position: |
| 195 | +two entries with the same `idx` are at the same list position. |
| 196 | +-/ |
| 197 | +theorem log_matching_single (rl : RaftLog) (hwf : rl.WF) |
| 198 | + (i j : Nat) (ei ej : RaftEntry) |
| 199 | + (hgi : rl.entries[i]? = some ei) |
| 200 | + (hgj : rl.entries[j]? = some ej) |
| 201 | + (hidx_eq : ei.idx = ej.idx) : |
| 202 | + i = j := by |
| 203 | + have hi := hwf.idxSeq i ei hgi -- ei.idx = i + 1 |
| 204 | + have hj := hwf.idxSeq j ej hgj -- ej.idx = j + 1 |
| 205 | + -- ei.idx = ej.idx implies i + 1 = j + 1 |
| 206 | + rw [hi, hj] at hidx_eq |
| 207 | + omega |
| 208 | + |
| 209 | +-- ============================================================================ |
| 210 | +-- § 8. Summary |
| 211 | +-- ============================================================================ |
| 212 | + |
| 213 | +/-! |
| 214 | +## Proof summary (V4) |
| 215 | +
|
| 216 | +| Label | Property | Theorem | |
| 217 | +|--------|-----------------------------------|-----------------------------| |
| 218 | +| V4-A | Commit monotonicity | `commitIndex_monotone` | |
| 219 | +| V4-B | Append isolation | `append_preserves_committed` | |
| 220 | +| V4-B′ | Committed entry stability | `committed_entry_stable` | |
| 221 | +| V4-C | WF preservation under append | `appendEntry_WF` | |
| 222 | +| V4-D | Log Matching (single-node) | `log_matching_single` | |
| 223 | +
|
| 224 | +The distributed Log Matching invariant (no divergence across replicas after |
| 225 | +commit) is established in the companion TLA+ model. |
| 226 | +-/ |
0 commit comments