Skip to content

Commit cde725b

Browse files
committed
feat(FLP): distributed algorithms for solving the consensus problem
1 parent 2e5e64d commit cde725b

4 files changed

Lines changed: 317 additions & 0 deletions

File tree

Cslib.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public import Cslib.Computability.Automata.NA.Prod
2222
public import Cslib.Computability.Automata.NA.Sum
2323
public import Cslib.Computability.Automata.NA.ToDA
2424
public import Cslib.Computability.Automata.NA.Total
25+
public import Cslib.Computability.Distributed.FLP.Algorithm
2526
public import Cslib.Computability.Languages.Congruences.BuchiCongruence
2627
public import Cslib.Computability.Languages.Congruences.RightCongruence
2728
public import Cslib.Computability.Languages.ExampleEventuallyZero
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Impossibility of distributed consensus
2+
3+
This directory contains a formalization of Völzer's proof [Volzer2004] of the famous result in
4+
distributed computing, first proved by Fischer, Lynch and Paterson [FLP1985], that distributed
5+
consensus is impossible in the presence of even a single crash fault.
6+
7+
## Lean files
8+
9+
1. `Algorithm.lean` defines the "syntax" of a distributed algorithm for solving the consensus problem
10+
and proves some basic properties.
11+
12+
2. `Consensus.lean` defines what it means for a distributed algorithm to solve the consensus problem
13+
in a fault-tolerant way and proves some basic properties.
14+
15+
3. `FairScheduler.lean` contains a technical machinery for constructing "fair executions", which is used
16+
in the proof of `PseudoConsensus.of_consensus` in `PseudoConsensus.lean` and in the proof of
17+
`OnePseudoConsensus.fair_nonUniform` in `Impossibility.lean`.
18+
19+
4. `CanReachVia.lean` defines the notion of reachability via a subset of processes and proves some of
20+
its properties.
21+
22+
5. `PseudoConsensus.lean` defines the notion of a fault-tolerant "pseudo-consensus" algorithm, which
23+
is central to Völzer's proof, and proves that every `f`-tolerant consensus algorithm is also a
24+
`f`-tolerant pseudo-consensus algorithm.
25+
26+
6. `OnePseudoConsensus.lean` focuses on 1-tolerant pseudo-consensus algorithms, defines the key notion
27+
of "nonuniformity", and proves a number of their properties.
28+
29+
7. `Impossibility.lean` proves that every 1-tolerant pseudo-consensus algorithms has a fair execution
30+
which doesn't contain any fault but never reaches a consensus, which then implies that there cannot
31+
be a consensus algorithm that can tolerate even a single fault.
32+
33+
Files #1 and #2 contains materials common to both [FLP1985] and [Volzer2004].
34+
File #3 provides proof details that are either completely omitted (in the case of
35+
`PseudoConsensus.of_consensus`) or only hinted at (in the case of
36+
`OnePseudoConsensus.fair_nonUniform`) in [Volzer2004].
37+
The remaining files follow the development in [Volzer2004] fairly closely,
38+
as is explained further in each file.
39+
40+
## References
41+
42+
[FLP1985]
43+
M.J. Fischer, N.A. Lynch, M.S. Paterson, Impossibility of distributed consensus with one faulty process,
44+
JACM 32 (2) (April 1985) 374–382.
45+
46+
[Volzer2004]
47+
H. Völzer, A constructive proof for FLP. Information Processing Letters 92(2), (October 2004) 83–87.

references.bib

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,24 @@ @article{ Chargueraud2012
101101
file = {Full Text PDF:/home/chenson/mount/Zotero/storage/WBJWAZGI/Charguéraud - 2012 - The Locally Nameless Representation.pdf:application/pdf},
102102
}
103103

104+
@article{ FLP1985,
105+
author = {Fischer, Michael J. and Lynch, Nancy A. and Paterson, Michael S.},
106+
title = {Impossibility of Distributed Consensus with One Faulty Process},
107+
year = {1985},
108+
issue_date = {April 1985},
109+
publisher = {Association for Computing Machinery},
110+
address = {New York, NY, USA},
111+
volume = {32},
112+
number = {2},
113+
issn = {0004-5411},
114+
url = {https://doi.org/10.1145/3149.214121},
115+
doi = {10.1145/3149.214121},
116+
journal = {J. ACM},
117+
month = {apr},
118+
pages = {374–382},
119+
numpages = {9}
120+
}
121+
104122
@article{ Girard1987,
105123
title={Linear logic},
106124
author={Girard, Jean-Yves},
@@ -358,6 +376,19 @@ @book{KearnsVazirani1994
358376
address = {Cambridge, MA, USA}
359377
}
360378

379+
@article{ Volzer2004,
380+
title = {A constructive proof for {FLP}},
381+
author = {V{\"o}lzer, Hagen},
382+
journal = {Information Processing Letters},
383+
volume = {92},
384+
number = {2},
385+
pages = {83--87},
386+
year = {2004},
387+
publisher = {Elsevier},
388+
doi = {10.1016/j.ipl.2004.06.008},
389+
url = {https://doi.org/10.1016/j.ipl.2004.06.008}
390+
}
391+
361392
@incollection{WinskelNielsen1995,
362393
author = {Winskel, Glynn and Nielsen, Mogens},
363394
isbn = {9780198537809},

0 commit comments

Comments
 (0)