forked from wenhuizhang/summer-school-2020
-
Notifications
You must be signed in to change notification settings - Fork 0
/
exercise01.dfy
361 lines (316 loc) · 12.7 KB
/
exercise01.dfy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
//#title Two Phase Commit Model
//#desc Model a distributed protocol using compound state machines.
/*
* Your goal is to model a 2-Phase Commit protocol. You're modeling a single
* instance of the problem -- a designated coordinator, no concurrent
* instances. Furthermore, you may assume there are no coordinator or
* participant failures. This is indeed a fairly simplistic setting, but it's
* still nontrivial, and is a nice example for reasoning about the
* state-machine composition framework.
*
* 2-Phase Commit Protocol english design doc:
*
* 1, Coordinator sends VOTE-REQ to all participants.
* 2. Each participant i sends back vote_i to coordinator.
* If vote_i=No then i sets decision_i := Abort.
* 3. Coordinator collects votes.
* If all votes are yes then coordinator sets decision_c := Commit and sends
* Commit to all participants.
* Else coordinator sets decision_c := Abort and sends Abort to all
* participants who voted yes.
* 4. Participants receiving Commit set decision_i := Commit
* (The slide is delightfully poorly specified. "else decision_i := Abort"!?
* When else? As soon as it doesn't hear Commit!?)
*
* This file provides a lot of helpful framework. You only need to
* define Types.Message and then fill in the state machine types and actions
* in module CoordinatorHost and module ParticipantHost.
*
* Unlike chapter01, where you could work exercises sequentially,
* WE STRONGLY ENCOURAGE YOU to read the entire file before beginning
* work. Later pieces of the file are helpful, for example by explaining
* the network model you're working in, and by helping you understand
* the role the Constants datatypes play.
*/
include "../../library/Library.dfy" // Some handy utilities.
module CommitTypes {
// How a particular participant feels.
datatype Vote = Yes | No
// What decision has been reached by the protocol.
datatype Decision = Commit | Abort
}
module Types {
import opened Library
import opened CommitTypes
type HostId = nat
// Have to define our message datatype so network can refer to it.
// (There are cleverer ways to parameterize the network module, but
// we're trying to avoid getting fancy with the Dafny module system.)
datatype Message =
ReplaceMe()
// A MessageOps is a "binding variable" used to connect a Host's Next step
// (what message got received, what got sent?) with the Network (only allow
// receipt of messages sent prior; record newly-sent messages).
// Note that both fields are Option. A step predicate can say recv.None?
// to indicate that it doesn't need to receive a message to occur.
// It can say send.None? to indicate that it doesn't want to transmit a message.
datatype MessageOps = MessageOps(recv:Option<Message>, send:Option<Message>)
}
// There are two host roles in 2PC, Coordinator and Participant. We'll define
// separate state machines for each.
// This state machine defines how a coordinator host should behave: what state
// it records, and what actions it's allowed to take in response to incoming
// messages (or spontaneously by thinking about its existing state).
module CoordinatorHost {
import opened CommitTypes
import opened Types
import opened Library
datatype Constants = Constants()
// What relationship must hold between this host's own constants and the
// structure of the overall group of hosts? It needs to have a correct
// count of the number of participants.
predicate ConstantsValidForGroup(c: Constants, participantCount: nat)
{
true // replace me
}
datatype Variables = Variables()
{
// WF is for a simple condition that relates every valid Variables state
// to the Constants.
predicate WF(c: Constants) {
true
}
}
predicate Init(c: Constants, v: Variables)
{
true // replace me
}
// Protocol steps. Define an action predicate for each step of the protocol
// that the coordinator is involved in.
// Hint: it's probably easiest to separate the receipt and recording of
// incoming votes from detecting that all have arrived and making a decision.
// JayNF
datatype Step =
ReplaceMeWithYourJayNFSteps()
// msgOps is a "binding variable" -- the host and the network have to agree
// on its assignment to make a valid transition. So the host explains what
// would happen if it could receive a particular message, and the network
// decides whether such a message is available for receipt.
predicate NextStep(c: Constants, v: Variables, v': Variables, step: Step, msgOps: MessageOps)
{
match step
case ReplaceMeWithYourJayNFSteps => true
}
predicate Next(c: Constants, v: Variables, v': Variables, msgOps: MessageOps)
{
exists step :: NextStep(c, v, v', step, msgOps)
}
}
module ParticipantHost {
import opened CommitTypes
import opened Types
import opened Library
datatype Constants = Constants()
// What relationship must hold between this host's own constants and the
// structure of the overall group of hosts? It needs to know its hostId.
predicate ConstantsValidForGroup(c: Constants, hostId: HostId)
{
true // replace me
}
datatype Variables = Variables()
{
predicate WF(c: Constants) {
true
}
}
predicate Init(c: Constants, v: Variables)
{
true // replace me
}
// Protocol steps. Define an action predicate for each step of the protocol
// that participant can take.
// JayNF
datatype Step =
ReplaceMeWithYourJayNFSteps()
predicate NextStep(c: Constants, v: Variables, v': Variables, step: Step, msgOps: MessageOps)
{
match step
case ReplaceMeWithYourJayNFSteps => true
}
predicate Next(c: Constants, v: Variables, v': Variables, msgOps: MessageOps)
{
exists step :: NextStep(c, v, v', step, msgOps)
}
}
// We define a generic Host as able to be either of the specific roles.
// This is the ultimate (untrusted) definition of the protocol we're
// trying to verify.
// This definition is given to you to clarify how the two protocol roles above
// are pulled together into the ultimate distributed system.
module Host {
import opened Library
import opened CommitTypes
import opened Types
import CoordinatorHost
import ParticipantHost
datatype Constants =
| CoordinatorConstants(coordinator: CoordinatorHost.Constants)
| ParticipantConstants(participant: ParticipantHost.Constants)
datatype Variables =
| CoordinatorVariables(coordinator: CoordinatorHost.Variables)
| ParticipantVariables(participant: ParticipantHost.Variables)
{
predicate WF(c: Constants) {
&& (CoordinatorVariables? <==> c.CoordinatorConstants?) // types of c & v agree
// subtype WF satisfied
&& (match c
case CoordinatorConstants(_) => coordinator.WF(c.coordinator)
case ParticipantConstants(_) => participant.WF(c.participant)
)
}
}
// What property must be true of any group of hosts to run the protocol?
// Generic DistributedSystem module calls back into this predicate to ensure
// that it has a well-formed *group* of hosts.
predicate GroupWFConstants(grp_c: seq<Constants>)
{
// There must at least be a coordinator
&& 0 < |grp_c|
// Last host is a coordinator
&& Last(grp_c).CoordinatorConstants?
// All the others are participants
&& (forall hostid:HostId | hostid < |grp_c|-1 :: grp_c[hostid].ParticipantConstants?)
// The coordinator's constants must correctly account for the number of participants
&& CoordinatorHost.ConstantsValidForGroup(Last(grp_c).coordinator, |grp_c|-1)
// The participants's constants must match their group positions.
// (Actually, they just need to be distinct from one another so we get
// non-conflicting votes, but this is an easy way to express that property.)
&& (forall hostid:HostId | hostid < |grp_c|-1
:: ParticipantHost.ConstantsValidForGroup(grp_c[hostid].participant, hostid))
}
predicate GroupWF(grp_c: seq<Constants>, grp_v: seq<Variables>)
{
&& GroupWFConstants(grp_c)
// Variables size matches group size defined by grp_c
&& |grp_v| == |grp_c|
// Each host is well-formed
&& (forall hostid:HostId | hostid < |grp_c| :: grp_v[hostid].WF(grp_c[hostid]))
}
// Generic DistributedSystem module calls back into this predicate to give
// the protocol an opportunity to set up constraints across the various
// hosts. Protocol requires one coordinator and the rest participants;
// coordinator must know how many participants, and participants must know
// own ids.
predicate GroupInit(grp_c: seq<Constants>, grp_v: seq<Variables>)
{
// constants & variables are well-formed (same number of host slots as constants expect)
&& GroupWF(grp_c, grp_v)
// Coordinator is initialized to know about the N-1 participants.
&& CoordinatorHost.Init(Last(grp_c).coordinator, Last(grp_v).coordinator)
// Participants initted with their ids.
&& (forall hostid:HostId | hostid < |grp_c|-1 ::
ParticipantHost.Init(grp_c[hostid].participant, grp_v[hostid].participant)
)
}
// Dispatch Next to appropriate underlying implementation.
predicate Next(c: Constants, v: Variables, v': Variables, msgOps: MessageOps)
{
&& v.WF(c)
&& v'.WF(c)
&& (match c
case CoordinatorConstants(_) => CoordinatorHost.Next(c.coordinator, v.coordinator, v'.coordinator, msgOps)
case ParticipantConstants(_) => ParticipantHost.Next(c.participant, v.participant, v'.participant, msgOps)
)
}
}
// The (trusted) model of the environment: There is a network that can only deliver
// messages that some Host (running the protocol) has sent, but once sent, messages
// can be delivered repeatedly and in any order.
// This definition is given to you because it's a common assumption you can
// reuse. Someday you might decide to assume a different network model (e.g.
// reliable or at-most-once delivery), but this is a good place to start.
module Network {
import opened CommitTypes
import opened Types
datatype Constants = Constants // no constants for network
// Network state is the set of messages ever sent. Once sent, we'll
// allow it to be delivered over and over.
// (We don't have packet headers, so duplication, besides being realistic,
// also doubles as how multiple parties can hear the message.)
datatype Variables = Variables(sentMsgs:set<Message>)
predicate Init(c: Constants, v: Variables)
{
&& v.sentMsgs == {}
}
predicate Next(c: Constants, v: Variables, v': Variables, msgOps: MessageOps)
{
// Only allow receipt of a message if we've seen it has been sent.
&& (msgOps.recv.Some? ==> msgOps.recv.value in v.sentMsgs)
// Record the sent message, if there was one.
&& v'.sentMsgs ==
v.sentMsgs + if msgOps.send.None? then {} else { msgOps.send.value }
}
}
// The (trusted) model of the distributed system: hosts don't share memory. Each
// takes its steps independently, interleaved in nondeterministic order with others.
// They only communicate through the network, and obey the communication model
// established by the Network model.
// This is given to you; it can be reused for just about any shared-nothing-concurrency
// protocol model.
module DistributedSystem {
import opened Library
import opened CommitTypes
import opened Types
import Network
import Host
import CoordinatorHost
import ParticipantHost
datatype Constants = Constants(
hosts: seq<Host.Constants>,
network: Network.Constants)
{
predicate WF() {
Host.GroupWFConstants(hosts)
}
predicate ValidHostId(id: HostId) {
id < |hosts|
}
}
datatype Variables = Variables(
hosts: seq<Host.Variables>,
network: Network.Variables)
{
predicate WF(c: Constants) {
&& c.WF()
&& Host.GroupWF(c.hosts, hosts)
}
}
predicate Init(c: Constants, v: Variables)
{
&& v.WF(c)
&& Host.GroupInit(c.hosts, v.hosts)
&& Network.Init(c.network, v.network)
}
predicate HostAction(c: Constants, v: Variables, v': Variables, hostid: HostId, msgOps: MessageOps)
{
&& v.WF(c)
&& v'.WF(c)
&& c.ValidHostId(hostid)
&& Host.Next(c.hosts[hostid], v.hosts[hostid], v'.hosts[hostid], msgOps)
// all other hosts UNCHANGED
&& (forall otherHost:nat | c.ValidHostId(otherHost) && otherHost != hostid :: v'.hosts[otherHost] == v.hosts[otherHost])
}
// JayNF is pretty simple as there's only a single action disjunct
datatype Step =
| HostActionStep(hostid: HostId, msgOps: MessageOps)
predicate NextStep(c: Constants, v: Variables, v': Variables, step: Step)
{
&& HostAction(c, v, v', step.hostid, step.msgOps)
// network agrees recv has been sent and records sent
&& Network.Next(c.network, v.network, v'.network, step.msgOps)
}
predicate Next(c: Constants, v: Variables, v': Variables)
{
exists step :: NextStep(c, v, v', step)
}
}