@@ -5,14 +5,15 @@ import (
5
5
"crypto/sha256"
6
6
"encoding/binary"
7
7
"fmt"
8
+ "math/rand"
9
+ "sort"
10
+
8
11
"github.com/keep-network/keep-core/pkg/bitcoin"
9
12
"github.com/keep-network/keep-core/pkg/chain"
10
13
"github.com/keep-network/keep-core/pkg/generator"
11
14
"github.com/keep-network/keep-core/pkg/net"
12
15
"github.com/keep-network/keep-core/pkg/protocol/group"
13
16
"golang.org/x/sync/semaphore"
14
- "math/rand"
15
- "sort"
16
17
)
17
18
18
19
const (
@@ -39,6 +40,10 @@ const (
39
40
// hash can be used as an ingredient for the coordination seed, computed
40
41
// for the given coordination window.
41
42
coordinationSafeBlockShift = 32
43
+ // coordinationHeartbeatProbability is the probability of proposing a
44
+ // heartbeat action during the coordination procedure, assuming no other
45
+ // higher-priority action is proposed.
46
+ coordinationHeartbeatProbability = float64 (0.125 )
42
47
)
43
48
44
49
// errCoordinationExecutorBusy is an error returned when the coordination
@@ -81,6 +86,25 @@ func (cw *coordinationWindow) isAfter(other *coordinationWindow) bool {
81
86
return cw .coordinationBlock > other .coordinationBlock
82
87
}
83
88
89
+ // index returns the index of the coordination window. The index is computed
90
+ // by dividing the coordination block number by the coordination frequency.
91
+ // A valid index is a positive integer.
92
+ //
93
+ // For example:
94
+ // - window starting at block 900 has index 1
95
+ // - window starting at block 1800 has index 2
96
+ // - window starting at block 2700 has index 3
97
+ //
98
+ // If the coordination block number is not a multiple of the coordination
99
+ // frequency, the index is 0.
100
+ func (cw * coordinationWindow ) index () uint64 {
101
+ if cw .coordinationBlock % coordinationFrequencyBlocks == 0 {
102
+ return cw .coordinationBlock / coordinationFrequencyBlocks
103
+ }
104
+
105
+ return 0
106
+ }
107
+
84
108
// watchCoordinationWindows watches for new coordination windows and runs
85
109
// the given callback when a new window is detected. The callback is run
86
110
// in a separate goroutine. It is guaranteed that the callback is not run
@@ -97,11 +121,11 @@ func watchCoordinationWindows(
97
121
for {
98
122
select {
99
123
case block := <- blocksChan :
100
- if block % coordinationFrequencyBlocks == 0 {
124
+ if window := newCoordinationWindow ( block ); window . index () > 0 {
101
125
// Make sure the current window is not the same as the last one.
102
126
// There is no guarantee that the block channel will not emit
103
127
// the same block again.
104
- if window := newCoordinationWindow ( block ); window .isAfter (lastWindow ) {
128
+ if window .isAfter (lastWindow ) {
105
129
lastWindow = window
106
130
// Run the callback in a separate goroutine to avoid blocking
107
131
// this loop and potentially missing the next block.
@@ -160,6 +184,17 @@ func (cf *coordinationFault) String() string {
160
184
)
161
185
}
162
186
187
+ // coordinationProposalGenerator is a function that generates a coordination
188
+ // proposal based on the given checklist of possible wallet actions.
189
+ // The checklist is a list of actions that should be checked for the given
190
+ // coordination window. The generator is expected to return a proposal
191
+ // for the first action from the checklist that is valid for the given
192
+ // wallet's state. If none of the actions are valid, the generator
193
+ // should return a noopProposal.
194
+ type coordinationProposalGenerator func (
195
+ actionsChecklist []WalletActionType ,
196
+ ) (coordinationProposal , error )
197
+
163
198
// coordinationProposal represents a single action proposal for the given wallet.
164
199
type coordinationProposal interface {
165
200
// actionType returns the specific type of the walletAction being subject
@@ -203,6 +238,16 @@ func (cr *coordinationResult) String() string {
203
238
)
204
239
}
205
240
241
+ // coordinationMessage represents a coordination message sent by the leader
242
+ // to their followers during the active phase of the coordination window.
243
+ type coordinationMessage struct {
244
+ // TODO: Add fields.
245
+ }
246
+
247
+ func (cm * coordinationMessage ) Type () string {
248
+ return "tbtc/coordination_message"
249
+ }
250
+
206
251
// coordinationExecutor is responsible for executing the coordination
207
252
// procedure for the given wallet.
208
253
type coordinationExecutor struct {
@@ -214,9 +259,13 @@ type coordinationExecutor struct {
214
259
membersIndexes []group.MemberIndex
215
260
operatorAddress chain.Address
216
261
262
+ proposalGenerator coordinationProposalGenerator
263
+
217
264
broadcastChannel net.BroadcastChannel
218
265
membershipValidator * group.MembershipValidator
219
266
protocolLatch * generator.ProtocolLatch
267
+
268
+ waitForBlockFn waitForBlockFn
220
269
}
221
270
222
271
// newCoordinationExecutor creates a new coordination executor for the
@@ -226,19 +275,23 @@ func newCoordinationExecutor(
226
275
coordinatedWallet wallet ,
227
276
membersIndexes []group.MemberIndex ,
228
277
operatorAddress chain.Address ,
278
+ proposalGenerator coordinationProposalGenerator ,
229
279
broadcastChannel net.BroadcastChannel ,
230
280
membershipValidator * group.MembershipValidator ,
231
281
protocolLatch * generator.ProtocolLatch ,
282
+ waitForBlockFn waitForBlockFn ,
232
283
) * coordinationExecutor {
233
284
return & coordinationExecutor {
234
285
lock : semaphore .NewWeighted (1 ),
235
286
chain : chain ,
236
287
coordinatedWallet : coordinatedWallet ,
237
288
membersIndexes : membersIndexes ,
238
289
operatorAddress : operatorAddress ,
290
+ proposalGenerator : proposalGenerator ,
239
291
broadcastChannel : broadcastChannel ,
240
292
membershipValidator : membershipValidator ,
241
293
protocolLatch : protocolLatch ,
294
+ waitForBlockFn : waitForBlockFn ,
242
295
}
243
296
}
244
297
@@ -263,26 +316,59 @@ func (ce *coordinationExecutor) coordinate(
263
316
ce .protocolLatch .Lock ()
264
317
defer ce .protocolLatch .Unlock ()
265
318
319
+ // Just in case, check if the window is valid.
320
+ if window .index () == 0 {
321
+ return nil , fmt .Errorf ("invalid coordination window [%v]" , window )
322
+ }
323
+
266
324
seed , err := ce .coordinationSeed (window )
267
325
if err != nil {
268
326
return nil , fmt .Errorf ("failed to compute coordination seed: [%v]" , err )
269
327
}
270
328
271
329
leader := ce .coordinationLeader (seed )
272
330
331
+ actionsChecklist := ce .actionsChecklist (window .index (), seed )
332
+
333
+ // Set up a context that is cancelled when the active phase of the
334
+ // coordination window ends.
335
+ ctx , cancelCtx := withCancelOnBlock (
336
+ context .Background (),
337
+ window .activePhaseEndBlock (),
338
+ ce .waitForBlockFn ,
339
+ )
340
+ defer cancelCtx ()
341
+
342
+ var proposal coordinationProposal
273
343
if leader == ce .operatorAddress {
274
- ce .leaderRoutine ()
344
+ proposal , err = ce .leaderRoutine (ctx , actionsChecklist )
345
+ if err != nil {
346
+ return nil , fmt .Errorf (
347
+ "failed to execute leader's routine: [%v]" ,
348
+ err ,
349
+ )
350
+ }
275
351
} else {
276
- ce .followerRoutine ()
352
+ proposal , err = ce .followerRoutine ()
353
+ if err != nil {
354
+ return nil , fmt .Errorf (
355
+ "failed to execute follower's routine: [%v]" ,
356
+ err ,
357
+ )
358
+ }
359
+ }
360
+
361
+ // Just in case, if the proposal is nil, set it to noop.
362
+ if proposal == nil {
363
+ proposal = & noopProposal {}
277
364
}
278
365
279
- // TODO: Implement the rest of the coordination procedure.
280
366
result := & coordinationResult {
281
367
wallet : ce .coordinatedWallet ,
282
368
window : window ,
283
- leader : ce . coordinatedWallet . signingGroupOperators [ 0 ] ,
284
- proposal : & noopProposal {} ,
285
- faults : nil ,
369
+ leader : leader ,
370
+ proposal : proposal ,
371
+ faults : nil , // TODO: Fill coordination faults.
286
372
}
287
373
288
374
return result , nil
@@ -350,10 +436,80 @@ func (ce *coordinationExecutor) coordinationLeader(seed [32]byte) chain.Address
350
436
return uniqueOperators [0 ]
351
437
}
352
438
353
- func (ce * coordinationExecutor ) leaderRoutine () {
354
- // TODO: Implement the leader routine.
439
+ // actionsChecklist returns a list of wallet actions that should be checked
440
+ // for the given coordination window.
441
+ func (ce * coordinationExecutor ) actionsChecklist (
442
+ windowIndex uint64 ,
443
+ seed [32 ]byte ,
444
+ ) []WalletActionType {
445
+ var actions []WalletActionType
446
+
447
+ // Redemption action is a priority action and should be checked on every
448
+ // coordination window.
449
+ actions = append (actions , ActionRedemption )
450
+
451
+ // Other actions should be checked with a lower frequency. The default
452
+ // frequency is every 16 coordination windows.
453
+ frequencyWindows := uint64 (16 )
454
+
455
+ // TODO: Increase frequency for the active wallet.
456
+ if windowIndex % frequencyWindows == 0 {
457
+ actions = append (actions , ActionDepositSweep )
458
+ }
459
+
460
+ if windowIndex % frequencyWindows == 0 {
461
+ actions = append (actions , ActionMovedFundsSweep )
462
+ }
463
+
464
+ // TODO: Increase frequency for old wallets.
465
+ if windowIndex % frequencyWindows == 0 {
466
+ actions = append (actions , ActionMovingFunds )
467
+ }
468
+
469
+ // #nosec G404 (insecure random number source (rand))
470
+ // Drawing a decision about heartbeat does not require secure randomness.
471
+ // Use first 8 bytes of the seed to initialize the RNG.
472
+ rng := rand .New (rand .NewSource (int64 (binary .BigEndian .Uint64 (seed [:8 ]))))
473
+ if rng .Float64 () < coordinationHeartbeatProbability {
474
+ actions = append (actions , ActionHeartbeat )
475
+ }
476
+
477
+ return actions
478
+ }
479
+
480
+ // leaderRoutine executes the leader's routine for the given coordination
481
+ // window. The routine generates a proposal and broadcasts it to the followers.
482
+ // It returns the generated proposal or an error if the routine failed.
483
+ func (ce * coordinationExecutor ) leaderRoutine (
484
+ ctx context.Context ,
485
+ actionsChecklist []WalletActionType ,
486
+ ) (coordinationProposal , error ) {
487
+ proposal , err := ce .proposalGenerator (actionsChecklist )
488
+ if err != nil {
489
+ return nil , fmt .Errorf ("failed to generate proposal: [%v]" , err )
490
+ }
491
+
492
+ message := & coordinationMessage {
493
+ // TODO: Initialize fields.
494
+ }
495
+
496
+ err = ce .broadcastChannel .Send (
497
+ ctx ,
498
+ message ,
499
+ net .BackoffRetransmissionStrategy ,
500
+ )
501
+ if err != nil {
502
+ return nil , fmt .Errorf ("failed to send coordination message: [%v]" , err )
503
+ }
504
+
505
+ return proposal , nil
355
506
}
356
507
357
- func (ce * coordinationExecutor ) followerRoutine () {
508
+ // followerRoutine executes the follower's routine for the given coordination
509
+ // window. The routine listens for the coordination message from the leader and
510
+ // validates it. If the leader's proposal is valid, it returns the received
511
+ // proposal. Returns an error if the routine failed.
512
+ func (ce * coordinationExecutor ) followerRoutine () (coordinationProposal , error ) {
358
513
// TODO: Implement the follower routine.
514
+ return nil , nil
359
515
}
0 commit comments