/
flip.go
223 lines (196 loc) · 7.39 KB
/
flip.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package flip
import (
"context"
"io"
"math/big"
"sync"
"time"
chat1 "github.com/keybase/client/go/protocol/chat1"
clockwork "github.com/keybase/clockwork"
)
// GameMessageEncoded is a game message that is shipped over the chat channel. Inside, it's a base64-encoded
// msgpack object (generated via AVDL->go compiler), but it's safe to think of it just as an opaque string.
type GameMessageEncoded string
// GameMessageWrappedEncoded contains a sender, a gameID and a Body. The GameID should never be reused.
type GameMessageWrappedEncoded struct {
Sender UserDevice
GameID chat1.FlipGameID // the game ID of this game, also specified (encoded) in GameMessageEncoded
Body GameMessageEncoded // base64-encoded GameMessaageBody that comes in over chat
FirstInConversation bool // on if this is the first message in the conversation
}
type CommitmentUpdate struct {
User UserDevice
Commitment Commitment
}
type RevealUpdate struct {
User UserDevice
Reveal Secret
}
// GameStateUpdateMessage is sent from the game dealer out to the calling chat client, to update him
// on changes to game state that happened. All update messages are relative to the given GameMetadata.
// For each update, only one of Err, Commitment, Reveal, CommitmentComplete or Result will be non-nil.
type GameStateUpdateMessage struct {
Metadata GameMetadata
// only one of the following will be non-nil
Err error
Commitment *CommitmentUpdate
Reveal *RevealUpdate
CommitmentComplete *CommitmentComplete
Result *Result
}
// Dealer is a peristent process that runs in the chat client that deals out a game. It can have multiple
// games running at once.
type Dealer struct {
sync.Mutex
dh DealersHelper
games map[GameKey](chan<- *GameMessageWrapped)
gameIDs map[GameIDKey]GameMetadata
shutdownMu sync.Mutex
shutdownCh chan struct{}
chatInputCh chan *GameMessageWrapped
gameUpdateCh chan GameStateUpdateMessage
previousGames map[GameIDKey]bool
}
// ReplayHelper contains hooks needed to replay a flip.
type ReplayHelper interface {
CLogf(ctx context.Context, fmt string, args ...interface{})
}
// DealersHelper is an interface that calling chat clients need to implement.
type DealersHelper interface {
ReplayHelper
Clock() clockwork.Clock
ServerTime(context.Context) (time.Time, error)
SendChat(ctx context.Context, ch chat1.ConversationID, gameID chat1.FlipGameID, msg GameMessageEncoded) error
Me() UserDevice
ShouldCommit(ctx context.Context) bool // Whether to send new commitments for games.
}
// NewDealer makes a new Dealer with a given DealersHelper
func NewDealer(dh DealersHelper) *Dealer {
return &Dealer{
dh: dh,
games: make(map[GameKey](chan<- *GameMessageWrapped)),
gameIDs: make(map[GameIDKey]GameMetadata),
chatInputCh: make(chan *GameMessageWrapped),
gameUpdateCh: make(chan GameStateUpdateMessage, 500),
previousGames: make(map[GameIDKey]bool),
}
}
// UpdateCh returns a channel that sends a sequence of GameStateUpdateMessages, each notifying the
// UI about changes to ongoing games.
func (d *Dealer) UpdateCh() <-chan GameStateUpdateMessage {
return d.gameUpdateCh
}
// Run a dealer in a given context. It wil run as long as it isn't shutdown.
func (d *Dealer) Run(ctx context.Context) error {
d.shutdownMu.Lock()
shutdownCh := make(chan struct{})
d.shutdownCh = shutdownCh
d.shutdownMu.Unlock()
for {
select {
case <-ctx.Done():
return ctx.Err()
// This channel never closes
case msg := <-d.chatInputCh:
err := d.handleMessage(ctx, msg)
if err != nil {
d.dh.CLogf(ctx, "Error reading message: %s", err.Error())
}
// exit the loop if we've shutdown
case <-shutdownCh:
return io.EOF
}
}
}
// Stop a dealer on process shutdown.
func (d *Dealer) Stop() {
d.shutdownMu.Lock()
if d.shutdownCh != nil {
close(d.shutdownCh)
d.shutdownCh = nil
}
d.shutdownMu.Unlock()
d.stopGames()
}
// StartFlip starts a new flip. Pass it some start parameters as well as a chat conversationID that it
// will take place in.
func (d *Dealer) StartFlip(ctx context.Context, start Start, conversationID chat1.ConversationID) (err error) {
_, err = d.startFlip(ctx, start, conversationID)
return err
}
// StartFlipWithGameID starts a new flip. Pass it some start parameters as well as a chat conversationID
// that it will take place in. Also takes a GameID
func (d *Dealer) StartFlipWithGameID(ctx context.Context, start Start, conversationID chat1.ConversationID,
gameID chat1.FlipGameID) (err error) {
_, err = d.startFlipWithGameID(ctx, start, conversationID, gameID)
return err
}
// InjectIncomingChat should be called whenever a new flip game comes in that's relevant for flips.
// Call this with the sender's information, the channel information, and the body data that came in.
// The last bool is true only if this is the first message in the channel. The current model is that only
// one "game" is allowed for each chat channel. So any prior messages in the channel mean it might be replay.
// This is significantly less general than an earlier model, which is why we introduced the concept of
// a gameID, so it might be changed in the future.
func (d *Dealer) InjectIncomingChat(ctx context.Context, sender UserDevice,
conversationID chat1.ConversationID, gameID chat1.FlipGameID, body GameMessageEncoded,
firstInConversation bool) error {
gmwe := GameMessageWrappedEncoded{
Sender: sender,
GameID: gameID,
Body: body,
FirstInConversation: firstInConversation,
}
msg, err := gmwe.Decode()
if err != nil {
return err
}
if !msg.Msg.Md.ConversationID.Eq(conversationID) {
return BadChannelError{G: msg.Msg.Md, C: conversationID}
}
if !msg.isForwardable() {
return UnforwardableMessageError{G: msg.Msg.Md}
}
if !msg.Msg.Md.GameID.Eq(gameID) {
return BadGameIDError{G: msg.Msg.Md, I: gameID}
}
d.chatInputCh <- msg
return nil
}
// NewStartWithBool makes new start parameters that yield a coinflip game.
func NewStartWithBool(now time.Time, nPlayers int) Start {
ret := newStart(now, nPlayers)
ret.Params = NewFlipParametersWithBool()
return ret
}
// NewStartWithInt makes new start parameters that yield a coinflip game that picks an int between
// 0 and mod.
func NewStartWithInt(now time.Time, mod int64, nPlayers int) Start {
ret := newStart(now, nPlayers)
ret.Params = NewFlipParametersWithInt(mod)
return ret
}
// NewStartWithBigInt makes new start parameters that yield a coinflip game that picks big int between
// 0 and mod.
func NewStartWithBigInt(now time.Time, mod *big.Int, nPlayers int) Start {
ret := newStart(now, nPlayers)
ret.Params = NewFlipParametersWithBig(mod.Bytes())
return ret
}
// NewStartWithShuffle makes new start parameters for a coinflip that randomly permutes the numbers
// between 0 and n, exclusive. This can be used to shuffle an array of names.
func NewStartWithShuffle(now time.Time, n int64, nPlayers int) Start {
ret := newStart(now, nPlayers)
ret.Params = NewFlipParametersWithShuffle(n)
return ret
}
func (d *Dealer) IsGameActive(ctx context.Context, conversationID chat1.ConversationID, gameID chat1.FlipGameID) bool {
d.Lock()
defer d.Unlock()
md, found := d.gameIDs[GameIDToKey(gameID)]
return found && md.ConversationID.Eq(conversationID)
}
func (d *Dealer) HasActiveGames(ctx context.Context) bool {
d.Lock()
defer d.Unlock()
return len(d.gameIDs) > 0
}