├── .gitignore ├── LICENSE ├── Lunch.md ├── Readme.md ├── ballot.go ├── ballot_test.go ├── cmd.go ├── cmd ├── lunch │ ├── lunch.go │ └── toml │ │ ├── 3of4.toml │ │ ├── 3tiers.toml │ │ ├── aliceinthemiddle.toml │ │ ├── cycle.toml │ │ ├── fig3.toml │ │ ├── simple.toml │ │ └── stars.toml └── scptxvm │ ├── Readme.md │ ├── handlers.go │ ├── main.go │ ├── msg.go │ ├── node.go │ ├── nominate.go │ ├── store.go │ └── val.go ├── doc.go ├── genset.go ├── go.mod ├── go.sum ├── msg.go ├── msg_test.go ├── node.go ├── node_test.go ├── qset.go ├── quorum.go ├── quorum_test.go ├── set.go ├── slot.go ├── slot_test.go ├── topic.go ├── value.go └── value_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bob Glickstein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Lunch.md: -------------------------------------------------------------------------------- 1 | # A round of lunch 2 | 3 | This repo includes a demonstration program called `lunch`. 4 | It simulates a group of friends or coworkers 5 | (organized into some configurable network of quorum slices) 6 | coming to consensus on what to order for lunch. 7 | 8 | This document contains the 9 | (simplified) 10 | output of one round of lunch consensus. 11 | It describes step by step how the network goes from conflicting nominations to agreeing on an outcome. 12 | 13 | This example was generated using the [“3 tiers”](https://github.com/bobg/scp/blob/master/cmd/lunch/toml/3tiers.toml) network configuration, 14 | in which: 15 | - a top tier of nodes — Alice, Bob, Carol, and Dave — each depends on two of its neighbors for a quorum; 16 | - a middle tier — Elsie, Fred, Gwen, and Hank — each depends on any two members of the top tier; and 17 | - a bottom tier — Inez and John — each depends on any two members of the middle tier. 18 | 19 | Note that lines ending with `-> ∅` 20 | (meaning the node produces no output in response to a protocol message) 21 | are ones the `lunch` program does not normally display, 22 | even though some of them do update the network’s internal state. 23 | They are included here to make the workings of the consensus algorithm clearer. 24 | 25 | ``` 26 | dave: ∅ -> (dave NOM X=[salads], Y=[]) 27 | ``` 28 | 29 | Dave votes to nominate salads. 30 | In an SCP protocol message, 31 | X is the set of nominees voted for. 32 | 33 | ``` 34 | elsie: ∅ -> (elsie NOM X=[burgers], Y=[]) 35 | ``` 36 | 37 | Elsie votes to nominate burgers. 38 | 39 | ``` 40 | gwen: ∅ -> ∅ 41 | ``` 42 | 43 | Gwen would like to nominate something, 44 | but she’s not in her own “high-priority neighbors” list 45 | (at this particular time for this particular slot), 46 | so she discards her own nomination. 47 | 48 | ``` 49 | bob: ∅ -> (bob NOM X=[indian], Y=[]) 50 | alice: ∅ -> (alice NOM X=[burritos], Y=[]) 51 | ``` 52 | 53 | Bob and Alice vote to nominate Indian food and burritos, respectively. 54 | 55 | ``` 56 | carol: ∅ -> ∅ 57 | ``` 58 | 59 | Carol, like Gwen, is not in her own high-priority-neighbors list, 60 | so she does not have the ability to nominate anything at the moment. 61 | 62 | ``` 63 | carol: (dave NOM X=[salads], Y=[]) -> (carol NOM X=[salads], Y=[]) 64 | ``` 65 | 66 | However, 67 | Dave _is_ one of Carol’s high-priority neighbors. 68 | She sees his vote to nominate salads and she echoes it. 69 | 70 | ``` 71 | dave: (elsie NOM X=[burgers], Y=[]) -> ∅ 72 | elsie: (dave NOM X=[salads], Y=[]) -> ∅ 73 | bob: (dave NOM X=[salads], Y=[]) -> ∅ 74 | dave: (bob NOM X=[indian], Y=[]) -> ∅ 75 | carol: (elsie NOM X=[burgers], Y=[]) -> ∅ 76 | alice: (dave NOM X=[salads], Y=[]) -> ∅ 77 | ``` 78 | 79 | These nodes all see nominations from their peers but, 80 | throttled by the priority mechanism, 81 | do not echo them. 82 | 83 | ``` 84 | gwen: (dave NOM X=[salads], Y=[]) -> (gwen NOM X=[salads], Y=[]) 85 | ``` 86 | 87 | Gwen does echo Dave’s nomination. 88 | 89 | ``` 90 | elsie: (bob NOM X=[indian], Y=[]) -> ∅ 91 | carol: (bob NOM X=[indian], Y=[]) -> ∅ 92 | bob: (elsie NOM X=[burgers], Y=[]) -> ∅ 93 | alice: (elsie NOM X=[burgers], Y=[]) -> ∅ 94 | gwen: (elsie NOM X=[burgers], Y=[]) -> ∅ 95 | dave: (alice NOM X=[burritos], Y=[]) -> ∅ 96 | elsie: (alice NOM X=[burritos], Y=[]) -> ∅ 97 | dave: (carol NOM X=[salads], Y=[]) -> ∅ 98 | ``` 99 | 100 | Dave sees Carol’s nomination of salads. 101 | Carol may or may not be one of Dave’s high-priority neighbors at the moment, 102 | but in any case Dave has already nominated salads himself, 103 | so this message from Carol does not cause any change in Dave’s state 104 | (which means Dave sends out no new message in response). 105 | 106 | ``` 107 | carol: (alice NOM X=[burritos], Y=[]) -> ∅ 108 | gwen: (bob NOM X=[indian], Y=[]) -> ∅ 109 | bob: (alice NOM X=[burritos], Y=[]) -> ∅ 110 | elsie: (carol NOM X=[salads], Y=[]) -> ∅ 111 | dave: (gwen NOM X=[salads], Y=[]) -> ∅ 112 | alice: (bob NOM X=[indian], Y=[]) -> ∅ 113 | carol: (gwen NOM X=[salads], Y=[]) -> ∅ 114 | bob: (carol NOM X=[salads], Y=[]) -> ∅ 115 | elsie: (gwen NOM X=[salads], Y=[]) -> ∅ 116 | gwen: (alice NOM X=[burritos], Y=[]) -> ∅ 117 | alice: (carol NOM X=[salads], Y=[]) -> ∅ 118 | bob: (gwen NOM X=[salads], Y=[]) -> ∅ 119 | gwen: (carol NOM X=[salads], Y=[]) -> ∅ 120 | alice: (gwen NOM X=[salads], Y=[]) -> ∅ 121 | ``` 122 | 123 | More throttled-or-redundant peer nominations. 124 | 125 | ``` 126 | hank: ∅ -> (hank NOM X=[salads], Y=[]) 127 | ``` 128 | 129 | Hank votes to nominate salads. 130 | 131 | ``` 132 | alice: (hank NOM X=[salads], Y=[]) -> ∅ 133 | hank: (dave NOM X=[salads], Y=[]) -> ∅ 134 | hank: (elsie NOM X=[burgers], Y=[]) -> ∅ 135 | carol: (hank NOM X=[salads], Y=[]) -> ∅ 136 | dave: (hank NOM X=[salads], Y=[]) -> ∅ 137 | bob: (hank NOM X=[salads], Y=[]) -> ∅ 138 | elsie: (hank NOM X=[salads], Y=[]) -> ∅ 139 | gwen: (hank NOM X=[salads], Y=[]) -> ∅ 140 | hank: (bob NOM X=[indian], Y=[]) -> ∅ 141 | hank: (alice NOM X=[burritos], Y=[]) -> ∅ 142 | fred: ∅ -> ∅ 143 | hank: (carol NOM X=[salads], Y=[]) -> ∅ 144 | hank: (gwen NOM X=[salads], Y=[]) -> ∅ 145 | fred: (dave NOM X=[salads], Y=[]) -> (fred NOM X=[salads], Y=[]) 146 | gwen: (fred NOM X=[salads], Y=[]) -> ∅ 147 | fred: (elsie NOM X=[burgers], Y=[]) -> ∅ 148 | elsie: (fred NOM X=[salads], Y=[]) -> ∅ 149 | dave: (fred NOM X=[salads], Y=[]) -> ∅ 150 | hank: (fred NOM X=[salads], Y=[]) -> ∅ 151 | alice: (fred NOM X=[salads], Y=[]) -> ∅ 152 | carol: (fred NOM X=[salads], Y=[]) -> ∅ 153 | bob: (fred NOM X=[salads], Y=[]) -> ∅ 154 | fred: (bob NOM X=[indian], Y=[]) -> ∅ 155 | fred: (alice NOM X=[burritos], Y=[]) -> ∅ 156 | fred: (carol NOM X=[salads], Y=[]) -> ∅ 157 | fred: (gwen NOM X=[salads], Y=[]) -> ∅ 158 | fred: (hank NOM X=[salads], Y=[]) -> ∅ 159 | elsie: (inez NOM X=[pizza], Y=[]) -> ∅ 160 | dave: (inez NOM X=[pizza], Y=[]) -> ∅ 161 | hank: (inez NOM X=[pizza], Y=[]) -> ∅ 162 | inez: (dave NOM X=[salads], Y=[]) -> ∅ 163 | inez: (elsie NOM X=[burgers], Y=[]) -> ∅ 164 | bob: (inez NOM X=[pizza], Y=[]) -> ∅ 165 | gwen: (inez NOM X=[pizza], Y=[]) -> ∅ 166 | carol: (inez NOM X=[pizza], Y=[]) -> ∅ 167 | fred: (inez NOM X=[pizza], Y=[]) -> ∅ 168 | alice: (inez NOM X=[pizza], Y=[]) -> ∅ 169 | inez: (bob NOM X=[indian], Y=[]) -> ∅ 170 | inez: (alice NOM X=[burritos], Y=[]) -> ∅ 171 | inez: (carol NOM X=[salads], Y=[]) -> ∅ 172 | inez: (gwen NOM X=[salads], Y=[]) -> ∅ 173 | inez: (hank NOM X=[salads], Y=[]) -> ∅ 174 | inez: (fred NOM X=[salads], Y=[]) -> ∅ 175 | john: ∅ -> (john NOM X=[sandwiches], Y=[]) 176 | hank: (john NOM X=[sandwiches], Y=[]) -> ∅ 177 | bob: (john NOM X=[sandwiches], Y=[]) -> ∅ 178 | inez: (john NOM X=[sandwiches], Y=[]) -> ∅ 179 | dave: (john NOM X=[sandwiches], Y=[]) -> ∅ 180 | john: (dave NOM X=[salads], Y=[]) -> ∅ 181 | fred: (john NOM X=[sandwiches], Y=[]) -> ∅ 182 | carol: (john NOM X=[sandwiches], Y=[]) -> ∅ 183 | elsie: (john NOM X=[sandwiches], Y=[]) -> ∅ 184 | gwen: (john NOM X=[sandwiches], Y=[]) -> ∅ 185 | alice: (john NOM X=[sandwiches], Y=[]) -> ∅ 186 | john: (elsie NOM X=[burgers], Y=[]) -> ∅ 187 | john: (bob NOM X=[indian], Y=[]) -> ∅ 188 | john: (alice NOM X=[burritos], Y=[]) -> ∅ 189 | elsie: (alice NOM X=[burritos], Y=[]) -> ∅ 190 | ``` 191 | 192 | The nomination process continues, 193 | with some nominations echoed, 194 | others throttled, 195 | and some nodes self-censoring. 196 | 197 | ``` 198 | gwen: ∅ -> (gwen NOM X=[pasta salads], Y=[]) 199 | ``` 200 | 201 | Gwen, 202 | who originally wanted to nominate something but self-censored because she wasn’t in her own high-priority-neighbors list, 203 | now is. 204 | This happened because enough time elapsed since Gwen began this nomination round that her high-priority list expanded. 205 | She was previously echoing a nomination for pasta but now adds her own nomination, for salads. 206 | 207 | ``` 208 | bob: (john NOM X=[sandwiches], Y=[]) -> ∅ 209 | carol: (elsie NOM X=[burgers], Y=[]) -> ∅ 210 | bob: (dave NOM X=[salads], Y=[]) -> ∅ 211 | carol: (gwen NOM X=[salads], Y=[]) -> ∅ 212 | ``` 213 | 214 | As mentioned, 215 | Gwen is now nominating both pasta and salads, 216 | but an older protocol message of hers, 217 | from when she was still nominating only salads, 218 | is only just now reaching Carol. 219 | (Carol does not respond because she is already nominating salads.) 220 | 221 | ``` 222 | alice: ∅ -> ∅ 223 | carol: (fred NOM X=[salads], Y=[]) -> ∅ 224 | carol: ∅ -> ∅ 225 | gwen: (dave NOM X=[salads], Y=[]) -> ∅ 226 | carol: (dave NOM X=[salads], Y=[]) -> ∅ 227 | gwen: (elsie NOM X=[burgers], Y=[]) -> ∅ 228 | carol: (bob NOM X=[indian], Y=[]) -> (carol NOM X=[indian salads], Y=[]) 229 | elsie: (hank NOM X=[salads], Y=[]) -> ∅ 230 | gwen: (alice NOM X=[burritos], Y=[]) -> ∅ 231 | carol: (alice NOM X=[burritos], Y=[]) -> ∅ 232 | elsie: ∅ -> ∅ 233 | gwen: (carol NOM X=[salads], Y=[]) -> ∅ 234 | alice: (bob NOM X=[indian], Y=[]) -> (alice NOM X=[burritos indian], Y=[]) 235 | ``` 236 | 237 | Alice’s high-priority-neighbor list has now expanded to include Bob, 238 | and so she starts echoing his nomination of Indian food. 239 | 240 | ``` 241 | gwen: (bob NOM X=[indian], Y=[]) -> ∅ 242 | carol: (hank NOM X=[salads], Y=[]) -> ∅ 243 | carol: (inez NOM X=[pizza], Y=[]) -> ∅ 244 | carol: (john NOM X=[sandwiches], Y=[]) -> ∅ 245 | bob: (alice NOM X=[burritos], Y=[]) -> ∅ 246 | bob: (carol NOM X=[salads], Y=[]) -> ∅ 247 | bob: (inez NOM X=[pizza], Y=[]) -> ∅ 248 | elsie: (dave NOM X=[salads], Y=[]) -> ∅ 249 | bob: (fred NOM X=[salads], Y=[]) -> ∅ 250 | bob: ∅ -> ∅ 251 | bob: (elsie NOM X=[burgers], Y=[]) -> ∅ 252 | bob: (gwen NOM X=[salads], Y=[]) -> ∅ 253 | bob: (hank NOM X=[salads], Y=[]) -> ∅ 254 | elsie: (bob NOM X=[indian], Y=[]) -> ∅ 255 | elsie: (inez NOM X=[pizza], Y=[]) -> ∅ 256 | elsie: (john NOM X=[sandwiches], Y=[]) -> ∅ 257 | alice: (gwen NOM X=[salads], Y=[]) -> ∅ 258 | elsie: (carol NOM X=[salads], Y=[]) -> ∅ 259 | alice: (hank NOM X=[salads], Y=[]) -> ∅ 260 | elsie: (gwen NOM X=[salads], Y=[]) -> ∅ 261 | alice: (fred NOM X=[salads], Y=[]) -> ∅ 262 | gwen: (hank NOM X=[salads], Y=[]) -> ∅ 263 | elsie: (fred NOM X=[salads], Y=[]) -> ∅ 264 | alice: (dave NOM X=[salads], Y=[]) -> ∅ 265 | gwen: (fred NOM X=[salads], Y=[]) -> ∅ 266 | alice: (elsie NOM X=[burgers], Y=[]) -> ∅ 267 | dave: (fred NOM X=[salads], Y=[]) -> ∅ 268 | gwen: (inez NOM X=[pizza], Y=[]) -> ∅ 269 | alice: (carol NOM X=[salads], Y=[]) -> ∅ 270 | dave: (inez NOM X=[pizza], Y=[]) -> ∅ 271 | gwen: (john NOM X=[sandwiches], Y=[]) -> ∅ 272 | alice: (inez NOM X=[pizza], Y=[]) -> ∅ 273 | dave: (elsie NOM X=[burgers], Y=[]) -> ∅ 274 | alice: (john NOM X=[sandwiches], Y=[]) -> ∅ 275 | inez: ∅ -> (inez NOM X=[pizza], Y=[]) 276 | dave: (bob NOM X=[indian], Y=[]) -> (dave NOM X=[indian salads], Y=[]) 277 | dave: (alice NOM X=[burritos], Y=[]) -> ∅ 278 | john: (carol NOM X=[salads], Y=[]) -> ∅ 279 | dave: (gwen NOM X=[salads], Y=[]) -> ∅ 280 | dave: ∅ -> ∅ 281 | dave: (carol NOM X=[salads], Y=[]) -> ∅ 282 | dave: (hank NOM X=[salads], Y=[]) -> ∅ 283 | dave: (john NOM X=[sandwiches], Y=[]) -> ∅ 284 | bob: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 285 | john: (gwen NOM X=[salads], Y=[]) -> ∅ 286 | dave: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 287 | inez: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 288 | inez: (carol NOM X=[indian salads], Y=[]) -> ∅ 289 | bob: (carol NOM X=[indian salads], Y=[]) -> ∅ 290 | elsie: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 291 | hank: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 292 | carol: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 293 | elsie: (carol NOM X=[indian salads], Y=[]) -> ∅ 294 | ``` 295 | 296 | Plenty more of the same. 297 | 298 | ``` 299 | dave: (carol NOM X=[indian salads], Y=[]) -> (dave NOM X=[salads], Y=[indian]) 300 | ``` 301 | 302 | Something new: Dave has moved Indian food from X, 303 | the set of values he’s voting to nominate, 304 | to Y, the set of values he _accepts_ as nominated. 305 | This happens when either: 306 | 307 | 1. A _quorum_ votes-or-accepts the same value; 308 | 2. A _blocking set_ accepts it. 309 | 310 | Dave previously had seen Bob vote to nominate Indian food and echoed that nomination. 311 | Now that Dave sees Carol also voting to nominate Indian food, 312 | condition 1 is satisfied: 313 | Bob, 314 | Carol, 315 | and Dave together form one of Dave’s quorums, 316 | all voting for the same thing. 317 | 318 | ``` 319 | gwen: (carol NOM X=[indian salads], Y=[]) -> ∅ 320 | john: (hank NOM X=[salads], Y=[]) -> ∅ 321 | inez: (alice NOM X=[burritos indian], Y=[]) -> ∅ 322 | john: (fred NOM X=[salads], Y=[]) -> ∅ 323 | john: (inez NOM X=[pizza], Y=[]) -> ∅ 324 | fred: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 325 | alice: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 326 | hank: (carol NOM X=[indian salads], Y=[]) -> ∅ 327 | elsie: (alice NOM X=[burritos indian], Y=[]) -> ∅ 328 | inez: (dave NOM X=[indian salads], Y=[]) -> ∅ 329 | ``` 330 | 331 | More redundant-or-throttled nominations. 332 | 333 | ``` 334 | bob: (alice NOM X=[burritos indian], Y=[]) -> (bob NOM X=[], Y=[indian]) 335 | carol: (alice NOM X=[burritos indian], Y=[]) -> (carol NOM X=[salads], Y=[indian]) 336 | ``` 337 | 338 | More “accepting” of the Indian-food nomination. 339 | 340 | ``` 341 | elsie: (dave NOM X=[indian salads], Y=[]) -> ∅ 342 | dave: (alice NOM X=[burritos indian], Y=[]) -> ∅ 343 | hank: (alice NOM X=[burritos indian], Y=[]) -> ∅ 344 | gwen: (alice NOM X=[burritos indian], Y=[]) -> ∅ 345 | inez: (dave NOM X=[salads], Y=[indian]) -> ∅ 346 | bob: (dave NOM X=[indian salads], Y=[]) -> ∅ 347 | hank: (dave NOM X=[indian salads], Y=[]) -> ∅ 348 | john: (gwen NOM X=[pasta salads], Y=[]) -> ∅ 349 | fred: (carol NOM X=[indian salads], Y=[]) -> ∅ 350 | fred: (alice NOM X=[burritos indian], Y=[]) -> ∅ 351 | alice: (carol NOM X=[indian salads], Y=[]) -> (alice NOM X=[burritos], Y=[indian]) 352 | alice: (dave NOM X=[indian salads], Y=[]) -> ∅ 353 | carol: (dave NOM X=[indian salads], Y=[]) -> ∅ 354 | ``` 355 | 356 | Nomination continues. 357 | 358 | ``` 359 | dave: (bob NOM X=[], Y=[indian]) -> ∅ 360 | ``` 361 | 362 | Dave sees that Bob now “accepts” Indian food as nominated. 363 | This will be important in a moment. 364 | 365 | ``` 366 | alice: (dave NOM X=[salads], Y=[indian]) -> ∅ 367 | bob: (dave NOM X=[salads], Y=[indian]) -> ∅ 368 | elsie: (dave NOM X=[salads], Y=[indian]) -> ∅ 369 | fred: (dave NOM X=[indian salads], Y=[]) -> (fred NOM X=[salads], Y=[indian]) 370 | hank: (dave NOM X=[salads], Y=[indian]) -> ∅ 371 | john: (carol NOM X=[indian salads], Y=[]) -> ∅ 372 | elsie: (bob NOM X=[], Y=[indian]) -> ∅ 373 | gwen: (dave NOM X=[indian salads], Y=[]) -> (gwen NOM X=[pasta salads], Y=[indian]) 374 | fred: (dave NOM X=[salads], Y=[indian]) -> ∅ 375 | ``` 376 | 377 | Nomination continues. 378 | 379 | ``` 380 | dave: (carol NOM X=[salads], Y=[indian]) -> (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) 381 | ``` 382 | 383 | Dave, 384 | who already “accepted” the Indian food nomination, 385 | now sees that Carol also accepts it. 386 | Dave earlier saw Bob accept it as well. 387 | Bob-Carol-Dave is one of Dave’s quorums, 388 | and when a quorum accepts something, that’s called _confirmation_. 389 | 390 | Dave confirms the nomination of Indian food and so is ready to begin _balloting_. 391 | A ballot is a pair, 392 | and balloting is the process of finding a ballot that all nodes can commit to. 393 | This happens through multiple rounds of voting on statements about ballots, 394 | beginning with ruling out ballots all nodes can agree _not_ to commit to — so-called “aborted” ballots. 395 | 396 | Dave votes to _prepare_ the ballot <1,indian>. 397 | This means that all lesser ballots are aborted and Dave promises never to commit to them. 398 | (There are no lesser ballots at this stage, 399 | but anyway that’s the meaning of a “prepare” vote.) 400 | 401 | ``` 402 | john: (alice NOM X=[burritos indian], Y=[]) -> ∅ 403 | inez: (bob NOM X=[], Y=[indian]) -> ∅ 404 | ``` 405 | 406 | For John and Inez, nomination continues. 407 | 408 | ``` 409 | alice: (bob NOM X=[], Y=[indian]) -> (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) 410 | ``` 411 | 412 | Alice joines Dave in balloting, also voting to prepare <1,indian>. 413 | 414 | ``` 415 | carol: (dave NOM X=[salads], Y=[indian]) -> ∅ 416 | bob: (carol NOM X=[salads], Y=[indian]) -> (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) 417 | inez: (carol NOM X=[salads], Y=[indian]) -> ∅ 418 | hank: (bob NOM X=[], Y=[indian]) -> ∅ 419 | inez: (alice NOM X=[burritos], Y=[indian]) -> ∅ 420 | dave: (alice NOM X=[burritos], Y=[indian]) -> ∅ 421 | hank: (carol NOM X=[salads], Y=[indian]) -> ∅ 422 | gwen: (dave NOM X=[salads], Y=[indian]) -> ∅ 423 | john: (dave NOM X=[indian salads], Y=[]) -> ∅ 424 | elsie: (carol NOM X=[salads], Y=[indian]) -> ∅ 425 | gwen: (bob NOM X=[], Y=[indian]) -> ∅ 426 | fred: (bob NOM X=[], Y=[indian]) -> ∅ 427 | inez: (fred NOM X=[salads], Y=[indian]) -> ∅ 428 | dave: (fred NOM X=[salads], Y=[indian]) -> ∅ 429 | dave: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 430 | alice: (carol NOM X=[salads], Y=[indian]) -> ∅ 431 | gwen: (carol NOM X=[salads], Y=[indian]) -> (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) 432 | bob: (alice NOM X=[burritos], Y=[indian]) -> ∅ 433 | carol: (bob NOM X=[], Y=[indian]) -> (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) 434 | carol: (alice NOM X=[burritos], Y=[indian]) -> ∅ 435 | inez: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 436 | hank: (alice NOM X=[burritos], Y=[indian]) -> ∅ 437 | gwen: (alice NOM X=[burritos], Y=[indian]) -> ∅ 438 | fred: (carol NOM X=[salads], Y=[indian]) -> (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) 439 | gwen: (fred NOM X=[salads], Y=[indian]) -> ∅ 440 | john: (dave NOM X=[salads], Y=[indian]) -> ∅ 441 | elsie: (alice NOM X=[burritos], Y=[indian]) -> ∅ 442 | inez: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 443 | alice: (fred NOM X=[salads], Y=[indian]) -> ∅ 444 | carol: (fred NOM X=[salads], Y=[indian]) -> ∅ 445 | carol: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 446 | fred: (alice NOM X=[burritos], Y=[indian]) -> ∅ 447 | carol: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 448 | inez: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 449 | dave: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 450 | ``` 451 | 452 | Dave sees that Alice is voting to prepare <1,indian>. 453 | This will be important in a moment. 454 | 455 | ``` 456 | fred: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 457 | alice: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 458 | bob: (fred NOM X=[salads], Y=[indian]) -> ∅ 459 | fred: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 460 | alice: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 461 | hank: (fred NOM X=[salads], Y=[indian]) -> ∅ 462 | gwen: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 463 | elsie: (fred NOM X=[salads], Y=[indian]) -> ∅ 464 | john: (bob NOM X=[], Y=[indian]) -> ∅ 465 | hank: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 466 | elsie: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 467 | inez: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 468 | hank: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 469 | ``` 470 | 471 | Nomination and balloting both continue. 472 | As it happens in this example, 473 | all prepare votes are for the same ballot, <1,indian>. 474 | But it’s possible to have competing prepare votes on differing ballots. 475 | 476 | ``` 477 | dave: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) 478 | ``` 479 | 480 | Dave sees that Bob is voting to prepare <1,indian>. 481 | He too is voting to prepare <1,indian>, 482 | and he previously saw Alice vote the same way. 483 | Alice-Bob-Dave is one of Dave’s quorums, 484 | and a quorum all voting the same way means Dave can now _accept_ that <1,indian> is prepared. 485 | Dave sets P to the value of the highest accepted-prepared ballot. 486 | 487 | ``` 488 | carol: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) 489 | ``` 490 | 491 | Carol follows suit upon seeing Alice’s prepare vote. 492 | 493 | ``` 494 | elsie: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 495 | fred: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 496 | bob: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 497 | alice: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) 498 | john: (carol NOM X=[salads], Y=[indian]) -> ∅ 499 | gwen: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 500 | dave: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 501 | elsie: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 502 | inez: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 503 | carol: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 504 | hank: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 505 | inez: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 506 | bob: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 507 | hank: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 508 | fred: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) 509 | inez: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 510 | john: (alice NOM X=[burritos], Y=[indian]) -> ∅ 511 | alice: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 512 | carol: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 513 | bob: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) 514 | john: (fred NOM X=[salads], Y=[indian]) -> ∅ 515 | gwen: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) 516 | hank: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 517 | alice: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 518 | john: (gwen NOM X=[pasta salads], Y=[indian]) -> ∅ 519 | dave: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 520 | elsie: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 521 | fred: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 522 | dave: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 523 | gwen: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 524 | carol: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 525 | carol: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 526 | john: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 527 | fred: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 528 | inez: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 529 | alice: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 530 | hank: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 531 | bob: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 532 | hank: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 533 | fred: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 534 | fred: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 535 | elsie: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 536 | dave: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 537 | ``` 538 | 539 | Nomination and balloting both continue. 540 | 541 | ``` 542 | carol: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 543 | ``` 544 | 545 | Carol sees that Alice accepts ballot <1,indian> as prepared. 546 | Carol does too, 547 | and earlier she saw Dave accept the same thing. 548 | This makes a quorum all accepting the same ballot, 549 | which means <1,indian> is now _confirmed_ prepared. 550 | 551 | Carol sets CN and HN to the counters in the lowest and highest confirmed-prepared ballots. 552 | Carol could also in theory have continued to accept new candidates from the nomination phase before now, 553 | but with a confirmed-prepared ballot she no longer can. 554 | 555 | By setting CN and HN, 556 | Carol not only notifies her peers that she confirms <1,indian> is prepared, 557 | but also votes to _commit_ to <1,indian>. 558 | 559 | Once a commit vote can be confirmed, 560 | a node considers consensus to be achieved and the value in the ballot can be _externalized_ (acted upon). 561 | 562 | ``` 563 | john: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 564 | hank: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 565 | alice: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 566 | bob: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 567 | gwen: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 568 | elsie: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 569 | dave: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 570 | fred: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 571 | bob: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 572 | elsie: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 573 | inez: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 574 | carol: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 575 | hank: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 576 | john: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 577 | alice: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 578 | dave: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 579 | gwen: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 580 | hank: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 581 | elsie: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 582 | hank: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 583 | fred: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 584 | hank: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 585 | elsie: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 586 | elsie: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 587 | elsie: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 588 | bob: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 589 | dave: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 590 | alice: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 591 | hank: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 592 | inez: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 593 | carol: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 594 | john: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 595 | dave: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 596 | carol: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 597 | gwen: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 598 | hank: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 599 | fred: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 600 | bob: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 601 | carol: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 602 | hank: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 603 | carol: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 604 | elsie: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 605 | gwen: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 606 | gwen: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 607 | dave: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 608 | inez: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 609 | alice: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 610 | john: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 611 | inez: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 612 | bob: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 613 | dave: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 614 | hank: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 615 | hank: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 616 | hank: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 617 | hank: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 618 | hank: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 619 | hank: (inez NOM X=[pizza], Y=[]) -> ∅ 620 | hank: (john NOM X=[sandwiches], Y=[]) -> ∅ 621 | hank: (elsie NOM X=[burgers], Y=[]) -> ∅ 622 | hank: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 623 | hank: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 624 | fred: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 625 | gwen: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 626 | john: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<> PP=<> CN=0 HN=0) -> ∅ 627 | elsie: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 628 | elsie: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 629 | inez: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 630 | gwen: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 631 | ``` 632 | 633 | More nomination and balloting, more accepting and confirming of prepare votes. 634 | 635 | ``` 636 | carol: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) 637 | ``` 638 | 639 | Carol has now seen a quorum all confirming preparation of the same ballot. (Alice-Carol-Dave.) 640 | Carol now _accepts_ that ballot is committed. 641 | 642 | ``` 643 | john: (dave NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 644 | hank: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) 645 | carol: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 646 | alice: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 647 | alice: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 648 | john: (carol NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 649 | fred: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 650 | bob: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 651 | dave: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) 652 | gwen: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 653 | bob: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 654 | elsie: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 655 | inez: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 656 | john: (alice NOM/PREP X=[burritos], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 657 | inez: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 658 | john: (fred NOM/PREP X=[salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 659 | hank: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 660 | elsie: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 661 | alice: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) 662 | carol: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 663 | john: (bob NOM/PREP X=[], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 664 | dave: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 665 | bob: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 666 | fred: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (fred COMMIT B=<1,indian> PN=1 CN=1 HN=1) 667 | hank: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 668 | bob: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (bob COMMIT B=<1,indian> PN=1 CN=1 HN=1) 669 | inez: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 670 | fred: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 671 | alice: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 672 | inez: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 673 | gwen: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 674 | elsie: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (elsie COMMIT B=<1,indian> PN=1 CN=1 HN=1) 675 | carol: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 676 | dave: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 677 | elsie: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 678 | elsie: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 679 | john: (gwen NOM/PREP X=[pasta salads], Y=[indian] B=<1,indian> P=<1,indian> PP=<> CN=0 HN=0) -> ∅ 680 | alice: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 681 | fred: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 682 | bob: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 683 | hank: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 684 | dave: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 685 | bob: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 686 | elsie: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 687 | inez: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (inez PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 688 | gwen: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (gwen COMMIT B=<1,indian> PN=1 CN=1 HN=1) 689 | john: (carol PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 690 | carol: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 691 | hank: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 692 | bob: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 693 | bob: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 694 | fred: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 695 | inez: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (inez COMMIT B=<1,indian> PN=1 CN=1 HN=1) 696 | dave: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 697 | alice: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 698 | john: (dave PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 699 | dave: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 700 | hank: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 701 | gwen: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 702 | elsie: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 703 | fred: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 704 | carol: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 705 | alice: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 706 | ``` 707 | 708 | More nodes confirm that <1,indian> is prepared. 709 | Some nodes accept that <1,indian> is committed. 710 | Other nodes are still catching up. 711 | 712 | ``` 713 | hank: (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (hank EXT C=<1,indian> HN=1) 714 | ``` 715 | 716 | Hank has now seen Alice, Carol, and Dave all accept that <1,indian> is committed. 717 | He is the first to confirm the ballot is committed and so can externalize the value “indian.” 718 | 719 | ``` 720 | inez: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 721 | dave: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 722 | bob: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 723 | elsie: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 724 | gwen: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 725 | inez: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 726 | john: (fred PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 727 | alice: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 728 | alice: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 729 | dave: (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (dave EXT C=<1,indian> HN=1) 730 | alice: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 731 | fred: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 732 | carol: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 733 | alice: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (alice EXT C=<1,indian> HN=1) 734 | gwen: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 735 | john: (alice PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 736 | bob: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 737 | inez: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 738 | gwen: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 739 | elsie: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 740 | gwen: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 741 | carol: (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (carol EXT C=<1,indian> HN=1) 742 | gwen: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 743 | bob: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 744 | fred: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 745 | elsie: (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (elsie EXT C=<1,indian> HN=1) 746 | fred: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 747 | gwen: (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (gwen EXT C=<1,indian> HN=1) 748 | bob: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (bob EXT C=<1,indian> HN=1) 749 | inez: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 750 | john: (hank PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (john PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) 751 | inez: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 752 | inez: (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 753 | inez: (fred COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (inez EXT C=<1,indian> HN=1) 754 | fred: (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (fred EXT C=<1,indian> HN=1) 755 | john: (elsie PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> (john COMMIT B=<1,indian> PN=1 CN=1 HN=1) 756 | john: (bob PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 757 | john: (gwen PREP B=<1,indian> P=<1,indian> PP=<> CN=1 HN=1) -> ∅ 758 | john: (carol COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 759 | john: (hank COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 760 | john: (dave COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 761 | john: (alice COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> ∅ 762 | john: (fred COMMIT B=<1,indian> PN=1 CN=1 HN=1) -> (john EXT C=<1,indian> HN=1) 763 | ``` 764 | 765 | The rest of the network catches up. By the end, everyone has externalized “indian.” 766 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # SCP - A standalone implementation of the Stellar Consensus Protocol 2 | 3 | This is an implementation in Go of SCP, 4 | the Stellar Consensus Protocol. 5 | It allows a leaderless network of participating nodes to reach consensus on proposals. 6 | 7 | In an SCP network, 8 | time is divided into consecutive _slots_, 9 | each of which produces consensus on a single proposal. 10 | In the early stages of a slot, 11 | nodes are able to nominate proposals they would like the network to adopt. 12 | As the slot progresses, 13 | some proposals may be discarded, 14 | others may be combined, 15 | and by the end, 16 | all nodes agree on one. 17 | After that, 18 | a new slot begins. 19 | 20 | In this implementation, 21 | proposals are represented by the abstract type `Value`. 22 | Concrete instantiations of this type may include any data that can be serialized and totally ordered. 23 | It must also be possible to write a deterministic, 24 | commutative `Combine` operation 25 | (reducing two `Values` to a single one). 26 | 27 | A toy demo can be found in 28 | [cmd/lunch](https://github.com/bobg/scp/tree/master/cmd/lunch). 29 | It takes the name of a TOML file as an argument. 30 | The TOML file specifies the network participants and topology. 31 | Sample TOML files are in 32 | [cmd/lunch/toml](https://github.com/bobg/scp/tree/master/cmd/lunch/toml). 33 | -------------------------------------------------------------------------------- /ballot.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Ballot is an SCP ballot. 8 | type Ballot struct { 9 | N int 10 | X Value 11 | } 12 | 13 | // ZeroBallot is the zero ballot. 14 | var ZeroBallot Ballot 15 | 16 | // IsZero tells whether b is the zero ballot. 17 | func (b Ballot) IsZero() bool { 18 | return b.N == 0 && isNilVal(b.X) 19 | } 20 | 21 | // Less tells whether a ballot is less than another. 22 | func (b Ballot) Less(other Ballot) bool { 23 | if b.N < other.N { 24 | return true 25 | } 26 | if b.N > other.N { 27 | return false 28 | } 29 | if isNilVal(b.X) { 30 | return !isNilVal(other.X) 31 | } 32 | if isNilVal(other.X) { 33 | return false 34 | } 35 | return b.X.Less(other.X) 36 | } 37 | 38 | // Equal tells whether a ballot is equal to another. 39 | func (b Ballot) Equal(other Ballot) bool { 40 | return b.N == other.N && ValueEqual(b.X, other.X) 41 | } 42 | 43 | // String produces a readable representation of a ballot. 44 | func (b Ballot) String() string { 45 | if b.IsZero() { 46 | return "<>" 47 | } 48 | return fmt.Sprintf("<%d,%s>", b.N, VString(b.X)) 49 | } 50 | -------------------------------------------------------------------------------- /ballot_test.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestBallotSetAdd(t *testing.T) { 10 | cases := []struct { 11 | s []Ballot 12 | b Ballot 13 | want []Ballot 14 | }{ 15 | { 16 | s: []Ballot{}, 17 | b: Ballot{1, valtype(1)}, 18 | want: []Ballot{{1, valtype(1)}}, 19 | }, 20 | { 21 | s: []Ballot{{1, valtype(1)}}, 22 | b: Ballot{1, valtype(1)}, 23 | want: []Ballot{{1, valtype(1)}}, 24 | }, 25 | { 26 | s: []Ballot{{1, valtype(1)}, {1, valtype(2)}, {1, valtype(3)}}, 27 | b: Ballot{1, valtype(0)}, 28 | want: []Ballot{{1, valtype(0)}, {1, valtype(1)}, {1, valtype(2)}, {1, valtype(3)}}, 29 | }, 30 | { 31 | s: []Ballot{{1, valtype(1)}, {1, valtype(2)}, {1, valtype(3)}}, 32 | b: Ballot{1, valtype(4)}, 33 | want: []Ballot{{1, valtype(1)}, {1, valtype(2)}, {1, valtype(3)}, {1, valtype(4)}}, 34 | }, 35 | { 36 | s: []Ballot{{1, valtype(1)}, {1, valtype(3)}}, 37 | b: Ballot{1, valtype(2)}, 38 | want: []Ballot{{1, valtype(1)}, {1, valtype(2)}, {1, valtype(3)}}, 39 | }, 40 | } 41 | for i, tc := range cases { 42 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 43 | got := BallotSet(tc.s).Add(tc.b) 44 | if !reflect.DeepEqual(got, BallotSet(tc.want)) { 45 | t.Errorf("got %v, want %v", got, tc.want) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // Commands for the Node goroutine. 9 | 10 | type Cmd interface{} 11 | 12 | type msgCmd struct { 13 | msg *Msg 14 | } 15 | 16 | type deferredUpdateCmd struct { 17 | slot *Slot 18 | } 19 | 20 | type newRoundCmd struct { 21 | slot *Slot 22 | } 23 | 24 | type rehandleCmd struct { 25 | slot *Slot 26 | } 27 | 28 | type delayCmd struct { 29 | ms int 30 | } 31 | 32 | // Internal channel for queueing and processing commands. 33 | 34 | type cmdChan struct { 35 | cond sync.Cond 36 | cmds []Cmd 37 | } 38 | 39 | func newCmdChan() *cmdChan { 40 | result := new(cmdChan) 41 | result.cond.L = new(sync.Mutex) 42 | return result 43 | } 44 | 45 | func (c *cmdChan) write(cmd Cmd) { 46 | c.cond.L.Lock() 47 | c.cmds = append(c.cmds, cmd) 48 | c.cond.Broadcast() 49 | c.cond.L.Unlock() 50 | } 51 | 52 | func (c *cmdChan) read(ctx context.Context) (Cmd, bool) { 53 | c.cond.L.Lock() 54 | defer c.cond.L.Unlock() 55 | 56 | for len(c.cmds) == 0 { 57 | ch := make(chan struct{}) 58 | go func() { 59 | c.cond.Wait() 60 | close(ch) 61 | }() 62 | 63 | select { 64 | case <-ctx.Done(): 65 | return nil, false 66 | 67 | case <-ch: 68 | if len(c.cmds) == 0 { 69 | continue 70 | } 71 | } 72 | } 73 | result := c.cmds[0] 74 | c.cmds = c.cmds[1:] 75 | return result, true 76 | } 77 | -------------------------------------------------------------------------------- /cmd/lunch/lunch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Usage: 4 | // lunch [-seed N] CONFIGFILE 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/binary" 10 | "flag" 11 | "io/ioutil" 12 | "log" 13 | "math/rand" 14 | 15 | "github.com/BurntSushi/toml" 16 | "github.com/bobg/scp" 17 | ) 18 | 19 | type valType string 20 | 21 | func (v valType) Less(other scp.Value) bool { 22 | return v < other.(valType) 23 | } 24 | 25 | func (v valType) Combine(other scp.Value, slotID scp.SlotID) scp.Value { 26 | if slotID%2 == 0 { 27 | if v > other.(valType) { 28 | return v 29 | } 30 | } else if v < other.(valType) { 31 | return v 32 | } 33 | return other 34 | } 35 | 36 | func (v valType) IsNil() bool { 37 | return v == "" 38 | } 39 | 40 | func (v valType) Bytes() []byte { 41 | buf := new(bytes.Buffer) 42 | binary.Write(buf, binary.BigEndian, v) 43 | return buf.Bytes() 44 | } 45 | 46 | func (v valType) String() string { 47 | return string(v) 48 | } 49 | 50 | type nodeconf struct { 51 | Q scp.QSet 52 | FP int 53 | FQ int 54 | } 55 | 56 | func main() { 57 | seed := flag.Int64("seed", 1, "RNG seed") 58 | delay := flag.Int("delay", 100, "random delay limit in milliseconds") 59 | flag.Parse() 60 | rand.Seed(*seed) 61 | 62 | if flag.NArg() < 1 { 63 | log.Fatal("usage: lunch [-seed N] CONFFILE") 64 | } 65 | confFile := flag.Arg(0) 66 | confBits, err := ioutil.ReadFile(confFile) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | var conf map[string]nodeconf 71 | _, err = toml.Decode(string(confBits), &conf) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | nodes := make(map[scp.NodeID]*scp.Node) 77 | ch := make(chan *scp.Msg) 78 | for nodeID, nconf := range conf { 79 | node := scp.NewNode(scp.NodeID(nodeID), nconf.Q, ch, nil) 80 | node.FP, node.FQ = nconf.FP, nconf.FQ 81 | nodes[node.ID] = node 82 | go node.Run(context.Background()) 83 | } 84 | 85 | for slotID := scp.SlotID(1); ; slotID++ { 86 | msgs := make(map[scp.NodeID]*scp.Msg) // holds the latest message seen from each node 87 | 88 | for _, node := range nodes { 89 | msgs[node.ID] = nil 90 | 91 | // New slot! Nominate something. 92 | val := foods[rand.Intn(len(foods))] 93 | nomMsg := scp.NewMsg(node.ID, slotID, node.Q, &scp.NomTopic{X: scp.ValueSet{val}}) 94 | node.Handle(nomMsg) 95 | } 96 | 97 | for msg := range ch { 98 | if msg.I < slotID { 99 | // discard messages about old slots 100 | continue 101 | } 102 | msgs[msg.V] = msg 103 | allExt := true 104 | for _, m := range msgs { 105 | if m == nil { 106 | allExt = false 107 | break 108 | } 109 | if _, ok := m.T.(*scp.ExtTopic); !ok { 110 | allExt = false 111 | break 112 | } 113 | } 114 | if allExt { 115 | log.Print("all externalized") 116 | break 117 | } 118 | for otherNodeID, otherNode := range nodes { 119 | if otherNodeID == msg.V { 120 | continue 121 | } 122 | if *delay > 0 { 123 | otherNode.Delay(rand.Intn(*delay)) 124 | } 125 | otherNode.Handle(msg) 126 | } 127 | } 128 | } 129 | } 130 | 131 | var foods = []valType{ 132 | "burgers", 133 | "burritos", 134 | "gyros", 135 | "indian", 136 | "pasta", 137 | "pizza", 138 | "salads", 139 | "sandwiches", 140 | "soup", 141 | "sushi", 142 | } 143 | -------------------------------------------------------------------------------- /cmd/lunch/toml/3of4.toml: -------------------------------------------------------------------------------- 1 | [alice] 2 | Q = {t = 2, m = [{n = "bob"}, {n = "carol"}, {n = "dave"}]} 3 | 4 | [bob] 5 | Q = {t = 2, m = [{n = "alice"}, {n = "carol"}, {n = "dave"}]} 6 | 7 | [carol] 8 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}, {n = "dave"}]} 9 | 10 | [dave] 11 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}, {n = "carol"}]} 12 | -------------------------------------------------------------------------------- /cmd/lunch/toml/3tiers.toml: -------------------------------------------------------------------------------- 1 | [alice] 2 | Q = {t = 2, m = [{n = "bob"}, {n = "carol"}, {n = "dave"}]} 3 | 4 | [bob] 5 | Q = {t = 2, m = [{n = "alice"}, {n = "carol"}, {n = "dave"}]} 6 | 7 | [carol] 8 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}, {n = "dave"}]} 9 | 10 | [dave] 11 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}, {n = "carol"}]} 12 | 13 | [elsie] 14 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}, {n = "carol"}, {n = "dave"}]} 15 | 16 | [fred] 17 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}, {n = "carol"}, {n = "dave"}]} 18 | 19 | [gwen] 20 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}, {n = "carol"}, {n = "dave"}]} 21 | 22 | [hank] 23 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}, {n = "carol"}, {n = "dave"}]} 24 | 25 | [inez] 26 | Q = {t = 2, m = [{n = "elsie"}, {n = "fred"}, {n = "gwen"}, {n = "hank"}]} 27 | 28 | [john] 29 | Q = {t = 2, m = [{n = "elsie"}, {n = "fred"}, {n = "gwen"}, {n = "hank"}]} 30 | -------------------------------------------------------------------------------- /cmd/lunch/toml/aliceinthemiddle.toml: -------------------------------------------------------------------------------- 1 | [alice] 2 | Q = {t = 1, m = [{q = {t = 2, m = [{n = "bob"}, {n = "carol"}]}}, 3 | {q = {t = 2, m = [{n = "dave"}, {n = "elsie"}]}}]} 4 | 5 | [bob] 6 | Q = {t = 2, m = [{n = "alice"}, {n = "carol"}]} 7 | 8 | [carol] 9 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}]} 10 | 11 | [dave] 12 | Q = {t = 2, m = [{n = "alice"}, {n = "elsie"}]} 13 | 14 | [elsie] 15 | Q = {t = 2, m = [{n = "alice"}, {n = "dave"}]} 16 | -------------------------------------------------------------------------------- /cmd/lunch/toml/cycle.toml: -------------------------------------------------------------------------------- 1 | [alice] 2 | Q = {t = 1, m = [{n = "bob"}]} 3 | 4 | [bob] 5 | Q = {t = 1, m = [{n = "carol"}]} 6 | 7 | [carol] 8 | Q = {t = 1, m = [{n = "alice"}]} 9 | -------------------------------------------------------------------------------- /cmd/lunch/toml/fig3.toml: -------------------------------------------------------------------------------- 1 | [alice] 2 | Q = {t = 2, m = [{n = "bob"}, {n = "carol"}]} 3 | 4 | [bob] 5 | Q = {t = 2, m = [{n = "alice"}, {n = "carol"}]} 6 | 7 | [carol] 8 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}]} 9 | 10 | [dave] 11 | Q = {t = 2, m = [{n = "bob"}, {n = "carol"}]} 12 | -------------------------------------------------------------------------------- /cmd/lunch/toml/simple.toml: -------------------------------------------------------------------------------- 1 | [alice] 2 | Q = {t = 2, m = [{n = "bob"}, {n = "carol"}]} 3 | 4 | [bob] 5 | Q = {t = 2, m = [{n = "alice"}, {n = "carol"}]} 6 | 7 | [carol] 8 | Q = {t = 2, m = [{n = "alice"}, {n = "bob"}]} 9 | -------------------------------------------------------------------------------- /cmd/lunch/toml/stars.toml: -------------------------------------------------------------------------------- 1 | ["Michelle Obama"] 2 | Q = {t = 2, m = [{q = {t = 6, m = [{n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Hugh Jackman"}, {n = "Eva Longoria"}, {n = "Fred Penner"}, {n = "Cameron Diaz"}, {n = "Ricky Gervais"}, {n = "Anna Faris"}, {n = "Joe Namath"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 3 | 4 | ["Eric Idle"] 5 | Q = {t = 2, m = [{q = {t = 6, m = [{n = "Tom Hanks"}, {n = "Rick Moranis"}, {n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Hugh Jackman"}, {n = "Cameron Diaz"}, {n = "Ricky Gervais"}, {n = "Anna Faris"}, {n = "Joe Namath"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 6 | 7 | ["Christopher Reeve"] 8 | Q = {t = 4, m = [{n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 9 | 10 | ["Anna Faris"] 11 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 12 | 13 | ["Michael Landon"] 14 | Q = {t = 4, m = [{n = "Jessica Alba"}, {n = "Korina Sanchez"}, {n = "Anna Faris"}, {n = "Celine Dion"}]} 15 | 16 | ["Jason Bateman"] 17 | Q = {t = 2, m = [{q = {t = 6, m = [{n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Hugh Jackman"}, {n = "Eva Longoria"}, {n = "Fred Penner"}, {n = "Cameron Diaz"}, {n = "Ricky Gervais"}, {n = "Anna Faris"}, {n = "Joe Namath"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 18 | 19 | ["Marisa Tomei"] 20 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 3, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}]}}]} 21 | 22 | ["Richard Kind"] 23 | Q = {t = 6, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Mel Gibson"}, {n = "Michael Jackson"}]} 24 | 25 | ["Ricky Gervais"] 26 | Q = {t = 6, m = [{n = "Michael Jackson"}, {n = "Anna Faris"}, {n = "Bill Irwin"}, {n = "Amy Poehler"}, {n = "Zac Efron"}, {n = "Richard Kind"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Fran Drescher"}]} 27 | 28 | ["Korina Sanchez"] 29 | Q = {t = 4, m = [{n = "Jessica Alba"}, {n = "Michael Landon"}, {n = "Anna Faris"}, {n = "Cab Calloway"}]} 30 | 31 | ["Mel Gibson"] 32 | Q = {t = 2, m = [{q = {t = 2, m = [{n = "Anna Faris"}, {n = "Joe Namath"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 33 | 34 | ["Celine Dion"] 35 | Q = {t = 2, m = [{n = "Amy Poehler"}, {n = "Zac Efron"}, {n = "Anna Faris"}]} 36 | 37 | ["Buzz Aldrin"] 38 | Q = {t = 6, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 39 | 40 | ["Cameron Diaz"] 41 | Q = {t = 6, m = [{n = "Michael Jackson"}, {n = "Anna Faris"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Bill Irwin"}, {n = "Barbara Eden"}, {n = "Joe Namath"}, {n = "Hugh Jackman"}, {n = "Mel Gibson"}]} 42 | 43 | ["Zachary Quinto"] 44 | Q = {t = 5, m = [{n = "Howie Mandel"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 45 | 46 | ["Adam Sandler"] 47 | Q = {t = 9, m = [{n = "Kelly Ripa"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Barbara Eden"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {n = "Tom Hanks"}, {n = "Eric Idle"}, {n = "Mel Gibson"}]} 48 | 49 | ["Zac Efron"] 50 | Q = {t = 2, m = [{n = "Amy Poehler"}, {n = "Celine Dion"}]} 51 | 52 | ["Howie Mandel"] 53 | Q = {t = 5, m = [{n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 54 | 55 | ["Fran Drescher"] 56 | Q = {t = 2, m = [{q = {t = 2, m = [{n = "Anna Faris"}, {n = "Joe Namath"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 57 | 58 | ["Eva Longoria"] 59 | Q = {t = 5, m = [{n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 60 | 61 | ["Jessica Alba"] 62 | Q = {t = 4, m = [{n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 63 | 64 | ["Donald Faison"] 65 | Q = {t = 6, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 66 | 67 | ["Mindy Kaling"] 68 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 6, m = [{n = "Alec Baldwin"}, {n = "John Candy"}, {n = "Peter Ustinov"}, {n = "Rick Moranis"}, {n = "Christopher Lloyd"}, {n = "Barbara Eden"}, {n = "Brad Garrett"}, {n = "Cameron Diaz"}, {n = "Michael Jackson"}]}}]} 69 | 70 | ["Andrea Martin"] 71 | Q = {t = 2, m = [{q = {t = 6, m = [{n = "Fran Drescher"}, {n = "Tom Hanks"}, {n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Hugh Jackman"}, {n = "Cameron Diaz"}, {n = "Ricky Gervais"}, {n = "Anna Faris"}, {n = "Joe Namath"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 72 | 73 | ["Charlize Theron"] 74 | Q = {t = 6, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 75 | 76 | ["Fred Penner"] 77 | Q = {t = 5, m = [{n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 78 | 79 | ["Jim Parsons"] 80 | Q = {t = 4, m = [{n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 81 | 82 | ["Tom Hanks"] 83 | Q = {t = 4, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {n = "Mel Gibson"}, {n = "Eric Idle"}, {n = "Fran Drescher"}]} 84 | 85 | ["Sandra Oh"] 86 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 3, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}]}}]} 87 | 88 | ["John Candy"] 89 | Q = {t = 4, m = [{n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Hugh Jackman"}, {q = {t = 5, m = [{n = "Michelle Obama"}, {n = "Andrea Martin"}, {n = "Mel Gibson"}, {n = "Eric Idle"}, {n = "Rick Moranis"}, {n = "Dennis Quaid"}, {n = "Jason Bateman"}, {n = "Tom Hanks"}, {n = "Fran Drescher"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 90 | 91 | ["Liam Neeson"] 92 | Q = {t = 6, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 93 | 94 | ["Barbara Eden"] 95 | Q = {t = 6, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 96 | 97 | ["Dennis Quaid"] 98 | Q = {t = 2, m = [{q = {t = 6, m = [{n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Hugh Jackman"}, {n = "Eva Longoria"}, {n = "Fred Penner"}, {n = "Cameron Diaz"}, {n = "Ricky Gervais"}, {n = "Anna Faris"}, {n = "Joe Namath"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 99 | 100 | ["Hugh Jackman"] 101 | Q = {t = 7, m = [{n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Anna Faris"}, {n = "Barbara Eden"}, {n = "Joe Namath"}, {n = "Michael Jackson"}, {q = {t = 3, m = [{n = "Amy Adams"}, {n = "Eva Longoria"}, {n = "Fred Penner"}, {n = "Roger Ebert"}, {n = "Howie Mandel"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}, {q = {t = 5, m = [{n = "Michelle Obama"}, {n = "Andrea Martin"}, {n = "Mel Gibson"}, {n = "Eric Idle"}, {n = "Rick Moranis"}, {n = "Dennis Quaid"}, {n = "Jason Bateman"}, {n = "Tom Hanks"}, {n = "Fran Drescher"}]}}]} 102 | 103 | ["Paul Reubens"] 104 | Q = {t = 4, m = [{n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 105 | 106 | ["Anne Hathaway"] 107 | Q = {t = 6, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 108 | 109 | ["Jimmy Fallon"] 110 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 3, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}]}}]} 111 | 112 | ["Amy Adams"] 113 | Q = {t = 5, m = [{n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 114 | 115 | ["Andy Samberg"] 116 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 3, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}]}}]} 117 | 118 | ["Joe Namath"] 119 | Q = {t = 4, m = [{n = "Zac Efron"}, {n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Ryan Reynolds"}]} 120 | 121 | ["Roger Ebert"] 122 | Q = {t = 5, m = [{n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 123 | 124 | ["Allison Janney"] 125 | Q = {t = 6, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 126 | 127 | ["Amy Poehler"] 128 | Q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}]} 129 | 130 | ["Jeremy Irons"] 131 | Q = {t = 4, m = [{n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 132 | 133 | ["Jim Carrey"] 134 | Q = {t = 8, m = [{n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Tony Hawk"}, {n = "Ralph Nader"}, {n = "Michael Jackson"}, {n = "Kristen Bell"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 135 | 136 | ["Christopher Lloyd"] 137 | Q = {t = 7, m = [{n = "Fred Penner"}, {n = "Anna Faris"}, {n = "Eric Idle"}, {n = "Tom Hanks"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Michael Jackson"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 138 | 139 | ["Ryan Reynolds"] 140 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {n = "Anna Faris"}]} 141 | 142 | ["James Taylor"] 143 | Q = {t = 4, m = [{n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 144 | 145 | ["Donny Osmond"] 146 | Q = {t = 5, m = [{n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 3, m = [{n = "Marisa Tomei"}, {n = "Andy Samberg"}, {n = "Jimmy Fallon"}]}}]} 147 | 148 | ["Tracey Ullman"] 149 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 3, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}]}}]} 150 | 151 | ["Michael Jackson"] 152 | Q = {t = 7, m = [{n = "Allison Janney"}, {n = "Anna Faris"}, {n = "Mindy Kaling"}, {n = "Peter Ustinov"}, {n = "Brad Garrett"}, {q = {t = 2, m = [{n = "Cameron Diaz"}, {n = "Ricky Gervais"}]}}, {q = {t = 3, m = [{n = "Amy Adams"}, {n = "Eva Longoria"}, {n = "Fred Penner"}, {n = "Roger Ebert"}, {n = "Howie Mandel"}]}}, {q = {t = 2, m = [{n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Hugh Jackman"}]}}, {q = {t = 5, m = [{n = "Michelle Obama"}, {n = "Andrea Martin"}, {n = "Mel Gibson"}, {n = "Dennis Quaid"}, {n = "Jason Bateman"}, {n = "Rick Moranis"}, {n = "Eric Idle"}, {n = "Tom Hanks"}, {n = "Fran Drescher"}]}}, {q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {n = "Barbara Eden"}, {n = "Joe Namath"}]}}]} 153 | 154 | ["Larry King"] 155 | Q = {t = 4, m = [{n = "Anna Faris"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 156 | 157 | ["Kelly Ripa"] 158 | Q = {t = 4, m = [{n = "Joe Namath"}, {n = "Ryan Reynolds"}, {n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {n = "Adam Sandler"}]} 159 | 160 | ["Brad Garrett"] 161 | Q = {t = 4, m = [{n = "Celine Dion"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]} 162 | 163 | ["Alec Baldwin"] 164 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 6, m = [{n = "Mindy Kaling"}, {n = "Allison Janney"}, {n = "Adam Sandler"}, {n = "Jason Bateman"}, {n = "Richard Kind"}, {n = "Anne Hathaway"}, {n = "Charlize Theron"}, {n = "Ricky Gervais"}, {n = "Michael Jackson"}]}}]} 165 | 166 | ["Rick Moranis"] 167 | Q = {t = 2, m = [{q = {t = 6, m = [{n = "Tom Hanks"}, {n = "Eric Idle"}, {n = "Ryan Reynolds"}, {n = "Bill Irwin"}, {n = "Hugh Jackman"}, {n = "Cameron Diaz"}, {n = "Ricky Gervais"}, {n = "Anna Faris"}, {n = "Joe Namath"}]}}, {q = {t = 2, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}]}}]} 168 | 169 | ["Bill Irwin"] 170 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {n = "Anna Faris"}]} 171 | 172 | ["Zoe Saldana"] 173 | Q = {t = 3, m = [{n = "Celine Dion"}, {n = "Zac Efron"}, {n = "Amy Poehler"}, {q = {t = 3, m = [{n = "Amy Adams"}, {n = "Anna Faris"}, {n = "Joe Namath"}, {n = "Ryan Reynolds"}]}}]} 174 | -------------------------------------------------------------------------------- /cmd/scptxvm/Readme.md: -------------------------------------------------------------------------------- 1 | # scptxvm 2 | 3 | > “Hey, you got your consensus protocol in my blockchain representation!” 4 | > 5 | > “You got your blockchain representation in my consensus protocol!” 6 | 7 | This is scptxvm, an experimental implementation of a decentralized 8 | blockchain using Chain’s TxVM as the transaction and block model, and 9 | the Stellar Consensus Protocol for achieving network consensus. 10 | 11 | The program manages a single SCP node. It is configured with one or 12 | more other nodes as its set of “quorum slices.” Each node’s ID is the 13 | URL at which it listens for inbound SCP messages. Nodes vote on which 14 | block should be chosen for each block height. 15 | 16 | SCP messages contain block IDs only. Each node may request the actual 17 | contents of a block, given its ID, from any other. No node may pass 18 | along a block ID without knowing the block’s contents, in case some 19 | other node asks for it. Nodes also use block contents when combining 20 | two valid proposed blocks into a new block proposal. 21 | 22 | The program also listens for proposed transactions, which are added to 23 | a transaction pool and periodically rolled up into a proposed block. 24 | The ease with which a node can get its pool transactions included in 25 | an adopted block depends heavily on the topology of the network. 26 | -------------------------------------------------------------------------------- /cmd/scptxvm/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/bobg/scp" 15 | "github.com/chain/txvm/protocol/bc" 16 | "github.com/golang/protobuf/proto" 17 | ) 18 | 19 | var nomHeight int32 20 | 21 | func protocolHandler(w http.ResponseWriter, r *http.Request) { 22 | if r.Method != "POST" { 23 | httperr(w, http.StatusBadRequest, "%s not supported", r.Method) 24 | return 25 | } 26 | if r.Body == nil { 27 | httperr(w, http.StatusBadRequest, "missing POST body") 28 | return 29 | } 30 | defer r.Body.Close() 31 | pmsg, err := ioutil.ReadAll(r.Body) 32 | if err != nil { 33 | httperr(w, http.StatusInternalServerError, "could not read POST body: %s", err) 34 | return 35 | } 36 | msg, err := unmarshal(pmsg) 37 | if err != nil { 38 | httperr(w, http.StatusBadRequest, "could not parse POST body: %s", err) 39 | return 40 | } 41 | 42 | msgTimesMu.Lock() 43 | msgTimes[msg.V] = time.Now() 44 | msgTimesMu.Unlock() 45 | 46 | nh := atomic.LoadInt32(&nomHeight) 47 | if int32(msg.I) >= nh { 48 | var bump bool 49 | switch msg.T.(type) { 50 | case *scp.CommitTopic: 51 | bump = true 52 | case *scp.ExtTopic: 53 | bump = true 54 | } 55 | if bump { 56 | // Can no longer nominate for slot nomHeight. 57 | atomic.StoreInt32(&nomHeight, int32(msg.I+1)) 58 | nomChan <- msg.I + 1 59 | } 60 | } 61 | 62 | maybeAdd := func(set scp.ValueSet, val scp.Value) scp.ValueSet { 63 | h := valToHash(val) 64 | if h.IsZero() { 65 | return set 66 | } 67 | return set.Add(val) 68 | } 69 | 70 | // Collect all block IDs mentioned in the new message. 71 | var blockIDs scp.ValueSet 72 | switch topic := msg.T.(type) { 73 | case *scp.NomTopic: 74 | blockIDs = blockIDs.Union(topic.X) 75 | blockIDs = blockIDs.Union(topic.Y) 76 | 77 | case *scp.PrepTopic: 78 | blockIDs = blockIDs.Add(topic.B.X) 79 | blockIDs = maybeAdd(blockIDs, topic.P.X) 80 | blockIDs = maybeAdd(blockIDs, topic.PP.X) 81 | 82 | case *scp.CommitTopic: 83 | blockIDs = maybeAdd(blockIDs, topic.B.X) 84 | 85 | case *scp.ExtTopic: 86 | blockIDs = maybeAdd(blockIDs, topic.C.X) 87 | } 88 | 89 | // Request the contents of any unknown blocks. 90 | req := blocksReq{Height: int(msg.I)} 91 | for _, blockID := range blockIDs { 92 | blockID := bc.Hash(blockID.(valtype)) 93 | have, err := haveBlock(int(msg.I), blockID) 94 | if err != nil { 95 | httperr(w, http.StatusInternalServerError, "could not check for block file: %s", err) 96 | return 97 | } 98 | if have { 99 | continue 100 | } 101 | req.BlockIDs = append(req.BlockIDs, blockID) 102 | } 103 | if len(req.BlockIDs) > 0 { 104 | node.Logf("requesting contents of %d block(s) from %s", len(req.BlockIDs), msg.V) 105 | u, err := url.Parse(string(msg.V)) 106 | if err != nil { 107 | httperr(w, http.StatusBadRequest, "sending node ID (%s) cannot be parsed as a URL: %s", msg.V, err) 108 | return 109 | } 110 | u.Path = "/blocks" 111 | body, err := json.Marshal(req) 112 | if err != nil { 113 | httperr(w, http.StatusInternalServerError, "cannot construct POST body: %s", err) 114 | return 115 | } 116 | req, err := http.NewRequest("POST", u.String(), bytes.NewReader(body)) 117 | if err != nil { 118 | httperr(w, http.StatusInternalServerError, "building POST request: %s", err) 119 | return 120 | } 121 | req = req.WithContext(r.Context()) 122 | req.Header.Set("Content-Type", "application/json") 123 | var c http.Client 124 | resp, err := c.Do(req) 125 | if err != nil { 126 | httperr(w, http.StatusInternalServerError, "requesting block contents: %s", err) 127 | return 128 | } 129 | // xxx check status code and content-type 130 | defer resp.Body.Close() 131 | respBits, err := ioutil.ReadAll(resp.Body) 132 | if err != nil { 133 | httperr(w, http.StatusInternalServerError, "reading response: %s", err) 134 | return 135 | } 136 | var blocks []*bc.Block 137 | err = json.Unmarshal(respBits, &blocks) 138 | if err != nil { 139 | httperr(w, http.StatusInternalServerError, "parsing response: %s", err) 140 | return 141 | } 142 | // xxx check all requested blocks are present 143 | for _, block := range blocks { 144 | err = storeBlock(block) 145 | if err != nil { 146 | httperr(w, http.StatusInternalServerError, "storing block: %s", err) 147 | return 148 | } 149 | } 150 | } 151 | 152 | node.Logf("* sending %s to node.Handle", msg) 153 | node.Handle(msg) 154 | w.WriteHeader(http.StatusNoContent) 155 | } 156 | 157 | func blocksHandler(w http.ResponseWriter, r *http.Request) { 158 | if r.Method != "POST" { 159 | httperr(w, http.StatusBadRequest, "%s not supported", r.Method) 160 | return 161 | } 162 | if r.Body == nil { 163 | httperr(w, http.StatusBadRequest, "missing POST body") 164 | return 165 | } 166 | defer r.Body.Close() 167 | reqBits, err := ioutil.ReadAll(r.Body) 168 | if err != nil { 169 | httperr(w, http.StatusInternalServerError, "reading request: %s", err) 170 | return 171 | } 172 | var req blocksReq 173 | err = json.Unmarshal(reqBits, &req) 174 | if err != nil { 175 | httperr(w, http.StatusBadRequest, "parsing request: %s", err) 176 | return 177 | } 178 | var result []*bc.Block 179 | for _, blockID := range req.BlockIDs { 180 | block, err := getBlock(req.Height, blockID) 181 | if err != nil { 182 | httperr(w, http.StatusNotFound, "could not resolve requested block %s (height %d): %s", blockID.String(), req.Height, err) 183 | return 184 | } 185 | result = append(result, block) 186 | } 187 | respBits, err := json.Marshal(result) 188 | if err != nil { 189 | httperr(w, http.StatusInternalServerError, "could not marshal response: %s", err) 190 | return 191 | } 192 | w.Header().Set("Content-Type", "application/json") 193 | _, err = w.Write(respBits) 194 | if err != nil { 195 | httperr(w, http.StatusInternalServerError, "could not write response: %s", err) 196 | return 197 | } 198 | } 199 | 200 | func submitHandler(w http.ResponseWriter, r *http.Request) { 201 | if r.Method != "POST" { 202 | httperr(w, http.StatusBadRequest, "%s not supported", r.Method) 203 | return 204 | } 205 | if r.Body == nil { 206 | httperr(w, http.StatusBadRequest, "missing POST body") 207 | return 208 | } 209 | defer r.Body.Close() 210 | reqBits, err := ioutil.ReadAll(r.Body) 211 | if err != nil { 212 | httperr(w, http.StatusInternalServerError, "reading request: %s", err) 213 | return 214 | } 215 | var rawtx bc.RawTx 216 | err = proto.Unmarshal(reqBits, &rawtx) 217 | if err != nil { 218 | httperr(w, http.StatusBadRequest, "parsing request: %s", err) 219 | return 220 | } 221 | tx, err := bc.NewTx(rawtx.Program, rawtx.Version, rawtx.Runlimit) 222 | if err != nil { 223 | httperr(w, http.StatusBadRequest, "validating transaction: %s", err) 224 | return 225 | } 226 | nomChan <- tx 227 | node.Logf("accepted tx submission %x", tx.ID.Bytes()) 228 | w.WriteHeader(http.StatusNoContent) 229 | } 230 | 231 | func subscribeHandler(w http.ResponseWriter, r *http.Request) { 232 | subscriber := r.FormValue("subscriber") 233 | maxStr := r.FormValue("max") 234 | max, err := strconv.Atoi(maxStr) 235 | if err != nil { 236 | httperr(w, http.StatusBadRequest, "cannot parse max value: %s", err) 237 | return 238 | } 239 | 240 | subscribersMu.Lock() 241 | subscribers[scp.NodeID(subscriber)] = time.Now() 242 | subscribersMu.Unlock() 243 | 244 | msgs := node.MsgsSince(scp.SlotID(max)) 245 | 246 | node.Logf("new subscriber %s sent max %d, responding with %d message(s)", subscriber, max, len(msgs)) 247 | 248 | var resp []json.RawMessage 249 | for _, msg := range msgs { 250 | bits, err := marshal(msg) 251 | if err != nil { 252 | httperr(w, http.StatusInternalServerError, "marshaling response: %s", err) 253 | return 254 | } 255 | resp = append(resp, bits) 256 | } 257 | respBits, err := json.Marshal(resp) 258 | if err != nil { 259 | httperr(w, http.StatusInternalServerError, "marshaling response: %s", err) 260 | return 261 | } 262 | w.Header().Set("Content-Type", "application/json") 263 | _, err = w.Write(respBits) 264 | if err != nil { 265 | httperr(w, http.StatusInternalServerError, "writing response: %s", err) 266 | return 267 | } 268 | } 269 | 270 | func shutdownHandler(w http.ResponseWriter, r *http.Request) { 271 | srv.Shutdown(bgctx) 272 | bgcancel() 273 | } 274 | 275 | func httperr(w http.ResponseWriter, code int, format string, args ...interface{}) { 276 | msg := fmt.Sprintf(format, args...) 277 | node.Logf("http response %d: %s", code, msg) 278 | http.Error(w, msg, code) 279 | } 280 | -------------------------------------------------------------------------------- /cmd/scptxvm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "path" 14 | "sync" 15 | "time" 16 | 17 | "github.com/BurntSushi/toml" 18 | "github.com/chain/txvm/crypto/ed25519" 19 | "github.com/chain/txvm/protocol" 20 | "github.com/chain/txvm/protocol/bc" 21 | 22 | "github.com/bobg/scp" 23 | ) 24 | 25 | var ( 26 | chain *protocol.Chain 27 | node *scp.Node 28 | prv ed25519.PrivateKey 29 | dir string 30 | srv http.Server 31 | 32 | bgctx context.Context 33 | bgcancel context.CancelFunc 34 | wg sync.WaitGroup 35 | 36 | heightChan = make(chan uint64, 1) 37 | nomChan = make(chan interface{}, 1000) 38 | msgChan = make(chan *scp.Msg, 1000) 39 | 40 | msgTimesMu sync.Mutex 41 | msgTimes = make(map[scp.NodeID]time.Time) 42 | ) 43 | 44 | func main() { 45 | bgctx = context.Background() 46 | bgctx, bgcancel = context.WithCancel(bgctx) 47 | defer bgcancel() 48 | 49 | var ( 50 | confFile string 51 | initialBlockFile string 52 | ) 53 | flag.StringVar(&dir, "dir", ".", "root of working dir") 54 | flag.StringVar(&confFile, "conf", "", "config file (default is conf.toml in root of working dir)") 55 | flag.StringVar(&initialBlockFile, "initial", "", "file containing initial block (default is blocks/1 under working dir)") 56 | 57 | flag.Parse() 58 | 59 | if confFile == "" { 60 | confFile = path.Join(dir, "conf.toml") 61 | } 62 | 63 | confBits, err := ioutil.ReadFile(confFile) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | var conf struct { 69 | Addr string 70 | Prv string 71 | Q scp.QSet 72 | } 73 | _, err = toml.Decode(string(confBits), &conf) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | 78 | if initialBlockFile == "" { 79 | initialBlockFile = path.Join(blockDir(), "1") 80 | } 81 | initialBlockBits, err := ioutil.ReadFile(initialBlockFile) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | var initialBlock bc.Block 86 | err = initialBlock.FromBytes(initialBlockBits) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | heightChan = make(chan uint64) 92 | 93 | chain, err = protocol.NewChain(bgctx, &initialBlock, &pstore{}, heightChan) 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | _, err = chain.Recover(bgctx) 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | 102 | height := chain.Height() 103 | block, err := chain.GetBlock(bgctx, height) 104 | if err != nil { 105 | log.Fatal(err) 106 | } 107 | 108 | prvBits, err := hex.DecodeString(conf.Prv) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | if len(prvBits) != ed25519.PrivateKeySize { 113 | log.Fatalf("prv is %d bytes long, want %d bytes", len(prvBits), ed25519.PrivateKeySize) 114 | } 115 | prv = ed25519.PrivateKey(prvBits) 116 | pubKey := prv.Public().(ed25519.PublicKey) 117 | pubKeyHex := hex.EncodeToString(pubKey) 118 | 119 | // xxx this is ugly! 120 | ext := map[scp.SlotID]*scp.ExtTopic{ 121 | scp.SlotID(chain.Height()): &scp.ExtTopic{ 122 | C: scp.Ballot{ 123 | N: 1, 124 | X: valtype(block.Hash()), 125 | }, 126 | HN: 1, 127 | }, 128 | } 129 | 130 | nodeID := fmt.Sprintf("http://%s/%s", conf.Addr, pubKeyHex) 131 | node = scp.NewNode(scp.NodeID(nodeID), conf.Q, msgChan, ext) 132 | 133 | go func() { 134 | node.Run(bgctx) 135 | wg.Done() 136 | }() 137 | go handleNodeOutput(bgctx) 138 | go nominate(bgctx) 139 | go subscribe(bgctx) 140 | wg.Add(4) 141 | 142 | handle("/"+pubKeyHex, protocolHandler) // scp protocol messages go here 143 | handle("/blocks", blocksHandler) // nodes resolve block ids here 144 | handle("/submit", submitHandler) // new txs get proposed here 145 | handle("/subscribe", subscribeHandler) 146 | handle("/shutdown", shutdownHandler) 147 | 148 | srv.Addr = conf.Addr 149 | node.Logf("node %s listening on %s", node.ID, conf.Addr) 150 | err = srv.ListenAndServe() 151 | node.Logf("ListenAndServe: %s", err) 152 | 153 | wg.Wait() 154 | } 155 | 156 | func handle(path string, handler http.HandlerFunc) { 157 | http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { 158 | node.Logf("handling %s %s", r.Method, path) 159 | handler(w, r) 160 | }) 161 | } 162 | 163 | type blocksReq struct { 164 | Height int 165 | BlockIDs []bc.Hash 166 | } 167 | 168 | func subscribe(ctx context.Context) { 169 | defer wg.Done() 170 | 171 | ticker := time.NewTicker(time.Minute) 172 | 173 | for { 174 | select { 175 | case <-ctx.Done(): 176 | node.Logf("context canceled, exiting subscribe loop") 177 | ticker.Stop() 178 | return 179 | 180 | case <-ticker.C: 181 | // Once per minute, subscribe to other nodes as necessary. 182 | // "Necessary" is: the other node is in the transitive closure of 183 | // this node's quorum slices and we have no message from it in the 184 | // past five minutes. 185 | others := node.AllKnown() 186 | for _, other := range others { 187 | msgTimesMu.Lock() 188 | t, ok := msgTimes[other] 189 | msgTimesMu.Unlock() 190 | 191 | if !ok || time.Since(t) > 5*time.Minute { 192 | node.Logf("refreshing subscription to %s", other) 193 | u, err := url.Parse(string(other)) 194 | if err != nil { 195 | panic(err) // xxx err 196 | } 197 | u.Path = "/subscribe" 198 | u.RawQuery = fmt.Sprintf("subscriber=%s&max=%d", url.QueryEscape(string(node.ID)), node.HighestExt()) 199 | resp, err := http.Get(u.String()) 200 | if err != nil { 201 | node.Logf("ERROR: cannot subscribe to %s: %s", other, err) 202 | continue 203 | } 204 | defer resp.Body.Close() 205 | 206 | msgTimesMu.Lock() 207 | msgTimes[other] = time.Now() 208 | msgTimesMu.Unlock() 209 | 210 | respBits, err := ioutil.ReadAll(resp.Body) 211 | if err != nil { 212 | node.Logf("ERROR: reading response: %s", err) 213 | continue 214 | } 215 | var rawMsgs []json.RawMessage 216 | err = json.Unmarshal(respBits, &rawMsgs) 217 | if err != nil { 218 | node.Logf("ERROR: parsing response: %s", err) 219 | continue 220 | } 221 | for _, r := range rawMsgs { 222 | msg, err := unmarshal(r) 223 | if err != nil { 224 | node.Logf("ERROR: parsing protocol message: %s", err) 225 | continue 226 | } 227 | node.Logf("* sending %s to node.Handle", msg) 228 | node.Handle(msg) 229 | } 230 | } 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /cmd/scptxvm/msg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/bobg/scp" 12 | "github.com/chain/txvm/crypto/ed25519" 13 | "github.com/chain/txvm/protocol/bc" 14 | ) 15 | 16 | type ( 17 | marshaled struct { 18 | M json.RawMessage 19 | S string // hex-encoded signature over marshaledPayload 20 | } 21 | 22 | marshaledPayload struct { 23 | C int32 24 | V string 25 | I int 26 | Q scp.QSet 27 | T marshaledTopic 28 | } 29 | 30 | marshaledTopic struct { 31 | Type int // scp.Phase values 32 | X, Y []bc.Hash 33 | B, C, P, PP marshaledBallot 34 | PN, HN, CN int 35 | } 36 | 37 | marshaledBallot struct { 38 | N int 39 | X bc.Hash 40 | } 41 | ) 42 | 43 | func marshal(msg *scp.Msg) ([]byte, error) { 44 | var mt marshaledTopic 45 | switch topic := msg.T.(type) { 46 | case *scp.NomTopic: 47 | mt.Type = int(scp.PhNom) 48 | 49 | var x, y []bc.Hash 50 | for _, val := range topic.X { 51 | if val != nil { 52 | x = append(x, valToHash(val)) 53 | } 54 | } 55 | for _, val := range topic.Y { 56 | if val != nil { 57 | y = append(y, valToHash(val)) 58 | } 59 | } 60 | mt.X = x 61 | mt.Y = y 62 | 63 | case *scp.PrepTopic: 64 | mt.Type = int(scp.PhPrep) 65 | 66 | mt.B = marshaledBallot{N: topic.B.N, X: valToHash(topic.B.X)} 67 | mt.P = marshaledBallot{N: topic.P.N, X: valToHash(topic.P.X)} 68 | mt.PP = marshaledBallot{N: topic.PP.N, X: valToHash(topic.PP.X)} 69 | mt.HN = topic.HN 70 | mt.CN = topic.CN 71 | 72 | case *scp.CommitTopic: 73 | mt.Type = int(scp.PhCommit) 74 | 75 | mt.B = marshaledBallot{N: topic.B.N, X: valToHash(topic.B.X)} 76 | mt.PN = topic.PN 77 | mt.HN = topic.HN 78 | mt.CN = topic.CN 79 | 80 | case *scp.ExtTopic: 81 | mt.Type = int(scp.PhExt) 82 | 83 | mt.C = marshaledBallot{N: topic.C.N, X: valToHash(topic.C.X)} 84 | mt.HN = topic.HN 85 | } 86 | mp := marshaledPayload{ 87 | C: msg.C, 88 | V: string(msg.V), 89 | I: int(msg.I), 90 | Q: msg.Q, 91 | T: mt, 92 | } 93 | mpbytes, err := json.Marshal(mp) // xxx json is subject to mutation in transit! 94 | if err != nil { 95 | return nil, err 96 | } 97 | sig := ed25519.Sign(prv, mpbytes) 98 | m := marshaled{ 99 | M: mpbytes, 100 | S: hex.EncodeToString(sig), 101 | } 102 | return json.Marshal(m) 103 | } 104 | 105 | func valToHash(v scp.Value) (result bc.Hash) { 106 | if v != nil { 107 | result = bc.Hash(v.(valtype)) 108 | } 109 | return result 110 | } 111 | 112 | func unmarshalBallot(mb marshaledBallot) scp.Ballot { 113 | return scp.Ballot{ 114 | N: mb.N, 115 | X: valtype(mb.X), 116 | } 117 | } 118 | 119 | func unmarshal(b []byte) (*scp.Msg, error) { 120 | var m marshaled 121 | err := json.Unmarshal(b, &m) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | var mp marshaledPayload 127 | err = json.Unmarshal(m.M, &mp) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | sig, err := hex.DecodeString(m.S) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | u, err := url.Parse(mp.V) 138 | if err != nil { 139 | return nil, err 140 | } 141 | pubkeyHex := u.Path 142 | pubkeyHex = strings.Trim(pubkeyHex, "/") 143 | pubkey, err := hex.DecodeString(pubkeyHex) 144 | if err != nil { 145 | return nil, err 146 | } 147 | if !ed25519.Verify(pubkey, m.M, sig) { 148 | return nil, errors.New("bad signature") 149 | } 150 | 151 | var topic scp.Topic 152 | switch scp.Phase(mp.T.Type) { 153 | case scp.PhNom: 154 | var x, y scp.ValueSet 155 | for _, v := range mp.T.X { 156 | x = append(x, valtype(v)) 157 | } 158 | for _, v := range mp.T.Y { 159 | y = append(y, valtype(v)) 160 | } 161 | topic = &scp.NomTopic{ 162 | X: x, 163 | Y: y, 164 | } 165 | 166 | case scp.PhPrep: 167 | topic = &scp.PrepTopic{ 168 | B: unmarshalBallot(mp.T.B), 169 | P: unmarshalBallot(mp.T.P), 170 | PP: unmarshalBallot(mp.T.PP), 171 | HN: mp.T.HN, 172 | CN: mp.T.CN, 173 | } 174 | 175 | case scp.PhCommit: 176 | topic = &scp.CommitTopic{ 177 | B: unmarshalBallot(mp.T.B), 178 | PN: mp.T.PN, 179 | HN: mp.T.HN, 180 | CN: mp.T.CN, 181 | } 182 | 183 | case scp.PhExt: 184 | topic = &scp.ExtTopic{ 185 | C: unmarshalBallot(mp.T.C), 186 | HN: mp.T.HN, 187 | } 188 | 189 | default: 190 | return nil, fmt.Errorf("unknown topic type %d", mp.T.Type) 191 | } 192 | 193 | msg := &scp.Msg{ 194 | C: mp.C, 195 | V: scp.NodeID(mp.V), 196 | I: scp.SlotID(mp.I), 197 | Q: mp.Q, 198 | T: topic, 199 | } 200 | return msg, nil 201 | } 202 | -------------------------------------------------------------------------------- /cmd/scptxvm/node.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net/http" 7 | "sync" 8 | "time" 9 | 10 | "github.com/bobg/scp" 11 | "github.com/chain/txvm/protocol/bc" 12 | ) 13 | 14 | var ( 15 | // Value is when the subscriber subscribed. 16 | subscribers = make(map[scp.NodeID]time.Time) 17 | subscribersMu sync.Mutex 18 | ) 19 | 20 | func handleNodeOutput(ctx context.Context) { 21 | defer wg.Done() 22 | 23 | var latest *scp.Msg 24 | ticker := time.Tick(time.Second) 25 | 26 | for { 27 | select { 28 | case <-ctx.Done(): 29 | node.Logf("context canceled, exiting node-output loop") 30 | return 31 | 32 | case latest = <-msgChan: 33 | if ext, ok := latest.T.(*scp.ExtTopic); ok { 34 | // We've externalized a block at a new height. 35 | 36 | // Update the tx pool to remove published and conflicting txs. 37 | block, err := getBlock(int(latest.I), bc.Hash(ext.C.X.(valtype))) 38 | if err != nil { 39 | panic(err) // xxx 40 | } 41 | nomChan <- block 42 | 43 | // Update the protocol.Chain object and anything waiting on it. 44 | heightChan <- uint64(latest.I) 45 | } 46 | 47 | case <-ticker: 48 | // Send only the latest protocol message (if any) to all peers 49 | // and subscribers no more than once per second. 50 | if latest == nil { 51 | continue 52 | } 53 | msg := latest 54 | pmsg, err := marshal(msg) 55 | if err != nil { 56 | panic(err) // xxx 57 | } 58 | 59 | latest = nil 60 | 61 | others := node.Peers() 62 | subscribersMu.Lock() 63 | for other := range subscribers { 64 | others = others.Add(other) 65 | } 66 | subscribersMu.Unlock() 67 | 68 | for _, other := range others { 69 | other := other 70 | go func() { 71 | node.Logf("* sending %s to %s", msg, other) 72 | req, err := http.NewRequest("POST", string(other), bytes.NewReader(pmsg)) 73 | if err != nil { 74 | node.Logf("error constructing protocol request to %s: %s", other, err) 75 | return 76 | } 77 | req = req.WithContext(ctx) 78 | req.Header.Set("Content-Type", "application/octet-stream") 79 | var c http.Client 80 | resp, err := c.Do(req) 81 | if err != nil { 82 | node.Logf("could not send protocol message to %s: %s", other, err) 83 | subscribersMu.Lock() 84 | delete(subscribers, other) 85 | subscribersMu.Unlock() 86 | } else if resp.StatusCode/100 != 2 { 87 | node.Logf("unexpected status code %d sending protocol message to %s", resp.StatusCode, other) 88 | subscribersMu.Lock() 89 | delete(subscribers, other) 90 | subscribersMu.Unlock() 91 | } 92 | }() 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /cmd/scptxvm/nominate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/bobg/scp" 8 | "github.com/chain/txvm/protocol" 9 | "github.com/chain/txvm/protocol/bc" 10 | ) 11 | 12 | func nominate(ctx context.Context) { 13 | defer wg.Done() 14 | 15 | txpool := make(map[bc.Hash]*bc.Tx) 16 | 17 | doNom := func() error { 18 | if len(txpool) == 0 { 19 | return nil 20 | } 21 | 22 | txs := make([]*bc.CommitmentsTx, 0, len(txpool)) 23 | for _, tx := range txpool { 24 | txs = append(txs, bc.NewCommitmentsTx(tx)) 25 | } 26 | 27 | timestampMS := bc.Millis(time.Now()) 28 | snapshot := chain.State() 29 | if snapshot.Header.TimestampMs > timestampMS { 30 | timestampMS = snapshot.Header.TimestampMs + 1 // xxx sleep until this time? error out? 31 | } 32 | 33 | // TODO: reuse a builder object 34 | bb := protocol.NewBlockBuilder() 35 | 36 | // generate a new block 37 | err := bb.Start(snapshot, timestampMS) 38 | if err != nil { 39 | return err 40 | } 41 | for _, tx := range txs { 42 | err = bb.AddTx(tx) 43 | if err == nil { 44 | continue 45 | } 46 | // TODO: There are other errors (from ApplyTx) that should cause 47 | // the tx to be removed from the pool, but those aren't 48 | // exported. 49 | if err == protocol.ErrTxTooOld { 50 | node.Logf("removing tx %x from the pool: %s", tx.Tx.ID.Bytes(), err) 51 | } else { 52 | node.Logf("skipping tx %x in this block, will retry: %s", tx.Tx.ID.Bytes(), err) 53 | } 54 | } 55 | ublock, _, err := bb.Build() 56 | if err != nil { 57 | return err 58 | } 59 | block, err := bc.SignBlock(ublock, snapshot.Header, nil) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | err = storeBlock(block) 65 | if err != nil { 66 | return err 67 | } 68 | n := &scp.NomTopic{ 69 | X: scp.ValueSet{valtype(block.Hash())}, 70 | } 71 | node.Logf("nominating block %x (%d tx(s)) at height %d", block.Hash().Bytes(), len(block.Transactions), block.Height) 72 | msg := scp.NewMsg(node.ID, scp.SlotID(block.Height), node.Q, n) // xxx slotID is 32 bits, block height is 64 73 | node.Handle(msg) 74 | return nil 75 | } 76 | 77 | for { 78 | select { 79 | case <-ctx.Done(): 80 | node.Logf("context canceled, exiting nominate loop") 81 | return 82 | 83 | case item := <-nomChan: 84 | switch item := item.(type) { 85 | case *bc.Tx: 86 | if _, ok := txpool[item.ID]; ok { 87 | // tx is already in the pool 88 | continue 89 | } 90 | node.Logf("adding tx %x to the pool", item.ID.Bytes()) 91 | txpool[item.ID] = item // xxx need to persist this 92 | err := doNom() 93 | if err != nil { 94 | panic(err) // xxx 95 | } 96 | 97 | case scp.SlotID: 98 | err := doNom() 99 | if err != nil { 100 | panic(err) // xxx 101 | } 102 | 103 | case *bc.Block: 104 | // Remove published and conflicting txs from txpool. 105 | spent := make(map[bc.Hash]struct{}) 106 | for _, tx := range item.Transactions { 107 | for _, inp := range tx.Inputs { 108 | spent[inp.ID] = struct{}{} 109 | } 110 | // Published tx. 111 | node.Logf("removing published tx %x from the pool", tx.ID.Bytes()) 112 | delete(txpool, tx.ID) 113 | } 114 | for id, tx := range txpool { 115 | for _, inp := range tx.Inputs { 116 | if _, ok := spent[inp.ID]; ok { 117 | // Conflicting tx. 118 | node.Logf("removing conflicting tx %x from the pool", id.Bytes()) 119 | delete(txpool, id) 120 | break 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /cmd/scptxvm/store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "strconv" 10 | "sync" 11 | 12 | "github.com/chain/txvm/protocol/bc" 13 | "github.com/chain/txvm/protocol/state" 14 | ) 15 | 16 | // Implements protocol.Store from github.com/chain/txvm 17 | type pstore struct{} 18 | 19 | func (s *pstore) Height(_ context.Context) (uint64, error) { 20 | infos, err := ioutil.ReadDir(blockDir()) 21 | if err != nil { 22 | return 0, err 23 | } 24 | var result int64 25 | for _, info := range infos { 26 | n, err := strconv.ParseInt(info.Name(), 10, 64) 27 | if err != nil { 28 | continue 29 | } 30 | if n > result { 31 | result = n 32 | } 33 | } 34 | return uint64(result), nil 35 | } 36 | 37 | func (s *pstore) GetBlock(_ context.Context, height uint64) (*bc.Block, error) { 38 | return readBlockFile(path.Join(blockDir(), strconv.FormatUint(height, 10))) 39 | } 40 | 41 | func (s *pstore) LatestSnapshot(_ context.Context) (*state.Snapshot, error) { 42 | infos, err := ioutil.ReadDir(snapshotDir()) 43 | if err != nil { 44 | return nil, err 45 | } 46 | var highest int 47 | for _, info := range infos { 48 | n, err := strconv.Atoi(info.Name()) 49 | if err != nil { 50 | continue 51 | } 52 | if n > highest { 53 | highest = n 54 | } 55 | } 56 | if highest <= 0 { 57 | return state.Empty(), nil 58 | } 59 | filename := path.Join(snapshotDir(), strconv.Itoa(highest)) 60 | b, err := ioutil.ReadFile(filename) 61 | if err != nil { 62 | return nil, err 63 | } 64 | var snapshot state.Snapshot 65 | err = snapshot.FromBytes(b) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return &snapshot, nil 70 | } 71 | 72 | func (s *pstore) SaveBlock(_ context.Context, block *bc.Block) error { 73 | err := storeBlock(block) 74 | if err != nil { 75 | return err 76 | } 77 | oldName := blockFilename(int(block.Height), block.Hash()) // xxx uint64->int 78 | newName := path.Join(blockDir(), strconv.FormatUint(block.Height, 10)) 79 | err = os.Link(oldName, newName) 80 | if os.IsExist(err) { 81 | return nil 82 | } 83 | return err 84 | } 85 | 86 | func (s *pstore) FinalizeHeight(_ context.Context, height uint64) error { 87 | return nil 88 | } 89 | 90 | func (s *pstore) SaveSnapshot(_ context.Context, snapshot *state.Snapshot) error { 91 | filename := path.Join(snapshotDir(), strconv.FormatUint(snapshot.Height(), 10)) 92 | b, err := snapshot.Bytes() 93 | if err != nil { 94 | return err 95 | } 96 | return ioutil.WriteFile(filename, b, 0644) 97 | } 98 | 99 | func blockFilename(height int, id bc.Hash) string { 100 | return path.Join(blockDir(), fmt.Sprintf("%d-%x", height, id.Bytes())) 101 | } 102 | 103 | func getBlock(height int, id bc.Hash) (*bc.Block, error) { 104 | return readBlockFile(blockFilename(height, id)) 105 | } 106 | 107 | func haveBlock(height int, id bc.Hash) (bool, error) { 108 | filename := blockFilename(height, id) 109 | _, err := os.Stat(filename) 110 | if err == nil { 111 | return true, nil 112 | } 113 | if os.IsNotExist(err) { 114 | return false, nil 115 | } 116 | return false, err 117 | } 118 | 119 | func readBlockFile(filename string) (*bc.Block, error) { 120 | bits, err := ioutil.ReadFile(filename) 121 | if err != nil { 122 | return nil, err 123 | } 124 | var block bc.Block 125 | err = block.FromBytes(bits) 126 | if err != nil { 127 | return nil, err 128 | } 129 | return &block, nil 130 | } 131 | 132 | var storeBlockMu sync.Mutex 133 | 134 | func storeBlock(block *bc.Block) error { 135 | storeBlockMu.Lock() 136 | defer storeBlockMu.Unlock() 137 | 138 | filename := blockFilename(int(block.Height), block.Hash()) // xxx uint64->int 139 | _, err := os.Stat(filename) 140 | if err == nil { 141 | // File exists already. 142 | return nil 143 | } 144 | if !os.IsNotExist(err) { 145 | // Problem is other than file-doesn't-exist. 146 | return err 147 | } 148 | bits, err := block.Bytes() 149 | if err != nil { 150 | return err 151 | } 152 | return ioutil.WriteFile(filename, bits, 0644) 153 | } 154 | 155 | func blockDir() string { 156 | return path.Join(dir, "blocks") 157 | } 158 | 159 | func snapshotDir() string { 160 | return path.Join(dir, "snapshots") 161 | } 162 | -------------------------------------------------------------------------------- /cmd/scptxvm/val.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "sort" 6 | 7 | "github.com/bobg/scp" 8 | "github.com/chain/txvm/protocol" 9 | "github.com/chain/txvm/protocol/bc" 10 | ) 11 | 12 | // The concrete type for scp.Value. This network votes on block 13 | // IDs. When a node needs to know the contents of a block, it can 14 | // inquire via RPC. 15 | type valtype bc.Hash 16 | 17 | func (v valtype) Less(otherval scp.Value) bool { 18 | other := otherval.(valtype) 19 | if v.V0 < other.V0 { 20 | return true 21 | } 22 | if v.V0 > other.V0 { 23 | return false 24 | } 25 | if v.V1 < other.V1 { 26 | return true 27 | } 28 | if v.V1 > other.V1 { 29 | return false 30 | } 31 | if v.V2 < other.V2 { 32 | return true 33 | } 34 | if v.V2 > other.V2 { 35 | return false 36 | } 37 | return v.V3 < other.V3 38 | } 39 | 40 | func (v valtype) Bytes() []byte { 41 | return bc.Hash(v).Bytes() 42 | } 43 | 44 | func (v valtype) String() string { 45 | return hex.EncodeToString(bc.Hash(v).Bytes()) 46 | } 47 | 48 | func (v valtype) Combine(otherval scp.Value, slotID scp.SlotID) scp.Value { 49 | other := otherval.(valtype) 50 | if other.Less(v) { 51 | return other.Combine(v, slotID) 52 | } 53 | if !v.Less(other) { 54 | // v == other 55 | return v 56 | } 57 | 58 | b1, err := getBlock(int(slotID), bc.Hash(v)) 59 | if err != nil { 60 | panic(err) // xxx is this OK? 61 | } 62 | b2, err := getBlock(int(slotID), bc.Hash(other)) 63 | if err != nil { 64 | panic(err) // xxx 65 | } 66 | 67 | txs := b1.Transactions 68 | txs = append(txs, b2.Transactions...) 69 | sort.Slice(txs, func(i, j int) bool { 70 | s := make(map[bc.Hash]struct{}) 71 | for _, out := range txs[i].Outputs { 72 | s[out.ID] = struct{}{} 73 | } 74 | for _, in := range txs[j].Inputs { 75 | if _, ok := s[in.ID]; ok { 76 | return true 77 | } 78 | } 79 | 80 | s = make(map[bc.Hash]struct{}) 81 | for _, out := range txs[j].Outputs { 82 | s[out.ID] = struct{}{} 83 | } 84 | for _, in := range txs[i].Inputs { 85 | if _, ok := s[in.ID]; ok { 86 | return false 87 | } 88 | } 89 | 90 | return valtype(txs[i].ID).Less(valtype(txs[j].ID)) 91 | }) 92 | 93 | // Eliminate duplicates. There should be no more than two of any 94 | // given txid, but this code handles any number of duplicates 95 | // anyway. 96 | var ( 97 | n = 0 98 | d = 1 99 | ) 100 | for n+d < len(txs) { // xxx double-check the logic in this loop 101 | if txs[n].ID == txs[n+d].ID { 102 | d++ 103 | } else { 104 | if d > 1 { 105 | txs[n+1] = txs[n+d] 106 | n++ 107 | } 108 | } 109 | } 110 | txs = txs[:n] 111 | cmtxs := make([]*bc.CommitmentsTx, 0, len(txs)) 112 | for _, tx := range txs { 113 | cmtxs = append(cmtxs, bc.NewCommitmentsTx(tx)) 114 | } 115 | 116 | // Use the earlier timestamp. 117 | timestampMS := b1.TimestampMs 118 | if b2.TimestampMs < timestampMS { 119 | timestampMS = b2.TimestampMs 120 | } 121 | 122 | dflt := func() valtype { 123 | // Cannot make a block from the combined set of txs. Choose one of 124 | // the input blocks as the winner. 125 | if slotID%2 == 0 { 126 | return v 127 | } 128 | return other 129 | } 130 | 131 | // TODO: reuse a builder object 132 | bb := protocol.NewBlockBuilder() 133 | 134 | snapshot := chain.State() 135 | err = bb.Start(snapshot, timestampMS) 136 | if err != nil { 137 | return dflt() 138 | } 139 | 140 | for _, tx := range cmtxs { 141 | err = bb.AddTx(tx) 142 | if err != nil { 143 | return dflt() 144 | } 145 | } 146 | ublock, _, err := bb.Build() 147 | if err != nil { 148 | return dflt() 149 | } 150 | 151 | block, err := bc.SignBlock(ublock, snapshot.Header, nil) 152 | if err != nil { 153 | return dflt() 154 | } 155 | 156 | err = storeBlock(block) 157 | if err != nil { 158 | panic(err) 159 | } 160 | 161 | return valtype(block.Hash()) 162 | } 163 | 164 | func (v valtype) IsNil() bool { 165 | h := bc.Hash(v) 166 | return h.IsZero() 167 | } 168 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Package scp is an implementation of the Stellar Consensus Protocol. 4 | 5 | A Node is a participant in an SCP network. A caller feeds incoming 6 | protocol messages (type Msg) to the node's Handle method. In most 7 | cases, the node will respond with another Msg, which the caller should 8 | then disseminate to other network nodes. 9 | 10 | The network votes on abstract Value objects proposed by its 11 | members. By means of the protocol, all participating nodes should 12 | eventually converge on a single value for any given "slot." When a 13 | node reaches a final choice of value for a slot, it is said to 14 | "externalize" the value. 15 | 16 | A caller may instantiate Value with any concrete type that can be 17 | totally ordered, and for which a deterministic, commutative Combine 18 | operation can be written (reducing two Values to a single one). 19 | 20 | A toy demo can be found in cmd/lunch. It takes the name of a TOML file 21 | as an argument. The TOML file specifies the network participants and 22 | topology. Sample TOML files are in cmd/lunch/toml. 23 | 24 | */ 25 | package scp 26 | -------------------------------------------------------------------------------- /genset.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "os" 9 | "sort" 10 | "strings" 11 | "unicode" 12 | ) 13 | 14 | func main() { 15 | out, err := os.Create("set.go") 16 | if err != nil { 17 | panic(err) 18 | } 19 | defer out.Close() 20 | 21 | fmt.Fprintln(out, "// This file generated by genset.go") 22 | fmt.Fprintln(out, "package scp\n") 23 | fmt.Fprintln(out, `import "sort"`) 24 | 25 | types := []string{"Value", "Ballot", "NodeID"} 26 | for _, typ := range types { 27 | in, err := os.Open("genset.go") 28 | if err != nil { 29 | panic(err) 30 | } 31 | func() { 32 | defer in.Close() 33 | 34 | scanner := bufio.NewScanner(in) 35 | for scanner.Scan() { 36 | if scanner.Text() == "// SEPARATOR" { 37 | break 38 | } 39 | } 40 | for scanner.Scan() { 41 | letter := unicode.ToLower(rune(typ[0])) 42 | line := scanner.Text() 43 | line = strings.Replace(line, "TTT", typ, -1) 44 | line = strings.Replace(line, "ttts", fmt.Sprintf("%cs", letter), -1) 45 | line = strings.Replace(line, "ttt", string(letter), -1) 46 | fmt.Fprintln(out, line) 47 | } 48 | }() 49 | } 50 | } 51 | 52 | type TTT int // not really 53 | 54 | func (t TTT) Less(other TTT) bool { return false } 55 | 56 | // SEPARATOR 57 | 58 | // TTTSet is a set of TTT, implemented as a sorted slice. 59 | type TTTSet []TTT 60 | 61 | func (ttts TTTSet) find(ttt TTT) int { 62 | return sort.Search(len(ttts), func(index int) bool { 63 | return !ttts[index].Less(ttt) 64 | }) 65 | } 66 | 67 | func TTTEqual(a, b TTT) bool { 68 | return !a.Less(b) && !b.Less(a) 69 | } 70 | 71 | // Add produces a TTTSet containing the members of ttts plus the 72 | // element ttt. 73 | func (ttts TTTSet) Add(ttt TTT) TTTSet { 74 | index := ttts.find(ttt) 75 | if index < len(ttts) && TTTEqual(ttt, ttts[index]) { 76 | return ttts 77 | } 78 | var result TTTSet 79 | result = append(result, ttts[:index]...) 80 | result = append(result, ttt) 81 | result = append(result, ttts[index:]...) 82 | return result 83 | } 84 | 85 | // Union produces a TTTSet containing all the members of both sets. 86 | func (ttts TTTSet) Union(other TTTSet) TTTSet { 87 | if len(ttts) == 0 { 88 | return other 89 | } 90 | if len(other) == 0 { 91 | return ttts 92 | } 93 | var ( 94 | i, j int 95 | result TTTSet 96 | ) 97 | for i < len(ttts) && j < len(other) { 98 | switch { 99 | case ttts[i].Less(other[j]): 100 | result = append(result, ttts[i]) 101 | i++ 102 | case other[j].Less(ttts[i]): 103 | result = append(result, other[j]) 104 | j++ 105 | default: 106 | result = append(result, ttts[i]) 107 | i++ 108 | j++ 109 | } 110 | } 111 | result = append(result, ttts[i:]...) 112 | result = append(result, other[j:]...) 113 | return result 114 | } 115 | 116 | // Intersection produces a TTTSet with only the elements in both 117 | // sets. 118 | func (ttts TTTSet) Intersection(other TTTSet) TTTSet { 119 | if len(ttts) == 0 || len(other) == 0 { 120 | return nil 121 | } 122 | var result TTTSet 123 | for i, j := 0, 0; i < len(ttts) && j < len(other); { 124 | switch { 125 | case ttts[i].Less(other[j]): 126 | i++ 127 | case other[j].Less(ttts[i]): 128 | j++ 129 | default: 130 | result = append(result, ttts[i]) 131 | i++ 132 | j++ 133 | } 134 | } 135 | return result 136 | } 137 | 138 | // Minus produces a TTTSet with only the members of ttts that don't 139 | // appear in other. 140 | func (ttts TTTSet) Minus(other TTTSet) TTTSet { 141 | if len(ttts) == 0 || len(other) == 0 { 142 | return ttts 143 | } 144 | var ( 145 | result TTTSet 146 | i, j int 147 | ) 148 | for i < len(ttts) && j < len(other) { 149 | switch { 150 | case ttts[i].Less(other[j]): 151 | result = append(result, ttts[i]) 152 | i++ 153 | case other[j].Less(ttts[i]): 154 | j++ 155 | default: 156 | i++ 157 | j++ 158 | } 159 | } 160 | result = append(result, ttts[i:]...) 161 | return result 162 | } 163 | 164 | // Remove produces a TTTSet without the specified element. 165 | func (ttts TTTSet) Remove(ttt TTT) TTTSet { 166 | index := ttts.find(ttt) 167 | if index >= len(ttts) || !TTTEqual(ttt, ttts[index]) { 168 | return ttts 169 | } 170 | var result TTTSet 171 | result = append(result, ttts[:index]...) 172 | result = append(result, ttts[index+1:]...) 173 | return result 174 | } 175 | 176 | // Contains tests whether ttts contains ttt. 177 | func (ttts TTTSet) Contains(ttt TTT) bool { 178 | index := ttts.find(ttt) 179 | return index < len(ttts) && TTTEqual(ttts[index], ttt) 180 | } 181 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bobg/scp 2 | 3 | go 1.14 4 | 5 | require github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892 h1:qg9VbHo1TlL0KDM0vYvBG9EY0X0Yku5WYIPoFWt8f6o= 2 | github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= 3 | -------------------------------------------------------------------------------- /msg.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "sync/atomic" 8 | ) 9 | 10 | // Msg is an SCP protocol message. 11 | type Msg struct { 12 | C int32 // A counter for identifying this envelope, does not participate in the protocol. 13 | V NodeID // ID of the node sending this message. 14 | I SlotID // ID of the slot that this message is about. 15 | Q QSet // Quorum slices of the sending node. 16 | T Topic // The payload: a *NomTopic, a *NomPrepTopic, *PrepTopic, *CommitTopic, or *ExtTopic. 17 | } 18 | 19 | var msgCounter int32 20 | 21 | // NewMsg produces a new message. 22 | func NewMsg(v NodeID, i SlotID, q QSet, t Topic) *Msg { 23 | c := atomic.AddInt32(&msgCounter, 1) 24 | return &Msg{ 25 | C: c, 26 | V: v, 27 | I: i, 28 | Q: q, 29 | T: t, 30 | } 31 | } 32 | 33 | func (e *Msg) valid() (err error) { 34 | defer func() { 35 | if err != nil { 36 | err = fmt.Errorf("%s: %s", err, e) 37 | } 38 | }() 39 | 40 | nom := func(topic *NomTopic) error { 41 | if len(topic.X.Intersection(topic.Y)) != 0 { 42 | return errors.New("non-empty intersection between X and Y") 43 | } 44 | return nil 45 | } 46 | prep := func(topic *PrepTopic) error { 47 | if !topic.P.IsZero() { 48 | if topic.B.Less(topic.P) { 49 | return errors.New("P > B") 50 | } 51 | if !topic.PP.IsZero() && !topic.PP.Less(topic.P) { 52 | return errors.New("PP >= P") 53 | } 54 | } 55 | if topic.CN > topic.HN { 56 | return errors.New("CN > HN (prepare)") 57 | } 58 | if topic.HN > topic.B.N { 59 | return errors.New("HN > BN") 60 | } 61 | return nil 62 | } 63 | 64 | switch topic := e.T.(type) { 65 | case *NomTopic: 66 | err := nom(topic) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | case *NomPrepTopic: 72 | err := nom(&topic.NomTopic) 73 | if err != nil { 74 | return err 75 | } 76 | return prep(&topic.PrepTopic) 77 | 78 | case *PrepTopic: 79 | return prep(topic) 80 | 81 | case *CommitTopic: 82 | if topic.CN > topic.HN { 83 | return errors.New("CN > HN (commit)") 84 | } 85 | } 86 | return nil 87 | } 88 | 89 | // Return the ballot counter (if any). 90 | func (e *Msg) bN() int { 91 | switch topic := e.T.(type) { 92 | case *NomPrepTopic: 93 | return topic.B.N 94 | 95 | case *PrepTopic: 96 | return topic.B.N 97 | 98 | case *CommitTopic: 99 | return topic.B.N 100 | 101 | case *ExtTopic: 102 | return math.MaxInt32 103 | } 104 | return 0 105 | } 106 | 107 | // Returns the set of values that e votes or accepts as nominated. 108 | func (e *Msg) votesOrAcceptsNominatedSet() ValueSet { 109 | result := e.acceptsNominatedSet() 110 | f := func(topic *NomTopic) { 111 | result = result.Union(topic.X) 112 | } 113 | switch topic := e.T.(type) { 114 | case *NomTopic: 115 | f(topic) 116 | case *NomPrepTopic: 117 | f(&topic.NomTopic) 118 | } 119 | return result 120 | } 121 | 122 | // Returns the set of values that e accepts as nominated. 123 | func (e *Msg) acceptsNominatedSet() ValueSet { 124 | switch topic := e.T.(type) { 125 | case *NomTopic: 126 | return topic.Y 127 | 128 | case *NomPrepTopic: 129 | return topic.Y 130 | } 131 | return nil 132 | } 133 | 134 | // Returns the set of ballots for which e votes or accepts "prepared." 135 | func (e *Msg) votesOrAcceptsPreparedSet() BallotSet { 136 | result := e.acceptsPreparedSet() 137 | f := func(topic *PrepTopic) { 138 | result = result.Add(topic.B) 139 | } 140 | 141 | switch topic := e.T.(type) { 142 | case *NomPrepTopic: 143 | f(&topic.PrepTopic) 144 | 145 | case *PrepTopic: 146 | f(topic) 147 | 148 | case *CommitTopic: 149 | result = result.Add(Ballot{N: math.MaxInt32, X: topic.B.X}) 150 | } 151 | return result 152 | } 153 | 154 | // Returns the set of ballots that e accepts as prepared. 155 | func (e *Msg) acceptsPreparedSet() BallotSet { 156 | var result BallotSet 157 | f := func(topic *PrepTopic) { 158 | if !topic.P.IsZero() { 159 | result = result.Add(topic.P) 160 | if !topic.PP.IsZero() { 161 | result = result.Add(topic.PP) 162 | } 163 | } 164 | if topic.HN > 0 { 165 | result = result.Add(Ballot{N: topic.HN, X: topic.B.X}) 166 | } 167 | } 168 | switch topic := e.T.(type) { 169 | case *NomPrepTopic: 170 | f(&topic.PrepTopic) 171 | 172 | case *PrepTopic: 173 | f(topic) 174 | 175 | case *CommitTopic: 176 | result = result.Add(Ballot{N: topic.PN, X: topic.B.X}) 177 | result = result.Add(Ballot{N: topic.HN, X: topic.B.X}) 178 | 179 | case *ExtTopic: 180 | result = result.Add(Ballot{N: math.MaxInt32, X: topic.C.X}) 181 | } 182 | return result 183 | } 184 | 185 | // Tells whether e votes commit(b) or accepts commit(b) for any ballot 186 | // b whose value is v and whose counter is in the range [min,max] 187 | // (inclusive). If so, returns the new min/max that is the overlap 188 | // between the input and what e votes for or accepts. 189 | func (e *Msg) votesOrAcceptsCommit(v Value, min, max int) (bool, int, int) { 190 | if res, newMin, newMax := e.acceptsCommit(v, min, max); res { 191 | // xxx newMin/newMax might be too narrow after accounting only for 192 | // "accepts" and not yet for "votes." do we care? 193 | return true, newMin, newMax 194 | } 195 | f := func(topic *PrepTopic) (bool, int, int) { 196 | if topic.CN == 0 || !ValueEqual(topic.B.X, v) { 197 | return false, 0, 0 198 | } 199 | if topic.CN > max || topic.HN < min { 200 | return false, 0, 0 201 | } 202 | if topic.CN > min { 203 | min = topic.CN 204 | } 205 | if topic.HN < max { 206 | max = topic.HN 207 | } 208 | return true, min, max 209 | } 210 | switch topic := e.T.(type) { 211 | case *NomPrepTopic: 212 | return f(&topic.PrepTopic) 213 | 214 | case *PrepTopic: 215 | return f(topic) 216 | 217 | case *CommitTopic: 218 | if !ValueEqual(topic.B.X, v) { 219 | return false, 0, 0 220 | } 221 | if topic.CN > max { 222 | return false, 0, 0 223 | } 224 | if topic.CN > min { 225 | min = topic.CN 226 | } 227 | return true, min, max 228 | } 229 | return false, 0, 0 230 | } 231 | 232 | // Tells whether e accepts commit(b) for any ballot b whose value is v 233 | // and whose counter is in the range [min,max] (inclusive). If so, 234 | // returns the new min/max that is the overlap between the input and 235 | // what e accepts. 236 | func (e *Msg) acceptsCommit(v Value, min, max int) (bool, int, int) { 237 | switch topic := e.T.(type) { 238 | case *CommitTopic: 239 | if !ValueEqual(topic.B.X, v) { 240 | return false, 0, 0 241 | } 242 | if topic.CN > max { 243 | return false, 0, 0 244 | } 245 | if topic.HN < min { 246 | return false, 0, 0 247 | } 248 | if topic.CN > min { 249 | min = topic.CN 250 | } 251 | if topic.HN < max { 252 | max = topic.HN 253 | } 254 | return true, min, max 255 | 256 | case *ExtTopic: 257 | if !ValueEqual(topic.C.X, v) { 258 | return false, 0, 0 259 | } 260 | if topic.C.N > max { 261 | return false, 0, 0 262 | } 263 | if topic.C.N > min { 264 | min = topic.C.N 265 | } 266 | return true, min, max 267 | } 268 | return false, 0, 0 269 | } 270 | 271 | // String produces a readable representation of a message. 272 | func (e *Msg) String() string { 273 | return fmt.Sprintf("(C=%d V=%s I=%d: %s)", e.C, e.V, e.I, e.T) 274 | } 275 | -------------------------------------------------------------------------------- /msg_test.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestAcceptsNominated(t *testing.T) { 11 | cases := []struct { 12 | m Topic 13 | wantA, wantVA []int 14 | }{ 15 | { 16 | m: &NomTopic{}, 17 | }, 18 | { 19 | m: &NomTopic{X: ValueSet{valtype(1)}}, 20 | wantVA: []int{1}, 21 | }, 22 | { 23 | m: &NomTopic{Y: ValueSet{valtype(1)}}, 24 | wantA: []int{1}, 25 | wantVA: []int{1}, 26 | }, 27 | { 28 | m: &NomTopic{X: ValueSet{valtype(1)}, Y: ValueSet{valtype(2)}}, 29 | wantA: []int{2}, 30 | wantVA: []int{1, 2}, 31 | }, 32 | { 33 | m: &NomPrepTopic{ 34 | NomTopic: NomTopic{X: ValueSet{valtype(1)}, Y: ValueSet{valtype(2)}}, 35 | PrepTopic: PrepTopic{B: Ballot{1, valtype(1)}}, 36 | }, 37 | wantA: []int{2}, 38 | wantVA: []int{1, 2}, 39 | }, 40 | { 41 | m: &PrepTopic{B: Ballot{1, valtype(1)}}, 42 | wantA: []int{}, 43 | wantVA: []int{}, 44 | }, 45 | { 46 | m: &PrepTopic{B: Ballot{1, valtype(1)}, P: Ballot{1, valtype(2)}}, 47 | wantA: []int{}, 48 | wantVA: []int{}, 49 | }, 50 | { 51 | m: &PrepTopic{B: Ballot{1, valtype(1)}, P: Ballot{1, valtype(2)}, PP: Ballot{1, valtype(3)}}, 52 | wantA: []int{}, 53 | wantVA: []int{}, 54 | }, 55 | { 56 | m: &CommitTopic{B: Ballot{1, valtype(1)}}, 57 | wantA: []int{}, 58 | wantVA: []int{}, 59 | }, 60 | { 61 | m: &ExtTopic{C: Ballot{1, valtype(1)}}, 62 | wantA: []int{}, 63 | wantVA: []int{}, 64 | }, 65 | } 66 | for i, tc := range cases { 67 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 68 | e := &Msg{T: tc.m} 69 | got := e.acceptsNominatedSet() 70 | var want ValueSet 71 | for _, val := range tc.wantA { 72 | want = want.Add(valtype(val)) 73 | } 74 | if !reflect.DeepEqual(got, want) { 75 | t.Errorf("got acceptsNominatedSet=%v, want %v", got, want) 76 | } 77 | want = nil 78 | for _, val := range tc.wantVA { 79 | want = want.Add(valtype(val)) 80 | } 81 | got = e.votesOrAcceptsNominatedSet() 82 | if !reflect.DeepEqual(got, want) { 83 | t.Errorf("got votesOrAcceptsNominatedSet=%v, want %v", got, want) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestAcceptsPrepared(t *testing.T) { 90 | cases := []struct { 91 | m Topic 92 | wantA, wantVA BallotSet 93 | }{ 94 | { 95 | m: &NomTopic{}, 96 | }, 97 | { 98 | m: &NomTopic{X: ValueSet{valtype(1)}}, 99 | }, 100 | { 101 | m: &NomTopic{Y: ValueSet{valtype(1)}}, 102 | }, 103 | { 104 | m: &PrepTopic{B: Ballot{5, valtype(1)}}, 105 | wantVA: BallotSet{Ballot{5, valtype(1)}}, 106 | }, 107 | { 108 | m: &PrepTopic{B: Ballot{5, valtype(1)}, P: Ballot{5, valtype(2)}}, 109 | wantA: BallotSet{Ballot{5, valtype(2)}}, 110 | wantVA: BallotSet{Ballot{5, valtype(1)}, Ballot{5, valtype(2)}}, 111 | }, 112 | { 113 | m: &PrepTopic{B: Ballot{5, valtype(1)}, P: Ballot{5, valtype(2)}, PP: Ballot{5, valtype(3)}}, 114 | wantA: BallotSet{Ballot{5, valtype(2)}, Ballot{5, valtype(3)}}, 115 | wantVA: BallotSet{Ballot{5, valtype(1)}, Ballot{5, valtype(2)}, Ballot{5, valtype(3)}}, 116 | }, 117 | { 118 | m: &PrepTopic{B: Ballot{5, valtype(1)}, P: Ballot{5, valtype(2)}, PP: Ballot{5, valtype(3)}, HN: 20}, 119 | wantA: BallotSet{Ballot{5, valtype(2)}, Ballot{5, valtype(3)}, Ballot{20, valtype(1)}}, 120 | wantVA: BallotSet{Ballot{5, valtype(1)}, Ballot{5, valtype(2)}, Ballot{5, valtype(3)}, Ballot{20, valtype(1)}}, 121 | }, 122 | { 123 | m: &PrepTopic{B: Ballot{5, valtype(1)}, P: Ballot{5, valtype(2)}, PP: Ballot{5, valtype(3)}, CN: 10, HN: 20}, 124 | wantA: BallotSet{Ballot{5, valtype(2)}, Ballot{5, valtype(3)}, Ballot{20, valtype(1)}}, 125 | wantVA: BallotSet{Ballot{5, valtype(1)}, Ballot{5, valtype(2)}, Ballot{5, valtype(3)}, Ballot{20, valtype(1)}}, 126 | }, 127 | { 128 | m: &CommitTopic{B: Ballot{20, valtype(1)}, CN: 10, HN: 30, PN: 15}, 129 | wantA: BallotSet{Ballot{15, valtype(1)}, Ballot{30, valtype(1)}}, 130 | wantVA: BallotSet{Ballot{15, valtype(1)}, Ballot{30, valtype(1)}, Ballot{math.MaxInt32, valtype(1)}}, 131 | }, 132 | { 133 | m: &ExtTopic{C: Ballot{1, valtype(1)}}, 134 | wantA: BallotSet{Ballot{math.MaxInt32, valtype(1)}}, 135 | wantVA: BallotSet{Ballot{math.MaxInt32, valtype(1)}}, 136 | }, 137 | } 138 | for i, tc := range cases { 139 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 140 | e := &Msg{T: tc.m} 141 | got := e.acceptsPreparedSet() 142 | if !reflect.DeepEqual(got, tc.wantA) { 143 | t.Errorf("got acceptsPreparedSet=%v, want %v", got, tc.wantA) 144 | } 145 | got = e.votesOrAcceptsPreparedSet() 146 | if !reflect.DeepEqual(got, tc.wantVA) { 147 | t.Errorf("got votesOrAcceptsPreparedSet=%v, want %v", got, tc.wantVA) 148 | } 149 | }) 150 | } 151 | } 152 | 153 | func TestAcceptsCommit(t *testing.T) { 154 | type want struct { 155 | ok bool 156 | min, max int 157 | } 158 | 159 | cases := []struct { 160 | m Topic 161 | min, max int 162 | a, va want 163 | }{ 164 | { 165 | m: &NomTopic{}, 166 | min: 1, 167 | max: 10, 168 | a: want{ok: false}, 169 | va: want{ok: false}, 170 | }, 171 | { 172 | m: &NomTopic{X: ValueSet{valtype(1)}}, 173 | min: 1, 174 | max: 10, 175 | a: want{ok: false}, 176 | va: want{ok: false}, 177 | }, 178 | { 179 | m: &NomTopic{Y: ValueSet{valtype(1)}}, 180 | min: 1, 181 | max: 10, 182 | a: want{ok: false}, 183 | va: want{ok: false}, 184 | }, 185 | { 186 | m: &PrepTopic{B: Ballot{1, valtype(1)}}, 187 | min: 1, 188 | max: 10, 189 | a: want{ok: false}, 190 | va: want{ok: false}, 191 | }, 192 | { 193 | m: &PrepTopic{B: Ballot{1, valtype(1)}, CN: 3, HN: 7}, 194 | min: 1, 195 | max: 10, 196 | a: want{ok: false}, 197 | va: want{ok: true, min: 3, max: 7}, 198 | }, 199 | { 200 | m: &PrepTopic{B: Ballot{1, valtype(1)}, CN: 3, HN: 7}, 201 | min: 5, 202 | max: 10, 203 | a: want{ok: false}, 204 | va: want{ok: true, min: 5, max: 7}, 205 | }, 206 | { 207 | m: &PrepTopic{B: Ballot{1, valtype(1)}, CN: 7, HN: 20}, 208 | min: 5, 209 | max: 10, 210 | a: want{ok: false}, 211 | va: want{ok: true, min: 7, max: 10}, 212 | }, 213 | { 214 | m: &PrepTopic{B: Ballot{1, valtype(2)}, CN: 3, HN: 7}, 215 | min: 1, 216 | max: 10, 217 | a: want{ok: false}, 218 | va: want{ok: false}, 219 | }, 220 | { 221 | m: &PrepTopic{B: Ballot{1, valtype(2)}, CN: 3, HN: 7}, 222 | min: 5, 223 | max: 10, 224 | a: want{ok: false}, 225 | va: want{ok: false}, 226 | }, 227 | { 228 | m: &PrepTopic{B: Ballot{1, valtype(2)}, CN: 7, HN: 20}, 229 | min: 5, 230 | max: 10, 231 | a: want{ok: false}, 232 | va: want{ok: false}, 233 | }, 234 | { 235 | m: &CommitTopic{B: Ballot{1, valtype(1)}, CN: 15, HN: 20}, 236 | min: 1, 237 | max: 10, 238 | a: want{ok: false}, 239 | va: want{ok: false}, 240 | }, 241 | { 242 | m: &CommitTopic{B: Ballot{1, valtype(1)}, CN: 1, HN: 4}, 243 | min: 5, 244 | max: 10, 245 | a: want{ok: false}, 246 | va: want{ok: true, min: 5, max: 10}, 247 | }, 248 | { 249 | m: &CommitTopic{B: Ballot{1, valtype(1)}, CN: 1, HN: 7}, 250 | min: 5, 251 | max: 10, 252 | a: want{ok: true, min: 5, max: 7}, 253 | va: want{ok: true, min: 5, max: 7}, 254 | }, 255 | { 256 | m: &CommitTopic{B: Ballot{1, valtype(1)}, CN: 4, HN: 12}, 257 | min: 5, 258 | max: 10, 259 | a: want{ok: true, min: 5, max: 10}, 260 | va: want{ok: true, min: 5, max: 10}, 261 | }, 262 | { 263 | m: &CommitTopic{B: Ballot{1, valtype(1)}, CN: 7, HN: 12}, 264 | min: 5, 265 | max: 10, 266 | a: want{ok: true, min: 7, max: 10}, 267 | va: want{ok: true, min: 7, max: 10}, 268 | }, 269 | { 270 | m: &ExtTopic{C: Ballot{5, valtype(1)}}, 271 | min: 1, 272 | max: 10, 273 | a: want{ok: true, min: 5, max: 10}, 274 | va: want{ok: true, min: 5, max: 10}, 275 | }, 276 | { 277 | m: &ExtTopic{C: Ballot{5, valtype(1)}}, 278 | min: 1, 279 | max: 4, 280 | a: want{ok: false}, 281 | va: want{ok: false}, 282 | }, 283 | { 284 | m: &ExtTopic{C: Ballot{5, valtype(1)}}, 285 | min: 6, 286 | max: 10, 287 | a: want{ok: true, min: 6, max: 10}, 288 | va: want{ok: true, min: 6, max: 10}, 289 | }, 290 | { 291 | m: &ExtTopic{C: Ballot{5, valtype(1)}}, 292 | min: 3, 293 | max: 7, 294 | a: want{ok: true, min: 5, max: 7}, 295 | va: want{ok: true, min: 5, max: 7}, 296 | }, 297 | } 298 | for i, tc := range cases { 299 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 300 | e := &Msg{T: tc.m} 301 | gotOK, gotMin, gotMax := e.acceptsCommit(valtype(1), tc.min, tc.max) 302 | if gotOK != tc.a.ok { 303 | t.Errorf("got acceptsCommit=%v, want %v", gotOK, tc.a.ok) 304 | } else if gotOK && (gotMin != tc.a.min || gotMax != tc.a.max) { 305 | t.Errorf("got min %d, max %d, want min %d, max %d", gotMin, gotMax, tc.a.min, tc.a.max) 306 | } 307 | 308 | gotOK, gotMin, gotMax = e.votesOrAcceptsCommit(valtype(1), tc.min, tc.max) 309 | if gotOK != tc.va.ok { 310 | t.Errorf("got votesOrAcceptsCommit=%v, want %v", gotOK, tc.va.ok) 311 | } else if gotOK && (gotMin != tc.va.min || gotMax != tc.va.max) { 312 | t.Errorf("got min %d, max %d, want min %d, max %d", gotMin, gotMax, tc.va.min, tc.va.max) 313 | } 314 | }) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | //go:generate go run genset.go 2 | package scp 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "crypto/sha256" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "math/big" 12 | "math/rand" 13 | "time" 14 | 15 | "github.com/davecgh/go-xdr/xdr" 16 | ) 17 | 18 | type NodeID string 19 | 20 | func (n NodeID) Less(other NodeID) bool { return n < other } 21 | 22 | // Node is the type of a participating SCP node. 23 | type Node struct { 24 | ID NodeID 25 | 26 | // Q is the node's set of quorum slices. 27 | // For compactness, 28 | // this representation does not include the node itself, 29 | // though the node is understood to be in every slice. 30 | Q QSet 31 | 32 | // FP/FQ is a rational giving the odds that a call to Handle will fail (drop the incoming message). 33 | // Self messages (from the node to itself) are never dropped. 34 | // FQ==0 is treated as 0/1. 35 | FP, FQ int 36 | 37 | // mu sync.Mutex 38 | 39 | // pending holds Slot objects during nomination and balloting. 40 | pending map[SlotID]*Slot 41 | 42 | // ext holds externalized values for slots that have completed 43 | // balloting. 44 | ext map[SlotID]*ExtTopic 45 | 46 | cmds *cmdChan 47 | send chan<- *Msg 48 | } 49 | 50 | // NewNode produces a new node. 51 | func NewNode(id NodeID, q QSet, ch chan<- *Msg, ext map[SlotID]*ExtTopic) *Node { 52 | if ext == nil { 53 | ext = make(map[SlotID]*ExtTopic) 54 | } 55 | return &Node{ 56 | ID: id, 57 | Q: q, 58 | pending: make(map[SlotID]*Slot), 59 | ext: ext, 60 | cmds: newCmdChan(), 61 | send: ch, 62 | } 63 | } 64 | 65 | // Run processes incoming events for the node. It returns only when 66 | // its context is canceled and should be launched as a goroutine. 67 | func (n *Node) Run(ctx context.Context) { 68 | var delayUntil *time.Time 69 | for { 70 | cmd, ok := n.cmds.read(ctx) 71 | if !ok { 72 | if ctx.Err() != nil { 73 | n.Logf("context canceled, Run exiting") 74 | } else { 75 | n.Logf("unknown command-reading error, Run exiting") 76 | } 77 | return 78 | } 79 | switch cmd := cmd.(type) { 80 | case *msgCmd: 81 | func() { 82 | if delayUntil != nil { 83 | dur := time.Until(*delayUntil) 84 | if dur > 0 { 85 | time.Sleep(dur) 86 | } 87 | delayUntil = nil 88 | } 89 | err := n.handle(cmd.msg) 90 | if err != nil { 91 | n.Logf("ERROR %s", err) 92 | } 93 | }() 94 | 95 | case *delayCmd: 96 | delayUntil = new(time.Time) 97 | *delayUntil = time.Now().Add(time.Duration(cmd.ms * int(time.Millisecond))) 98 | 99 | case *deferredUpdateCmd: 100 | func() { 101 | cmd.slot.deferredUpdate() 102 | }() 103 | 104 | case *newRoundCmd: 105 | func() { 106 | err := cmd.slot.newRound() 107 | if err != nil { 108 | n.Logf("ERROR %s", err) 109 | } 110 | }() 111 | 112 | case *rehandleCmd: 113 | func() { 114 | for _, msg := range cmd.slot.M { 115 | err := n.handle(msg) 116 | if err != nil { 117 | n.Logf("ERROR %s", err) 118 | } 119 | } 120 | }() 121 | } 122 | } 123 | } 124 | 125 | func (n *Node) deferredUpdate(s *Slot) { 126 | n.cmds.write(&deferredUpdateCmd{slot: s}) 127 | } 128 | 129 | func (n *Node) newRound(s *Slot) { 130 | n.cmds.write(&newRoundCmd{slot: s}) 131 | } 132 | 133 | func (n *Node) rehandle(s *Slot) { 134 | n.cmds.write(&rehandleCmd{slot: s}) 135 | } 136 | 137 | // Handle queues an incoming protocol message. When processed it will 138 | // send a protocol message in response on n.send unless the incoming 139 | // message is ignored. (A message is ignored if it's invalid, 140 | // redundant, or older than another message already received from the 141 | // same sender.) 142 | func (n *Node) Handle(msg *Msg) { 143 | if msg.V != n.ID && n.FQ > 0 && n.FP < n.FQ { 144 | // decide whether to simulate dropping this message 145 | if rand.Intn(n.FQ) < n.FP { 146 | n.Logf("dropping message %s", msg) 147 | return 148 | } 149 | } 150 | n.cmds.write(&msgCmd{msg: msg}) 151 | } 152 | 153 | // Delay simulates a network delay. 154 | func (n *Node) Delay(ms int) { 155 | n.cmds.write(&delayCmd{ms: ms}) 156 | } 157 | 158 | func (n *Node) handle(msg *Msg) error { 159 | if topic, ok := n.ext[msg.I]; ok { 160 | // This node has already externalized a value for the given slot. 161 | // Send an EXTERNALIZE message outbound, unless the inbound 162 | // message is also EXTERNALIZE. 163 | if inTopic, ok := msg.T.(*ExtTopic); ok { 164 | // Double check that the inbound EXTERNALIZE value agrees with 165 | // this node. 166 | if !ValueEqual(inTopic.C.X, topic.C.X) { 167 | n.Logf("inbound message %s disagrees with externalized value %s!", msg, topic.C.X) 168 | panic("consensus failure") 169 | } 170 | } else { 171 | n.send <- NewMsg(n.ID, msg.I, n.Q, topic) 172 | } 173 | return nil 174 | } 175 | 176 | s, ok := n.pending[msg.I] 177 | if !ok { 178 | var err error 179 | s, err = newSlot(msg.I, n) 180 | if err != nil { 181 | panic(fmt.Sprintf("cannot create slot %d: %s", msg.I, err)) 182 | } 183 | n.pending[msg.I] = s 184 | } 185 | 186 | outbound, err := s.handle(msg) 187 | if err != nil { 188 | // delete(n.Pending, msg.I) // xxx ? 189 | return err 190 | } 191 | 192 | if outbound == nil { 193 | return nil 194 | } 195 | 196 | if extTopic, ok := outbound.T.(*ExtTopic); ok { 197 | // Handling the inbound message resulted in externalizing a value. 198 | // We can now save the EXTERNALIZE message and get rid of the Slot 199 | // object. 200 | n.ext[msg.I] = extTopic 201 | delete(n.pending, msg.I) 202 | } 203 | 204 | n.send <- outbound 205 | return nil 206 | } 207 | 208 | func (n *Node) ping() error { 209 | for _, s := range n.pending { 210 | for _, msg := range s.M { 211 | err := n.handle(msg) 212 | if err != nil { 213 | return err 214 | } 215 | } 216 | } 217 | return nil 218 | } 219 | 220 | // ErrNoPrev occurs when trying to compute a hash (with Node.G) for 221 | // slot i before the node has externalized a value for slot i-1. 222 | var ErrNoPrev = errors.New("no previous value") 223 | 224 | // G produces a node- and slot-specific 32-byte hash for a given 225 | // message m. It is an error to call this on slot i>1 before n has 226 | // externalized a value for slot i-1. 227 | func (n *Node) G(i SlotID, m []byte) (result [32]byte, err error) { // xxx unexport 228 | hasher := sha256.New() 229 | 230 | var prevValBytes []byte 231 | if i > 1 { 232 | topic, ok := n.ext[i-1] 233 | if !ok { 234 | return result, ErrNoPrev 235 | } 236 | prevValBytes = topic.C.X.Bytes() 237 | } 238 | 239 | r, _ := xdr.Marshal(i) 240 | hasher.Write(r) 241 | hasher.Write(prevValBytes) 242 | hasher.Write(m) 243 | hasher.Sum(result[:0]) 244 | 245 | return result, nil 246 | } 247 | 248 | // Weight returns the fraction of n's quorum slices in which id 249 | // appears. Return value is the fraction and (as an optimization) a 250 | // bool indicating whether it's exactly 1. 251 | func (n *Node) Weight(id NodeID) (float64, bool) { 252 | if id == n.ID { 253 | return 1.0, true 254 | } 255 | return n.Q.weight(id) 256 | } 257 | 258 | // Peers returns a flattened, uniquified list of the node IDs in n's 259 | // quorum slices, not including n's own ID. 260 | func (n *Node) Peers() NodeIDSet { 261 | return n.Q.Nodes() 262 | } 263 | 264 | // Neighbors produces a deterministic subset of a node's peers (which 265 | // may include itself) that is specific to a given slot and 266 | // nomination-round. 267 | func (n *Node) Neighbors(i SlotID, num int) (NodeIDSet, error) { 268 | peers := n.Peers() 269 | peers = peers.Add(n.ID) 270 | var result NodeIDSet 271 | for _, nodeID := range peers { 272 | weight64, is1 := n.Weight(nodeID) 273 | var hwBytes []byte 274 | if is1 { 275 | hwBytes = maxUint256[:] 276 | } else { 277 | w := big.NewFloat(weight64) 278 | w.Mul(w, hmax) 279 | hwInt, _ := w.Int(nil) 280 | hwBytes = hwInt.Bytes() 281 | } 282 | var hw [32]byte 283 | copy(hw[32-len(hwBytes):], hwBytes) // hw is now a big-endian uint256 284 | 285 | m := new(bytes.Buffer) 286 | m.WriteByte('N') 287 | numBytes, _ := xdr.Marshal(num) 288 | m.Write(numBytes) 289 | m.WriteString(string(nodeID)) 290 | g, err := n.G(i, m.Bytes()) 291 | if err != nil { 292 | return nil, err 293 | } 294 | if bytes.Compare(g[:], hw[:]) < 0 { 295 | result = result.Add(nodeID) 296 | } 297 | } 298 | return result, nil 299 | } 300 | 301 | // Priority computes a priority for a given peer node that is specific 302 | // to a given slot and nomination-round. The result is a big-endian 303 | // 256-bit integer expressed as a [32]byte. 304 | func (n *Node) Priority(i SlotID, num int, nodeID NodeID) ([32]byte, error) { 305 | m := new(bytes.Buffer) 306 | m.WriteByte('P') 307 | numBytes, _ := xdr.Marshal(num) 308 | m.Write(numBytes) 309 | m.WriteString(string(nodeID)) 310 | return n.G(i, m.Bytes()) 311 | } 312 | 313 | // AllKnown gives the complete set of reachable node IDs, 314 | // excluding n.ID. 315 | func (n *Node) AllKnown() NodeIDSet { 316 | result := n.Peers() 317 | for _, s := range n.pending { 318 | for _, msg := range s.M { 319 | result = result.Union(msg.Q.Nodes()) 320 | } 321 | } 322 | result = result.Remove(n.ID) 323 | return result 324 | } 325 | 326 | // HighestExt returns the ID of the highest slot for which this node 327 | // has an externalized value. 328 | func (n *Node) HighestExt() SlotID { 329 | var result SlotID 330 | for slotID := range n.ext { 331 | if slotID > result { 332 | result = slotID 333 | } 334 | } 335 | return result 336 | } 337 | 338 | // MsgsSince returns all this node's messages with slotID > since. 339 | // TODO: need a better interface, this list could get hella big. 340 | func (n *Node) MsgsSince(since SlotID) []*Msg { 341 | var result []*Msg 342 | 343 | for slotID, topic := range n.ext { 344 | if slotID <= since { 345 | continue 346 | } 347 | msg := &Msg{ 348 | V: n.ID, 349 | I: slotID, 350 | Q: n.Q, 351 | T: topic, 352 | } 353 | result = append(result, msg) 354 | } 355 | for slotID, slot := range n.pending { 356 | if slotID <= since { 357 | continue 358 | } 359 | result = append(result, slot.Msg()) 360 | } 361 | return result 362 | } 363 | 364 | // Logf produces log output prefixed with the node's identity. 365 | func (n *Node) Logf(f string, a ...interface{}) { 366 | f = "node %s: " + f 367 | a = append([]interface{}{n.ID}, a...) 368 | log.Printf(f, a...) 369 | } 370 | 371 | var maxUint256 = [32]byte{ 372 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 373 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 374 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 375 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 376 | } 377 | 378 | // maxuint256, as a float 379 | var hmax *big.Float 380 | 381 | func init() { 382 | hmaxInt := new(big.Int) 383 | hmaxInt.SetBytes(maxUint256[:]) 384 | hmax = new(big.Float) 385 | hmax.SetInt(hmaxInt) 386 | } 387 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestPeers(t *testing.T) { 11 | cases := []struct { 12 | slices []string 13 | want string 14 | }{ 15 | {}, 16 | { 17 | slices: []string{"a"}, 18 | want: "a", 19 | }, 20 | { 21 | slices: []string{"a", "a"}, 22 | want: "a", 23 | }, 24 | { 25 | slices: []string{"a b", "a c"}, 26 | want: "a b c", 27 | }, 28 | { 29 | slices: []string{"a b", "c d"}, 30 | want: "a b c d", 31 | }, 32 | } 33 | for i, tc := range cases { 34 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 35 | var q []NodeIDSet 36 | for _, slice := range tc.slices { 37 | ns := toNodeIDSet(slice) 38 | q = append(q, ns) 39 | } 40 | ch := make(chan *Msg) 41 | n := NewNode("x", slicesToQSet(q), ch, nil) 42 | got := n.Peers() 43 | want := toNodeIDSet(tc.want) 44 | if !reflect.DeepEqual(got, NodeIDSet(want)) { 45 | t.Errorf("got %v, want %v", got, want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestWeight(t *testing.T) { 52 | cases := []struct { 53 | slices []string 54 | wantW float64 55 | wantIs1 bool 56 | }{ 57 | { 58 | slices: []string{"a"}, 59 | }, 60 | { 61 | slices: []string{"a", "b"}, 62 | }, 63 | { 64 | slices: []string{"a b", "a z"}, 65 | wantW: 0.5, 66 | }, 67 | { 68 | slices: []string{"a b", "a c", "a d", "a z"}, 69 | wantW: 0.25, 70 | wantIs1: false, 71 | }, 72 | } 73 | for i, tc := range cases { 74 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 75 | var q []NodeIDSet 76 | for _, slice := range tc.slices { 77 | ns := toNodeIDSet(slice) 78 | q = append(q, ns) 79 | } 80 | ch := make(chan *Msg) 81 | n := NewNode("x", slicesToQSet(q), ch, nil) 82 | _, is1 := n.Weight(n.ID) 83 | if !is1 { 84 | t.Errorf("got !is1, want is1 for n.Weight(n.ID)") 85 | } 86 | got, is1 := n.Weight("z") 87 | if got != tc.wantW || is1 != tc.wantIs1 { 88 | t.Errorf("got %f (%v), want %f (%v)", got, is1, tc.wantW, tc.wantIs1) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | func toNodeIDSet(s string) NodeIDSet { 95 | var result NodeIDSet 96 | fields := strings.Fields(s) 97 | for _, f := range fields { 98 | result = result.Add(NodeID(f)) 99 | } 100 | return result 101 | } 102 | 103 | func slicesToQSet(slices []NodeIDSet) QSet { 104 | result := QSet{T: 1} 105 | for _, slice := range slices { 106 | sub := QSet{T: len(slice)} 107 | for _, nodeID := range slice { 108 | nodeID := nodeID 109 | sub.M = append(sub.M, QSetMember{N: &nodeID}) 110 | } 111 | result.M = append(result.M, QSetMember{Q: &sub}) 112 | } 113 | return result 114 | } 115 | -------------------------------------------------------------------------------- /qset.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/big" 7 | ) 8 | 9 | type ( 10 | // QSet is a compact representation for a set of quorum slices. 11 | // A quorum slice is any T items from M, 12 | // where 0 < T <= len(M). 13 | // An item in M is either a node or a nested QSet. 14 | // If the latter, 15 | // any of the recursively defined subslices count as one "item" here. 16 | QSet struct { 17 | T int `json:"threshold"` 18 | M []QSetMember `json:"members"` 19 | } 20 | 21 | // QSetMember is a member of a QSet. 22 | // It's either a node ID or a nested QSet. 23 | // Exactly one of its fields is non-nil. 24 | QSetMember struct { 25 | N *NodeID `json:"node_id,omitempty"` 26 | Q *QSet `json:"qset,omitempty"` 27 | } 28 | ) 29 | 30 | // Checks that at least one node in each quorum slice satisfies pred 31 | // (excluding the slot's node). 32 | // 33 | // Works by finding len(q.M)-q.T+1 members for which pred is true 34 | func (q QSet) findBlockingSet(msgs map[NodeID]*Msg, pred predicate) (NodeIDSet, predicate) { 35 | return findBlockingSetHelper(len(q.M)-q.T+1, q.M, msgs, pred, nil) 36 | } 37 | 38 | func findBlockingSetHelper(needed int, members []QSetMember, msgs map[NodeID]*Msg, pred predicate, sofar NodeIDSet) (NodeIDSet, predicate) { 39 | if needed == 0 { 40 | return sofar, pred 41 | } 42 | if needed > len(members) { 43 | return nil, pred 44 | } 45 | m0 := members[0] 46 | switch { 47 | case m0.N != nil: 48 | if msg, ok := msgs[*m0.N]; ok { 49 | if nextPred := pred.test(msg); nextPred != nil { 50 | return findBlockingSetHelper(needed-1, members[1:], msgs, nextPred, sofar.Add(*m0.N)) 51 | } 52 | } 53 | 54 | case m0.Q != nil: 55 | sofar2, pred2 := findBlockingSetHelper(len(m0.Q.M)-m0.Q.T+1, m0.Q.M, msgs, pred, sofar) 56 | if len(sofar2) > 0 { 57 | return findBlockingSetHelper(needed-1, members[1:], msgs, pred2, sofar2) 58 | } 59 | } 60 | return findBlockingSetHelper(needed, members[1:], msgs, pred, sofar) 61 | } 62 | 63 | // Finds a quorum in which every node satisfies the given 64 | // predicate. The slot's node itself is presumed to satisfy the 65 | // predicate. 66 | func (q QSet) findQuorum(nodeID NodeID, m map[NodeID]*Msg, pred predicate) (NodeIDSet, predicate) { 67 | return findQuorumHelper(q.T, q.M, m, pred, NodeIDSet{nodeID}) 68 | } 69 | 70 | func findQuorumHelper(threshold int, members []QSetMember, msgs map[NodeID]*Msg, pred predicate, sofar NodeIDSet) (NodeIDSet, predicate) { 71 | if threshold == 0 { 72 | return sofar, pred 73 | } 74 | if threshold > len(members) { 75 | return nil, pred 76 | } 77 | m0 := members[0] 78 | switch { 79 | case m0.N != nil: 80 | if sofar.Contains(*m0.N) { 81 | return findQuorumHelper(threshold-1, members[1:], msgs, pred, sofar) 82 | } 83 | if msg, ok := msgs[*m0.N]; ok { 84 | if nextPred := pred.test(msg); nextPred != nil { 85 | sofar2, pred2 := findQuorumHelper(msg.Q.T, msg.Q.M, msgs, nextPred, sofar.Add(*m0.N)) 86 | if len(sofar2) > 0 { 87 | return findQuorumHelper(threshold-1, members[1:], msgs, pred2, sofar2) 88 | } 89 | } 90 | } 91 | 92 | case m0.Q != nil: 93 | sofar2, pred2 := findQuorumHelper(m0.Q.T, m0.Q.M, msgs, pred, sofar) 94 | if len(sofar2) > 0 { 95 | return findQuorumHelper(threshold-1, members[1:], msgs, pred2, sofar2) 96 | } 97 | } 98 | return findQuorumHelper(threshold, members[1:], msgs, pred, sofar) 99 | } 100 | 101 | // Function weight returns the fraction of q's quorum slices in which id appears. 102 | // Return value is the fraction and 103 | // (as an optimization) 104 | // a bool indicating whether it's exactly 1. 105 | func (q QSet) weight(id NodeID) (float64, bool) { 106 | num, denom := q.NodeFrac(id) 107 | if num == denom { 108 | return 1.0, true 109 | } 110 | return float64(num) / float64(denom), false 111 | } 112 | 113 | // Slices calls f once for each slice represented by q. 114 | // It continues until all slices have been generated or f returns false to terminate early. 115 | func (q QSet) Slices(f func(NodeIDSet) bool) { 116 | slicesHelper(q.T, q.M, f, nil, 0) 117 | } 118 | 119 | func slicesHelper(t int, members []QSetMember, f func(NodeIDSet) bool, sofar NodeIDSet, depth int) (out bool) { 120 | if t == 0 { 121 | return f(sofar) 122 | } 123 | if t > len(members) { 124 | return true 125 | } 126 | 127 | m0 := members[0] 128 | switch { 129 | case m0.N != nil: 130 | if !slicesHelper(t-1, members[1:], f, append(sofar, *m0.N), depth+1) { 131 | return false 132 | } 133 | 134 | case m0.Q != nil: 135 | ok := slicesHelper( 136 | m0.Q.T, 137 | m0.Q.M, 138 | func(slice NodeIDSet) bool { 139 | return slicesHelper(t-1, members[1:], f, sofar.Union(slice), depth+1) 140 | }, 141 | sofar, 142 | depth+1, 143 | ) 144 | if !ok { 145 | return false 146 | } 147 | } 148 | return slicesHelper(t, members[1:], f, sofar, depth+1) 149 | } 150 | 151 | // Nodes returns a flattened set of all nodes contained in q and its nested QSets. 152 | func (q QSet) Nodes() NodeIDSet { 153 | var result NodeIDSet 154 | 155 | for _, m := range q.M { 156 | switch { 157 | case m.N != nil: 158 | result = result.Add(*m.N) 159 | 160 | case m.Q != nil: 161 | result = result.Union(m.Q.Nodes()) 162 | } 163 | } 164 | 165 | return result 166 | } 167 | 168 | func (q QSet) NumSlices() *big.Int { 169 | result := new(big.Int) 170 | result.Binomial(int64(len(q.M)), int64(q.T)) 171 | for _, m := range q.M { 172 | if m.Q != nil { 173 | result.Mul(result, m.Q.NumSlices()) 174 | } 175 | } 176 | return result 177 | } 178 | 179 | // NodeFrac gives the fraction of slices in q containing the given node. 180 | // It assumes that id appears in at most one QSet 181 | // (either the top level one or a single reachable nested one) 182 | // and then only once in that QSet. 183 | func (q QSet) NodeFrac(id NodeID) (num, denom int) { 184 | for _, m := range q.M { 185 | switch { 186 | case m.N != nil: 187 | if *m.N == id { 188 | return q.T, len(q.M) 189 | } 190 | 191 | case m.Q != nil: 192 | num2, denom2 := m.Q.NodeFrac(id) 193 | if num2 > 0 { 194 | return q.T * num2, len(q.M) * denom2 195 | } 196 | } 197 | } 198 | return 0, 1 199 | } 200 | 201 | func (m QSetMember) String() string { 202 | switch { 203 | case m.N != nil: 204 | return fmt.Sprintf("N:%s", *m.N) 205 | 206 | case m.Q != nil: 207 | b := new(bytes.Buffer) 208 | fmt.Fprintf(b, "Q:{T=%d [", m.Q.T) 209 | for i, mm := range m.Q.M { 210 | if i > 0 { 211 | b.WriteByte(' ') 212 | } 213 | b.WriteString(mm.String()) 214 | } 215 | fmt.Fprint(b, "]}") 216 | return b.String() 217 | } 218 | return "" 219 | } 220 | -------------------------------------------------------------------------------- /quorum.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | // This file contains functions for finding "blocking sets" and 4 | // "quorums" that satisfy a given predicate. 5 | // 6 | // Each node specifies one or more "quorum slices." Each quorum slice 7 | // is a set of trusted peer nodes. Each quorum slice conceptually 8 | // includes the node itself, though in this implementation that is not 9 | // explicit. 10 | // 11 | // A quorum slice is not necessarily a quorum in itself. A peer in a 12 | // quorum slice may have a dependency on a third-party node, as may 13 | // that node, and so on. A quorum (with respect to a given node) is 14 | // thus the transitive closure over any of its quorum slices. A node 15 | // may have many different quorums, and they may overlap one another. 16 | // 17 | // Every protocol message includes the sending node's set of quorum 18 | // slices. Every node saves the latest message seen from a given 19 | // node. If enough messages have been seen, it is possible for a node 20 | // to know the complete membership of one or more quorums. 21 | // 22 | // A "blocking set" is related to the idea of a quorum, but is 23 | // simpler. It's any set of peers among a node's quorum slices that 24 | // blocks the possibility of a quorum. A blocking set satisfying 25 | // statement X precludes the existence of any quorum satisfying !X. A 26 | // single peer from each of a node's quorum slices is sufficient to 27 | // form a blocking set. 28 | 29 | // Checks that at least one node in each quorum slice satisfies pred 30 | // (excluding the slot's node). 31 | func (s *Slot) findBlockingSet(pred predicate) NodeIDSet { 32 | res, _ := s.V.Q.findBlockingSet(s.M, pred) 33 | return res 34 | } 35 | 36 | // Finds a quorum in which every node satisfies the given 37 | // predicate. The slot's node itself is presumed to satisfy the 38 | // predicate. 39 | func (s *Slot) findQuorum(pred predicate) NodeIDSet { 40 | res, _ := s.V.Q.findQuorum(s.V.ID, s.M, pred) 41 | return res 42 | } 43 | 44 | // Tells whether a statement can be accepted, either because a 45 | // blocking set accepts, or because a quorum votes-or-accepts it. The 46 | // function f should produce an "accepts" predicate when its argument 47 | // is false and a "votes-or-accepts" predicate when its argument is 48 | // true. 49 | func (s *Slot) accept(f func(bool) predicate) NodeIDSet { 50 | // 1. If s's node accepts the statement, 51 | // we're done 52 | // (since it is its own blocking set and, 53 | // more intuitively, 54 | // node N can accept X if N already accepts X). 55 | acceptsPred := f(false) 56 | if s.sent != nil && acceptsPred.test(s.sent) != nil { 57 | return NodeIDSet{s.V.ID} 58 | } 59 | 60 | // 2. Look for a blocking set apart from s.V that accepts. 61 | nodeIDs := s.findBlockingSet(acceptsPred) 62 | if len(nodeIDs) > 0 { 63 | return nodeIDs 64 | } 65 | 66 | // 3. Look for a quorum that votes-or-accepts. 67 | // The quorum necessarily includes s's node. 68 | votesOrAcceptsPred := f(true) 69 | if s.sent == nil || votesOrAcceptsPred.test(s.sent) == nil { 70 | return nil 71 | } 72 | return s.findQuorum(votesOrAcceptsPred) 73 | } 74 | 75 | // Abstract predicate. Concrete types below. 76 | type predicate interface { 77 | // Tests whether a node's latest message satisfies this predicate. 78 | // If it does not, the return value must be nil. 79 | // If it does, the return value should be the predicate, 80 | // or an updated copy of the predicate for use in a subsequent call to test. 81 | // The original predicate should not change, because when findQuorum needs to backtrack, 82 | // it also unwinds to earlier values of the predicate. 83 | test(*Msg) predicate 84 | } 85 | 86 | // This is a simple function predicate. It does not change from one 87 | // call to the next. 88 | type fpred func(*Msg) bool 89 | 90 | func (f fpred) test(msg *Msg) predicate { 91 | if f(msg) { 92 | return f 93 | } 94 | return nil 95 | } 96 | 97 | // This is a predicate that can narrow a set of values as it traverses 98 | // nodes. 99 | type valueSetPred struct { 100 | vals ValueSet 101 | finalVals *ValueSet 102 | testfn func(*Msg, ValueSet) ValueSet 103 | } 104 | 105 | func (p *valueSetPred) test(msg *Msg) predicate { 106 | if len(p.vals) == 0 { 107 | return nil 108 | } 109 | nextVals := p.testfn(msg, p.vals) 110 | if len(nextVals) == 0 { 111 | return nil 112 | } 113 | if p.finalVals != nil { 114 | *p.finalVals = nextVals 115 | } 116 | return &valueSetPred{ 117 | vals: nextVals, 118 | finalVals: p.finalVals, 119 | testfn: p.testfn, 120 | } 121 | } 122 | 123 | // This is a predicate that can narrow a set of ballots as it traverses 124 | // nodes. 125 | type ballotSetPred struct { 126 | ballots BallotSet 127 | finalBallots *BallotSet 128 | testfn func(*Msg, BallotSet) BallotSet 129 | } 130 | 131 | func (p *ballotSetPred) test(msg *Msg) predicate { 132 | if len(p.ballots) == 0 { 133 | return nil 134 | } 135 | nextBallots := p.testfn(msg, p.ballots) 136 | if len(nextBallots) == 0 { 137 | return nil 138 | } 139 | if p.finalBallots != nil { 140 | *p.finalBallots = nextBallots 141 | } 142 | return &ballotSetPred{ 143 | ballots: nextBallots, 144 | finalBallots: p.finalBallots, 145 | testfn: p.testfn, 146 | } 147 | } 148 | 149 | // This is a predicate that can narrow a set of min/max bounds as it 150 | // traverses nodes. 151 | type minMaxPred struct { 152 | min, max int // the current min/max bounds 153 | finalMin, finalMax *int // each call to next updates the min/max bounds these point to 154 | testfn func(msg *Msg, min, max int) (bool, int, int) 155 | } 156 | 157 | func (p *minMaxPred) test(msg *Msg) predicate { 158 | if p.min > p.max { 159 | return nil 160 | } 161 | res, min, max := p.testfn(msg, p.min, p.max) 162 | if !res { 163 | return nil 164 | } 165 | nextMin, nextMax := min, max 166 | if p.finalMin != nil { 167 | *p.finalMin = nextMin 168 | } 169 | if p.finalMax != nil { 170 | *p.finalMax = nextMax 171 | } 172 | return &minMaxPred{ 173 | min: nextMin, 174 | max: nextMax, 175 | finalMin: p.finalMin, 176 | finalMax: p.finalMax, 177 | testfn: p.testfn, 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /quorum_test.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | "unicode" 10 | ) 11 | 12 | func TestFindBlockingSet(t *testing.T) { 13 | cases := []struct { 14 | network string 15 | msgs string 16 | want string 17 | }{ 18 | { 19 | network: "x(a b) a(b x) b(a x)", 20 | msgs: "a b x", 21 | want: "", 22 | }, 23 | { 24 | network: "x(a z) a(z x) z(a x)", 25 | msgs: "a b z", 26 | want: "z", 27 | }, 28 | { 29 | network: "x(a z / b) a(z x) b(a x) z(a x)", 30 | msgs: "a b z", 31 | want: "", 32 | }, 33 | { 34 | network: "x(a z / b z) a(z x) b(a x) z(a x)", 35 | msgs: "a b z", 36 | want: "z", 37 | }, 38 | } 39 | for i, tc := range cases { 40 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 41 | network := toNetwork(tc.network) 42 | ch := make(chan *Msg) 43 | node := NewNode("x", slicesToQSet(network["x"]), ch, nil) 44 | slot, _ := newSlot(1, node) 45 | for _, vstr := range strings.Fields(tc.msgs) { 46 | v := NodeID(vstr) 47 | slot.M[v] = &Msg{ 48 | V: v, 49 | I: 1, 50 | Q: slicesToQSet(network[v]), 51 | } 52 | } 53 | got := slot.findBlockingSet(fpred(func(msg *Msg) bool { 54 | return strings.Contains(string(msg.V), "z") 55 | })) 56 | want := toNodeIDSet(tc.want) 57 | if !reflect.DeepEqual(got, want) { 58 | t.Errorf("got %v, want %v", got, want) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestFindQuorum(t *testing.T) { 65 | cases := []struct { 66 | id NodeID 67 | q QSet 68 | m map[NodeID]*Msg 69 | want NodeIDSet 70 | }{ 71 | { 72 | id: "x", 73 | q: QSet{T: 0}, 74 | m: map[NodeID]*Msg{}, 75 | want: NodeIDSet{"x"}, 76 | }, 77 | { 78 | id: "x", 79 | q: QSet{ 80 | T: 1, 81 | M: []QSetMember{ 82 | {N: nodeIDPtr("x1")}, 83 | {N: nodeIDPtr("y1")}, 84 | }, 85 | }, 86 | m: map[NodeID]*Msg{ 87 | "x1": &Msg{ 88 | V: "x1", 89 | Q: QSet{T: 0}, 90 | }, 91 | "y1": &Msg{ 92 | V: "y1", 93 | Q: QSet{T: 0}, 94 | }, 95 | }, 96 | want: NodeIDSet{"x", "x1"}, 97 | }, 98 | { 99 | id: "x", 100 | q: QSet{ 101 | T: 1, 102 | M: []QSetMember{ 103 | {N: nodeIDPtr("x1")}, 104 | {N: nodeIDPtr("y1")}, 105 | }, 106 | }, 107 | m: map[NodeID]*Msg{ 108 | "x1": &Msg{ 109 | V: "x1", 110 | Q: QSet{ 111 | T: 1, 112 | M: []QSetMember{ 113 | {N: nodeIDPtr("y1")}, 114 | {N: nodeIDPtr("z1")}, 115 | }, 116 | }, 117 | }, 118 | "y1": &Msg{ 119 | V: "y1", 120 | Q: QSet{T: 0}, 121 | }, 122 | }, 123 | want: nil, 124 | }, 125 | { 126 | id: "x", 127 | q: QSet{ 128 | T: 2, 129 | M: []QSetMember{ 130 | {N: nodeIDPtr("x1")}, 131 | {N: nodeIDPtr("y1")}, 132 | {N: nodeIDPtr("x2")}, 133 | }, 134 | }, 135 | m: map[NodeID]*Msg{ 136 | "x1": &Msg{ 137 | V: "x1", 138 | Q: QSet{T: 0}, 139 | }, 140 | "y1": &Msg{ 141 | V: "y1", 142 | Q: QSet{T: 0}, 143 | }, 144 | "x2": &Msg{ 145 | V: "x2", 146 | Q: QSet{T: 0}, 147 | }, 148 | }, 149 | want: NodeIDSet{"x", "x1", "x2"}, 150 | }, 151 | { 152 | id: "x", 153 | q: QSet{ 154 | T: 2, 155 | M: []QSetMember{ 156 | { 157 | Q: &QSet{ 158 | T: 1, 159 | M: []QSetMember{ 160 | {N: nodeIDPtr("x1")}, 161 | {N: nodeIDPtr("y1")}, 162 | }, 163 | }, 164 | }, 165 | { 166 | Q: &QSet{ 167 | T: 1, 168 | M: []QSetMember{ 169 | {N: nodeIDPtr("x2")}, 170 | {N: nodeIDPtr("y2")}, 171 | }, 172 | }, 173 | }, 174 | }, 175 | }, 176 | m: map[NodeID]*Msg{ 177 | "x1": &Msg{ 178 | V: "x1", 179 | Q: QSet{T: 0}, 180 | }, 181 | "y1": &Msg{ 182 | V: "y1", 183 | Q: QSet{T: 0}, 184 | }, 185 | "x2": &Msg{ 186 | V: "x2", 187 | Q: QSet{T: 0}, 188 | }, 189 | }, 190 | want: NodeIDSet{"x", "x1", "x2"}, 191 | }, 192 | { 193 | id: "x", 194 | q: QSet{ 195 | T: 2, 196 | M: []QSetMember{ 197 | {N: nodeIDPtr("x1")}, 198 | {N: nodeIDPtr("y2")}, 199 | { 200 | Q: &QSet{ 201 | T: 2, 202 | M: []QSetMember{ 203 | {N: nodeIDPtr("x3")}, 204 | {N: nodeIDPtr("y4")}, 205 | {N: nodeIDPtr("y5")}, 206 | }, 207 | }, 208 | }, 209 | { 210 | Q: &QSet{ 211 | T: 2, 212 | M: []QSetMember{ 213 | {N: nodeIDPtr("x6")}, 214 | {N: nodeIDPtr("y7")}, 215 | {N: nodeIDPtr("x8")}, 216 | }, 217 | }, 218 | }, 219 | {N: nodeIDPtr("y9")}, 220 | }, 221 | }, 222 | m: map[NodeID]*Msg{ 223 | "x1": &Msg{V: "x1", Q: QSet{T: 0}}, 224 | "y2": &Msg{V: "y2", Q: QSet{T: 0}}, 225 | "x3": &Msg{V: "x3", Q: QSet{T: 0}}, 226 | "y4": &Msg{V: "y4", Q: QSet{T: 0}}, 227 | "y5": &Msg{V: "y5", Q: QSet{T: 0}}, 228 | "x6": &Msg{V: "x6", Q: QSet{T: 0}}, 229 | "y7": &Msg{V: "y7", Q: QSet{T: 0}}, 230 | "x8": &Msg{V: "x8", Q: QSet{T: 0}}, 231 | "y9": &Msg{V: "y9", Q: QSet{T: 0}}, 232 | }, 233 | want: NodeIDSet{"x", "x1", "x6", "x8"}, 234 | }, 235 | { 236 | id: "x", 237 | q: QSet{ 238 | T: 2, 239 | M: []QSetMember{ 240 | {N: nodeIDPtr("x1")}, 241 | {N: nodeIDPtr("y2")}, 242 | { 243 | Q: &QSet{ 244 | T: 2, 245 | M: []QSetMember{ 246 | {N: nodeIDPtr("x3")}, 247 | {N: nodeIDPtr("y4")}, 248 | {N: nodeIDPtr("y5")}, 249 | }, 250 | }, 251 | }, 252 | { 253 | Q: &QSet{ 254 | T: 2, 255 | M: []QSetMember{ 256 | {N: nodeIDPtr("x6")}, 257 | {N: nodeIDPtr("y7")}, 258 | {N: nodeIDPtr("x8")}, 259 | }, 260 | }, 261 | }, 262 | {N: nodeIDPtr("y9")}, 263 | }, 264 | }, 265 | m: map[NodeID]*Msg{ 266 | "x1": &Msg{V: "x1", Q: QSet{T: 0}}, 267 | "y2": &Msg{V: "y2", Q: QSet{T: 0}}, 268 | "x3": &Msg{V: "x3", Q: QSet{T: 0}}, 269 | "y4": &Msg{V: "y4", Q: QSet{T: 0}}, 270 | "y5": &Msg{V: "y5", Q: QSet{T: 0}}, 271 | "x6": &Msg{ 272 | V: "x6", 273 | Q: QSet{ 274 | T: 1, 275 | M: []QSetMember{ 276 | {N: nodeIDPtr("x3")}, 277 | {N: nodeIDPtr("y4")}, 278 | }, 279 | }, 280 | }, 281 | "y7": &Msg{V: "y7", Q: QSet{T: 0}}, 282 | "x8": &Msg{V: "x8", Q: QSet{T: 0}}, 283 | "y9": &Msg{V: "y9", Q: QSet{T: 0}}, 284 | }, 285 | want: NodeIDSet{"x", "x1", "x3", "x6", "x8"}, 286 | }, 287 | { 288 | id: "x", 289 | q: QSet{ 290 | T: 1, 291 | M: []QSetMember{ 292 | { 293 | Q: &QSet{ 294 | T: 2, 295 | M: []QSetMember{ 296 | {N: nodeIDPtr("x1")}, 297 | {N: nodeIDPtr("y1")}, 298 | }, 299 | }, 300 | }, 301 | { 302 | Q: &QSet{ 303 | T: 2, 304 | M: []QSetMember{ 305 | {N: nodeIDPtr("x2")}, 306 | {N: nodeIDPtr("y2")}, 307 | }, 308 | }, 309 | }, 310 | }, 311 | }, 312 | m: map[NodeID]*Msg{ 313 | "x1": &Msg{ 314 | V: "x1", 315 | Q: QSet{ 316 | T: 2, 317 | M: []QSetMember{ 318 | {N: nodeIDPtr("x")}, 319 | {N: nodeIDPtr("y1")}, 320 | }, 321 | }, 322 | }, 323 | "y1": &Msg{ 324 | V: "y1", 325 | Q: QSet{ 326 | T: 2, 327 | M: []QSetMember{ 328 | {N: nodeIDPtr("x")}, 329 | {N: nodeIDPtr("x1")}, 330 | }, 331 | }, 332 | }, 333 | "x2": &Msg{ 334 | V: "x2", 335 | Q: QSet{ 336 | T: 2, 337 | M: []QSetMember{ 338 | {N: nodeIDPtr("x")}, 339 | {N: nodeIDPtr("y2")}, 340 | }, 341 | }, 342 | }, 343 | "y2": &Msg{ 344 | V: "y2", 345 | Q: QSet{ 346 | T: 2, 347 | M: []QSetMember{ 348 | {N: nodeIDPtr("x")}, 349 | {N: nodeIDPtr("x2")}, 350 | }, 351 | }, 352 | }, 353 | }, 354 | want: nil, 355 | }, 356 | { 357 | id: "x", 358 | q: QSet{ 359 | T: 1, 360 | M: []QSetMember{ 361 | { 362 | Q: &QSet{ 363 | T: 2, 364 | M: []QSetMember{ 365 | {N: nodeIDPtr("x1")}, 366 | {N: nodeIDPtr("x2")}, 367 | }, 368 | }, 369 | }, 370 | { 371 | Q: &QSet{ 372 | T: 2, 373 | M: []QSetMember{ 374 | {N: nodeIDPtr("y1")}, 375 | {N: nodeIDPtr("y2")}, 376 | }, 377 | }, 378 | }, 379 | }, 380 | }, 381 | m: map[NodeID]*Msg{ 382 | "x1": &Msg{ 383 | V: "x1", 384 | Q: QSet{ 385 | T: 2, 386 | M: []QSetMember{ 387 | {N: nodeIDPtr("x")}, 388 | {N: nodeIDPtr("x2")}, 389 | }, 390 | }, 391 | }, 392 | "y1": &Msg{ 393 | V: "y1", 394 | Q: QSet{ 395 | T: 2, 396 | M: []QSetMember{ 397 | {N: nodeIDPtr("x")}, 398 | {N: nodeIDPtr("y2")}, 399 | }, 400 | }, 401 | }, 402 | "x2": &Msg{ 403 | V: "x2", 404 | Q: QSet{ 405 | T: 2, 406 | M: []QSetMember{ 407 | {N: nodeIDPtr("x")}, 408 | {N: nodeIDPtr("x1")}, 409 | }, 410 | }, 411 | }, 412 | "y2": &Msg{ 413 | V: "y2", 414 | Q: QSet{ 415 | T: 2, 416 | M: []QSetMember{ 417 | {N: nodeIDPtr("x")}, 418 | {N: nodeIDPtr("y1")}, 419 | }, 420 | }, 421 | }, 422 | }, 423 | want: NodeIDSet{"x", "x1", "x2"}, 424 | }, 425 | } 426 | for i, tc := range cases { 427 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 428 | got, _ := tc.q.findQuorum(tc.id, tc.m, fpred(func(msg *Msg) bool { 429 | return msg.V[0] == tc.id[0] 430 | })) 431 | if !reflect.DeepEqual(got, tc.want) { 432 | t.Errorf("got %v, want %v", got, tc.want) 433 | } 434 | }) 435 | } 436 | } 437 | 438 | func nodeIDPtr(s string) *NodeID { 439 | return (*NodeID)(&s) 440 | } 441 | 442 | // input: "b(a c / d e) c(a b) d(e) e(d)" 443 | // output: map[NodeID][]NodeIDSet{"b": {{"a", "c"}, {"d", "e"}}, "c": {{"a", "b"}}, "d": {{"e"}}, "e": {{"d"}}} 444 | func toNetwork(s string) map[NodeID][]NodeIDSet { 445 | splitFunc := func(data []byte, atEOF bool) (advance int, token []byte, err error) { 446 | for len(data) > 0 && unicode.IsSpace(rune(data[0])) { 447 | data = data[1:] 448 | advance++ 449 | } 450 | if len(data) == 0 { 451 | return 0, nil, nil 452 | } 453 | switch data[0] { 454 | case '(', ')', '/': 455 | return 1 + advance, data[:1], nil 456 | } 457 | token = data 458 | var advance2 int 459 | for len(data) > 0 && unicode.IsLetter(rune(data[0])) { 460 | data = data[1:] 461 | advance2++ 462 | } 463 | if advance2 == 0 { 464 | panic("scan error") 465 | } 466 | if len(data) == 0 && !atEOF { 467 | return 0, nil, nil // need to read more to find the end of the token 468 | } 469 | return advance + advance2, token[:advance2], nil 470 | } 471 | scanner := bufio.NewScanner(strings.NewReader(s)) 472 | scanner.Split(splitFunc) 473 | 474 | var ( 475 | nodeID NodeID 476 | nodeSet NodeIDSet 477 | inParen bool 478 | nodeSets []NodeIDSet 479 | ) 480 | result := make(map[NodeID][]NodeIDSet) 481 | 482 | for scanner.Scan() { 483 | tok := scanner.Text() 484 | switch tok { 485 | case "(": 486 | if inParen || nodeID == "" { 487 | panic("parse error") 488 | } 489 | inParen = true 490 | 491 | case ")": 492 | if !inParen || len(nodeSet) == 0 { 493 | panic("parse error") 494 | } 495 | nodeSets = append(nodeSets, nodeSet) 496 | 497 | result[nodeID] = nodeSets 498 | 499 | nodeID = "" 500 | nodeSet = nil 501 | inParen = false 502 | nodeSets = nil 503 | 504 | case "/": 505 | if !inParen || len(nodeSet) == 0 { 506 | panic("parse error") 507 | } 508 | nodeSets = append(nodeSets, nodeSet) 509 | nodeSet = nil 510 | 511 | default: 512 | if inParen { 513 | nodeSet = nodeSet.Add(NodeID(tok)) 514 | } else { 515 | if nodeID != "" { 516 | panic("cannot parse") 517 | } 518 | nodeID = NodeID(tok) 519 | } 520 | } 521 | } 522 | if err := scanner.Err(); err != nil { 523 | panic(err) 524 | } 525 | return result 526 | } 527 | -------------------------------------------------------------------------------- /set.go: -------------------------------------------------------------------------------- 1 | // This file generated by genset.go 2 | package scp 3 | 4 | import "sort" 5 | 6 | // ValueSet is a set of Value, implemented as a sorted slice. 7 | type ValueSet []Value 8 | 9 | func (vs ValueSet) find(v Value) int { 10 | return sort.Search(len(vs), func(index int) bool { 11 | return !vs[index].Less(v) 12 | }) 13 | } 14 | 15 | func ValueEqual(a, b Value) bool { 16 | return !a.Less(b) && !b.Less(a) 17 | } 18 | 19 | // Add produces a ValueSet containing the members of vs plus the 20 | // element v. 21 | func (vs ValueSet) Add(v Value) ValueSet { 22 | index := vs.find(v) 23 | if index < len(vs) && ValueEqual(v, vs[index]) { 24 | return vs 25 | } 26 | var result ValueSet 27 | result = append(result, vs[:index]...) 28 | result = append(result, v) 29 | result = append(result, vs[index:]...) 30 | return result 31 | } 32 | 33 | // Union produces a ValueSet containing all the members of both sets. 34 | func (vs ValueSet) Union(other ValueSet) ValueSet { 35 | if len(vs) == 0 { 36 | return other 37 | } 38 | if len(other) == 0 { 39 | return vs 40 | } 41 | var ( 42 | i, j int 43 | result ValueSet 44 | ) 45 | for i < len(vs) && j < len(other) { 46 | switch { 47 | case vs[i].Less(other[j]): 48 | result = append(result, vs[i]) 49 | i++ 50 | case other[j].Less(vs[i]): 51 | result = append(result, other[j]) 52 | j++ 53 | default: 54 | result = append(result, vs[i]) 55 | i++ 56 | j++ 57 | } 58 | } 59 | result = append(result, vs[i:]...) 60 | result = append(result, other[j:]...) 61 | return result 62 | } 63 | 64 | // Intersection produces a ValueSet with only the elements in both 65 | // sets. 66 | func (vs ValueSet) Intersection(other ValueSet) ValueSet { 67 | if len(vs) == 0 || len(other) == 0 { 68 | return nil 69 | } 70 | var result ValueSet 71 | for i, j := 0, 0; i < len(vs) && j < len(other); { 72 | switch { 73 | case vs[i].Less(other[j]): 74 | i++ 75 | case other[j].Less(vs[i]): 76 | j++ 77 | default: 78 | result = append(result, vs[i]) 79 | i++ 80 | j++ 81 | } 82 | } 83 | return result 84 | } 85 | 86 | // Minus produces a ValueSet with only the members of vs that don't 87 | // appear in other. 88 | func (vs ValueSet) Minus(other ValueSet) ValueSet { 89 | if len(vs) == 0 || len(other) == 0 { 90 | return vs 91 | } 92 | var ( 93 | result ValueSet 94 | i, j int 95 | ) 96 | for i < len(vs) && j < len(other) { 97 | switch { 98 | case vs[i].Less(other[j]): 99 | result = append(result, vs[i]) 100 | i++ 101 | case other[j].Less(vs[i]): 102 | j++ 103 | default: 104 | i++ 105 | j++ 106 | } 107 | } 108 | result = append(result, vs[i:]...) 109 | return result 110 | } 111 | 112 | // Remove produces a ValueSet without the specified element. 113 | func (vs ValueSet) Remove(v Value) ValueSet { 114 | index := vs.find(v) 115 | if index >= len(vs) || !ValueEqual(v, vs[index]) { 116 | return vs 117 | } 118 | var result ValueSet 119 | result = append(result, vs[:index]...) 120 | result = append(result, vs[index+1:]...) 121 | return result 122 | } 123 | 124 | // Contains tests whether vs contains v. 125 | func (vs ValueSet) Contains(v Value) bool { 126 | index := vs.find(v) 127 | return index < len(vs) && ValueEqual(vs[index], v) 128 | } 129 | 130 | // BallotSet is a set of Ballot, implemented as a sorted slice. 131 | type BallotSet []Ballot 132 | 133 | func (bs BallotSet) find(b Ballot) int { 134 | return sort.Search(len(bs), func(index int) bool { 135 | return !bs[index].Less(b) 136 | }) 137 | } 138 | 139 | func BallotEqual(a, b Ballot) bool { 140 | return !a.Less(b) && !b.Less(a) 141 | } 142 | 143 | // Add produces a BallotSet containing the members of bs plus the 144 | // element b. 145 | func (bs BallotSet) Add(b Ballot) BallotSet { 146 | index := bs.find(b) 147 | if index < len(bs) && BallotEqual(b, bs[index]) { 148 | return bs 149 | } 150 | var result BallotSet 151 | result = append(result, bs[:index]...) 152 | result = append(result, b) 153 | result = append(result, bs[index:]...) 154 | return result 155 | } 156 | 157 | // Union produces a BallotSet containing all the members of both sets. 158 | func (bs BallotSet) Union(other BallotSet) BallotSet { 159 | if len(bs) == 0 { 160 | return other 161 | } 162 | if len(other) == 0 { 163 | return bs 164 | } 165 | var ( 166 | i, j int 167 | result BallotSet 168 | ) 169 | for i < len(bs) && j < len(other) { 170 | switch { 171 | case bs[i].Less(other[j]): 172 | result = append(result, bs[i]) 173 | i++ 174 | case other[j].Less(bs[i]): 175 | result = append(result, other[j]) 176 | j++ 177 | default: 178 | result = append(result, bs[i]) 179 | i++ 180 | j++ 181 | } 182 | } 183 | result = append(result, bs[i:]...) 184 | result = append(result, other[j:]...) 185 | return result 186 | } 187 | 188 | // Intersection produces a BallotSet with only the elements in both 189 | // sets. 190 | func (bs BallotSet) Intersection(other BallotSet) BallotSet { 191 | if len(bs) == 0 || len(other) == 0 { 192 | return nil 193 | } 194 | var result BallotSet 195 | for i, j := 0, 0; i < len(bs) && j < len(other); { 196 | switch { 197 | case bs[i].Less(other[j]): 198 | i++ 199 | case other[j].Less(bs[i]): 200 | j++ 201 | default: 202 | result = append(result, bs[i]) 203 | i++ 204 | j++ 205 | } 206 | } 207 | return result 208 | } 209 | 210 | // Minus produces a BallotSet with only the members of bs that don't 211 | // appear in other. 212 | func (bs BallotSet) Minus(other BallotSet) BallotSet { 213 | if len(bs) == 0 || len(other) == 0 { 214 | return bs 215 | } 216 | var ( 217 | result BallotSet 218 | i, j int 219 | ) 220 | for i < len(bs) && j < len(other) { 221 | switch { 222 | case bs[i].Less(other[j]): 223 | result = append(result, bs[i]) 224 | i++ 225 | case other[j].Less(bs[i]): 226 | j++ 227 | default: 228 | i++ 229 | j++ 230 | } 231 | } 232 | result = append(result, bs[i:]...) 233 | return result 234 | } 235 | 236 | // Remove produces a BallotSet without the specified element. 237 | func (bs BallotSet) Remove(b Ballot) BallotSet { 238 | index := bs.find(b) 239 | if index >= len(bs) || !BallotEqual(b, bs[index]) { 240 | return bs 241 | } 242 | var result BallotSet 243 | result = append(result, bs[:index]...) 244 | result = append(result, bs[index+1:]...) 245 | return result 246 | } 247 | 248 | // Contains tests whether bs contains b. 249 | func (bs BallotSet) Contains(b Ballot) bool { 250 | index := bs.find(b) 251 | return index < len(bs) && BallotEqual(bs[index], b) 252 | } 253 | 254 | // NodeIDSet is a set of NodeID, implemented as a sorted slice. 255 | type NodeIDSet []NodeID 256 | 257 | func (ns NodeIDSet) find(n NodeID) int { 258 | return sort.Search(len(ns), func(index int) bool { 259 | return !ns[index].Less(n) 260 | }) 261 | } 262 | 263 | func NodeIDEqual(a, b NodeID) bool { 264 | return !a.Less(b) && !b.Less(a) 265 | } 266 | 267 | // Add produces a NodeIDSet containing the members of ns plus the 268 | // element n. 269 | func (ns NodeIDSet) Add(n NodeID) NodeIDSet { 270 | index := ns.find(n) 271 | if index < len(ns) && NodeIDEqual(n, ns[index]) { 272 | return ns 273 | } 274 | var result NodeIDSet 275 | result = append(result, ns[:index]...) 276 | result = append(result, n) 277 | result = append(result, ns[index:]...) 278 | return result 279 | } 280 | 281 | // Union produces a NodeIDSet containing all the members of both sets. 282 | func (ns NodeIDSet) Union(other NodeIDSet) NodeIDSet { 283 | if len(ns) == 0 { 284 | return other 285 | } 286 | if len(other) == 0 { 287 | return ns 288 | } 289 | var ( 290 | i, j int 291 | result NodeIDSet 292 | ) 293 | for i < len(ns) && j < len(other) { 294 | switch { 295 | case ns[i].Less(other[j]): 296 | result = append(result, ns[i]) 297 | i++ 298 | case other[j].Less(ns[i]): 299 | result = append(result, other[j]) 300 | j++ 301 | default: 302 | result = append(result, ns[i]) 303 | i++ 304 | j++ 305 | } 306 | } 307 | result = append(result, ns[i:]...) 308 | result = append(result, other[j:]...) 309 | return result 310 | } 311 | 312 | // Intersection produces a NodeIDSet with only the elements in both 313 | // sets. 314 | func (ns NodeIDSet) Intersection(other NodeIDSet) NodeIDSet { 315 | if len(ns) == 0 || len(other) == 0 { 316 | return nil 317 | } 318 | var result NodeIDSet 319 | for i, j := 0, 0; i < len(ns) && j < len(other); { 320 | switch { 321 | case ns[i].Less(other[j]): 322 | i++ 323 | case other[j].Less(ns[i]): 324 | j++ 325 | default: 326 | result = append(result, ns[i]) 327 | i++ 328 | j++ 329 | } 330 | } 331 | return result 332 | } 333 | 334 | // Minus produces a NodeIDSet with only the members of ns that don't 335 | // appear in other. 336 | func (ns NodeIDSet) Minus(other NodeIDSet) NodeIDSet { 337 | if len(ns) == 0 || len(other) == 0 { 338 | return ns 339 | } 340 | var ( 341 | result NodeIDSet 342 | i, j int 343 | ) 344 | for i < len(ns) && j < len(other) { 345 | switch { 346 | case ns[i].Less(other[j]): 347 | result = append(result, ns[i]) 348 | i++ 349 | case other[j].Less(ns[i]): 350 | j++ 351 | default: 352 | i++ 353 | j++ 354 | } 355 | } 356 | result = append(result, ns[i:]...) 357 | return result 358 | } 359 | 360 | // Remove produces a NodeIDSet without the specified element. 361 | func (ns NodeIDSet) Remove(n NodeID) NodeIDSet { 362 | index := ns.find(n) 363 | if index >= len(ns) || !NodeIDEqual(n, ns[index]) { 364 | return ns 365 | } 366 | var result NodeIDSet 367 | result = append(result, ns[:index]...) 368 | result = append(result, ns[index+1:]...) 369 | return result 370 | } 371 | 372 | // Contains tests whether ns contains n. 373 | func (ns NodeIDSet) Contains(n NodeID) bool { 374 | index := ns.find(n) 375 | return index < len(ns) && NodeIDEqual(ns[index], n) 376 | } 377 | -------------------------------------------------------------------------------- /slot.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "reflect" 7 | "time" 8 | ) 9 | 10 | // SlotID is the type of a slot ID. 11 | type SlotID int 12 | 13 | // Slot maintains the state of a node's slot while it is undergoing 14 | // nomination and balloting. 15 | type Slot struct { 16 | ID SlotID 17 | V *Node 18 | Ph Phase // PhNom -> PhNomPrep -> PhPrep -> PhCommit -> PhExt 19 | M map[NodeID]*Msg // latest message from each peer 20 | sent *Msg // latest message sent 21 | 22 | T time.Time // time at which this slot was created (for computing the nomination round) 23 | X ValueSet // votes for nominate(val) 24 | Y ValueSet // votes for accept(nominate(val)) 25 | Z ValueSet // confirmed nominated values 26 | 27 | maxPriPeers NodeIDSet // set of peers that have ever had max priority 28 | lastRound int // latest round at which maxPriPeers was updated 29 | nextRoundTimer *time.Timer 30 | 31 | B Ballot 32 | P, PP Ballot // two highest "accepted prepared" ballots with differing values 33 | C, H Ballot // lowest and highest confirmed-prepared or accepted-commit ballots (depending on phase) 34 | 35 | Upd *time.Timer // timer for invoking a deferred update 36 | } 37 | 38 | // Phase is the type of a slot's phase. 39 | type Phase int 40 | 41 | const ( 42 | PhNom Phase = iota 43 | PhNomPrep 44 | PhPrep 45 | PhCommit 46 | PhExt 47 | ) 48 | 49 | func newSlot(id SlotID, n *Node) (*Slot, error) { 50 | s := &Slot{ 51 | ID: id, 52 | V: n, 53 | Ph: PhNom, 54 | T: time.Now(), 55 | M: make(map[NodeID]*Msg), 56 | } 57 | peerID, err := s.findMaxPriPeer(1) 58 | if err != nil { 59 | return nil, err 60 | } 61 | s.maxPriPeers = s.maxPriPeers.Add(peerID) 62 | s.lastRound = 1 63 | s.scheduleRound() 64 | return s, nil 65 | } 66 | 67 | var ( 68 | // NomRoundInterval determines the duration of a nomination "round." 69 | // Round N lasts for a duration of (2+N)*NomRoundInterval. A node's 70 | // neighbor set changes from one round to the next, as do the 71 | // priorities of the peers in that set. 72 | NomRoundInterval = time.Second 73 | 74 | // DeferredUpdateInterval determines the delay between arming a 75 | // deferred-update timer and firing it. The delay is 76 | // (1+N)*DeferredUpdateInterval, where N is the value of the slot's 77 | // ballot counter (B.N). 78 | DeferredUpdateInterval = time.Second 79 | ) 80 | 81 | // This embodies most of the nomination and balloting protocols. It 82 | // processes an incoming protocol message and returns an outbound 83 | // protocol message in response, or nil if the incoming message is 84 | // ignored. 85 | // TODO: prune quorum searches when receiving a "confirm" vote. ("Once 86 | // "v" enters the confirmed state, it may issue a _confirm_ "a" 87 | // message to help other nodes confirm "a" more efficiently by pruning 88 | // their quorum search at "v".") 89 | func (s *Slot) handle(msg *Msg) (resp *Msg, err error) { 90 | if s.V.ID == msg.V && !s.isNomPhase() { 91 | // A node doesn't message itself except during nomination. 92 | return nil, nil 93 | } 94 | 95 | err = msg.valid() 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | defer func() { 101 | if err == nil { 102 | if resp != nil { 103 | if s.sent != nil && reflect.DeepEqual(resp.T, s.sent.T) { 104 | resp = nil 105 | } else { 106 | s.sent = resp 107 | } 108 | } 109 | if resp != nil { 110 | s.Logf("%s -> %v", msg, resp) 111 | } 112 | } 113 | }() 114 | 115 | if have, ok := s.M[msg.V]; ok && !have.T.Less(msg.T) { 116 | // We already have a message from this sender that's the same or 117 | // newer; use that instead. 118 | msg = have 119 | } else { 120 | s.M[msg.V] = msg 121 | } 122 | 123 | if s.isNomPhase() { 124 | s.doNomPhase(msg) 125 | } 126 | 127 | if s.isPrepPhase() { 128 | s.doPrepPhase() 129 | } 130 | 131 | if s.Ph == PhCommit { 132 | s.doCommitPhase() 133 | } 134 | 135 | return s.Msg(), nil 136 | } 137 | 138 | func (s *Slot) isNomPhase() bool { 139 | return s.Ph == PhNom || s.Ph == PhNomPrep 140 | } 141 | 142 | func (s *Slot) isPrepPhase() bool { 143 | return s.Ph == PhNomPrep || s.Ph == PhPrep 144 | } 145 | 146 | func (s *Slot) doNomPhase(msg *Msg) { 147 | if len(s.Z) == 0 && s.maxPrioritySender(msg.V) { 148 | // "Echo" nominated values by adding them to s.X. 149 | f := func(topic *NomTopic) { 150 | s.X = s.X.Union(topic.X) 151 | s.X = s.X.Union(topic.Y) 152 | } 153 | switch topic := msg.T.(type) { 154 | case *NomTopic: 155 | f(topic) 156 | case *NomPrepTopic: 157 | f(&topic.NomTopic) 158 | } 159 | } 160 | 161 | // Promote accepted-nominated values from X to Y, and 162 | // confirmed-nominated values from Y to Z. 163 | s.updateYZ() 164 | 165 | if s.Ph == PhNom { 166 | if len(s.Z) > 0 { 167 | // Some value is confirmed nominated, start PREPARE phase. 168 | s.Ph = PhNomPrep 169 | s.B.N = 1 170 | s.setBX() 171 | } else { 172 | s.updateP() 173 | if !s.P.IsZero() { 174 | s.Ph = PhNomPrep 175 | s.B.N = 1 176 | s.setBX() 177 | } 178 | } 179 | } 180 | } 181 | 182 | func (s *Slot) doPrepPhase() { 183 | s.updateP() // xxx may be redundant with the call in doNomPhase 184 | 185 | // Update s.H, the highest confirmed-prepared ballot. 186 | s.H = ZeroBallot 187 | var cpIn, cpOut BallotSet 188 | if !s.P.IsZero() { 189 | cpIn = cpIn.Add(s.P) 190 | if !s.PP.IsZero() { 191 | cpIn = cpIn.Add(s.PP) 192 | } 193 | } 194 | nodeIDs := s.findQuorum(&ballotSetPred{ 195 | ballots: cpIn, 196 | finalBallots: &cpOut, 197 | testfn: func(msg *Msg, ballots BallotSet) BallotSet { 198 | return ballots.Intersection(msg.acceptsPreparedSet()) 199 | }, 200 | }) 201 | if len(nodeIDs) > 0 { 202 | h := cpOut[len(cpOut)-1] 203 | if ValueEqual(s.B.X, h.X) { 204 | s.H = h 205 | } 206 | if s.Ph == PhNomPrep { 207 | // Some ballot is confirmed prepared, exit NOMINATE phase. 208 | s.Ph = PhPrep 209 | s.cancelRounds() 210 | } 211 | } 212 | 213 | s.updateB() 214 | 215 | // Update s.C. 216 | if !s.C.IsZero() { 217 | if s.H.N == 0 || (s.C.Less(s.P) && !ValueEqual(s.P.X, s.C.X)) || (s.C.Less(s.PP) && !ValueEqual(s.PP.X, s.C.X)) { 218 | s.C = ZeroBallot 219 | } 220 | } 221 | if s.C.IsZero() && s.H.N > 0 && s.H.N == s.B.N { 222 | s.C = s.B 223 | } 224 | 225 | // The PREPARE phase ends at a node when the statement "commit 226 | // b" reaches the accept state in federated voting for some 227 | // ballot "b". 228 | if s.updateAcceptsCommitBounds() { 229 | // Accept commit(). 230 | s.Ph = PhCommit 231 | } 232 | } 233 | 234 | func (s *Slot) doCommitPhase() { 235 | s.cancelRounds() 236 | s.updateP() 237 | s.updateAcceptsCommitBounds() 238 | s.updateB() 239 | 240 | // As soon as a node confirms "commit b" for any ballot "b", it 241 | // moves to the EXTERNALIZE stage. 242 | var cn, hn int 243 | nodeIDs := s.findQuorum(&minMaxPred{ 244 | min: s.C.N, 245 | max: s.H.N, 246 | finalMin: &cn, 247 | finalMax: &hn, 248 | testfn: func(msg *Msg, min, max int) (bool, int, int) { 249 | return msg.acceptsCommit(s.B.X, min, max) 250 | }, 251 | }) 252 | if len(nodeIDs) > 0 { 253 | s.Ph = PhExt // \o/ 254 | s.C.N = cn 255 | s.H.N = hn 256 | s.cancelUpd() 257 | } 258 | } 259 | 260 | func (s *Slot) cancelRounds() { 261 | if s.nextRoundTimer == nil { 262 | return 263 | } 264 | stopTimer(s.nextRoundTimer) 265 | s.nextRoundTimer = nil 266 | } 267 | 268 | func (s *Slot) updateAcceptsCommitBounds() bool { 269 | var cn, hn int 270 | nodeIDs := s.accept(func(isQuorum bool) predicate { 271 | return &minMaxPred{ 272 | min: 1, 273 | max: math.MaxInt32, 274 | finalMin: &cn, 275 | finalMax: &hn, 276 | testfn: func(msg *Msg, min, max int) (bool, int, int) { 277 | rangeFn := msg.acceptsCommit 278 | if isQuorum { 279 | rangeFn = msg.votesOrAcceptsCommit 280 | } 281 | return rangeFn(s.B.X, min, max) 282 | }, 283 | } 284 | }) 285 | if len(nodeIDs) > 0 { 286 | s.C.N = cn 287 | s.C.X = s.B.X 288 | s.H.N = hn 289 | s.H.X = s.B.X 290 | return true 291 | } 292 | return false 293 | } 294 | 295 | func (s *Slot) Msg() *Msg { 296 | msg := NewMsg(s.V.ID, s.ID, s.V.Q, nil) 297 | switch s.Ph { 298 | case PhNom: 299 | if len(s.X) == 0 && len(s.Y) == 0 { 300 | return nil 301 | } 302 | msg.T = &NomTopic{ 303 | X: s.X, 304 | Y: s.Y, 305 | } 306 | 307 | case PhNomPrep: 308 | msg.T = &NomPrepTopic{ 309 | NomTopic: NomTopic{ 310 | X: s.X, 311 | Y: s.Y, 312 | }, 313 | PrepTopic: PrepTopic{ 314 | B: s.B, 315 | P: s.P, 316 | PP: s.PP, 317 | HN: s.H.N, 318 | CN: s.C.N, 319 | }, 320 | } 321 | 322 | case PhPrep: 323 | msg.T = &PrepTopic{ 324 | B: s.B, 325 | P: s.P, 326 | PP: s.PP, 327 | HN: s.H.N, 328 | CN: s.C.N, 329 | } 330 | 331 | case PhCommit: 332 | msg.T = &CommitTopic{ 333 | B: s.B, 334 | PN: s.P.N, 335 | HN: s.H.N, 336 | CN: s.C.N, 337 | } 338 | 339 | case PhExt: 340 | msg.T = &ExtTopic{ 341 | C: s.C, 342 | HN: s.H.N, 343 | } 344 | } 345 | return msg 346 | } 347 | 348 | // "When a node sees sees messages from a quorum to which it belongs 349 | // such that each message's "ballot.counter" is greater than or equal 350 | // to the local "ballot.counter", the node arms a timer for its local 351 | // "ballot.counter + 1" seconds." 352 | func (s *Slot) maybeScheduleUpd() { 353 | if s.Upd != nil { 354 | // Don't bother if a timer's already armed. 355 | return 356 | } 357 | nodeIDs := s.findQuorum(fpred(func(msg *Msg) bool { 358 | return msg.bN() >= s.B.N 359 | })) 360 | if len(nodeIDs) == 0 { 361 | return 362 | } 363 | s.Upd = time.AfterFunc(time.Duration((1+s.B.N)*int(DeferredUpdateInterval)), func() { 364 | s.V.deferredUpdate(s) 365 | }) 366 | } 367 | 368 | func (s *Slot) deferredUpdate() { 369 | if s.Upd == nil { 370 | return 371 | } 372 | 373 | s.Upd = nil 374 | s.B.N++ 375 | s.setBX() 376 | 377 | if s.isPrepPhase() { 378 | s.doPrepPhase() 379 | } 380 | if s.Ph == PhCommit { 381 | s.doCommitPhase() 382 | } 383 | 384 | msg := s.Msg() 385 | 386 | s.Logf("deferred update: %s", msg) 387 | 388 | s.V.send <- msg 389 | } 390 | 391 | func (s *Slot) cancelUpd() { 392 | if s.Upd == nil { 393 | return 394 | } 395 | stopTimer(s.Upd) 396 | s.Upd = nil 397 | } 398 | 399 | func (s *Slot) updateB() { 400 | // Update s.B. 401 | if s.B.Less(s.H) { 402 | // raise B to the highest confirmed-prepared ballot 403 | s.B = s.H 404 | s.cancelUpd() 405 | return 406 | } 407 | 408 | s.maybeScheduleUpd() 409 | 410 | // If nodes forming a blocking threshold all have 411 | // "ballot.counter" values greater than the local 412 | // "ballot.counter", then the local node immediately increases 413 | // "ballot.counter" to the lowest value such that this is no 414 | // longer the case. (When doing so, it also disables any 415 | // pending timers associated with the old "counter".) 416 | var ( 417 | doSetBX bool 418 | setBN = s.B.N 419 | ) 420 | for { // loop until no such blocking set is found 421 | nodeIDs := s.findBlockingSet(fpred(func(msg *Msg) bool { 422 | return msg.bN() > setBN 423 | })) 424 | if len(nodeIDs) == 0 { 425 | break 426 | } 427 | 428 | doSetBX = true 429 | s.cancelUpd() 430 | var innerSetBN int 431 | for i, nodeID := range nodeIDs { 432 | msg := s.M[nodeID] 433 | bn := msg.bN() 434 | if i == 0 || bn < innerSetBN { 435 | innerSetBN = bn 436 | } 437 | } 438 | if innerSetBN > setBN { 439 | setBN = innerSetBN 440 | } 441 | } 442 | 443 | if setBN == s.B.N { 444 | return 445 | } 446 | 447 | // To avoid exhausting `ballot.counter`, its value must always be 448 | // less then 1,000 plus the number of seconds a node has been 449 | // running SCP on the current slot. Should any of the above rules 450 | // require increasing the counter beyond this value, a node either 451 | // increases `ballot.counter` to the maximum permissible value, 452 | // or, if it is already at this maximum, waits up to one second 453 | // before increasing the value. 454 | maxBN := 1000 + int(time.Since(s.T)/time.Second) 455 | if setBN <= maxBN { 456 | s.B.N = setBN 457 | } else if s.B.N < maxBN { 458 | s.Logf("limiting B.N to %d (from %d)", maxBN, setBN) 459 | s.B.N = maxBN 460 | } else { 461 | setBN = maxBN + 1 462 | 463 | // The time when it's ok to set s.B.N to setBN (i.e., after it's been running for setBN-1000 seconds) 464 | oktime := s.T.Add(time.Duration(setBN-1000) * time.Second) 465 | until := time.Until(oktime) 466 | 467 | s.Logf("limiting B.N to %d after a %s sleep", setBN, until) 468 | time.Sleep(until) 469 | s.B.N = setBN 470 | } 471 | if doSetBX { 472 | s.setBX() 473 | s.maybeScheduleUpd() 474 | } 475 | } 476 | 477 | func (s *Slot) setBX() { 478 | if s.Ph >= PhCommit { 479 | return 480 | } 481 | switch { 482 | case !s.H.IsZero(): 483 | s.B.X = s.H.X 484 | 485 | case len(s.Z) > 0: 486 | s.B.X = s.Z.Combine(s.ID) 487 | 488 | case !s.P.IsZero(): 489 | s.B.X = s.P.X 490 | } 491 | } 492 | 493 | // Round tells the current (time-based) nomination round. 494 | // 495 | // Nomination round N lasts for a duration of 496 | // (2+N)*NomRoundInterval. Also, the first round is round 1. Via the 497 | // quadratic formula this tells us that after an elapsed time of T, 498 | // it's round 1 + ((sqrt(8T+25)-5) / 2) 499 | func (s *Slot) Round() int { 500 | return round(time.Since(s.T)) 501 | } 502 | 503 | func round(d time.Duration) int { 504 | elapsed := float64(d) / float64(NomRoundInterval) 505 | r := math.Sqrt(8.0*elapsed + 25.0) 506 | return 1 + int((r-5.0)/2.0) 507 | } 508 | 509 | func (s *Slot) roundTime(r int) time.Time { 510 | r-- 511 | intervals := r * (r + 5) / 2 512 | return s.T.Add(time.Duration(intervals * int(NomRoundInterval))) 513 | } 514 | 515 | func (s *Slot) newRound() error { 516 | if s.nextRoundTimer == nil { 517 | return nil 518 | } 519 | 520 | curRound := s.Round() 521 | 522 | for r := s.lastRound + 1; r <= curRound; r++ { 523 | peerID, err := s.findMaxPriPeer(r) 524 | if err != nil { 525 | return err 526 | } 527 | s.maxPriPeers = s.maxPriPeers.Add(peerID) 528 | } 529 | // s.Logf("round %d, peers %v", curRound, s.maxPriPeers) 530 | s.lastRound = curRound 531 | s.V.rehandle(s) 532 | s.scheduleRound() 533 | return nil 534 | } 535 | 536 | func (s *Slot) scheduleRound() { 537 | dur := time.Until(s.roundTime(s.lastRound + 1)) 538 | // s.Logf("scheduling round %d for %s from now", s.lastRound+1, dur) 539 | s.nextRoundTimer = time.AfterFunc(dur, func() { 540 | s.V.newRound(s) 541 | }) 542 | } 543 | 544 | func (s *Slot) findMaxPriPeer(r int) (NodeID, error) { 545 | neighbors, err := s.V.Neighbors(s.ID, r) 546 | if err != nil { 547 | return "", err 548 | } 549 | var ( 550 | maxPriority [32]byte 551 | result NodeID 552 | ) 553 | for _, neighbor := range neighbors { 554 | priority, err := s.V.Priority(s.ID, r, neighbor) 555 | if err != nil { 556 | return "", err 557 | } 558 | if bytes.Compare(priority[:], maxPriority[:]) > 0 { 559 | maxPriority = priority 560 | result = neighbor 561 | } 562 | } 563 | return result, nil 564 | } 565 | 566 | // Tells whether the given peer has or had the maximum priority in the 567 | // current or any earlier nomination round. 568 | func (s *Slot) maxPrioritySender(nodeID NodeID) bool { 569 | return s.maxPriPeers.Contains(nodeID) 570 | } 571 | 572 | func (s *Slot) updateYZ() { 573 | // Look for values to promote from s.X to s.Y. 574 | var promote ValueSet 575 | 576 | nodeIDs := s.accept(func(isQuorum bool) predicate { 577 | return &valueSetPred{ 578 | vals: s.X, 579 | finalVals: &promote, 580 | testfn: func(msg *Msg, vals ValueSet) ValueSet { 581 | setFn := msg.acceptsNominatedSet 582 | if isQuorum { 583 | setFn = msg.votesOrAcceptsNominatedSet 584 | } 585 | return vals.Intersection(setFn()) 586 | }, 587 | } 588 | }) 589 | if len(nodeIDs) > 0 { 590 | s.Y = s.Y.Union(promote) 591 | } 592 | s.X = s.X.Minus(s.Y) 593 | 594 | // Look for values in s.Y to confirm, moving slot to the PREPARE 595 | // phase. 596 | promote = nil 597 | nodeIDs = s.findQuorum(&valueSetPred{ 598 | vals: s.Y, 599 | finalVals: &promote, 600 | testfn: func(msg *Msg, vals ValueSet) ValueSet { 601 | return vals.Intersection(msg.acceptsNominatedSet()) 602 | }, 603 | }) 604 | if len(nodeIDs) > 0 { 605 | s.Z = s.Z.Union(promote) 606 | } 607 | } 608 | 609 | // Update s.P and s.PP, the two highest accepted-prepared ballots. 610 | // TODO: this gives the highest accepted-prepared ballots in the 611 | // blocking set or, if there isn't one, in the first quorum 612 | // found. There might be higher accepted-prepared ballots in other 613 | // quorums. 614 | func (s *Slot) updateP() { 615 | var apIn BallotSet 616 | 617 | if !s.B.IsZero() { 618 | apIn.Add(s.B) 619 | } 620 | if !s.P.IsZero() { 621 | apIn.Add(s.P) 622 | if !s.PP.IsZero() { 623 | apIn.Add(s.PP) 624 | } 625 | } 626 | 627 | s.P = ZeroBallot 628 | s.PP = ZeroBallot 629 | 630 | var apOut BallotSet 631 | peers := s.V.Peers() 632 | for _, peerID := range peers { 633 | if msg, ok := s.M[peerID]; ok { 634 | apIn = apIn.Union(msg.votesOrAcceptsPreparedSet()) 635 | } 636 | } 637 | nodeIDs := s.accept(func(isQuorum bool) predicate { 638 | return &ballotSetPred{ 639 | ballots: apIn, 640 | finalBallots: &apOut, 641 | testfn: func(msg *Msg, ballots BallotSet) BallotSet { 642 | setFn := msg.acceptsPreparedSet 643 | if isQuorum { 644 | setFn = msg.votesOrAcceptsPreparedSet 645 | } 646 | b := setFn() 647 | return ballots.Intersection(b) 648 | }, 649 | } 650 | }) 651 | if len(nodeIDs) > 0 { 652 | if !s.B.IsZero() { 653 | // Exclude ballots with N > B.N, if s.B is set. 654 | // If it's not set, we're still in NOMINATE phase and can set 655 | // s.P to anything. 656 | for len(apOut) > 0 && apOut[len(apOut)-1].N > s.B.N { 657 | apOut = apOut[:len(apOut)-1] 658 | } 659 | } 660 | if len(apOut) > 0 { 661 | s.P = apOut[len(apOut)-1] 662 | if !s.B.IsZero() && s.P.N == s.B.N && s.B.X.Less(s.P.X) { 663 | s.P.N-- 664 | } 665 | if s.Ph == PhPrep { 666 | s.PP = ZeroBallot 667 | for i := len(apOut) - 2; i >= 0; i-- { 668 | ap := apOut[i] 669 | if ap.N < s.P.N && !ValueEqual(ap.X, s.P.X) { 670 | s.PP = ap 671 | break 672 | } 673 | } 674 | } 675 | } 676 | } 677 | } 678 | 679 | func (s *Slot) Logf(f string, a ...interface{}) { 680 | f = "slot %d: " + f 681 | a = append([]interface{}{s.ID}, a...) 682 | s.V.Logf(f, a...) 683 | } 684 | 685 | // To prevent a timer created with NewTimer from firing after a call 686 | // to Stop, check the return value and drain the 687 | // channel. https://golang.org/pkg/time/#Timer.Stop 688 | // 689 | // HOWEVER, it looks like a straight read of the timer's channel can 690 | // sometimes block even when Stop returns false. This works around 691 | // that by making the drain be non-blocking. 692 | func stopTimer(t *time.Timer) { 693 | if !t.Stop() { 694 | select { 695 | case <-t.C: 696 | default: 697 | } 698 | } 699 | } 700 | -------------------------------------------------------------------------------- /slot_test.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestRound(t *testing.T) { 9 | cases := []struct { 10 | d time.Duration 11 | want int 12 | }{ 13 | {0, 1}, 14 | {1 * NomRoundInterval, 1}, 15 | {2 * NomRoundInterval, 1}, 16 | {3 * NomRoundInterval, 2}, 17 | {4 * NomRoundInterval, 2}, 18 | {5 * NomRoundInterval, 2}, 19 | {6 * NomRoundInterval, 2}, 20 | {7 * NomRoundInterval, 3}, 21 | } 22 | for _, tc := range cases { 23 | got := round(tc.d) 24 | if got != tc.want { 25 | t.Errorf("got round(%s) = %d, want %d", tc.d, got, tc.want) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /topic.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import "fmt" 4 | 5 | // Topic is the abstract type of the payload of an SCP message 6 | // (conveyed in an envelope, see type Msg). The concrete type is one 7 | // of NomTopic, NomPrepTopic, PrepTopic, CommitTopic, and ExtTopic. 8 | type Topic interface { 9 | Less(Topic) bool 10 | String() string 11 | } 12 | 13 | // NomTopic is the payload of a nomination protocol message. 14 | type NomTopic struct { 15 | X, Y ValueSet 16 | } 17 | 18 | func (nt *NomTopic) Less(other Topic) bool { 19 | o, ok := other.(*NomTopic) 20 | if !ok { 21 | return true // NOMINATE messages are less than all other messages 22 | } 23 | if len(nt.Y) < len(o.Y) { 24 | return true 25 | } 26 | if len(nt.Y) > len(o.Y) { 27 | return false 28 | } 29 | return len(nt.X) < len(o.X) 30 | } 31 | 32 | func (nt *NomTopic) String() string { 33 | return fmt.Sprintf("NOM X=%s, Y=%s", nt.X, nt.Y) 34 | } 35 | 36 | // NomPrepTopic is the combined payload of a NOMINATE and a PREPARE 37 | // message. 38 | type NomPrepTopic struct { 39 | NomTopic 40 | PrepTopic 41 | } 42 | 43 | func (npt *NomPrepTopic) Less(other Topic) bool { 44 | switch other := other.(type) { 45 | case *NomTopic: 46 | return false 47 | 48 | case *NomPrepTopic: 49 | if npt.NomTopic.Less(&other.NomTopic) { 50 | return true 51 | } 52 | if other.NomTopic.Less(&npt.NomTopic) { 53 | return false 54 | } 55 | return npt.PrepTopic.Less(&other.PrepTopic) 56 | 57 | default: 58 | return true 59 | } 60 | } 61 | 62 | func (npt *NomPrepTopic) String() string { 63 | return fmt.Sprintf("NOM/PREP X=%s, Y=%s B=%s P=%s PP=%s CN=%d HN=%d", npt.X, npt.Y, npt.B, npt.P, npt.PP, npt.CN, npt.HN) 64 | } 65 | 66 | // PrepTopic is the payload of a PREPARE message in the ballot protocol. 67 | type PrepTopic struct { 68 | B, P, PP Ballot 69 | HN, CN int 70 | } 71 | 72 | func (pt *PrepTopic) Less(other Topic) bool { 73 | switch other := other.(type) { 74 | case *NomTopic: 75 | return false 76 | case *NomPrepTopic: 77 | return false 78 | case *PrepTopic: 79 | if pt.B.Less(other.B) { 80 | return true 81 | } 82 | if other.B.Less(pt.B) { 83 | return false 84 | } 85 | if pt.P.Less(other.P) { 86 | return true 87 | } 88 | if other.P.Less(pt.P) { 89 | return false 90 | } 91 | if pt.PP.Less(other.PP) { 92 | return true 93 | } 94 | if other.PP.Less(pt.PP) { 95 | return false 96 | } 97 | return pt.HN < other.HN 98 | } 99 | return true 100 | } 101 | 102 | func (pt *PrepTopic) String() string { 103 | return fmt.Sprintf("PREP B=%s P=%s PP=%s CN=%d HN=%d", pt.B, pt.P, pt.PP, pt.CN, pt.HN) 104 | } 105 | 106 | // CommitTopic is the payload of a COMMIT message in the ballot 107 | // protocol. 108 | type CommitTopic struct { 109 | B Ballot 110 | PN, HN, CN int 111 | } 112 | 113 | func (ct *CommitTopic) Less(other Topic) bool { 114 | switch other := other.(type) { 115 | case *NomTopic: 116 | return false 117 | case *NomPrepTopic: 118 | return false 119 | case *PrepTopic: 120 | return false 121 | case *CommitTopic: 122 | if ct.B.Less(other.B) { 123 | return true 124 | } 125 | if other.B.Less(ct.B) { 126 | return false 127 | } 128 | if ct.PN < other.PN { 129 | return true 130 | } 131 | if other.PN < ct.PN { 132 | return false 133 | } 134 | return ct.HN < other.HN 135 | } 136 | return true 137 | } 138 | 139 | func (ct *CommitTopic) String() string { 140 | return fmt.Sprintf("COMMIT B=%s PN=%d CN=%d HN=%d", ct.B, ct.PN, ct.CN, ct.HN) 141 | } 142 | 143 | // ExtTopic is the payload of an EXTERNALIZE message in the ballot 144 | // protocol. 145 | type ExtTopic struct { 146 | C Ballot 147 | HN int 148 | } 149 | 150 | func (et *ExtTopic) Less(other Topic) bool { 151 | if other, ok := other.(*ExtTopic); ok { 152 | return et.HN < other.HN 153 | } 154 | return false 155 | } 156 | 157 | func (et *ExtTopic) String() string { 158 | return fmt.Sprintf("EXT C=%s HN=%d", et.C, et.HN) 159 | } 160 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Value is the abstract type of values being voted on by the network. 9 | type Value interface { 10 | // Less tells whether this value is less than another. Values must be totally ordered. 11 | Less(Value) bool 12 | 13 | // Combine combines this value with another to produce a third 14 | // (which may be the same as either of the inputs). The operation 15 | // should be deterministic and commutative. 16 | Combine(Value, SlotID) Value 17 | 18 | // IsNil tells whether this should be considered a nil value. 19 | IsNil() bool 20 | 21 | // Bytes produces a byte-string representation of the value, not 22 | // meant for human consumption. 23 | Bytes() []byte 24 | 25 | // String produces a readable representation of the value. 26 | String() string 27 | } 28 | 29 | // VString calls a Value's String method. If the value is nil, returns 30 | // the string "". 31 | func VString(v Value) string { 32 | if isNilVal(v) { 33 | return "" 34 | } 35 | return v.String() 36 | } 37 | 38 | // Combine reduces the members of vs to a single value using 39 | // Value.Combine. The result is nil if vs is empty. 40 | func (vs ValueSet) Combine(slotID SlotID) Value { 41 | if len(vs) == 0 { 42 | return nil 43 | } 44 | result := vs[0] 45 | for _, v := range vs[1:] { 46 | result = result.Combine(v, slotID) 47 | } 48 | return result 49 | } 50 | 51 | func (vs ValueSet) String() string { 52 | var strs []string 53 | for _, v := range vs { 54 | strs = append(strs, VString(v)) 55 | } 56 | return fmt.Sprintf("[%s]", strings.Join(strs, " ")) 57 | } 58 | 59 | func isNilVal(v Value) bool { 60 | return v == nil || v.IsNil() 61 | } 62 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | package scp 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | type valtype uint32 12 | 13 | func (v valtype) IsNil() bool { return false } 14 | 15 | func (v valtype) Less(other Value) bool { 16 | return v < other.(valtype) 17 | } 18 | 19 | func (v valtype) Combine(other Value, _ SlotID) Value { 20 | return valtype(v + other.(valtype)) 21 | } 22 | 23 | func (v valtype) Bytes() []byte { 24 | var buf [4]byte 25 | binary.BigEndian.PutUint32(buf[:], uint32(v)) 26 | return buf[:] 27 | } 28 | 29 | func (v valtype) String() string { 30 | return strconv.Itoa(int(v)) 31 | } 32 | 33 | func TestValueSetAdd(t *testing.T) { 34 | cases := []struct { 35 | s []valtype 36 | v valtype 37 | want []valtype 38 | }{ 39 | { 40 | s: []valtype{}, 41 | v: 1, 42 | want: []valtype{1}, 43 | }, 44 | { 45 | s: []valtype{1}, 46 | v: 1, 47 | want: []valtype{1}, 48 | }, 49 | { 50 | s: []valtype{1, 2, 3}, 51 | v: 0, 52 | want: []valtype{0, 1, 2, 3}, 53 | }, 54 | { 55 | s: []valtype{1, 2, 3}, 56 | v: 4, 57 | want: []valtype{1, 2, 3, 4}, 58 | }, 59 | { 60 | s: []valtype{1, 3}, 61 | v: 2, 62 | want: []valtype{1, 2, 3}, 63 | }, 64 | } 65 | for i, tc := range cases { 66 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 67 | var vs ValueSet 68 | for _, val := range tc.s { 69 | vs = append(vs, val) 70 | } 71 | got := vs.Add(tc.v) 72 | var wantvs ValueSet 73 | for _, val := range tc.want { 74 | wantvs = append(wantvs, val) 75 | } 76 | if !reflect.DeepEqual(got, wantvs) { 77 | t.Errorf("got %v, want %v", got, wantvs) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestValueSetRemove(t *testing.T) { 84 | cases := []struct { 85 | s []valtype 86 | v valtype 87 | want []valtype 88 | }{ 89 | { 90 | s: []valtype{}, 91 | v: 1, 92 | want: []valtype{}, 93 | }, 94 | { 95 | s: []valtype{1}, 96 | v: 1, 97 | want: []valtype{}, 98 | }, 99 | { 100 | s: []valtype{1, 2, 3}, 101 | v: 2, 102 | want: []valtype{1, 3}, 103 | }, 104 | { 105 | s: []valtype{1, 2, 3}, 106 | v: 1, 107 | want: []valtype{2, 3}, 108 | }, 109 | { 110 | s: []valtype{1, 2, 3}, 111 | v: 3, 112 | want: []valtype{1, 2}, 113 | }, 114 | } 115 | for i, tc := range cases { 116 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 117 | var vs ValueSet 118 | for _, val := range tc.s { 119 | vs = append(vs, val) 120 | } 121 | got := vs.Remove(tc.v) 122 | var wantvs ValueSet 123 | for _, val := range tc.want { 124 | wantvs = append(wantvs, val) 125 | } 126 | if !reflect.DeepEqual(got, wantvs) { 127 | t.Errorf("got %v, want %v", got, wantvs) 128 | } 129 | }) 130 | } 131 | } 132 | 133 | func TestValueSetContains(t *testing.T) { 134 | cases := []struct { 135 | s []valtype 136 | v valtype 137 | want bool 138 | }{ 139 | { 140 | s: []valtype{}, 141 | v: 1, 142 | want: false, 143 | }, 144 | { 145 | s: []valtype{1}, 146 | v: 1, 147 | want: true, 148 | }, 149 | { 150 | s: []valtype{1, 2, 3}, 151 | v: 0, 152 | want: false, 153 | }, 154 | { 155 | s: []valtype{1, 2, 3}, 156 | v: 4, 157 | want: false, 158 | }, 159 | { 160 | s: []valtype{1, 2, 3}, 161 | v: 2, 162 | want: true, 163 | }, 164 | { 165 | s: []valtype{1, 3}, 166 | v: 2, 167 | want: false, 168 | }, 169 | } 170 | for i, tc := range cases { 171 | t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { 172 | var vs ValueSet 173 | for _, val := range tc.s { 174 | vs = append(vs, val) 175 | } 176 | got := vs.Contains(tc.v) 177 | if got != tc.want { 178 | t.Errorf("got %v, want %v", got, tc.want) 179 | } 180 | }) 181 | } 182 | } 183 | --------------------------------------------------------------------------------