|
| 1 | +/- |
| 2 | +Copyright (c) 2026 Ching-Tsun Chou. All rights reserved. |
| 3 | +Released under Apache 2.0 license as described in the file LICENSE. |
| 4 | +Authors: Ching-Tsun Chou |
| 5 | +-/ |
| 6 | + |
| 7 | +module |
| 8 | + |
| 9 | +public import Cslib.Foundations.Semantics.LTS.OmegaExecution |
| 10 | + |
| 11 | +/-! # Distributed algorithms for solving the consensus problem |
| 12 | +
|
| 13 | +In the consensus problem, each process of a distributed algorithm is given a boolean value |
| 14 | +at the beginning. Then by exchanging messages asynchronously, they are supposed to agree on |
| 15 | +one of the initial boolean values. This file contains a very general definition of such |
| 16 | +a distributed algorithm. |
| 17 | +
|
| 18 | +Borrowing an idea from Leslie Lamport, we allow the LTS defined by such an algorithm to |
| 19 | +"stutter" at any time, in the sense of taking a dummy step without changing the |
| 20 | +global state of the distributed algorithm. This idea enables us to focus on infinite |
| 21 | +executions alone without loss of generality, because an algorithm that has run out of |
| 22 | +useful steps to take can always take the stuttering step. Pathological executions in which |
| 23 | +the stuttering step is taken forever when there is useful work to be done are outlawed by |
| 24 | +the fairness assumptions defined in `Consensus.lean`. |
| 25 | +
|
| 26 | +The types `P`, `M`, and `S` below are the types of processes (more precisely, process identifiers), |
| 27 | +messages contents, and process states. Eventually `P` will be assumed to be finite in the form of |
| 28 | +`[Fintype P]`, but that assumption will be added only where necessary. No assumption whatsoever |
| 29 | +will be made about `M` and `S`. In particular, they could be infinite. |
| 30 | +-/ |
| 31 | + |
| 32 | +@[expose] public section |
| 33 | + |
| 34 | +namespace Cslib.FLP |
| 35 | + |
| 36 | +open Function Set Sum Multiset |
| 37 | + |
| 38 | +variable {P M S : Type*} [DecidableEq P] [DecidableEq M] |
| 39 | + |
| 40 | +/-- The type of messages that processes send to each other. -/ |
| 41 | +@[ext] |
| 42 | +structure Message (P M : Type*) where |
| 43 | + /-- The destination of a message. -/ |
| 44 | + dest : P |
| 45 | + /-- The content of the message, where the `Bool` option is used to carry to the |
| 46 | + initial boolean value to each process. -/ |
| 47 | + msg : Bool ⊕ M |
| 48 | +deriving DecidableEq |
| 49 | + |
| 50 | +/-- The type of a process's local state. -/ |
| 51 | +structure ProcState (S : Type*) where |
| 52 | + /-- The internal state of a process. -/ |
| 53 | + state : S |
| 54 | + /-- The state component used by a process to signal the boolean value it decides on. -/ |
| 55 | + out : Option Bool |
| 56 | + |
| 57 | +/-- The global state of the distributed algorithm. -/ |
| 58 | +structure State (P M S : Type*) where |
| 59 | + /-- A multiset containing all messages that are in-flight (namely, they have been sent but |
| 60 | + not yet received. Note that being a multiset implies that the messages are not ordered. -/ |
| 61 | + msgs : Multiset (Message P M) |
| 62 | + /-- A map giving the local states of all processes. -/ |
| 63 | + proc : P → ProcState S |
| 64 | + |
| 65 | +/-- The specification of a distributed algorithm for solving the consensus problem. |
| 66 | +Note that each field below can depend on a process's identifier (recall that each `Message` |
| 67 | +contains its destination's identifier), so the algorithm is not required to be uniform |
| 68 | +across processes. -/ |
| 69 | +structure Algorithm (P M S : Type*) where |
| 70 | + /-- A map specifying the initial state of each process. -/ |
| 71 | + init : P → S |
| 72 | + /-- A map specifying how a process changes its internal state upon receiving a message. -/ |
| 73 | + next : Message P M → ProcState S → S |
| 74 | + /-- A map specifying what messages a process sends out upon receiving a message. -/ |
| 75 | + send : Message P M → ProcState S → Multiset (Message P M) |
| 76 | + /-- A map specifying the boolean decision a process makes upon receiving a message, |
| 77 | + where `none` means that no decision is made. -/ |
| 78 | + out : Message P M → ProcState S → Option Bool |
| 79 | + |
| 80 | +/-- The type of labels of the LTS defined by an `Algorithm`, where `some m` denotes |
| 81 | +the reception of message `m` and `none` denotes a stuttering step. -/ |
| 82 | +abbrev Action (P M : Type*) := Option (Message P M) |
| 83 | + |
| 84 | +/-- `Dest ps x` means that if `x ≠ none`, then `x = some m` with `m.dest ∈ ps`. -/ |
| 85 | +def DestIn (ps : Set P) : Action P M → Prop |
| 86 | + | some m => m.dest ∈ ps |
| 87 | + | none => True |
| 88 | + |
| 89 | +/-- Given `inp : P → Bool`, the initial state of the algorithm `a` contains a single message |
| 90 | +carrying the boolean value `inp p` to each process `p`, where the initial internal state is |
| 91 | +`a.init p` and no decision has been made. The assumption `[Fintyep P]` is made because |
| 92 | +a multiset may contain only finitely many elements. -/ |
| 93 | +def Algorithm.start [Fintype P] (a : Algorithm P M S) (inp : P → Bool) : State P M S where |
| 94 | + msgs := Multiset.map (fun p ↦ ⟨p, inl (inp p)⟩) Finset.univ.val |
| 95 | + proc := fun p ↦ ⟨a.init p, none⟩ |
| 96 | + |
| 97 | +/-- The specification of how the global state of the algorithm is changed when one of its |
| 98 | +processes `p` receives a message `m`. Note that once `p` has made a boolean decision in |
| 99 | +its `out` field, it is not allowed to "change its mind" later. -/ |
| 100 | +def Algorithm.recvMsg (a : Algorithm P M S) (m : Message P M) (s : State P M S) : State P M S := |
| 101 | + let p := m.dest |
| 102 | + { msgs := s.msgs.erase m + a.send m (s.proc p) |
| 103 | + proc := fun q ↦ |
| 104 | + if q = p then |
| 105 | + ⟨ a.next m (s.proc p), |
| 106 | + if (s.proc p).out.isNone then a.out m (s.proc p) else (s.proc p).out ⟩ |
| 107 | + else s.proc q } |
| 108 | + |
| 109 | +/-- The transition relation of the LTS defined by the algorithm `a`. |
| 110 | +Note that the stuttering step is always allowed. -/ |
| 111 | +def Algorithm.LTS (a : Algorithm P M S) : LTS (State P M S) (Action P M) where |
| 112 | + Tr s x s' := match x with |
| 113 | + | some m => m ∈ s.msgs ∧ s' = a.recvMsg m s |
| 114 | + | none => s' = s |
| 115 | + |
| 116 | +/-- `a.Reachable inp s` means that `s` is a reachable state of `a` given the initial `inp`. -/ |
| 117 | +def Algorithm.Reachable [Fintype P] |
| 118 | + (a : Algorithm P M S) (inp : P → Bool) (s : State P M S) : Prop := |
| 119 | + a.LTS.CanReach (a.start inp) s |
| 120 | + |
| 121 | +/-- `s.ProcDecided p b` means that process `p` is decided on the boolean value `b` |
| 122 | +in the state `s`. -/ |
| 123 | +abbrev State.ProcDecided (s : State P M S) (p : P) (b : Bool) : Prop := |
| 124 | + (s.proc p).out = some b |
| 125 | + |
| 126 | +/-- `s.Decided b` means that at least one process is decided on the boolean value `b` |
| 127 | +in the state `s`. -/ |
| 128 | +def State.Decided (s : State P M S) (b : Bool) : Prop := |
| 129 | + ∃ p, s.ProcDecided p b |
| 130 | + |
| 131 | +/-- `s.Agreed` says that all boolean values decided on in state `s` must agree with each other. -/ |
| 132 | +def State.Agreed (s : State P M S) : Prop := |
| 133 | + ∀ b b', s.Decided b ∧ s.Decided b' → b = b' |
| 134 | + |
| 135 | +/-- `a.SafeConsensus` says that in any reachable state of `a`, (1) all boolean values decided on |
| 136 | +in that state must agree with each other and (2) that boolean value (if it exists) must be |
| 137 | +one of the boolean values given by `inp` at the beginning. `a.SafeConsensus` is the minimal |
| 138 | +correctness requirement on `a` and is a safety property (hence its name). -/ |
| 139 | +def Algorithm.SafeConsensus [Fintype P] (a : Algorithm P M S) : Prop := |
| 140 | + ∀ inp s, a.Reachable inp s → s.Agreed ∧ ∀ b, s.Decided b → ∃ p, inp p = b |
| 141 | + |
| 142 | +namespace Algorithm |
| 143 | + |
| 144 | +variable {a : Algorithm P M S} {inp : P → Bool} |
| 145 | + |
| 146 | +/-- The stuttering step does not change the global state. -/ |
| 147 | +theorem tr_none {s s' : State P M S} (h : a.LTS.Tr s none s') : s = s' := by |
| 148 | + grind [Algorithm.LTS] |
| 149 | + |
| 150 | +/-- The initial state is reachable. -/ |
| 151 | +theorem reachable_start [Fintype P] : |
| 152 | + a.Reachable inp (a.start inp) := by |
| 153 | + simp [Algorithm.Reachable, LTS.CanReach.refl] |
| 154 | + |
| 155 | +/-- If `s` is reachable from the initial state and `s'` is reachable from `s`, |
| 156 | +then `s'` is reachable from the initial state. -/ |
| 157 | +theorem reachable_stable [Fintype P] {s s' : State P M S} |
| 158 | + (hr : a.Reachable inp s) (hc : a.LTS.CanReach s s') : a.Reachable inp s' := by |
| 159 | + obtain ⟨xs, _⟩ := hr |
| 160 | + obtain ⟨xs', _⟩ := hc |
| 161 | + use xs ++ xs' |
| 162 | + grind [LTS.MTr.comp] |
| 163 | + |
| 164 | +/-- If `p` is decided on the boolean value `b` in `s` and `s'` is reachable from `s`, |
| 165 | +then `p` is still decided on `b` in `s'`. -/ |
| 166 | +theorem procDecided_stable {s s' : State P M S} {p : P} {b : Bool} |
| 167 | + (hd : s.ProcDecided p b) (hc : a.LTS.CanReach s s') : s'.ProcDecided p b := by |
| 168 | + obtain ⟨xs, h_mtr⟩ := hc |
| 169 | + induction h_mtr <;> grind [Algorithm.LTS, Algorithm.recvMsg] |
| 170 | + |
| 171 | +/-- If at least one process is decided on the boolean value `b` in `s` and `s'` is reachable |
| 172 | +from `s`, then at least one process is still decided on `b` in `s'`. -/ |
| 173 | +theorem decided_stable {s s' : State P M S} {b : Bool} |
| 174 | + (hd : s.Decided b) (hc : a.LTS.CanReach s s') : s'.Decided b := by |
| 175 | + obtain ⟨p, _⟩ := hd |
| 176 | + use p |
| 177 | + grind [procDecided_stable] |
| 178 | + |
| 179 | +/-- If `m1` and `m2` are both inflight and they have different destinations, |
| 180 | +then receiving them in either order produces the same end state. -/ |
| 181 | +theorem recvMsg_comm {m1 m2 : Message P M} {s : State P M S} |
| 182 | + (hd : m1.dest ≠ m2.dest) (h1 : m1 ∈ s.msgs) (h2 : m2 ∈ s.msgs) : |
| 183 | + m2 ∈ (a.recvMsg m1 s).msgs ∧ m1 ∈ (a.recvMsg m2 s).msgs ∧ |
| 184 | + a.recvMsg m2 (a.recvMsg m1 s) = a.recvMsg m1 (a.recvMsg m2 s) := by |
| 185 | + rw [State.mk.injEq] |
| 186 | + split_ands |
| 187 | + · grind [Algorithm.recvMsg, mem_erase_of_ne] |
| 188 | + · grind [Algorithm.recvMsg, mem_erase_of_ne] |
| 189 | + · have he1 (x) : (s.msgs.erase m1 + x).erase m2 = (s.msgs.erase m1).erase m2 + x := by |
| 190 | + grind [erase_add_left_pos, mem_erase_of_ne] |
| 191 | + have he2 (x) : (s.msgs.erase m2 + x).erase m1 = (s.msgs.erase m1).erase m2 + x := by |
| 192 | + grind [erase_add_left_pos, mem_erase_of_ne, erase_comm] |
| 193 | + simp [Algorithm.recvMsg, hd, hd.symm, he1, he2, add_assoc] |
| 194 | + grind [add_comm] |
| 195 | + · ext p |
| 196 | + by_cases h_p1 : p = m1.dest <;> by_cases h_p2 : p = m2.dest <;> |
| 197 | + simp [Algorithm.recvMsg, h_p1, h_p2, hd, hd.symm] |
| 198 | + |
| 199 | +/-- A diamond property for the transition relation `a.LTS.Tr`. -/ |
| 200 | +theorem tr_diamond {ps : Set P} {x1 x2 : Action P M} {s s1 s2 : State P M S} |
| 201 | + (hx1 : DestIn ps x1) (hs1 : a.LTS.Tr s x1 s1) |
| 202 | + (hx2 : DestIn psᶜ x2) (hs2 : a.LTS.Tr s x2 s2) : |
| 203 | + ∃ s', a.LTS.Tr s1 x2 s' ∧ a.LTS.Tr s2 x1 s' := by |
| 204 | + cases x1 <;> cases x2 <;> try grind [Algorithm.LTS] |
| 205 | + case some m1 m2 => |
| 206 | + have hd : m1.dest ≠ m2.dest := by grind [DestIn] |
| 207 | + obtain ⟨h_m1, rfl⟩ := hs1 |
| 208 | + obtain ⟨h_m2, rfl⟩ := hs2 |
| 209 | + simp only [Algorithm.LTS, exists_eq_right_right] |
| 210 | + grind [recvMsg_comm (a := a) hd h_m1 h_m2] |
| 211 | + |
| 212 | +/-- A message that is in-flight stays in-flight as long as it is not received |
| 213 | +(finite execution version). -/ |
| 214 | +theorem mTr_notRcvd_enabled {s t : State P M S} {xl : List (Action P M)} {m : Message P M} |
| 215 | + (hst : a.LTS.MTr s xl t) (hs : m ∈ s.msgs) (hxl : ¬ some m ∈ xl) : m ∈ t.msgs := by |
| 216 | + induction hst |
| 217 | + case refl _ => assumption |
| 218 | + case stepL s x s1 xl t h_tr h_mtr h_ind => |
| 219 | + rcases Option.eq_none_or_eq_some x <;> |
| 220 | + grind [Algorithm.LTS, Algorithm.recvMsg, mem_erase_of_ne] |
| 221 | + |
| 222 | +/-- A message that is in-flight stays in-flight as long as it is not received |
| 223 | +(infinite execution version). -/ |
| 224 | +theorem omega_notRcvd_enabled |
| 225 | + {ss : ωSequence (State P M S)} {xs : ωSequence (Action P M)} {k : ℕ} {m : Message P M} |
| 226 | + (he : a.LTS.OmegaExecution ss xs) (hm : m ∈ (ss k).msgs) (hn : ∀ j, k ≤ j → xs j ≠ some m) : |
| 227 | + ∀ j, k ≤ j → m ∈ (ss j).msgs := by |
| 228 | + intro j h_j |
| 229 | + obtain ⟨i, rfl⟩ : ∃ i, j = k + i := by use j - k; grind |
| 230 | + induction i |
| 231 | + case zero => grind |
| 232 | + case succ i _ => |
| 233 | + rcases Option.eq_none_or_eq_some (xs (k + i)) <;> |
| 234 | + grind [he (k + i), Algorithm.LTS, Algorithm.recvMsg, mem_erase_of_ne] |
| 235 | + |
| 236 | +end Algorithm |
| 237 | + |
| 238 | +end Cslib.FLP |
0 commit comments