Skip to content

Commit 558f5e6

Browse files
Actions checklist and leader's routine
Here we are implementing the code responsible for building an actions checklist that will be used to generate a proposal. We are also adding an outline of the leader's routine.
1 parent 9f7534b commit 558f5e6

File tree

3 files changed

+186
-14
lines changed

3 files changed

+186
-14
lines changed

pkg/tbtc/coordination.go

+169-13
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import (
55
"crypto/sha256"
66
"encoding/binary"
77
"fmt"
8+
"math/rand"
9+
"sort"
10+
811
"github.com/keep-network/keep-core/pkg/bitcoin"
912
"github.com/keep-network/keep-core/pkg/chain"
1013
"github.com/keep-network/keep-core/pkg/generator"
1114
"github.com/keep-network/keep-core/pkg/net"
1215
"github.com/keep-network/keep-core/pkg/protocol/group"
1316
"golang.org/x/sync/semaphore"
14-
"math/rand"
15-
"sort"
1617
)
1718

1819
const (
@@ -39,6 +40,10 @@ const (
3940
// hash can be used as an ingredient for the coordination seed, computed
4041
// for the given coordination window.
4142
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)
4247
)
4348

4449
// errCoordinationExecutorBusy is an error returned when the coordination
@@ -81,6 +86,25 @@ func (cw *coordinationWindow) isAfter(other *coordinationWindow) bool {
8186
return cw.coordinationBlock > other.coordinationBlock
8287
}
8388

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+
84108
// watchCoordinationWindows watches for new coordination windows and runs
85109
// the given callback when a new window is detected. The callback is run
86110
// in a separate goroutine. It is guaranteed that the callback is not run
@@ -97,11 +121,11 @@ func watchCoordinationWindows(
97121
for {
98122
select {
99123
case block := <-blocksChan:
100-
if block%coordinationFrequencyBlocks == 0 {
124+
if window := newCoordinationWindow(block); window.index() > 0 {
101125
// Make sure the current window is not the same as the last one.
102126
// There is no guarantee that the block channel will not emit
103127
// the same block again.
104-
if window := newCoordinationWindow(block); window.isAfter(lastWindow) {
128+
if window.isAfter(lastWindow) {
105129
lastWindow = window
106130
// Run the callback in a separate goroutine to avoid blocking
107131
// this loop and potentially missing the next block.
@@ -160,6 +184,17 @@ func (cf *coordinationFault) String() string {
160184
)
161185
}
162186

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+
163198
// coordinationProposal represents a single action proposal for the given wallet.
164199
type coordinationProposal interface {
165200
// actionType returns the specific type of the walletAction being subject
@@ -203,6 +238,16 @@ func (cr *coordinationResult) String() string {
203238
)
204239
}
205240

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+
206251
// coordinationExecutor is responsible for executing the coordination
207252
// procedure for the given wallet.
208253
type coordinationExecutor struct {
@@ -214,9 +259,13 @@ type coordinationExecutor struct {
214259
membersIndexes []group.MemberIndex
215260
operatorAddress chain.Address
216261

262+
proposalGenerator coordinationProposalGenerator
263+
217264
broadcastChannel net.BroadcastChannel
218265
membershipValidator *group.MembershipValidator
219266
protocolLatch *generator.ProtocolLatch
267+
268+
waitForBlockFn waitForBlockFn
220269
}
221270

222271
// newCoordinationExecutor creates a new coordination executor for the
@@ -226,19 +275,23 @@ func newCoordinationExecutor(
226275
coordinatedWallet wallet,
227276
membersIndexes []group.MemberIndex,
228277
operatorAddress chain.Address,
278+
proposalGenerator coordinationProposalGenerator,
229279
broadcastChannel net.BroadcastChannel,
230280
membershipValidator *group.MembershipValidator,
231281
protocolLatch *generator.ProtocolLatch,
282+
waitForBlockFn waitForBlockFn,
232283
) *coordinationExecutor {
233284
return &coordinationExecutor{
234285
lock: semaphore.NewWeighted(1),
235286
chain: chain,
236287
coordinatedWallet: coordinatedWallet,
237288
membersIndexes: membersIndexes,
238289
operatorAddress: operatorAddress,
290+
proposalGenerator: proposalGenerator,
239291
broadcastChannel: broadcastChannel,
240292
membershipValidator: membershipValidator,
241293
protocolLatch: protocolLatch,
294+
waitForBlockFn: waitForBlockFn,
242295
}
243296
}
244297

@@ -263,26 +316,59 @@ func (ce *coordinationExecutor) coordinate(
263316
ce.protocolLatch.Lock()
264317
defer ce.protocolLatch.Unlock()
265318

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+
266324
seed, err := ce.coordinationSeed(window)
267325
if err != nil {
268326
return nil, fmt.Errorf("failed to compute coordination seed: [%v]", err)
269327
}
270328

271329
leader := ce.coordinationLeader(seed)
272330

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
273343
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+
}
275351
} 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{}
277364
}
278365

279-
// TODO: Implement the rest of the coordination procedure.
280366
result := &coordinationResult{
281367
wallet: ce.coordinatedWallet,
282368
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.
286372
}
287373

288374
return result, nil
@@ -350,10 +436,80 @@ func (ce *coordinationExecutor) coordinationLeader(seed [32]byte) chain.Address
350436
return uniqueOperators[0]
351437
}
352438

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
355506
}
356507

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) {
358513
// TODO: Implement the follower routine.
514+
return nil, nil
359515
}

pkg/tbtc/marshaling.go

+12
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,18 @@ func (sdm *signingDoneMessage) Unmarshal(bytes []byte) error {
125125
return nil
126126
}
127127

128+
// Marshal converts the coordinationMessage to a byte array.
129+
func (cm *coordinationMessage) Marshal() ([]byte, error) {
130+
// TODO: Implement.
131+
return nil, nil
132+
}
133+
134+
// Unmarshal converts a byte array back to the coordinationMessage.
135+
func (cm *coordinationMessage) Unmarshal(bytes []byte) error {
136+
// TODO: Implement.
137+
return nil
138+
}
139+
128140
// marshalPublicKey converts an ECDSA public key to a byte
129141
// array (uncompressed).
130142
func marshalPublicKey(publicKey *ecdsa.PublicKey) ([]byte, error) {

pkg/tbtc/node.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,9 @@ func (n *node) getCoordinationExecutor(
348348
return nil, false, fmt.Errorf("failed to get broadcast channel: [%v]", err)
349349
}
350350

351-
// TODO: Register unmarshalers
351+
broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler {
352+
return &coordinationMessage{}
353+
})
352354

353355
membershipValidator := group.NewMembershipValidator(
354356
executorLogger,
@@ -382,9 +384,11 @@ func (n *node) getCoordinationExecutor(
382384
wallet,
383385
membersIndexes,
384386
operatorAddress,
387+
nil, // TODO: Set a proper proposal generator.
385388
broadcastChannel,
386389
membershipValidator,
387390
n.protocolLatch,
391+
n.waitForBlockHeight,
388392
)
389393

390394
n.coordinationExecutors[executorKey] = executor

0 commit comments

Comments
 (0)