github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/flip/dealer.go (about) 1 package flip 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/hex" 7 "fmt" 8 "io" 9 "math" 10 "math/big" 11 "strings" 12 "time" 13 14 chat1 "github.com/keybase/client/go/protocol/chat1" 15 clockwork "github.com/keybase/clockwork" 16 ) 17 18 // Excludes `Params` from being logged. 19 func (s Start) String() string { 20 return fmt.Sprintf("{StartTime:%v CommitmentWindowMsec:%v RevealWindowMsec:%v SlackMsec:%v CommitmentCompleteWindowMsec:%v}", 21 s.StartTime, s.CommitmentWindowMsec, s.RevealWindowMsec, s.SlackMsec, s.CommitmentWindowMsec) 22 } 23 24 type GameMessageWrapped struct { 25 Sender UserDevice 26 Msg GameMessageV1 27 Me *playerControl 28 Forward bool 29 FirstInConversation bool 30 } 31 32 func (m GameMessageWrapped) isForwardable() bool { 33 t, _ := m.Msg.Body.T() 34 return t != MessageType_END 35 } 36 37 func (m GameMessageWrapped) GameMetadata() GameMetadata { 38 return m.Msg.Md 39 } 40 41 func (g GameMetadata) ToKey() GameKey { 42 return GameKey(strings.Join([]string{g.Initiator.U.String(), g.Initiator.D.String(), g.ConversationID.String(), g.GameID.String()}, ",")) 43 } 44 45 func (g GameMetadata) String() string { 46 return string(g.ToKey()) 47 } 48 49 func (g GameMetadata) check() bool { 50 return g.Initiator.check() && !g.ConversationID.IsNil() && g.GameID.Check() 51 } 52 53 type GameKey string 54 type GameIDKey string 55 type UserDeviceKey string 56 57 func (u UserDevice) ToKey() UserDeviceKey { 58 return UserDeviceKey(strings.Join([]string{u.U.String(), u.D.String()}, ",")) 59 } 60 61 func (u UserDevice) check() bool { 62 return u.U.Bytes() != nil && u.D.Bytes() != nil 63 } 64 65 func GameIDToKey(g chat1.FlipGameID) GameIDKey { 66 return GameIDKey(g.String()) 67 } 68 69 type Result struct { 70 Shuffle []int 71 Bool *bool 72 Int *int64 73 Big *big.Int 74 } 75 76 type Game struct { 77 md GameMetadata 78 msgID int 79 clockSkew time.Duration 80 start time.Time 81 isLeader bool 82 params Start 83 key GameKey 84 msgCh <-chan *GameMessageWrapped 85 stage Stage 86 stageForTimeout Stage 87 players map[UserDeviceKey]*GamePlayerState 88 commitments map[string]bool 89 gameUpdateCh chan GameStateUpdateMessage 90 nPlayers int 91 dealer *Dealer 92 me *playerControl 93 commitmentCompleteHash Hash 94 clock func() clockwork.Clock 95 clogf func(ctx context.Context, fmt string, args ...interface{}) 96 97 // To handle reorderings between CommitmentComplete and commitements, 98 // wee need some extra bookkeeping. 99 iWasIncluded bool 100 iOptedOutOfCommit bool 101 gotCommitmentComplete bool 102 latecomers map[UserDeviceKey]bool 103 } 104 105 type GamePlayerState struct { 106 ud UserDevice 107 commitment *Commitment 108 commitmentTime time.Time 109 leaderCommitment *Commitment 110 included bool 111 secret *Secret 112 } 113 114 func (g *Game) GameMetadata() GameMetadata { 115 return g.md 116 } 117 118 func MakeGameMessageEncoded(s string) GameMessageEncoded { 119 return GameMessageEncoded(s) 120 } 121 122 func (e GameMessageEncoded) String() string { 123 return string(e) 124 } 125 126 func (e GameMessageEncoded) Decode() (*GameMessageV1, error) { 127 raw, err := base64.StdEncoding.DecodeString(string(e)) 128 if err != nil { 129 return nil, err 130 } 131 var msg GameMessage 132 err = msgpackDecode(&msg, raw) 133 if err != nil { 134 return nil, err 135 } 136 v, err := msg.V() 137 if err != nil { 138 return nil, err 139 } 140 if v != Version_V1 { 141 return nil, BadVersionError(v) 142 } 143 tmp := msg.V1() 144 if !tmp.Md.check() { 145 return nil, ErrBadData 146 } 147 return &tmp, nil 148 } 149 150 func (e *GameMessageWrappedEncoded) Decode() (*GameMessageWrapped, error) { 151 v1, err := e.Body.Decode() 152 if err != nil { 153 return nil, err 154 } 155 ret := GameMessageWrapped{Sender: e.Sender, Msg: *v1, FirstInConversation: e.FirstInConversation} 156 if !e.GameID.Eq(ret.Msg.Md.GameID) { 157 return nil, BadGameIDError{G: ret.Msg.Md, I: e.GameID} 158 } 159 return &ret, nil 160 } 161 162 func (m GameMessageWrapped) Encode() (GameMessageEncoded, error) { 163 return m.Msg.Encode() 164 } 165 166 func (b GameMessageBody) Encode(md GameMetadata) (GameMessageEncoded, error) { 167 v1 := GameMessageV1{Md: md, Body: b} 168 return v1.Encode() 169 } 170 171 func (v GameMessageV1) Encode() (GameMessageEncoded, error) { 172 msg := NewGameMessageWithV1(v) 173 raw, err := msgpackEncode(msg) 174 if err != nil { 175 return GameMessageEncoded(""), err 176 } 177 return GameMessageEncoded(base64.StdEncoding.EncodeToString(raw)), nil 178 } 179 180 func (d *Dealer) run(ctx context.Context, game *Game) { 181 doneCh := make(chan error) 182 key := game.key 183 go game.run(ctx, doneCh) 184 err := <-doneCh 185 186 if err != nil { 187 d.dh.CLogf(ctx, "[%s] Error running game %s: %s", d.dh.Me(), key, err.Error()) 188 189 } else { 190 d.dh.CLogf(ctx, "Game %s ended cleanly", key) 191 } 192 193 // If the game was shutdown via the Dealer#Stop call, then 194 // don't close the channel (again) or remove the channel from the 195 // map, it's already dead. 196 if _, ok := err.(GameShutdownError); ok { 197 return 198 } 199 200 d.Lock() 201 if ch := d.games[key]; ch != nil { 202 close(ch) 203 delete(d.games, key) 204 delete(d.gameIDs, GameIDToKey(game.md.GameID)) 205 } 206 d.Unlock() 207 } 208 209 func (g *Game) getNextTimer() <-chan time.Time { 210 dl := g.nextDeadline() 211 return g.clock().AfterTime(dl) 212 } 213 214 func (g *Game) CommitmentEndTime() time.Time { 215 // If we're the leader, then let's cut off when we say we're going to cut off 216 // If we're not, then let's give extra time (a multiple of 2) to the leader. 217 return g.start.Add(g.params.CommitmentWindowWithSlack(g.isLeader)) 218 } 219 220 func (g *Game) RevealEndTime() time.Time { 221 return g.start.Add(g.params.RevealWindowWithSlack()) 222 } 223 224 func (g *Game) nextDeadline() time.Time { 225 switch g.stageForTimeout { 226 case Stage_ROUND1: 227 return g.CommitmentEndTime() 228 case Stage_ROUND2: 229 return g.RevealEndTime() 230 default: 231 return time.Time{} 232 } 233 } 234 235 func (g Game) commitmentPayload() CommitmentPayload { 236 return CommitmentPayload{ 237 V: Version_V1, 238 U: g.md.Initiator.U, 239 D: g.md.Initiator.D, 240 C: g.md.ConversationID, 241 G: g.md.GameID, 242 S: g.params.StartTime, 243 } 244 } 245 246 func (g *Game) setSecret(ctx context.Context, ps *GamePlayerState, secret Secret) error { 247 expected, err := secret.computeCommitment(g.commitmentPayload()) 248 if err != nil { 249 return err 250 } 251 if ps.secret != nil { 252 return DuplicateRevealError{G: g.md, U: ps.ud} 253 } 254 if !expected.Eq(*ps.commitment) { 255 return BadRevealError{G: g.md, U: ps.ud} 256 } 257 ps.secret = &secret 258 return nil 259 } 260 261 func (g *Game) finishGame(ctx context.Context) error { 262 var xor Secret 263 for _, ps := range g.players { 264 if !ps.included { 265 continue 266 } 267 if ps.secret == nil { 268 return NoRevealError{G: g.md, U: ps.ud} 269 } 270 xor.XOR(*ps.secret) 271 } 272 prng := NewPRNG(xor) 273 err := g.doFlip(ctx, prng) 274 g.sendOutgoingChat(ctx, NewGameMessageBodyWithEnd()) 275 return err 276 } 277 278 func (g *Game) doFlip(ctx context.Context, prng *PRNG) error { 279 params := g.params.Params 280 t, err := params.T() 281 if err != nil { 282 return err 283 } 284 var res Result 285 switch t { 286 case FlipType_BOOL: 287 tmp := prng.Bool() 288 res.Bool = &tmp 289 case FlipType_INT: 290 tmp := prng.Int(params.Int()) 291 res.Int = &tmp 292 case FlipType_BIG: 293 var modulus big.Int 294 modulus.SetBytes(params.Big()) 295 res.Big = prng.Big(&modulus) 296 case FlipType_SHUFFLE: 297 res.Shuffle = prng.Permutation(int(params.Shuffle())) 298 default: 299 return BadFlipTypeError{G: g.GameMetadata(), T: t} 300 } 301 302 g.gameUpdateCh <- GameStateUpdateMessage{ 303 Metadata: g.GameMetadata(), 304 Result: &res, 305 } 306 return nil 307 } 308 309 func (g *Game) playerCommitedInTime(ps *GamePlayerState, now time.Time) bool { 310 diff := ps.commitmentTime.Sub(g.start) 311 return diff < g.params.CommitmentWindowWithSlack(true) 312 } 313 314 func (g *Game) getPlayerState(ud UserDevice) *GamePlayerState { 315 key := ud.ToKey() 316 ret := g.players[key] 317 if ret != nil { 318 return ret 319 } 320 ret = &GamePlayerState{ud: ud} 321 g.players[key] = ret 322 return ret 323 } 324 325 func (g *Game) handleCommitment(ctx context.Context, sender UserDevice, now time.Time, com Commitment) (err error) { 326 ps := g.getPlayerState(sender) 327 if ps.commitment != nil { 328 return DuplicateRegistrationError{g.md, sender} 329 } 330 if ps.leaderCommitment != nil && !ps.leaderCommitment.Eq(com) { 331 return CommitmentMismatchError{G: g.GameMetadata(), U: sender} 332 } 333 ps.commitment = &com 334 ps.commitmentTime = now 335 336 comHex := hex.EncodeToString(com[:]) 337 if g.commitments[comHex] { 338 return DuplicateCommitmentError{} 339 } 340 g.commitments[comHex] = true 341 342 // If this user was a latecomer (we got the commitment after we got the CommitmentComplete), 343 // then we mark them as being accounted for. 344 delete(g.latecomers, sender.ToKey()) 345 346 g.gameUpdateCh <- GameStateUpdateMessage{ 347 Metadata: g.GameMetadata(), 348 Commitment: &CommitmentUpdate{ 349 User: sender, 350 Commitment: com, 351 }, 352 } 353 return g.maybeReveal(ctx) 354 } 355 356 func (g *Game) maybeReveal(ctx context.Context) (err error) { 357 358 if !g.gotCommitmentComplete { 359 return nil 360 } 361 if len(g.latecomers) > 0 { 362 return nil 363 } 364 365 g.stage = Stage_ROUND2 366 g.stageForTimeout = Stage_ROUND2 367 368 if g.me == nil { 369 return nil 370 } 371 372 if !g.iWasIncluded && !g.iOptedOutOfCommit { 373 g.clogf(ctx, "The leader didn't include me (%s) so not sending a reveal (%s)", g.me.me, g.md) 374 return nil 375 } 376 377 reveal := Reveal{ 378 Secret: g.me.secret, 379 Cch: g.commitmentCompleteHash, 380 } 381 g.sendOutgoingChat(ctx, NewGameMessageBodyWithReveal(reveal)) 382 return nil 383 } 384 385 func (g *Game) handleCommitmentCompletePlayer(ctx context.Context, u UserDeviceCommitment) (err error) { 386 387 ps := g.getPlayerState(u.Ud) 388 if ps.leaderCommitment != nil { 389 return DuplicateCommitmentCompleteError{G: g.md, U: u.Ud} 390 } 391 if ps.commitment != nil && !ps.commitment.Eq(u.C) { 392 return CommitmentMismatchError{G: g.md, U: u.Ud} 393 } 394 if ps.commitment == nil { 395 g.latecomers[u.Ud.ToKey()] = true 396 } 397 ps.leaderCommitment = &u.C 398 ps.included = true 399 g.nPlayers++ 400 401 if g.me != nil && g.me.me.Eq(u.Ud) { 402 g.iWasIncluded = true 403 } 404 return nil 405 } 406 407 func (g *Game) handleCommitmentComplete(ctx context.Context, sender UserDevice, now time.Time, cc CommitmentComplete) (err error) { 408 409 if !sender.Eq(g.md.Initiator) { 410 return WrongSenderError{G: g.md, Expected: g.md.Initiator, Actual: sender} 411 } 412 413 if !checkUserDeviceCommitments(cc.Players) { 414 return CommitmentCompleteSortError{G: g.md} 415 } 416 417 for _, u := range cc.Players { 418 err = g.handleCommitmentCompletePlayer(ctx, u) 419 if err != nil { 420 return err 421 } 422 } 423 424 cch, err := hashUserDeviceCommitments(cc.Players) 425 if err != nil { 426 return err 427 } 428 429 g.commitmentCompleteHash = cch 430 g.gotCommitmentComplete = true 431 432 // for now, just warn if users who made it in on time weren't included. 433 for _, ps := range g.players { 434 if !ps.included && g.playerCommitedInTime(ps, now) && g.clogf != nil { 435 g.clogf(ctx, "User %s wasn't included, but they should have been", ps.ud) 436 } 437 } 438 439 g.gameUpdateCh <- GameStateUpdateMessage{ 440 Metadata: g.GameMetadata(), 441 CommitmentComplete: &cc, 442 } 443 444 return g.maybeReveal(ctx) 445 } 446 447 func errToOk(err error) string { 448 if err == nil { 449 return "ok" 450 } 451 return "ERROR: " + err.Error() 452 } 453 454 func (g *Game) handleMessage(ctx context.Context, msg *GameMessageWrapped, now time.Time) (err error) { 455 456 msgID := g.msgID 457 g.msgID++ 458 459 g.clogf(ctx, "+ Game#handleMessage: %s@%d <- %+v", g.GameMetadata(), msgID, *msg) 460 defer func() { g.clogf(ctx, "- Game#handleMessage: %s@%d -> %s", g.GameMetadata(), msgID, errToOk(err)) }() 461 462 t, err := msg.Msg.Body.T() 463 if err != nil { 464 return err 465 } 466 badStage := func() error { 467 return BadMessageForStageError{G: g.GameMetadata(), MessageType: t, Stage: g.stage} 468 } 469 switch t { 470 471 case MessageType_START: 472 return badStage() 473 474 case MessageType_END: 475 return io.EOF 476 477 case MessageType_COMMITMENT: 478 if g.stage != Stage_ROUND1 { 479 g.clogf(ctx, "User %s sent a commitment too late, not included in game %s", msg.Sender, g.md) 480 return nil 481 } 482 483 err = g.handleCommitment(ctx, msg.Sender, now, msg.Msg.Body.Commitment()) 484 if err != nil { 485 return err 486 } 487 488 case MessageType_COMMITMENT_COMPLETE: 489 if g.stage != Stage_ROUND1 { 490 return badStage() 491 } 492 493 err = g.handleCommitmentComplete(ctx, msg.Sender, now, msg.Msg.Body.CommitmentComplete()) 494 if err != nil { 495 return err 496 } 497 498 case MessageType_REVEAL: 499 if g.stage != Stage_ROUND2 { 500 return badStage() 501 } 502 503 key := msg.Sender.ToKey() 504 ps := g.players[key] 505 506 if ps == nil { 507 g.clogf(ctx, "Skipping unregistered revealer %s for game %s", msg.Sender, g.md) 508 return nil 509 } 510 if !ps.included { 511 g.clogf(ctx, "Skipping unincluded revealer %s for game %s", msg.Sender, g.md) 512 return nil 513 } 514 if now.After(g.RevealEndTime()) { 515 return RevealTooLateError{G: g.md, U: msg.Sender} 516 } 517 518 reveal := msg.Msg.Body.Reveal() 519 if !g.commitmentCompleteHash.Eq(reveal.Cch) { 520 return BadCommitmentCompleteHashError{G: g.GameMetadata(), U: msg.Sender} 521 } 522 err := g.setSecret(ctx, ps, reveal.Secret) 523 if err != nil { 524 return err 525 } 526 g.gameUpdateCh <- GameStateUpdateMessage{ 527 Metadata: g.GameMetadata(), 528 Reveal: &RevealUpdate{ 529 User: msg.Sender, 530 Reveal: reveal.Secret, 531 }, 532 } 533 534 g.nPlayers-- 535 if g.nPlayers == 0 { 536 return g.finishGame(ctx) 537 } 538 539 default: 540 return BadMessageError{G: g.GameMetadata()} 541 } 542 543 return nil 544 } 545 546 func (g *Game) userDeviceCommitmentList() []UserDeviceCommitment { 547 var ret []UserDeviceCommitment 548 for _, p := range g.players { 549 if p.commitment != nil { 550 ret = append(ret, UserDeviceCommitment{Ud: p.ud, C: *p.commitment}) 551 } 552 } 553 sortUserDeviceCommitments(ret) 554 return ret 555 } 556 557 func (g *Game) completeCommitments(ctx context.Context) error { 558 cc := CommitmentComplete{ 559 Players: g.userDeviceCommitmentList(), 560 } 561 body := NewGameMessageBodyWithCommitmentComplete(cc) 562 g.stageForTimeout = Stage_ROUND2 563 g.sendOutgoingChat(ctx, body) 564 return nil 565 } 566 567 func (g *Game) absentees() []UserDevice { 568 var bad []UserDevice 569 for _, p := range g.players { 570 if p.included && p.secret == nil { 571 bad = append(bad, p.ud) 572 } 573 } 574 return bad 575 } 576 577 func (g *Game) sendOutgoingChat(ctx context.Context, body GameMessageBody) { 578 // Call back into the dealer, to reroute a message back into our 579 // game, but do so in a Go routine so we don't deadlock. There could be 580 // 100 incoming messages in front of us, all coming off the chat channel, 581 // so we're ok to send when we can. If use the game in the context of 582 // replay, the dealer will be nil, so no need to send. 583 if g.dealer != nil { 584 go func() { 585 err := g.dealer.sendOutgoingChat(ctx, g.GameMetadata(), nil, body) 586 if err != nil { 587 g.clogf(ctx, "Error sending outgoing chat %+v", err) 588 } 589 }() 590 } 591 } 592 593 func (g *Game) handleTimerEvent(ctx context.Context) error { 594 if g.isLeader && g.stageForTimeout == Stage_ROUND1 { 595 return g.completeCommitments(ctx) 596 } 597 598 absentees := g.absentees() 599 600 if g.stageForTimeout == Stage_ROUND2 && len(absentees) > 0 { 601 return AbsenteesError{Absentees: absentees} 602 } 603 604 return TimeoutError{G: g.md, Stage: g.stageForTimeout} 605 } 606 607 func (g *Game) runMain(ctx context.Context) error { 608 for { 609 timer := g.getNextTimer() 610 var err error 611 select { 612 case <-timer: 613 err = g.handleTimerEvent(ctx) 614 case msg, ok := <-g.msgCh: 615 if !ok { 616 return GameShutdownError{G: g.GameMetadata()} 617 } 618 err = g.handleMessage(ctx, msg, g.clock().Now()) 619 case <-ctx.Done(): 620 return ctx.Err() 621 } 622 if err == io.EOF { 623 return nil 624 } 625 if err != nil { 626 g.gameUpdateCh <- GameStateUpdateMessage{ 627 Metadata: g.GameMetadata(), 628 Err: err, 629 } 630 return err 631 } 632 } 633 } 634 635 func (g *Game) runDrain(ctx context.Context) { 636 i := 0 637 for range g.msgCh { 638 i++ 639 } 640 if i > 0 { 641 g.clogf(ctx, "drained %d messages on shutdown in game %s", i, g.md) 642 } 643 } 644 645 func (g *Game) run(ctx context.Context, doneCh chan error) { 646 doneCh <- g.runMain(ctx) 647 g.runDrain(ctx) 648 } 649 650 func absDuration(d time.Duration) time.Duration { 651 if d < time.Duration(0) { 652 return time.Duration(-1) * d 653 } 654 return d 655 } 656 657 func (d *Dealer) computeClockSkew(ctx context.Context, md GameMetadata, leaderTime time.Time, myNow time.Time) (skew time.Duration, err error) { 658 serverTime, err := d.dh.ServerTime(ctx) 659 if err != nil { 660 return skew, err 661 } 662 return computeClockSkew(md, serverTime, leaderTime, myNow) 663 } 664 665 func computeClockSkew(md GameMetadata, serverTime time.Time, leaderTime time.Time, myNow time.Time) (skew time.Duration, err error) { 666 localTime := myNow 667 leaderSkew := leaderTime.Sub(serverTime) 668 localSkew := localTime.Sub(serverTime) 669 670 if absDuration(localSkew) > MaxClockSkew { 671 return time.Duration(0), BadLocalClockError{G: md} 672 } 673 if absDuration(leaderSkew) > MaxClockSkew { 674 return time.Duration(0), BadLeaderClockError{G: md} 675 } 676 totalSkew := localTime.Sub(leaderTime) 677 678 return totalSkew, nil 679 } 680 681 func (d *Dealer) handleMessageStart(ctx context.Context, msg *GameMessageWrapped, start Start) error { 682 d.Lock() 683 defer d.Unlock() 684 md := msg.GameMetadata() 685 key := md.ToKey() 686 gameIDKey := GameIDToKey(md.GameID) 687 if d.games[key] != nil { 688 return GameAlreadyStartedError{G: md} 689 } 690 if _, found := d.gameIDs[gameIDKey]; found { 691 return GameReplayError{G: md.GameID} 692 } 693 if !msg.Sender.Eq(md.Initiator) { 694 return WrongSenderError{G: md, Expected: msg.Sender, Actual: md.Initiator} 695 } 696 cs, err := d.computeClockSkew(ctx, md, start.StartTime.Time(), d.dh.Clock().Now()) 697 if err != nil { 698 return err 699 } 700 701 if !msg.FirstInConversation { 702 return GameReplayError{md.GameID} 703 } 704 705 isLeader := true 706 me := msg.Me 707 // Make a new follower player controller if one didn't already exist (since we were 708 // the Leader) 709 if me == nil { 710 me, err = d.newPlayerControl(d.dh.Me(), md, start) 711 if err != nil { 712 return err 713 } 714 isLeader = false 715 } 716 717 optedOutOfCommit := false 718 if !isLeader { 719 optedOutOfCommit = !d.dh.ShouldCommit(ctx) 720 } 721 722 msgCh := make(chan *GameMessageWrapped) 723 game := &Game{ 724 md: msg.GameMetadata(), 725 isLeader: isLeader, 726 clockSkew: cs, 727 start: d.dh.Clock().Now(), 728 key: key, 729 params: start, 730 msgCh: msgCh, 731 stage: Stage_ROUND1, 732 stageForTimeout: Stage_ROUND1, 733 gameUpdateCh: d.gameUpdateCh, 734 players: make(map[UserDeviceKey]*GamePlayerState), 735 commitments: make(map[string]bool), 736 dealer: d, 737 me: me, 738 clock: d.dh.Clock, 739 clogf: d.dh.CLogf, 740 latecomers: make(map[UserDeviceKey]bool), 741 iOptedOutOfCommit: optedOutOfCommit, 742 } 743 d.games[key] = msgCh 744 d.gameIDs[gameIDKey] = md 745 d.previousGames[GameIDToKey(md.GameID)] = true 746 747 go d.run(ctx, game) 748 749 // Once the game has started, we are free to send a message into the channel 750 // with our commitment. We are now in the inner loop of the Dealer, so we 751 // have to do this send in a Go-routine, so as not to deadlock the Dealer. 752 if !isLeader && !optedOutOfCommit { 753 go func() { 754 err := d.sendCommitment(ctx, md, me) 755 if err != nil { 756 game.clogf(ctx, "Error sending commitment: %+v", err) 757 } 758 }() 759 } 760 return nil 761 } 762 763 func (d *Dealer) handleMessageOthers(c context.Context, msg *GameMessageWrapped) error { 764 d.Lock() 765 defer d.Unlock() 766 md := msg.GameMetadata() 767 key := md.ToKey() 768 game := d.games[key] 769 if game == nil { 770 return GameFinishedError{G: md} 771 } 772 game <- msg 773 return nil 774 } 775 776 func (d *Dealer) handleMessage(ctx context.Context, msg *GameMessageWrapped) error { 777 d.dh.CLogf(ctx, "flip.Dealer: Incoming: %+v", msg) 778 779 t, err := msg.Msg.Body.T() 780 if err != nil { 781 return err 782 } 783 switch t { 784 case MessageType_START: 785 err = d.handleMessageStart(ctx, msg, msg.Msg.Body.Start()) 786 if err != nil { 787 d.gameUpdateCh <- GameStateUpdateMessage{ 788 Metadata: msg.Msg.Md, 789 Err: err, 790 } 791 } 792 default: 793 err = d.handleMessageOthers(ctx, msg) 794 } 795 if err != nil { 796 return err 797 } 798 if !(msg.Forward && msg.isForwardable()) { 799 return nil 800 } 801 // Encode and send the message through the external server-routed chat channel 802 emsg, err := msg.Encode() 803 if err != nil { 804 return err 805 } 806 err = d.dh.SendChat(ctx, msg.Msg.Md.Initiator.U, msg.Msg.Md.ConversationID, msg.Msg.Md.GameID, emsg) 807 if err != nil { 808 return err 809 } 810 return nil 811 } 812 813 func (d *Dealer) stopGames() { 814 d.Lock() 815 defer d.Unlock() 816 for k, ch := range d.games { 817 delete(d.games, k) 818 close(ch) 819 } 820 } 821 822 type playerControl struct { 823 me UserDevice 824 md GameMetadata 825 secret Secret 826 commitment Commitment 827 start Start 828 dealer *Dealer 829 } 830 831 func (d *Dealer) newPlayerControl(me UserDevice, md GameMetadata, start Start) (*playerControl, error) { 832 secret := GenerateSecret() 833 cp := CommitmentPayload{ 834 V: Version_V1, 835 U: md.Initiator.U, 836 D: md.Initiator.D, 837 C: md.ConversationID, 838 G: md.GameID, 839 S: start.StartTime, 840 } 841 commitment, err := secret.computeCommitment(cp) 842 if err != nil { 843 return nil, err 844 } 845 return &playerControl{ 846 me: me, 847 md: md, 848 secret: secret, 849 commitment: commitment, 850 start: start, 851 dealer: d, 852 }, nil 853 } 854 855 func (p *playerControl) GameMetadata() GameMetadata { 856 return p.md 857 } 858 859 func (d *Dealer) startFlip(ctx context.Context, start Start, conversationID chat1.ConversationID) (pc *playerControl, err error) { 860 return d.startFlipWithGameID(ctx, start, conversationID, GenerateGameID()) 861 } 862 863 func (d *Dealer) startFlipWithGameID(ctx context.Context, start Start, conversationID chat1.ConversationID, 864 gameID chat1.FlipGameID) (pc *playerControl, err error) { 865 md := GameMetadata{ 866 Initiator: d.dh.Me(), 867 ConversationID: conversationID, 868 GameID: gameID, 869 } 870 pc, err = d.newPlayerControl(d.dh.Me(), md, start) 871 if err != nil { 872 return nil, err 873 } 874 err = d.sendOutgoingChatWithFirst(ctx, md, pc, NewGameMessageBodyWithStart(start), true) 875 if err != nil { 876 return nil, err 877 } 878 err = d.sendCommitment(ctx, md, pc) 879 if err != nil { 880 return nil, err 881 } 882 return pc, nil 883 } 884 885 func (d *Dealer) sendCommitment(ctx context.Context, md GameMetadata, pc *playerControl) error { 886 return d.sendOutgoingChat(ctx, md, nil, NewGameMessageBodyWithCommitment(pc.commitment)) 887 } 888 889 func (d *Dealer) sendOutgoingChat(ctx context.Context, md GameMetadata, me *playerControl, body GameMessageBody) error { 890 return d.sendOutgoingChatWithFirst(ctx, md, me, body, false) 891 } 892 893 func (d *Dealer) sendOutgoingChatWithFirst(ctx context.Context, md GameMetadata, me *playerControl, body GameMessageBody, firstInConversation bool) error { 894 895 gmw := GameMessageWrapped{ 896 Sender: d.dh.Me(), 897 Me: me, 898 FirstInConversation: firstInConversation, 899 Msg: GameMessageV1{ 900 Md: md, 901 Body: body, 902 }, 903 } 904 905 // Only mark the forward bit to be true on messages that we can forward. 906 gmw.Forward = gmw.isForwardable() 907 908 // Reinject the message into the state machine. 909 d.chatInputCh <- &gmw 910 911 return nil 912 } 913 914 var DefaultCommitmentWindowMsec int64 = 3 * 1000 915 var DefaultRevealWindowMsec int64 = 30 * 1000 916 var DefaultCommitmentCompleteWindowMsec int64 = 15 * 1000 917 var DefaultSlackMsec int64 = 1 * 1000 918 919 // For bigger groups, everything is slower, like the time to digest all required messages. So we're 920 // going to inflate our timeouts. 921 func inflateTimeout(timeout int64, nPlayers int) int64 { 922 if nPlayers <= 5 { 923 return timeout 924 } 925 return int64(math.Ceil(math.Log(float64(nPlayers)) * float64(timeout) / math.Log(5.0))) 926 } 927 928 func newStart(now time.Time, nPlayers int) Start { 929 return Start{ 930 StartTime: ToTime(now), 931 CommitmentWindowMsec: inflateTimeout(DefaultCommitmentWindowMsec, nPlayers), 932 RevealWindowMsec: inflateTimeout(DefaultRevealWindowMsec, nPlayers), 933 CommitmentCompleteWindowMsec: inflateTimeout(DefaultCommitmentCompleteWindowMsec, nPlayers), 934 SlackMsec: inflateTimeout(DefaultSlackMsec, nPlayers), 935 } 936 }