Skip to content

Commit 696f981

Browse files
committed
add dag-consensus
Signed-off-by: Giuliano Losa <giuliano@losa.fr>
1 parent b51b804 commit 696f981

File tree

13 files changed

+611
-44
lines changed

13 files changed

+611
-44
lines changed

README.md

Lines changed: 45 additions & 44 deletions
Large diffs are not rendered by default.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
----------------------------- MODULE BlockDag -----------------------------
2+
3+
(**************************************************************************************)
4+
(* In this specification we define notions on DAGs useful for DAG-based consensus *)
5+
(* protocols (which build DAGs of blocks) *)
6+
(**************************************************************************************)
7+
8+
EXTENDS FiniteSets, Sequences, Integers, Utils, Digraph, TLC
9+
10+
CONSTANTS
11+
N \* The set of all network nodes (not DAG nodes)
12+
, R \* The set of rounds
13+
, Leader(_) \* operator mapping each round to its leader
14+
15+
(**************************************************************************************)
16+
(* For our purpose of checking safety and liveness, DAG vertices just consist of a *)
17+
(* node and a round. *)
18+
(**************************************************************************************)
19+
V == N \times R \* the set of possible DAG vertices
20+
Node(v) == v[1]
21+
Round(v) == IF v = <<>> THEN 0 ELSE v[2] \* accomodates <<>> as default value
22+
23+
(**************************************************************************************)
24+
(* Next we define leader vertices: *)
25+
(**************************************************************************************)
26+
LeaderVertex(r) == IF r > 0 THEN <<Leader(r), r>> ELSE <<>>
27+
IsLeader(v) == LeaderVertex(Round(v)) = v
28+
Genesis == <<>>
29+
ASSUME IsLeader(Genesis) \* this should hold
30+
31+
(**************************************************************************************)
32+
(* OrderSet(S) arbitrarily order the members of the set S. Note that, in TLA+, *)
33+
(* `CHOOSE' is deterministic but arbitrary choice, i.e. `CHOOSE e \in S : TRUE' is *)
34+
(* always the same `e' if `S' is the same *)
35+
(**************************************************************************************)
36+
RECURSIVE OrderSet(_)
37+
OrderSet(S) == IF S = {} THEN <<>> ELSE
38+
LET e == CHOOSE e \in S : TRUE
39+
IN Append(OrderSet(S \ {e}), e)
40+
41+
(**************************************************************************************)
42+
(* PreviousLeader(dag, r) returns the leader vertex in dag whose round is the *)
43+
(* largest but smaller than r. We assume that dag contains at least the genesis *)
44+
(* vertex. *)
45+
(**************************************************************************************)
46+
PreviousLeader(dag, r) == CHOOSE l \in Vertices(dag) :
47+
/\ IsLeader(l)
48+
/\ Round(l) = Max({Round(l2) : l2 \in
49+
{l2 \in Vertices(dag) : IsLeader(l2) /\ Round(l2) < r}})
50+
51+
(**************************************************************************************)
52+
(* Linearize a DAG. In a real blockchain we should use a topological ordering, but, *)
53+
(* for the purpose of ensuring agreement, an arbitrary ordering (as provided by *)
54+
(* OrderSet) is fine. This assume a DAG where all paths end with the Genesis *)
55+
(* vertex. *)
56+
(**************************************************************************************)
57+
RECURSIVE Linearize(_, _)
58+
Linearize(dag, l) == IF Vertices(dag) = {<<>>} THEN <<>> ELSE
59+
LET dagOfL == SubDag(dag, {l})
60+
prevL == PreviousLeader(dagOfL, Round(l))
61+
dagOfPrev == SubDag(dag, {prevL})
62+
remaining == Vertices(dagOfL) \ Vertices(dagOfPrev)
63+
IN Linearize(dagOfPrev, prevL) \o OrderSet(remaining \ {l}) \o <<l>>
64+
65+
Compatible(s1, s2) == \* whether the sequence s1 is a prefix of the sequence s2, or vice versa
66+
\A i \in 1..Min({Len(s1), Len(s2)}) : s1[i] = s2[i]
67+
=========================================================================

specifications/dag-consensus/BlockDagTest.cfg

Whitespace-only changes.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
----------------------------- MODULE BlockDagTest -----------------------------
2+
3+
(**************************************************************************************)
4+
(* Tests for BlockDag operators using small concrete DAGs. *)
5+
(**************************************************************************************)
6+
7+
EXTENDS FiniteSets, Sequences, Integers, TLC
8+
9+
N == {1, 2}
10+
R == 1..3
11+
Leader(r) == CASE
12+
r = 1 -> 1
13+
[] r = 2 -> 2
14+
[] r = 3 -> 1
15+
16+
INSTANCE BlockDag WITH N <- N, R <- R, Leader <- Leader
17+
18+
v11 == <<1, 1>> \* leader
19+
v21 == <<2, 1>>
20+
v12 == <<1, 2>>
21+
v22 == <<2, 2>> \* leader
22+
v13 == <<1, 3>> \* leader
23+
v23 == <<2, 3>>
24+
25+
ASSUME TestNodeRound == Node(v12) = 1 /\ Round(v12) = 2
26+
27+
ASSUME TestLeaderVertice ==
28+
/\ LeaderVertex(1) = v11
29+
/\ LeaderVertex(2) = v22
30+
/\ LeaderVertex(3) = v13
31+
32+
ASSUME TestOrderSetPermutation ==
33+
LET SeqToSet(seq) == {seq[i] : i \in DOMAIN seq}
34+
IsPermutation(seq, s) == SeqToSet(seq) = s /\ Len(seq) = Cardinality(s)
35+
IN IsPermutation(OrderSet({v11, v21}), {v11, v21})
36+
37+
dag1 ==
38+
<< {Genesis, v11, v21, v12, v22, v13, v23},
39+
{<<v11, Genesis>>, <<v21, Genesis>>,
40+
<<v12, v21>>, <<v22, v11>>, <<v13, v22>>,
41+
<<v13, v21>>, <<v13, v12>>, <<v23, v22>>} >>
42+
43+
ASSUME TestPreviousLeader1 == PreviousLeader(dag1, 3) = v22
44+
ASSUME TestPreviousLeader2 == PreviousLeader(dag1, 2) = v11
45+
ASSUME TestPreviousLeaderBase == PreviousLeader(dag1, 1) = <<>>
46+
47+
ASSUME TestLinearize == Linearize(dag1, v13) =
48+
<<<<1, 1>>, <<2, 2>>>> \o OrderSet({<<2, 1>>, <<1, 2>>}) \o <<<<1, 3>>>>
49+
50+
=========================================================================
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
----------------------------- MODULE Digraph -----------------------------
2+
3+
(**************************************************************************************)
4+
(* A digraph is a pair consisting of a set of vertices and a set of edges *)
5+
(**************************************************************************************)
6+
7+
Vertices(digraph) == digraph[1]
8+
Edges(digraph) == digraph[2]
9+
10+
IsDigraph(digraph) ==
11+
/\ digraph = <<Vertices(digraph), Edges(digraph)>>
12+
/\ \A e \in Edges(digraph) :
13+
/\ e = <<e[1],e[2]>>
14+
/\ {e[1],e[2]} \subseteq Vertices(digraph)
15+
16+
Children(digraph, v) ==
17+
{c \in Vertices(digraph) : <<v, c>> \in Edges(digraph)}
18+
19+
(**************************************************************************************)
20+
(* Descendants(dag, vs) is the set of vertices reachable from any vertex in vs *)
21+
(**************************************************************************************)
22+
RECURSIVE Descendants(_, _)
23+
Descendants(dag, vs) == IF vs = {} THEN {} ELSE
24+
LET children == {c \in Vertices(dag) : \E v \in vs : <<v,c>> \in Edges(dag)} IN
25+
children \cup Descendants(dag, children)
26+
27+
(**************************************************************************************)
28+
(* The sub-dag reachable from the set of vertices vs: *)
29+
(**************************************************************************************)
30+
SubDag(dag, vs) ==
31+
LET vs2 == Descendants(dag, vs) \cup vs
32+
es2 == {e \in Edges(dag) : e[1] \in vs2} \* implies e[2] \in vs2
33+
IN <<vs2, es2>>
34+
35+
==========================================================================
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Formal specifications of DAG-based consensus protocols
2+
3+
Copied from [nano-o/dag-consensus](https://github.com/nano-o/dag-consensus).
4+
5+
## Block DAGs
6+
7+
[BlockDag.tla](./BlockDag.tla) defines block DAGs and how DAG-based consensus protocols linearize them.
8+
This specification should be reusable for other DAG-based consensus protocols.
9+
To run some basic tests, run `make block-dag-test`.
10+
11+
## Sailfish
12+
13+
[Sailfish.tla](./Sailfish.tla) contains a high-level formal specification modeling both the Sailfish and Sailfish++ protocols (at the level of abstraction of the specification, the differences between the protocols are not visible).
14+
15+
Sailfish is described in the paper ["Sailfish: Towards Improving the Latency of DAG-based BFT"](https://eprint.iacr.org/2024/472), S&P 2025, and Sailfish++ appears in the paper ["Optimistic, Signature-Free Reliable Broadcast and Its Applications"](https://arxiv.org/abs/2505.02761), CCS 2025.
16+
17+
To run TLC on the specification, first translate the PlusCal part to TLA+ with `make trans TLA_SPEC=Sailfish.tla` and then run `make run-tlc TLA_SPEC=TLCSailfish1.tla`. The specification `TLCSailfish1.tla` and the associated config file `TLCSailfish1.cfg` fix a concrete system size, model-checking bounds, and the properties to check (typing invariant, agreement, and liveness).
18+
19+
Have a look at the Makefile to tweak TLC options.
20+
21+
Notes:
22+
- `make trans` rewrites the TLA+ module in place after PlusCal translation.
23+
- The Makefile expects `java` and `wget` to be available to download/run `tla2tools.jar`.
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
----------------------------- MODULE Sailfish -----------------------------
2+
3+
(**************************************************************************************)
4+
(* This is a high-level specification of the Sailfish and Sailfish++ (also called *)
5+
(* signature-free Sailfish) algorithms. At the level of abstraction of this *)
6+
(* specification, the differences between the two algorithms are not visible. *)
7+
(**************************************************************************************)
8+
9+
EXTENDS Integers, FiniteSets, Sequences
10+
11+
CONSTANTS
12+
N \* The set of all nodes
13+
, F \* The set of Byzantine nodes
14+
, R \* The set of rounds
15+
, IsQuorum(_) \* Whether a set is a quorum (i.e. cardinality >= n-f)
16+
, IsBlocking(_) \* Whether a set is a blocking set (i.e. cardinality >= f+1)
17+
, Leader(_) \* operator mapping each round to its leader
18+
, GST \* the first round in which the system is synchronous
19+
20+
ASSUME \E n \in R : R = 1..n \* rounds start at 1; 0 is used as default placeholder
21+
22+
INSTANCE BlockDag \* Import definitions related to DAGs of blocks
23+
24+
(**************************************************************************************)
25+
(* Now we specify the algorithm in the PlusCal language. *)
26+
(**************************************************************************************)
27+
(*--algorithm Sailfish {
28+
variables
29+
vs = {Genesis}, \* the vertices of the DAG
30+
es = {}; \* the edges of the DAG
31+
define {
32+
dag == <<vs, es>>
33+
NoLeaderVoteQuorum(r, vertices, add) ==
34+
LET NoLeaderVote == {v \in vertices : LeaderVertex(r-1) \notin Children(dag, v)}
35+
IN IsQuorum({Node(v) : v \in NoLeaderVote} \cup add)
36+
}
37+
process (correctNode \in N \ F)
38+
variables
39+
round = 0, \* current round; 0 means the node has not started execution
40+
log = <<>>; \* delivered log
41+
{
42+
l0: while (TRUE) {
43+
if (round = 0) { \* start the first round r=1
44+
round := 1;
45+
vs := vs \cup {<<self, 1>>};
46+
es := es \cup {<<<<self, 1>>, Genesis>>}
47+
}
48+
else { \* start a round r>1
49+
with (r \in {r \in R : r > round})
50+
with (deliveredVertices \in SUBSET {v \in vs : Round(v) = r-1}) {
51+
\* we enter a round only if we have a quorum of vertices:
52+
await IsQuorum({Node(v) : v \in deliveredVertices});
53+
\* if this is after GST, we must have all correct vertices:
54+
await r >= GST => (N \ F) \subseteq {Node(v) : v \in deliveredVertices};
55+
\* enter round r:
56+
round := r;
57+
\* if the r-1 leader does not reference the r-2 leader,
58+
\* then we must be sure the r-2 leader cannot commit:
59+
await LeaderVertex(r-1) \in deliveredVertices =>
60+
\/ LeaderVertex(r-2) \in Children(dag, LeaderVertex(r-1))
61+
\/ NoLeaderVoteQuorum(r-1, deliveredVertices, {});
62+
\* if we are the leader, then we must include the r-1 leader or
63+
\* have evidence it cannot commit:
64+
if (Leader(r) = self)
65+
await \/ LeaderVertex(r-1) \in deliveredVertices
66+
\/ NoLeaderVoteQuorum(r, {v \in vs : Round(v) = r}, {self});
67+
\* create a new vertex:
68+
with (newV = <<self, r>>) {
69+
vs := vs \cup {newV};
70+
es := es \cup {<<newV, pv>> : pv \in deliveredVertices};
71+
};
72+
\* commit if there is a quorum of votes for the leader of r-2:
73+
if (r > 2)
74+
with (votesForLeader = {pv \in deliveredVertices : <<pv, LeaderVertex(r-2)>> \in es})
75+
if (IsQuorum({Node(pv) : pv \in votesForLeader}))
76+
log := Linearize(dag, LeaderVertex(r-2))
77+
}
78+
}
79+
}
80+
}
81+
(**************************************************************************************)
82+
(* Next comes our model of Byzantine nodes. Because the real protocol *)
83+
(* disseminates DAG vertices using reliable broadcast, Byzantine nodes cannot *)
84+
(* equivocate and cannot deviate much from the protocol (lest their messages *)
85+
(* be ignored). *)
86+
(**************************************************************************************)
87+
process (byzantineNode \in F)
88+
{
89+
l0: while (TRUE) {
90+
with (r \in R)
91+
with (newV = <<self, r>>) {
92+
when newV \notin vs; \* no equivocation
93+
if (r = 1) {
94+
vs := vs \cup {newV};
95+
es := es \cup {<<newV, Genesis>>}
96+
}
97+
else
98+
with (delivered \in SUBSET {v \in vs : Round(v) = r-1}) {
99+
await IsQuorum({Node(v) : v \in delivered}); \* ignored otherwise
100+
vs := vs \cup {newV};
101+
es := es \cup {<<newV, pv>> : pv \in delivered}
102+
}
103+
}
104+
}
105+
}
106+
}*)
107+
\* BEGIN TRANSLATION (chksum(pcal) = "c16dfa43" /\ chksum(tla) = "9cdbd4f5")
108+
\* Label l0 of process correctNode at line 42 col 9 changed to l0_
109+
VARIABLES vs, es
110+
111+
(* define statement *)
112+
dag == <<vs, es>>
113+
NoLeaderVoteQuorum(r, vertices, add) ==
114+
LET NoLeaderVote == {v \in vertices : LeaderVertex(r-1) \notin Children(dag, v)}
115+
IN IsQuorum({Node(v) : v \in NoLeaderVote} \cup add)
116+
117+
VARIABLES round, log
118+
119+
vars == << vs, es, round, log >>
120+
121+
ProcSet == (N \ F) \cup (F)
122+
123+
Init == (* Global variables *)
124+
/\ vs = {Genesis}
125+
/\ es = {}
126+
(* Process correctNode *)
127+
/\ round = [self \in N \ F |-> 0]
128+
/\ log = [self \in N \ F |-> <<>>]
129+
130+
correctNode(self) == IF round[self] = 0
131+
THEN /\ round' = [round EXCEPT ![self] = 1]
132+
/\ vs' = (vs \cup {<<self, 1>>})
133+
/\ es' = (es \cup {<<<<self, 1>>, Genesis>>})
134+
/\ log' = log
135+
ELSE /\ \E r \in {r \in R : r > round[self]}:
136+
\E deliveredVertices \in SUBSET {v \in vs : Round(v) = r-1}:
137+
/\ IsQuorum({Node(v) : v \in deliveredVertices})
138+
/\ r >= GST => (N \ F) \subseteq {Node(v) : v \in deliveredVertices}
139+
/\ round' = [round EXCEPT ![self] = r]
140+
/\ LeaderVertex(r-1) \in deliveredVertices =>
141+
\/ LeaderVertex(r-2) \in Children(dag, LeaderVertex(r-1))
142+
\/ NoLeaderVoteQuorum(r-1, deliveredVertices, {})
143+
/\ IF Leader(r) = self
144+
THEN /\ \/ LeaderVertex(r-1) \in deliveredVertices
145+
\/ NoLeaderVoteQuorum(r, {v \in vs : Round(v) = r}, {self})
146+
ELSE /\ TRUE
147+
/\ LET newV == <<self, r>> IN
148+
/\ vs' = (vs \cup {newV})
149+
/\ es' = (es \cup {<<newV, pv>> : pv \in deliveredVertices})
150+
/\ IF r > 2
151+
THEN /\ LET votesForLeader == {pv \in deliveredVertices : <<pv, LeaderVertex(r-2)>> \in es'} IN
152+
IF IsQuorum({Node(pv) : pv \in votesForLeader})
153+
THEN /\ log' = [log EXCEPT ![self] = Linearize(dag, LeaderVertex(r-2))]
154+
ELSE /\ TRUE
155+
/\ log' = log
156+
ELSE /\ TRUE
157+
/\ log' = log
158+
159+
byzantineNode(self) == /\ \E r \in R:
160+
LET newV == <<self, r>> IN
161+
/\ newV \notin vs
162+
/\ IF r = 1
163+
THEN /\ vs' = (vs \cup {newV})
164+
/\ es' = (es \cup {<<newV, Genesis>>})
165+
ELSE /\ \E delivered \in SUBSET {v \in vs : Round(v) = r-1}:
166+
/\ IsQuorum({Node(v) : v \in delivered})
167+
/\ vs' = (vs \cup {newV})
168+
/\ es' = (es \cup {<<newV, pv>> : pv \in delivered})
169+
/\ UNCHANGED << round, log >>
170+
171+
Next == (\E self \in N \ F: correctNode(self))
172+
\/ (\E self \in F: byzantineNode(self))
173+
174+
Spec == Init /\ [][Next]_vars
175+
176+
\* END TRANSLATION
177+
178+
(**************************************************************************************)
179+
(* Basic type invariant: *)
180+
(**************************************************************************************)
181+
TypeOK ==
182+
/\ \A v \in vs \ {<<>>} :
183+
/\ Node(v) \in N /\ Round(v) \in Nat \ {0}
184+
/\ \A c \in Children(dag, v) : Round(c) = Round(v) - 1
185+
/\ \A e \in es :
186+
/\ e = <<e[1],e[2]>>
187+
/\ {e[1], e[2]} \subseteq vs
188+
/\ \A n \in N \ F : round[n] \in Nat
189+
190+
(**************************************************************************************)
191+
(* Next we define the safety and liveness properties *)
192+
(**************************************************************************************)
193+
194+
Agreement == \A n1,n2 \in N \ F : Compatible(log[n1], log[n2])
195+
196+
Liveness == \A r \in R : r >= GST /\ Leader(r) \notin F =>
197+
\A n \in N \ F : round[n] >= r+2 =>
198+
\E i \in DOMAIN log[n] : log[n][i] = LeaderVertex(r)
199+
200+
===========================================================================
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
SPECIFICATION TerminatingSpec
2+
3+
CONSTANTS
4+
n1 = n1
5+
n2 = n2
6+
n3 = n3
7+
8+
INVARIANT TypeOK
9+
INVARIANT Agreement
10+
INVARIANT Liveness
11+
\* INVARIANT Falsy3
12+
13+
CONSTRAINT StateConstraint

0 commit comments

Comments
 (0)