From ff636e416f40de7d9d74c58c0626fce4f80de1a4 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 24 Nov 2023 16:56:52 +0100 Subject: [PATCH 01/14] Calculate coordination seed --- pkg/chain/ethereum/ethereum.go | 13 ++++++ pkg/tbtc/chain.go | 2 + pkg/tbtc/chain_test.go | 37 ++++++++++++++++ pkg/tbtc/coordination.go | 80 ++++++++++++++++++++++++++-------- pkg/tbtc/coordination_test.go | 47 ++++++++++++++++++++ pkg/tbtc/node.go | 11 ++++- pkg/tbtc/node_test.go | 7 ++- 7 files changed, 176 insertions(+), 21 deletions(-) diff --git a/pkg/chain/ethereum/ethereum.go b/pkg/chain/ethereum/ethereum.go index 549b7ffa5c..c641a545ae 100644 --- a/pkg/chain/ethereum/ethereum.go +++ b/pkg/chain/ethereum/ethereum.go @@ -414,6 +414,19 @@ func (bc *baseChain) GetBlockNumberByTimestamp( return block.NumberU64(), nil } +// GetBlockHashByNumber gets the block hash for the given block number. +func (bc *baseChain) GetBlockHashByNumber(blockNumber uint64) ( + [32]byte, + error, +) { + block, err := bc.blockByNumber(blockNumber) + if err != nil { + return [32]byte{}, fmt.Errorf("cannot get block: [%v]", err) + } + + return block.Hash(), nil +} + // currentBlock fetches the current block. func (bc *baseChain) currentBlock() (*types.Block, error) { currentBlockNumber, err := bc.blockCounter.CurrentBlock() diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index f6aa241b1c..fe2240b0c4 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -452,6 +452,8 @@ type Chain interface { // If the aforementioned is not possible, it tries to return the closest // possible block. GetBlockNumberByTimestamp(timestamp uint64) (uint64, error) + // GetBlockHashByNumber gets the block hash for the given block number. + GetBlockHashByNumber(blockNumber uint64) ([32]byte, error) sortition.Chain GroupSelectionChain diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 45d60b830c..eaad15c787 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -50,6 +50,9 @@ type localChain struct { blocksByTimestampMutex sync.Mutex blocksByTimestamp map[uint64]uint64 + blocksHashesByNumberMutex sync.Mutex + blocksHashesByNumber map[uint64][32]byte + pastDepositRevealedEventsMutex sync.Mutex pastDepositRevealedEvents map[[32]byte][]*DepositRevealedEvent @@ -105,6 +108,39 @@ func (lc *localChain) setBlockNumberByTimestamp(timestamp uint64, block uint64) lc.blocksByTimestamp[timestamp] = block } +func (lc *localChain) GetBlockHashByNumber(blockNumber uint64) ( + [32]byte, + error, +) { + lc.blocksHashesByNumberMutex.Lock() + defer lc.blocksHashesByNumberMutex.Unlock() + + blockHash, ok := lc.blocksHashesByNumber[blockNumber] + if !ok { + return [32]byte{}, fmt.Errorf("block not found") + } + + return blockHash, nil +} + +func (lc *localChain) setBlockHashByNumber( + blockNumber uint64, + blockHashString string, +) { + lc.blocksHashesByNumberMutex.Lock() + defer lc.blocksHashesByNumberMutex.Unlock() + + blockHashBytes, err := hex.DecodeString(blockHashString) + if err != nil { + panic(err) + } + + var blockHash [32]byte + copy(blockHash[:], blockHashBytes) + + lc.blocksHashesByNumber[blockNumber] = blockHash +} + func (lc *localChain) OperatorToStakingProvider() (chain.Address, bool, error) { panic("unsupported") } @@ -823,6 +859,7 @@ func ConnectWithKey( ), wallets: make(map[[20]byte]*WalletChainData), blocksByTimestamp: make(map[uint64]uint64), + blocksHashesByNumber: make(map[uint64][32]byte), pastDepositRevealedEvents: make(map[[32]byte][]*DepositRevealedEvent), depositSweepProposalValidations: make(map[[32]byte]bool), pendingRedemptionRequests: make(map[[32]byte]*RedemptionRequest), diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 4c1859499e..bc5e9323b1 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -2,7 +2,9 @@ package tbtc import ( "context" + "crypto/sha256" "fmt" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" @@ -29,6 +31,11 @@ const ( // coordination window. coordinationDurationBlocks = coordinationActivePhaseDurationBlocks + coordinationPassivePhaseDurationBlocks + // coordinationSafeBlockShift is the number of blocks by which the + // coordination block is shifted to obtain a safe block whose 32-byte + // hash can be used as an ingredient for the coordination seed, computed + // for the given coordination window. + coordinationSafeBlockShift = 32 ) // errCoordinationExecutorBusy is an error returned when the coordination @@ -138,9 +145,7 @@ func (cft CoordinationFaultType) String() string { // coordinationFault represents a single coordination fault. type coordinationFault struct { - // culprit is the address of the operator that is responsible for the fault. - culprit chain.Address - // faultType is the type of the fault. + culprit chain.Address // address of the operator responsible for the fault faultType CoordinationFaultType } @@ -200,7 +205,11 @@ func (cr *coordinationResult) String() string { type coordinationExecutor struct { lock *semaphore.Weighted - signers []*signer // TODO: Do we need whole signers? + chain Chain + + coordinatedWallet wallet + membersIndexes []group.MemberIndex + broadcastChannel net.BroadcastChannel membershipValidator *group.MembershipValidator protocolLatch *generator.ProtocolLatch @@ -209,29 +218,34 @@ type coordinationExecutor struct { // newCoordinationExecutor creates a new coordination executor for the // given wallet. func newCoordinationExecutor( - signers []*signer, + chain Chain, + coordinatedWallet wallet, + membersIndexes []group.MemberIndex, broadcastChannel net.BroadcastChannel, membershipValidator *group.MembershipValidator, protocolLatch *generator.ProtocolLatch, ) *coordinationExecutor { return &coordinationExecutor{ lock: semaphore.NewWeighted(1), - signers: signers, + chain: chain, + coordinatedWallet: coordinatedWallet, + membersIndexes: membersIndexes, broadcastChannel: broadcastChannel, membershipValidator: membershipValidator, protocolLatch: protocolLatch, } } -// wallet returns the wallet this executor is responsible for. -func (ce *coordinationExecutor) wallet() wallet { - // All signers belong to one wallet. Take that wallet from the - // first signer. - return ce.signers[0].wallet +// walletPublicKeyHash returns the 20-byte public key hash of the +// coordinated wallet. +func (ce *coordinationExecutor) walletPublicKeyHash() [20]byte { + return bitcoin.PublicKeyHash(ce.coordinatedWallet.publicKey) } // coordinate executes the coordination procedure for the given coordination // window. +// +// TODO: Add logging. func (ce *coordinationExecutor) coordinate( window *coordinationWindow, ) (*coordinationResult, error) { @@ -240,18 +254,48 @@ func (ce *coordinationExecutor) coordinate( } defer ce.lock.Release(1) - // TODO: Implement coordination logic. Remember about: - // - Setting up the right context - // - Using the protocol latch - // - Using the membership validator - // Example result: + ce.protocolLatch.Lock() + defer ce.protocolLatch.Unlock() + + coordinationSeed, err := ce.coordinationSeed(window) + if err != nil { + return nil, fmt.Errorf("failed to compute coordination seed: [%v]", err) + } + + fmt.Printf("coordinationSeed: %v\n", coordinationSeed) + + // TODO: Implement the rest of the coordination procedure. result := &coordinationResult{ - wallet: ce.wallet(), + wallet: ce.coordinatedWallet, window: window, - leader: ce.wallet().signingGroupOperators[0], + leader: ce.coordinatedWallet.signingGroupOperators[0], proposal: &noopProposal{}, faults: nil, } return result, nil } + +// coordinationSeed computes the coordination seed for the given coordination +// window. +func (ce *coordinationExecutor) coordinationSeed( + window *coordinationWindow, +) ([32]byte, error) { + walletPublicKeyHash := ce.walletPublicKeyHash() + + safeBlockNumber := window.coordinationBlock - coordinationSafeBlockShift + safeBlockHash, err := ce.chain.GetBlockHashByNumber(safeBlockNumber) + if err != nil { + return [32]byte{}, fmt.Errorf( + "failed to get safe block hash: [%v]", + err, + ) + } + + return sha256.Sum256( + append( + walletPublicKeyHash[:], + safeBlockHash[:]..., + ), + ), nil +} diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index e6cfcc4c1b..3e99dd9db2 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -2,6 +2,7 @@ package tbtc import ( "context" + "encoding/hex" "testing" "time" @@ -116,3 +117,49 @@ func TestWatchCoordinationWindows(t *testing.T) { int(receivedWindows[1].coordinationBlock), ) } + +func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { + window := newCoordinationWindow(900) + + localChain := Connect() + + localChain.setBlockHashByNumber( + window.coordinationBlock-32, + "1322996cbcbc38fc924a46f4df5f9064279d3ab43396e58386dac9b87440d64f", + ) + + // Uncompressed public key corresponding to the 20-byte public key hash: + // aa768412ceed10bd423c025542ca90071f9fb62d. + publicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + coordinatedWallet := wallet{ + // Set only relevant fields. + publicKey: unmarshalPublicKey(publicKeyHex), + } + + executor := &coordinationExecutor{ + chain: localChain, + coordinatedWallet: coordinatedWallet, + } + + seed, err := executor.coordinationSeed(window) + if err != nil { + t.Fatal(err) + } + + // Expected seed is sha256(wallet_public_key_hash | safe_block_hash). + expectedSeed := "e55c779d6d83183409ddc90c6cd5130567f0593349a9c82494b402048ec2d03d" + + testutils.AssertStringsEqual( + t, + "coordination seed", + expectedSeed, + hex.EncodeToString(seed[:]), + ) +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 48303a62ca..9c0650af38 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -370,8 +370,17 @@ func (n *node) getCoordinationExecutor( len(signers), ) + // The coordination executor does not need access to signers' key material. + // It is enough to pass only their member indexes. + membersIndexes := make([]group.MemberIndex, len(signers)) + for i, s := range signers { + membersIndexes[i] = s.signingGroupMemberIndex + } + executor := newCoordinationExecutor( - signers, + n.chain, + wallet, + membersIndexes, broadcastChannel, membershipValidator, n.protocolLatch, diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index 5fc3ca15d0..200789dff5 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -198,10 +198,13 @@ func TestNode_GetCoordinationExecutor(t *testing.T) { t, "signers count", 1, - len(executor.signers), + len(executor.membersIndexes), ) - if !reflect.DeepEqual(signer, executor.signers[0]) { + if !reflect.DeepEqual( + signer.signingGroupMemberIndex, + executor.membersIndexes[0], + ) { t.Errorf("executor holds an unexpected signer") } From 9f7534b6443b60886d681aecea795607a1d9d596 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Mon, 27 Nov 2023 11:51:45 +0100 Subject: [PATCH 02/14] Get coordination leader Here we implement the logic of designating the coordination leader according to the specification presented in RFC 12. --- pkg/tbtc/coordination.go | 62 +++++++++++++++++++++++++++++++++-- pkg/tbtc/coordination_test.go | 44 +++++++++++++++++++++++++ pkg/tbtc/node.go | 16 ++++++--- 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index bc5e9323b1..90b5d97ff8 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -3,6 +3,7 @@ package tbtc import ( "context" "crypto/sha256" + "encoding/binary" "fmt" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" @@ -10,6 +11,8 @@ import ( "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/group" "golang.org/x/sync/semaphore" + "math/rand" + "sort" ) const ( @@ -209,6 +212,7 @@ type coordinationExecutor struct { coordinatedWallet wallet membersIndexes []group.MemberIndex + operatorAddress chain.Address broadcastChannel net.BroadcastChannel membershipValidator *group.MembershipValidator @@ -221,6 +225,7 @@ func newCoordinationExecutor( chain Chain, coordinatedWallet wallet, membersIndexes []group.MemberIndex, + operatorAddress chain.Address, broadcastChannel net.BroadcastChannel, membershipValidator *group.MembershipValidator, protocolLatch *generator.ProtocolLatch, @@ -230,6 +235,7 @@ func newCoordinationExecutor( chain: chain, coordinatedWallet: coordinatedWallet, membersIndexes: membersIndexes, + operatorAddress: operatorAddress, broadcastChannel: broadcastChannel, membershipValidator: membershipValidator, protocolLatch: protocolLatch, @@ -257,12 +263,18 @@ func (ce *coordinationExecutor) coordinate( ce.protocolLatch.Lock() defer ce.protocolLatch.Unlock() - coordinationSeed, err := ce.coordinationSeed(window) + seed, err := ce.coordinationSeed(window) if err != nil { return nil, fmt.Errorf("failed to compute coordination seed: [%v]", err) } - fmt.Printf("coordinationSeed: %v\n", coordinationSeed) + leader := ce.coordinationLeader(seed) + + if leader == ce.operatorAddress { + ce.leaderRoutine() + } else { + ce.followerRoutine() + } // TODO: Implement the rest of the coordination procedure. result := &coordinationResult{ @@ -299,3 +311,49 @@ func (ce *coordinationExecutor) coordinationSeed( ), ), nil } + +// coordinationLeader returns the address of the coordination leader for the +// given coordination seed. +func (ce *coordinationExecutor) coordinationLeader(seed [32]byte) chain.Address { + // First, take all operators backing the wallet. + allOperators := chain.Addresses(ce.coordinatedWallet.signingGroupOperators) + + // Determine a list of unique operators. + uniqueOperators := make([]chain.Address, 0) + for operator := range allOperators.Set() { + uniqueOperators = append(uniqueOperators, operator) + } + + // Sort the list of unique operators in ascending order. + sort.Slice( + uniqueOperators, + func(i, j int) bool { + return uniqueOperators[i] < uniqueOperators[j] + }, + ) + + // #nosec G404 (insecure random number source (rand)) + // Shuffling operators does not require secure randomness. + // Use first 8 bytes of the seed to initialize the RNG. + rng := rand.New(rand.NewSource(int64(binary.BigEndian.Uint64(seed[:8])))) + + // Shuffle the list of unique operators. + rng.Shuffle( + len(uniqueOperators), + func(i, j int) { + uniqueOperators[i], uniqueOperators[j] = + uniqueOperators[j], uniqueOperators[i] + }, + ) + + // The first operator in the shuffled list is the leader. + return uniqueOperators[0] +} + +func (ce *coordinationExecutor) leaderRoutine() { + // TODO: Implement the leader routine. +} + +func (ce *coordinationExecutor) followerRoutine() { + // TODO: Implement the follower routine. +} diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index 3e99dd9db2..e9ec82e3b5 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -3,6 +3,7 @@ package tbtc import ( "context" "encoding/hex" + "github.com/keep-network/keep-core/pkg/chain" "testing" "time" @@ -144,6 +145,7 @@ func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { } executor := &coordinationExecutor{ + // Set only relevant fields. chain: localChain, coordinatedWallet: coordinatedWallet, } @@ -163,3 +165,45 @@ func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { hex.EncodeToString(seed[:]), ) } + +func TestCoordinationExecutor_CoordinationLeader(t *testing.T) { + seedBytes, err := hex.DecodeString( + "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + ) + if err != nil { + t.Fatal(err) + } + + var seed [32]byte + copy(seed[:], seedBytes) + + coordinatedWallet := wallet{ + // Set only relevant fields. + signingGroupOperators: []chain.Address{ + "957ECF59507a6A74b8d98747f07a74De270D3CC3", // member 1 + "5E14c0f27612fbfB7A6FE40b5A6Ec997fA62fc04", // member 2 + "D2662604f8b4540336fBd3c1F48d7e9cdFbD079c", // member 3 + "7CBD87ABC182216A7Aa0E8d19aA21abFA2511383", // member 4 + "FAc73b03884d94a08a5c6c7BB12Ac0b20571F162", // member 5 + "705C76445651530fe0D25eeE287b6164cE2c7216", // member 6 + "7CBD87ABC182216A7Aa0E8d19aA21abFA2511383", // member 7 (same operator as member 4) + "405ad1f632b49A0617fbdc1fD427aF54BA9Bb3dd", // member 8 + "7CBD87ABC182216A7Aa0E8d19aA21abFA2511383", // member 9 (same operator as member 4) + "5E14c0f27612fbfB7A6FE40b5A6Ec997fA62fc04", // member 10 (same operator as member 2) + }, + } + + executor := &coordinationExecutor{ + // Set only relevant fields. + coordinatedWallet: coordinatedWallet, + } + + leader := executor.coordinationLeader(seed) + + testutils.AssertStringsEqual( + t, + "coordination leader", + "D2662604f8b4540336fBd3c1F48d7e9cdFbD079c", + leader.String(), + ) +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 9c0650af38..54b79c0a9e 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -365,11 +365,6 @@ func (n *node) getCoordinationExecutor( ) } - executorLogger.Infof( - "coordination executor created; controlling [%v] signers", - len(signers), - ) - // The coordination executor does not need access to signers' key material. // It is enough to pass only their member indexes. membersIndexes := make([]group.MemberIndex, len(signers)) @@ -377,10 +372,16 @@ func (n *node) getCoordinationExecutor( membersIndexes[i] = s.signingGroupMemberIndex } + operatorAddress, err := n.operatorAddress() + if err != nil { + return nil, false, fmt.Errorf("failed to get operator address: [%v]", err) + } + executor := newCoordinationExecutor( n.chain, wallet, membersIndexes, + operatorAddress, broadcastChannel, membershipValidator, n.protocolLatch, @@ -388,6 +389,11 @@ func (n *node) getCoordinationExecutor( n.coordinationExecutors[executorKey] = executor + executorLogger.Infof( + "coordination executor created; controlling [%v] signers", + len(signers), + ) + return executor, true, nil } From 558f5e6a7e06a36ea0d896aa6a10b01675e65b42 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Mon, 27 Nov 2023 15:57:27 +0100 Subject: [PATCH 03/14] 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. --- pkg/tbtc/coordination.go | 182 ++++++++++++++++++++++++++++++++++++--- pkg/tbtc/marshaling.go | 12 +++ pkg/tbtc/node.go | 6 +- 3 files changed, 186 insertions(+), 14 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 90b5d97ff8..45b0cc6b07 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -5,14 +5,15 @@ import ( "crypto/sha256" "encoding/binary" "fmt" + "math/rand" + "sort" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/group" "golang.org/x/sync/semaphore" - "math/rand" - "sort" ) const ( @@ -39,6 +40,10 @@ const ( // hash can be used as an ingredient for the coordination seed, computed // for the given coordination window. coordinationSafeBlockShift = 32 + // coordinationHeartbeatProbability is the probability of proposing a + // heartbeat action during the coordination procedure, assuming no other + // higher-priority action is proposed. + coordinationHeartbeatProbability = float64(0.125) ) // errCoordinationExecutorBusy is an error returned when the coordination @@ -81,6 +86,25 @@ func (cw *coordinationWindow) isAfter(other *coordinationWindow) bool { return cw.coordinationBlock > other.coordinationBlock } +// index returns the index of the coordination window. The index is computed +// by dividing the coordination block number by the coordination frequency. +// A valid index is a positive integer. +// +// For example: +// - window starting at block 900 has index 1 +// - window starting at block 1800 has index 2 +// - window starting at block 2700 has index 3 +// +// If the coordination block number is not a multiple of the coordination +// frequency, the index is 0. +func (cw *coordinationWindow) index() uint64 { + if cw.coordinationBlock%coordinationFrequencyBlocks == 0 { + return cw.coordinationBlock / coordinationFrequencyBlocks + } + + return 0 +} + // watchCoordinationWindows watches for new coordination windows and runs // the given callback when a new window is detected. The callback is run // in a separate goroutine. It is guaranteed that the callback is not run @@ -97,11 +121,11 @@ func watchCoordinationWindows( for { select { case block := <-blocksChan: - if block%coordinationFrequencyBlocks == 0 { + if window := newCoordinationWindow(block); window.index() > 0 { // Make sure the current window is not the same as the last one. // There is no guarantee that the block channel will not emit // the same block again. - if window := newCoordinationWindow(block); window.isAfter(lastWindow) { + if window.isAfter(lastWindow) { lastWindow = window // Run the callback in a separate goroutine to avoid blocking // this loop and potentially missing the next block. @@ -160,6 +184,17 @@ func (cf *coordinationFault) String() string { ) } +// coordinationProposalGenerator is a function that generates a coordination +// proposal based on the given checklist of possible wallet actions. +// The checklist is a list of actions that should be checked for the given +// coordination window. The generator is expected to return a proposal +// for the first action from the checklist that is valid for the given +// wallet's state. If none of the actions are valid, the generator +// should return a noopProposal. +type coordinationProposalGenerator func( + actionsChecklist []WalletActionType, +) (coordinationProposal, error) + // coordinationProposal represents a single action proposal for the given wallet. type coordinationProposal interface { // actionType returns the specific type of the walletAction being subject @@ -203,6 +238,16 @@ func (cr *coordinationResult) String() string { ) } +// coordinationMessage represents a coordination message sent by the leader +// to their followers during the active phase of the coordination window. +type coordinationMessage struct { + // TODO: Add fields. +} + +func (cm *coordinationMessage) Type() string { + return "tbtc/coordination_message" +} + // coordinationExecutor is responsible for executing the coordination // procedure for the given wallet. type coordinationExecutor struct { @@ -214,9 +259,13 @@ type coordinationExecutor struct { membersIndexes []group.MemberIndex operatorAddress chain.Address + proposalGenerator coordinationProposalGenerator + broadcastChannel net.BroadcastChannel membershipValidator *group.MembershipValidator protocolLatch *generator.ProtocolLatch + + waitForBlockFn waitForBlockFn } // newCoordinationExecutor creates a new coordination executor for the @@ -226,9 +275,11 @@ func newCoordinationExecutor( coordinatedWallet wallet, membersIndexes []group.MemberIndex, operatorAddress chain.Address, + proposalGenerator coordinationProposalGenerator, broadcastChannel net.BroadcastChannel, membershipValidator *group.MembershipValidator, protocolLatch *generator.ProtocolLatch, + waitForBlockFn waitForBlockFn, ) *coordinationExecutor { return &coordinationExecutor{ lock: semaphore.NewWeighted(1), @@ -236,9 +287,11 @@ func newCoordinationExecutor( coordinatedWallet: coordinatedWallet, membersIndexes: membersIndexes, operatorAddress: operatorAddress, + proposalGenerator: proposalGenerator, broadcastChannel: broadcastChannel, membershipValidator: membershipValidator, protocolLatch: protocolLatch, + waitForBlockFn: waitForBlockFn, } } @@ -263,6 +316,11 @@ func (ce *coordinationExecutor) coordinate( ce.protocolLatch.Lock() defer ce.protocolLatch.Unlock() + // Just in case, check if the window is valid. + if window.index() == 0 { + return nil, fmt.Errorf("invalid coordination window [%v]", window) + } + seed, err := ce.coordinationSeed(window) if err != nil { return nil, fmt.Errorf("failed to compute coordination seed: [%v]", err) @@ -270,19 +328,47 @@ func (ce *coordinationExecutor) coordinate( leader := ce.coordinationLeader(seed) + actionsChecklist := ce.actionsChecklist(window.index(), seed) + + // Set up a context that is cancelled when the active phase of the + // coordination window ends. + ctx, cancelCtx := withCancelOnBlock( + context.Background(), + window.activePhaseEndBlock(), + ce.waitForBlockFn, + ) + defer cancelCtx() + + var proposal coordinationProposal if leader == ce.operatorAddress { - ce.leaderRoutine() + proposal, err = ce.leaderRoutine(ctx, actionsChecklist) + if err != nil { + return nil, fmt.Errorf( + "failed to execute leader's routine: [%v]", + err, + ) + } } else { - ce.followerRoutine() + proposal, err = ce.followerRoutine() + if err != nil { + return nil, fmt.Errorf( + "failed to execute follower's routine: [%v]", + err, + ) + } + } + + // Just in case, if the proposal is nil, set it to noop. + if proposal == nil { + proposal = &noopProposal{} } - // TODO: Implement the rest of the coordination procedure. result := &coordinationResult{ wallet: ce.coordinatedWallet, window: window, - leader: ce.coordinatedWallet.signingGroupOperators[0], - proposal: &noopProposal{}, - faults: nil, + leader: leader, + proposal: proposal, + faults: nil, // TODO: Fill coordination faults. } return result, nil @@ -350,10 +436,80 @@ func (ce *coordinationExecutor) coordinationLeader(seed [32]byte) chain.Address return uniqueOperators[0] } -func (ce *coordinationExecutor) leaderRoutine() { - // TODO: Implement the leader routine. +// actionsChecklist returns a list of wallet actions that should be checked +// for the given coordination window. +func (ce *coordinationExecutor) actionsChecklist( + windowIndex uint64, + seed [32]byte, +) []WalletActionType { + var actions []WalletActionType + + // Redemption action is a priority action and should be checked on every + // coordination window. + actions = append(actions, ActionRedemption) + + // Other actions should be checked with a lower frequency. The default + // frequency is every 16 coordination windows. + frequencyWindows := uint64(16) + + // TODO: Increase frequency for the active wallet. + if windowIndex%frequencyWindows == 0 { + actions = append(actions, ActionDepositSweep) + } + + if windowIndex%frequencyWindows == 0 { + actions = append(actions, ActionMovedFundsSweep) + } + + // TODO: Increase frequency for old wallets. + if windowIndex%frequencyWindows == 0 { + actions = append(actions, ActionMovingFunds) + } + + // #nosec G404 (insecure random number source (rand)) + // Drawing a decision about heartbeat does not require secure randomness. + // Use first 8 bytes of the seed to initialize the RNG. + rng := rand.New(rand.NewSource(int64(binary.BigEndian.Uint64(seed[:8])))) + if rng.Float64() < coordinationHeartbeatProbability { + actions = append(actions, ActionHeartbeat) + } + + return actions +} + +// leaderRoutine executes the leader's routine for the given coordination +// window. The routine generates a proposal and broadcasts it to the followers. +// It returns the generated proposal or an error if the routine failed. +func (ce *coordinationExecutor) leaderRoutine( + ctx context.Context, + actionsChecklist []WalletActionType, +) (coordinationProposal, error) { + proposal, err := ce.proposalGenerator(actionsChecklist) + if err != nil { + return nil, fmt.Errorf("failed to generate proposal: [%v]", err) + } + + message := &coordinationMessage{ + // TODO: Initialize fields. + } + + err = ce.broadcastChannel.Send( + ctx, + message, + net.BackoffRetransmissionStrategy, + ) + if err != nil { + return nil, fmt.Errorf("failed to send coordination message: [%v]", err) + } + + return proposal, nil } -func (ce *coordinationExecutor) followerRoutine() { +// followerRoutine executes the follower's routine for the given coordination +// window. The routine listens for the coordination message from the leader and +// validates it. If the leader's proposal is valid, it returns the received +// proposal. Returns an error if the routine failed. +func (ce *coordinationExecutor) followerRoutine() (coordinationProposal, error) { // TODO: Implement the follower routine. + return nil, nil } diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index 93eeb72dcb..1c992e539a 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -125,6 +125,18 @@ func (sdm *signingDoneMessage) Unmarshal(bytes []byte) error { return nil } +// Marshal converts the coordinationMessage to a byte array. +func (cm *coordinationMessage) Marshal() ([]byte, error) { + // TODO: Implement. + return nil, nil +} + +// Unmarshal converts a byte array back to the coordinationMessage. +func (cm *coordinationMessage) Unmarshal(bytes []byte) error { + // TODO: Implement. + return nil +} + // marshalPublicKey converts an ECDSA public key to a byte // array (uncompressed). func marshalPublicKey(publicKey *ecdsa.PublicKey) ([]byte, error) { diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 54b79c0a9e..746dddccce 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -348,7 +348,9 @@ func (n *node) getCoordinationExecutor( return nil, false, fmt.Errorf("failed to get broadcast channel: [%v]", err) } - // TODO: Register unmarshalers + broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &coordinationMessage{} + }) membershipValidator := group.NewMembershipValidator( executorLogger, @@ -382,9 +384,11 @@ func (n *node) getCoordinationExecutor( wallet, membersIndexes, operatorAddress, + nil, // TODO: Set a proper proposal generator. broadcastChannel, membershipValidator, n.protocolLatch, + n.waitForBlockHeight, ) n.coordinationExecutors[executorKey] = executor From 63a7ba2cfbed2b216e4d11538edb63419b35aaa7 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 28 Nov 2023 11:15:14 +0100 Subject: [PATCH 04/14] Unit tests for checklist and leader's routine --- pkg/tbtc/coordination.go | 10 +- pkg/tbtc/coordination_test.go | 252 ++++++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 2 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 45b0cc6b07..e82de75d28 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -304,7 +304,7 @@ func (ce *coordinationExecutor) walletPublicKeyHash() [20]byte { // coordinate executes the coordination procedure for the given coordination // window. // -// TODO: Add logging. +// TODO: Add logging and cover with unit tests. func (ce *coordinationExecutor) coordinate( window *coordinationWindow, ) (*coordinationResult, error) { @@ -437,11 +437,17 @@ func (ce *coordinationExecutor) coordinationLeader(seed [32]byte) chain.Address } // actionsChecklist returns a list of wallet actions that should be checked -// for the given coordination window. +// for the given coordination window. Returns nil for incorrect coordination +// windows whose index is 0. func (ce *coordinationExecutor) actionsChecklist( windowIndex uint64, seed [32]byte, ) []WalletActionType { + // Return nil checklist for incorrect coordination windows. + if windowIndex == 0 { + return nil + } + var actions []WalletActionType // Redemption action is a priority action and should be checked on every diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index e9ec82e3b5..7de45adf13 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -2,8 +2,14 @@ package tbtc import ( "context" + "crypto/sha256" "encoding/hex" + "github.com/go-test/deep" "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/net" + netlocal "github.com/keep-network/keep-core/pkg/net/local" + "math/big" + "reflect" "testing" "time" @@ -65,6 +71,47 @@ func TestCoordinationWindow_IsAfterActivePhase(t *testing.T) { ) } +func TestCoordinationWindow_Index(t *testing.T) { + tests := map[string]struct { + coordinationBlock uint64 + expectedIndex uint64 + }{ + "block 0": { + coordinationBlock: 0, + expectedIndex: 0, + }, + "block 900": { + coordinationBlock: 900, + expectedIndex: 1, + }, + "block 1800": { + coordinationBlock: 1800, + expectedIndex: 2, + }, + "block 9000": { + coordinationBlock: 9000, + expectedIndex: 10, + }, + "block 9001": { + coordinationBlock: 9001, + expectedIndex: 0, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + window := newCoordinationWindow(test.coordinationBlock) + + testutils.AssertIntsEqual( + t, + "index", + int(test.expectedIndex), + int(window.index()), + ) + }) + } +} + func TestWatchCoordinationWindows(t *testing.T) { watchBlocksFn := func(ctx context.Context) <-chan uint64 { blocksChan := make(chan uint64) @@ -207,3 +254,208 @@ func TestCoordinationExecutor_CoordinationLeader(t *testing.T) { leader.String(), ) } + +func TestCoordinationExecutor_ActionsChecklist(t *testing.T) { + tests := map[string]struct { + coordinationBlock uint64 + expectedChecklist []WalletActionType + }{ + // Incorrect coordination window. + "block 0": { + coordinationBlock: 0, + expectedChecklist: nil, + }, + "block 900": { + coordinationBlock: 900, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + // Incorrect coordination window. + "block 901": { + coordinationBlock: 901, + expectedChecklist: nil, + }, + "block 1800": { + coordinationBlock: 1800, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 2700": { + coordinationBlock: 2700, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 3600": { + coordinationBlock: 3600, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 4500": { + coordinationBlock: 4500, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + // Heartbeat randomly selected for the 6th coordination window. + "block 5400": { + coordinationBlock: 5400, + expectedChecklist: []WalletActionType{ + ActionRedemption, + ActionHeartbeat, + }, + }, + "block 6300": { + coordinationBlock: 6300, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 7200": { + coordinationBlock: 7200, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 8100": { + coordinationBlock: 8100, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 9000": { + coordinationBlock: 9000, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 9900": { + coordinationBlock: 9900, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 10800": { + coordinationBlock: 10800, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + "block 11700": { + coordinationBlock: 11700, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + // Heartbeat randomly selected for the 14th coordination window. + "block 12600": { + coordinationBlock: 12600, + expectedChecklist: []WalletActionType{ + ActionRedemption, + ActionHeartbeat, + }, + }, + "block 13500": { + coordinationBlock: 13500, + expectedChecklist: []WalletActionType{ActionRedemption}, + }, + // 16th coordination window so, all actions should be on the checklist. + "block 14400": { + coordinationBlock: 14400, + expectedChecklist: []WalletActionType{ + ActionRedemption, + ActionDepositSweep, + ActionMovedFundsSweep, + ActionMovingFunds, + }, + }, + } + + executor := &coordinationExecutor{} + + for testName, test := range tests { + t.Run( + testName, func(t *testing.T) { + window := newCoordinationWindow(test.coordinationBlock) + + // Build an arbitrary seed based on the coordination block number. + seed := sha256.Sum256( + big.NewInt(int64(window.coordinationBlock) + 1).Bytes(), + ) + + checklist := executor.actionsChecklist(window.index(), seed) + + if diff := deep.Equal( + checklist, + test.expectedChecklist, + ); diff != nil { + t.Errorf( + "compare failed: %v\nactual: %s\nexpected: %s", + diff, + checklist, + test.expectedChecklist, + ) + } + }, + ) + } +} + +func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { + provider := netlocal.Connect() + + broadcastChannel, err := provider.BroadcastChannelFor("test") + if err != nil { + t.Fatal(err) + } + + broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &coordinationMessage{} + }) + + proposalGenerator := func(actionsChecklist []WalletActionType) ( + coordinationProposal, + error, + ) { + for _, action := range actionsChecklist { + if action == ActionDepositSweep { + return &DepositSweepProposal{ + // Set just one field to make the proposal non-empty. + SweepTxFee: big.NewInt(1000), + }, nil + } + } + + return &noopProposal{}, nil + } + + executor := &coordinationExecutor{ + // Set only relevant fields. + proposalGenerator: proposalGenerator, + broadcastChannel: broadcastChannel, + } + + actionsChecklist := []WalletActionType{ + ActionRedemption, + ActionDepositSweep, + ActionMovedFundsSweep, + ActionMovingFunds, + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second) + defer cancelCtx() + + var broadcastedProposal coordinationProposal + broadcastChannel.Recv(ctx, func(message net.Message) { + // Set broadcastedProposal from message. + }) + + proposal, err := executor.leaderRoutine(ctx, actionsChecklist) + if err != nil { + t.Fatal(err) + } + + expectedProposal := &DepositSweepProposal{ + SweepTxFee: big.NewInt(1000), + } + + if !reflect.DeepEqual(expectedProposal, proposal) { + t.Errorf( + "unexpected proposal returned by leader's routine: \n"+ + "expected: %v\n"+ + "actual: %v", + expectedProposal, + proposal, + ) + } + + // TODO: Modify this condition when the time comes. + if !reflect.DeepEqual(nil, broadcastedProposal) { + t.Errorf( + "unexpected proposal broadcasted to the followers: \n"+ + "expected: %v\n"+ + "actual: %v", + nil, + broadcastedProposal, + ) + } +} From 33a1475cef27fd38f5cfee4b273efa6a21e23f77 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 28 Nov 2023 15:25:27 +0100 Subject: [PATCH 05/14] Coordination message and marshaling machinery Here we implement the coordination message type along with all marshaling machinery necessary to transfer it over the wire. --- pkg/tbtc/coordination.go | 41 ++- pkg/tbtc/coordination_test.go | 99 +++++-- pkg/tbtc/deposit_sweep.go | 1 + pkg/tbtc/gen/pb/message.pb.go | 492 +++++++++++++++++++++++++++++++++- pkg/tbtc/gen/pb/message.proto | 32 +++ pkg/tbtc/heartbeat.go | 2 +- pkg/tbtc/marshaling.go | 254 +++++++++++++++++- pkg/tbtc/marshaling_test.go | 227 ++++++++++++++++ pkg/tbtc/node_test.go | 8 + pkg/tbtc/redemption.go | 1 + pkg/tbtc/wallet.go | 20 ++ pkg/tbtc/wallet_test.go | 59 ++++ 12 files changed, 1188 insertions(+), 48 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index e82de75d28..3e795ed348 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -5,6 +5,8 @@ import ( "crypto/sha256" "encoding/binary" "fmt" + "github.com/keep-network/keep-core/pkg/internal/pb" + "golang.org/x/exp/slices" "math/rand" "sort" @@ -197,11 +199,14 @@ type coordinationProposalGenerator func( // coordinationProposal represents a single action proposal for the given wallet. type coordinationProposal interface { + pb.Marshaler + pb.Unmarshaler + // actionType returns the specific type of the walletAction being subject // of this proposal. actionType() WalletActionType // validityBlocks returns the number of blocks for which the proposal is - // valid. + // valid. This value SHOULD NOT be marshaled/unmarshaled. validityBlocks() uint64 } @@ -241,7 +246,10 @@ func (cr *coordinationResult) String() string { // coordinationMessage represents a coordination message sent by the leader // to their followers during the active phase of the coordination window. type coordinationMessage struct { - // TODO: Add fields. + senderID group.MemberIndex + coordinationBlock uint64 + walletPublicKeyHash [20]byte + proposal coordinationProposal } func (cm *coordinationMessage) Type() string { @@ -318,10 +326,13 @@ func (ce *coordinationExecutor) coordinate( // Just in case, check if the window is valid. if window.index() == 0 { - return nil, fmt.Errorf("invalid coordination window [%v]", window) + return nil, fmt.Errorf( + "invalid coordination block [%v]", + window.coordinationBlock, + ) } - seed, err := ce.coordinationSeed(window) + seed, err := ce.coordinationSeed(window.coordinationBlock) if err != nil { return nil, fmt.Errorf("failed to compute coordination seed: [%v]", err) } @@ -341,7 +352,11 @@ func (ce *coordinationExecutor) coordinate( var proposal coordinationProposal if leader == ce.operatorAddress { - proposal, err = ce.leaderRoutine(ctx, actionsChecklist) + proposal, err = ce.leaderRoutine( + ctx, + window.coordinationBlock, + actionsChecklist, + ) if err != nil { return nil, fmt.Errorf( "failed to execute leader's routine: [%v]", @@ -377,11 +392,11 @@ func (ce *coordinationExecutor) coordinate( // coordinationSeed computes the coordination seed for the given coordination // window. func (ce *coordinationExecutor) coordinationSeed( - window *coordinationWindow, + coordinationBlock uint64, ) ([32]byte, error) { walletPublicKeyHash := ce.walletPublicKeyHash() - safeBlockNumber := window.coordinationBlock - coordinationSafeBlockShift + safeBlockNumber := coordinationBlock - coordinationSafeBlockShift safeBlockHash, err := ce.chain.GetBlockHashByNumber(safeBlockNumber) if err != nil { return [32]byte{}, fmt.Errorf( @@ -488,6 +503,7 @@ func (ce *coordinationExecutor) actionsChecklist( // It returns the generated proposal or an error if the routine failed. func (ce *coordinationExecutor) leaderRoutine( ctx context.Context, + coordinationBlock uint64, actionsChecklist []WalletActionType, ) (coordinationProposal, error) { proposal, err := ce.proposalGenerator(actionsChecklist) @@ -495,8 +511,17 @@ func (ce *coordinationExecutor) leaderRoutine( return nil, fmt.Errorf("failed to generate proposal: [%v]", err) } + // Sort members indexes in ascending order, just in case. Choose the first + // member as the sender of the coordination message. + membersIndexes := append([]group.MemberIndex{}, ce.membersIndexes...) + slices.Sort(membersIndexes) + senderID := membersIndexes[0] + message := &coordinationMessage{ - // TODO: Initialize fields. + senderID: senderID, + coordinationBlock: coordinationBlock, + walletPublicKeyHash: ce.walletPublicKeyHash(), + proposal: proposal, } err = ce.broadcastChannel.Send( diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index 7de45adf13..bbd66829e2 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -8,6 +8,7 @@ import ( "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/net" netlocal "github.com/keep-network/keep-core/pkg/net/local" + "github.com/keep-network/keep-core/pkg/protocol/group" "math/big" "reflect" "testing" @@ -167,12 +168,12 @@ func TestWatchCoordinationWindows(t *testing.T) { } func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { - window := newCoordinationWindow(900) + coordinationBlock := uint64(900) localChain := Connect() localChain.setBlockHashByNumber( - window.coordinationBlock-32, + coordinationBlock-32, "1322996cbcbc38fc924a46f4df5f9064279d3ab43396e58386dac9b87440d64f", ) @@ -197,7 +198,7 @@ func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { coordinatedWallet: coordinatedWallet, } - seed, err := executor.coordinationSeed(window) + seed, err := executor.coordinationSeed(coordinationBlock) if err != nil { t.Fatal(err) } @@ -381,26 +382,34 @@ func TestCoordinationExecutor_ActionsChecklist(t *testing.T) { } func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { - provider := netlocal.Connect() - - broadcastChannel, err := provider.BroadcastChannelFor("test") + // Uncompressed public key corresponding to the 20-byte public key hash: + // aa768412ceed10bd423c025542ca90071f9fb62d. + publicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) if err != nil { t.Fatal(err) } - broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { - return &coordinationMessage{} - }) + coordinatedWallet := wallet{ + // Set only relevant fields. + publicKey: unmarshalPublicKey(publicKeyHex), + } + + // Deliberately use an unsorted list of members indexes to make sure the + // leader routine sorts them before determining the coordination message + // sender. + membersIndexes := []group.MemberIndex{77, 5, 10} proposalGenerator := func(actionsChecklist []WalletActionType) ( coordinationProposal, error, ) { for _, action := range actionsChecklist { - if action == ActionDepositSweep { - return &DepositSweepProposal{ - // Set just one field to make the proposal non-empty. - SweepTxFee: big.NewInt(1000), + if action == ActionHeartbeat { + return &HeartbeatProposal{ + Message: []byte("heartbeat message"), }, nil } } @@ -408,8 +417,21 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { return &noopProposal{}, nil } + provider := netlocal.Connect() + + broadcastChannel, err := provider.BroadcastChannelFor("test") + if err != nil { + t.Fatal(err) + } + + broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &coordinationMessage{} + }) + executor := &coordinationExecutor{ // Set only relevant fields. + coordinatedWallet: coordinatedWallet, + membersIndexes: membersIndexes, proposalGenerator: proposalGenerator, broadcastChannel: broadcastChannel, } @@ -419,28 +441,40 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { ActionDepositSweep, ActionMovedFundsSweep, ActionMovingFunds, + ActionHeartbeat, } - ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second) + ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second) defer cancelCtx() - var broadcastedProposal coordinationProposal - broadcastChannel.Recv(ctx, func(message net.Message) { - // Set broadcastedProposal from message. + var message *coordinationMessage + broadcastChannel.Recv(ctx, func(m net.Message) { + cm, ok := m.Payload().(*coordinationMessage) + if !ok { + t.Fatal("unexpected message type") + } + + // Capture the message for later assertions. + message = cm + + // Cancel the context to proceed with the test quicker. + cancelCtx() }) - proposal, err := executor.leaderRoutine(ctx, actionsChecklist) + proposal, err := executor.leaderRoutine(ctx, 900, actionsChecklist) if err != nil { t.Fatal(err) } - expectedProposal := &DepositSweepProposal{ - SweepTxFee: big.NewInt(1000), + <-ctx.Done() + + expectedProposal := &HeartbeatProposal{ + Message: []byte("heartbeat message"), } if !reflect.DeepEqual(expectedProposal, proposal) { t.Errorf( - "unexpected proposal returned by leader's routine: \n"+ + "unexpected proposal: \n"+ "expected: %v\n"+ "actual: %v", expectedProposal, @@ -448,14 +482,27 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { ) } - // TODO: Modify this condition when the time comes. - if !reflect.DeepEqual(nil, broadcastedProposal) { + buffer, err := hex.DecodeString("aa768412ceed10bd423c025542ca90071f9fb62d") + if err != nil { + t.Fatal(err) + } + var expectedWalletPublicKeyHash [20]byte + copy(expectedWalletPublicKeyHash[:], buffer) + + expectedMessage := &coordinationMessage{ + senderID: 5, + coordinationBlock: 900, + walletPublicKeyHash: expectedWalletPublicKeyHash, + proposal: expectedProposal, + } + + if !reflect.DeepEqual(expectedMessage, message) { t.Errorf( - "unexpected proposal broadcasted to the followers: \n"+ + "unexpected message: \n"+ "expected: %v\n"+ "actual: %v", - nil, - broadcastedProposal, + expectedMessage, + message, ) } } diff --git a/pkg/tbtc/deposit_sweep.go b/pkg/tbtc/deposit_sweep.go index 8a1ad2792a..49df18b3bc 100644 --- a/pkg/tbtc/deposit_sweep.go +++ b/pkg/tbtc/deposit_sweep.go @@ -57,6 +57,7 @@ const ( // DepositSweepProposal represents a deposit sweep proposal issued by a // wallet's coordination leader. type DepositSweepProposal struct { + // TODO: Remove WalletPublicKeyHash field. WalletPublicKeyHash [20]byte DepositsKeys []struct { FundingTxHash bitcoin.Hash diff --git a/pkg/tbtc/gen/pb/message.pb.go b/pkg/tbtc/gen/pb/message.pb.go index b84c30ba61..424df0eacd 100644 --- a/pkg/tbtc/gen/pb/message.pb.go +++ b/pkg/tbtc/gen/pb/message.pb.go @@ -99,6 +99,352 @@ func (x *SigningDoneMessage) GetEndBlock() uint64 { return 0 } +type CoordinationProposal struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ActionType uint32 `protobuf:"varint,1,opt,name=actionType,proto3" json:"actionType,omitempty"` + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` +} + +func (x *CoordinationProposal) Reset() { + *x = CoordinationProposal{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CoordinationProposal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CoordinationProposal) ProtoMessage() {} + +func (x *CoordinationProposal) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CoordinationProposal.ProtoReflect.Descriptor instead. +func (*CoordinationProposal) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{1} +} + +func (x *CoordinationProposal) GetActionType() uint32 { + if x != nil { + return x.ActionType + } + return 0 +} + +func (x *CoordinationProposal) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +type CoordinationMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SenderID uint32 `protobuf:"varint,1,opt,name=senderID,proto3" json:"senderID,omitempty"` + CoordinationBlock uint64 `protobuf:"varint,2,opt,name=coordinationBlock,proto3" json:"coordinationBlock,omitempty"` + WalletPublicKeyHash []byte `protobuf:"bytes,3,opt,name=walletPublicKeyHash,proto3" json:"walletPublicKeyHash,omitempty"` + Proposal *CoordinationProposal `protobuf:"bytes,4,opt,name=proposal,proto3" json:"proposal,omitempty"` +} + +func (x *CoordinationMessage) Reset() { + *x = CoordinationMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CoordinationMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CoordinationMessage) ProtoMessage() {} + +func (x *CoordinationMessage) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CoordinationMessage.ProtoReflect.Descriptor instead. +func (*CoordinationMessage) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{2} +} + +func (x *CoordinationMessage) GetSenderID() uint32 { + if x != nil { + return x.SenderID + } + return 0 +} + +func (x *CoordinationMessage) GetCoordinationBlock() uint64 { + if x != nil { + return x.CoordinationBlock + } + return 0 +} + +func (x *CoordinationMessage) GetWalletPublicKeyHash() []byte { + if x != nil { + return x.WalletPublicKeyHash + } + return nil +} + +func (x *CoordinationMessage) GetProposal() *CoordinationProposal { + if x != nil { + return x.Proposal + } + return nil +} + +type HeartbeatProposal struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *HeartbeatProposal) Reset() { + *x = HeartbeatProposal{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HeartbeatProposal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeartbeatProposal) ProtoMessage() {} + +func (x *HeartbeatProposal) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeartbeatProposal.ProtoReflect.Descriptor instead. +func (*HeartbeatProposal) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{3} +} + +func (x *HeartbeatProposal) GetMessage() []byte { + if x != nil { + return x.Message + } + return nil +} + +type DepositSweepProposal struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositsKeys []*DepositSweepProposal_DepositKey `protobuf:"bytes,1,rep,name=depositsKeys,proto3" json:"depositsKeys,omitempty"` + SweepTxFee []byte `protobuf:"bytes,2,opt,name=sweepTxFee,proto3" json:"sweepTxFee,omitempty"` + DepositsRevealBlocks []uint64 `protobuf:"varint,3,rep,packed,name=depositsRevealBlocks,proto3" json:"depositsRevealBlocks,omitempty"` +} + +func (x *DepositSweepProposal) Reset() { + *x = DepositSweepProposal{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DepositSweepProposal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DepositSweepProposal) ProtoMessage() {} + +func (x *DepositSweepProposal) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DepositSweepProposal.ProtoReflect.Descriptor instead. +func (*DepositSweepProposal) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{4} +} + +func (x *DepositSweepProposal) GetDepositsKeys() []*DepositSweepProposal_DepositKey { + if x != nil { + return x.DepositsKeys + } + return nil +} + +func (x *DepositSweepProposal) GetSweepTxFee() []byte { + if x != nil { + return x.SweepTxFee + } + return nil +} + +func (x *DepositSweepProposal) GetDepositsRevealBlocks() []uint64 { + if x != nil { + return x.DepositsRevealBlocks + } + return nil +} + +type RedemptionProposal struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RedeemersOutputScripts [][]byte `protobuf:"bytes,1,rep,name=redeemersOutputScripts,proto3" json:"redeemersOutputScripts,omitempty"` + RedemptionTxFee []byte `protobuf:"bytes,2,opt,name=redemptionTxFee,proto3" json:"redemptionTxFee,omitempty"` +} + +func (x *RedemptionProposal) Reset() { + *x = RedemptionProposal{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RedemptionProposal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RedemptionProposal) ProtoMessage() {} + +func (x *RedemptionProposal) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RedemptionProposal.ProtoReflect.Descriptor instead. +func (*RedemptionProposal) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{5} +} + +func (x *RedemptionProposal) GetRedeemersOutputScripts() [][]byte { + if x != nil { + return x.RedeemersOutputScripts + } + return nil +} + +func (x *RedemptionProposal) GetRedemptionTxFee() []byte { + if x != nil { + return x.RedemptionTxFee + } + return nil +} + +type DepositSweepProposal_DepositKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FundingTxHash []byte `protobuf:"bytes,1,opt,name=fundingTxHash,proto3" json:"fundingTxHash,omitempty"` + FundingOutputIndex uint32 `protobuf:"varint,2,opt,name=fundingOutputIndex,proto3" json:"fundingOutputIndex,omitempty"` +} + +func (x *DepositSweepProposal_DepositKey) Reset() { + *x = DepositSweepProposal_DepositKey{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DepositSweepProposal_DepositKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DepositSweepProposal_DepositKey) ProtoMessage() {} + +func (x *DepositSweepProposal_DepositKey) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tbtc_gen_pb_message_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DepositSweepProposal_DepositKey.ProtoReflect.Descriptor instead. +func (*DepositSweepProposal_DepositKey) Descriptor() ([]byte, []int) { + return file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP(), []int{4, 0} +} + +func (x *DepositSweepProposal_DepositKey) GetFundingTxHash() []byte { + if x != nil { + return x.FundingTxHash + } + return nil +} + +func (x *DepositSweepProposal_DepositKey) GetFundingOutputIndex() uint32 { + if x != nil { + return x.FundingOutputIndex + } + return 0 +} + var File_pkg_tbtc_gen_pb_message_proto protoreflect.FileDescriptor var file_pkg_tbtc_gen_pb_message_proto_rawDesc = []byte{ @@ -115,8 +461,54 @@ var file_pkg_tbtc_gen_pb_message_proto_rawDesc = []byte{ 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x42, 0x6c, 0x6f, - 0x63, 0x6b, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x63, 0x6b, 0x22, 0x50, 0x0a, 0x14, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x22, 0xc9, 0x01, 0x0a, 0x13, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, + 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, + 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x49, 0x44, 0x12, 0x2c, 0x0a, 0x11, 0x63, 0x6f, 0x6f, 0x72, + 0x64, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x11, 0x63, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x30, 0x0a, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x36, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x70, + 0x6f, 0x73, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x62, 0x74, + 0x63, 0x2e, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, + 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, + 0x22, 0x2d, 0x0a, 0x11, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x50, 0x72, 0x6f, + 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x99, 0x02, 0x0a, 0x14, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, + 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x12, 0x49, 0x0a, 0x0c, 0x64, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, + 0x2e, 0x74, 0x62, 0x74, 0x63, 0x2e, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x53, 0x77, 0x65, + 0x65, 0x70, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, 0x6c, 0x2e, 0x44, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x0c, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x4b, + 0x65, 0x79, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, 0x46, 0x65, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x77, 0x65, 0x65, 0x70, 0x54, 0x78, + 0x46, 0x65, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x52, + 0x65, 0x76, 0x65, 0x61, 0x6c, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x04, 0x52, 0x14, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x52, 0x65, 0x76, 0x65, 0x61, + 0x6c, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x1a, 0x62, 0x0a, 0x0a, 0x44, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x0d, 0x66, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x54, 0x78, 0x48, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x66, 0x75, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x54, 0x78, 0x48, 0x61, 0x73, 0x68, 0x12, 0x2e, 0x0a, 0x12, 0x66, + 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, + 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x66, 0x75, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x76, 0x0a, 0x12, 0x52, + 0x65, 0x64, 0x65, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x61, + 0x6c, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x65, 0x72, 0x73, 0x4f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0c, 0x52, 0x16, 0x72, 0x65, 0x64, 0x65, 0x65, 0x6d, 0x65, 0x72, 0x73, 0x4f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x72, 0x65, 0x64, + 0x65, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x78, 0x46, 0x65, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x65, 0x64, 0x65, 0x6d, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x78, + 0x46, 0x65, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -131,16 +523,24 @@ func file_pkg_tbtc_gen_pb_message_proto_rawDescGZIP() []byte { return file_pkg_tbtc_gen_pb_message_proto_rawDescData } -var file_pkg_tbtc_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_pkg_tbtc_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_pkg_tbtc_gen_pb_message_proto_goTypes = []interface{}{ - (*SigningDoneMessage)(nil), // 0: tbtc.SigningDoneMessage + (*SigningDoneMessage)(nil), // 0: tbtc.SigningDoneMessage + (*CoordinationProposal)(nil), // 1: tbtc.CoordinationProposal + (*CoordinationMessage)(nil), // 2: tbtc.CoordinationMessage + (*HeartbeatProposal)(nil), // 3: tbtc.HeartbeatProposal + (*DepositSweepProposal)(nil), // 4: tbtc.DepositSweepProposal + (*RedemptionProposal)(nil), // 5: tbtc.RedemptionProposal + (*DepositSweepProposal_DepositKey)(nil), // 6: tbtc.DepositSweepProposal.DepositKey } var file_pkg_tbtc_gen_pb_message_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 1, // 0: tbtc.CoordinationMessage.proposal:type_name -> tbtc.CoordinationProposal + 6, // 1: tbtc.DepositSweepProposal.depositsKeys:type_name -> tbtc.DepositSweepProposal.DepositKey + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_pkg_tbtc_gen_pb_message_proto_init() } @@ -161,6 +561,78 @@ func file_pkg_tbtc_gen_pb_message_proto_init() { return nil } } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CoordinationProposal); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CoordinationMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HeartbeatProposal); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DepositSweepProposal); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RedemptionProposal); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_tbtc_gen_pb_message_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DepositSweepProposal_DepositKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -168,7 +640,7 @@ func file_pkg_tbtc_gen_pb_message_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_pkg_tbtc_gen_pb_message_proto_rawDesc, NumEnums: 0, - NumMessages: 1, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/tbtc/gen/pb/message.proto b/pkg/tbtc/gen/pb/message.proto index 5bab60171d..1da799a18f 100644 --- a/pkg/tbtc/gen/pb/message.proto +++ b/pkg/tbtc/gen/pb/message.proto @@ -9,4 +9,36 @@ message SigningDoneMessage { uint64 attemptNumber = 3; bytes signature = 4; uint64 endBlock = 5; +} + +message CoordinationProposal { + uint32 actionType = 1; + bytes payload = 2; +} + +message CoordinationMessage { + uint32 senderID = 1; + uint64 coordinationBlock = 2; + bytes walletPublicKeyHash = 3; + CoordinationProposal proposal = 4; +} + +message HeartbeatProposal { + bytes message = 1; +} + +message DepositSweepProposal { + message DepositKey { + bytes fundingTxHash = 1; + uint32 fundingOutputIndex = 2; + } + + repeated DepositKey depositsKeys = 1; + bytes sweepTxFee = 2; + repeated uint64 depositsRevealBlocks = 3; +} + +message RedemptionProposal { + repeated bytes redeemersOutputScripts = 1; + bytes redemptionTxFee = 2; } \ No newline at end of file diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 09f704d178..57714f899a 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -31,7 +31,7 @@ const ( ) type HeartbeatProposal struct { - // TODO: Proposal fields. + Message []byte } func (hp *HeartbeatProposal) actionType() WalletActionType { diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index 1c992e539a..470736192c 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -4,6 +4,8 @@ import ( "crypto/ecdsa" "crypto/elliptic" "fmt" + "github.com/keep-network/keep-core/pkg/bitcoin" + "math" "math/big" "google.golang.org/protobuf/proto" @@ -127,13 +129,259 @@ func (sdm *signingDoneMessage) Unmarshal(bytes []byte) error { // Marshal converts the coordinationMessage to a byte array. func (cm *coordinationMessage) Marshal() ([]byte, error) { - // TODO: Implement. - return nil, nil + proposalBytes, err := cm.proposal.Marshal() + if err != nil { + return nil, err + } + + pbProposal := &pb.CoordinationProposal{ + ActionType: uint32(cm.proposal.actionType()), + Payload: proposalBytes, + } + + return proto.Marshal( + &pb.CoordinationMessage{ + SenderID: uint32(cm.senderID), + CoordinationBlock: cm.coordinationBlock, + WalletPublicKeyHash: append([]byte{}, cm.walletPublicKeyHash[:]...), + Proposal: pbProposal, + }, + ) } // Unmarshal converts a byte array back to the coordinationMessage. func (cm *coordinationMessage) Unmarshal(bytes []byte) error { - // TODO: Implement. + pbMsg := pb.CoordinationMessage{} + if err := proto.Unmarshal(bytes, &pbMsg); err != nil { + return fmt.Errorf("failed to unmarshal CoordinationMessage: [%v]", err) + } + + if err := validateMemberIndex(pbMsg.SenderID); err != nil { + return err + } + + walletPublicKeyHash, err := unmarshalWalletPublicKeyHash(pbMsg.WalletPublicKeyHash) + if err != nil { + return fmt.Errorf( + "failed to unmarshal wallet public key hash: [%v]", + err, + ) + } + + if pbMsg.Proposal == nil { + return fmt.Errorf("missing proposal") + } + proposal, err := unmarshalCoordinationProposal( + pbMsg.Proposal.ActionType, + pbMsg.Proposal.Payload, + ) + if err != nil { + return fmt.Errorf("failed to unmarshal proposal: [%v]", err) + } + + cm.senderID = group.MemberIndex(pbMsg.SenderID) + cm.coordinationBlock = pbMsg.CoordinationBlock + cm.walletPublicKeyHash = walletPublicKeyHash + cm.proposal = proposal + + return nil +} + +// unmarshalWalletPublicKeyHash converts a byte array to a wallet public key +// hash. +func unmarshalWalletPublicKeyHash(bytes []byte) ([20]byte, error) { + if len(bytes) != 20 { + return [20]byte{}, fmt.Errorf( + "invalid wallet public key hash length: [%v]", + len(bytes), + ) + } + + var walletPublicKeyHash [20]byte + copy(walletPublicKeyHash[:], bytes) + + return walletPublicKeyHash, nil +} + +// unmarshalCoordinationProposal converts a byte array back to the coordination +// proposal. +func unmarshalCoordinationProposal(actionType uint32, payload []byte) ( + coordinationProposal, + error, +) { + if actionType > math.MaxUint8 { + return nil, fmt.Errorf( + "invalid proposal action type value: [%v]", + actionType, + ) + } + + parsedActionType, err := ParseWalletActionType(uint8(actionType)) + if err != nil { + return nil, fmt.Errorf( + "failed to parse proposal action type: [%v]", + err, + ) + } + + proposal, ok := map[WalletActionType]coordinationProposal{ + ActionNoop: &noopProposal{}, + ActionHeartbeat: &HeartbeatProposal{}, + ActionDepositSweep: &DepositSweepProposal{}, + ActionRedemption: &RedemptionProposal{}, + // TODO: Uncomment when moving funds support is implemented. + // ActionMovingFunds: &MovingFundsProposal{}, + // ActionMovedFundsSweep: &MovedFundsSweepProposal{}, + }[parsedActionType] + if !ok { + return nil, fmt.Errorf( + "no unmarshaler for proposal action type: [%v]", + parsedActionType, + ) + } + + if err := proposal.Unmarshal(payload); err != nil { + return nil, fmt.Errorf("cannot unmarshal proposal payload: [%v]", err) + } + + return proposal, nil +} + +// Marshal converts the noopProposal to a byte array. +func (np *noopProposal) Marshal() ([]byte, error) { + return []byte{}, nil +} + +// Unmarshal converts a byte array back to the noopProposal. +func (np *noopProposal) Unmarshal([]byte) error { + return nil +} + +// Marshal converts the heartbeatProposal to a byte array. +func (hp *HeartbeatProposal) Marshal() ([]byte, error) { + return proto.Marshal( + &pb.HeartbeatProposal{ + Message: hp.Message, + }, + ) +} + +// Unmarshal converts a byte array back to the heartbeatProposal. +func (hp *HeartbeatProposal) Unmarshal(bytes []byte) error { + pbMsg := pb.HeartbeatProposal{} + if err := proto.Unmarshal(bytes, &pbMsg); err != nil { + return fmt.Errorf("failed to unmarshal HeartbeatProposal: [%v]", err) + } + + hp.Message = pbMsg.Message + + return nil +} + +// Marshal converts the depositSweepProposal to a byte array. +func (dsp *DepositSweepProposal) Marshal() ([]byte, error) { + depositsKeys := make( + []*pb.DepositSweepProposal_DepositKey, + len(dsp.DepositsKeys), + ) + for i, depositKey := range dsp.DepositsKeys { + depositsKeys[i] = &pb.DepositSweepProposal_DepositKey{ + FundingTxHash: append([]byte{}, depositKey.FundingTxHash[:]...), + FundingOutputIndex: depositKey.FundingOutputIndex, + } + } + + depositsRevealBlocks := make([]uint64, len(dsp.DepositsRevealBlocks)) + for i, block := range dsp.DepositsRevealBlocks { + depositsRevealBlocks[i] = block.Uint64() + } + + return proto.Marshal( + &pb.DepositSweepProposal{ + DepositsKeys: depositsKeys, + SweepTxFee: dsp.SweepTxFee.Bytes(), + DepositsRevealBlocks: depositsRevealBlocks, + }, + ) +} + +// Unmarshal converts a byte array back to the depositSweepProposal. +func (dsp *DepositSweepProposal) Unmarshal(bytes []byte) error { + pbMsg := pb.DepositSweepProposal{} + if err := proto.Unmarshal(bytes, &pbMsg); err != nil { + return fmt.Errorf("failed to unmarshal DepositSweepProposal: [%v]", err) + } + + depositsKeys := make( + []struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + }, + len(pbMsg.DepositsKeys), + ) + for i, depositKey := range pbMsg.DepositsKeys { + hash, err := bitcoin.NewHash( + depositKey.FundingTxHash, + bitcoin.InternalByteOrder, + ) + if err != nil { + return fmt.Errorf( + "failed to unmarshal funding tx hash: [%v]", + err, + ) + } + + depositsKeys[i] = struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + }{ + FundingTxHash: hash, + FundingOutputIndex: depositKey.FundingOutputIndex, + } + } + + depositsRevealBlocks := make([]*big.Int, len(pbMsg.DepositsRevealBlocks)) + for i, block := range pbMsg.DepositsRevealBlocks { + depositsRevealBlocks[i] = big.NewInt(int64(block)) + } + + dsp.DepositsKeys = depositsKeys + dsp.SweepTxFee = new(big.Int).SetBytes(pbMsg.SweepTxFee) + dsp.DepositsRevealBlocks = depositsRevealBlocks + + return nil +} + +// Marshal converts the redemptionProposal to a byte array. +func (rp *RedemptionProposal) Marshal() ([]byte, error) { + redeemersOutputScripts := make([][]byte, len(rp.RedeemersOutputScripts)) + for i, script := range rp.RedeemersOutputScripts { + redeemersOutputScripts[i] = script + } + + return proto.Marshal( + &pb.RedemptionProposal{ + RedeemersOutputScripts: redeemersOutputScripts, + RedemptionTxFee: rp.RedemptionTxFee.Bytes(), + }, + ) +} + +// Unmarshal converts a byte array back to the redemptionProposal. +func (rp *RedemptionProposal) Unmarshal(bytes []byte) error { + pbMsg := pb.RedemptionProposal{} + if err := proto.Unmarshal(bytes, &pbMsg); err != nil { + return fmt.Errorf("failed to unmarshal RedemptionProposal: [%v]", err) + } + + redeemersOutputScripts := make([]bitcoin.Script, len(pbMsg.RedeemersOutputScripts)) + for i, script := range pbMsg.RedeemersOutputScripts { + redeemersOutputScripts[i] = script + } + + rp.RedeemersOutputScripts = redeemersOutputScripts + rp.RedemptionTxFee = new(big.Int).SetBytes(pbMsg.RedemptionTxFee) + return nil } diff --git a/pkg/tbtc/marshaling_test.go b/pkg/tbtc/marshaling_test.go index 312daac5cf..e1e11abfad 100644 --- a/pkg/tbtc/marshaling_test.go +++ b/pkg/tbtc/marshaling_test.go @@ -3,6 +3,8 @@ package tbtc import ( "crypto/ecdsa" "crypto/elliptic" + "encoding/hex" + "github.com/keep-network/keep-core/pkg/bitcoin" "math/big" "reflect" "testing" @@ -104,3 +106,228 @@ func TestFuzzSigningDoneMessage_MarshalingRoundtrip(t *testing.T) { func TestFuzzSigningDoneMessage_Unmarshaler(t *testing.T) { pbutils.FuzzUnmarshaler(&signingDoneMessage{}) } + +func TestCoordinationMessage_MarshalingRoundtrip(t *testing.T) { + parseHash := func(hash string) bitcoin.Hash { + parsed, err := bitcoin.NewHashFromString(hash, bitcoin.InternalByteOrder) + if err != nil { + t.Fatal(err) + } + + return parsed + } + + parseScript := func(script string) bitcoin.Script { + parsed, err := hex.DecodeString(script) + if err != nil { + t.Fatal(err) + } + + return parsed + } + + tests := map[string]struct { + proposal coordinationProposal + }{ + "with noop proposal": { + proposal: &noopProposal{}, + }, + "with heartbeat proposal": { + proposal: &HeartbeatProposal{ + Message: []byte("heartbeat message"), + }, + }, + "with deposit sweep proposal": { + proposal: &DepositSweepProposal{ + DepositsKeys: []struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + }{ + { + FundingTxHash: parseHash("709b55bd3da0f5a838125bd0ee20c5bfdd7caba173912d4281cae816b79a201b"), + FundingOutputIndex: 0, + }, + { + FundingTxHash: parseHash("27ca64c092a959c7edc525ed45e845b1de6a7590d173fd2fad9133c8a779a1e3"), + FundingOutputIndex: 1, + }, + }, + SweepTxFee: big.NewInt(10000), + DepositsRevealBlocks: []*big.Int{ + big.NewInt(100), + big.NewInt(300), + }, + }, + }, + "with redemption proposal": { + proposal: &RedemptionProposal{ + RedeemersOutputScripts: []bitcoin.Script{ + parseScript("00148db50eb52063ea9d98b3eac91489a90f738986f6"), + parseScript("76a9148db50eb52063ea9d98b3eac91489a90f738986f688ac"), + }, + RedemptionTxFee: big.NewInt(10000), + }, + }, + // TODO: Uncomment when moving funds support is implemented. + // "with moving funds proposal": { + // proposal: &MovingFundsProposal{}, + // }, + // "with moved funds sweep proposal": { + // proposal: &MovedFundsSweepProposal{}, + // }, + } + + walletPublicKeyHashBytes, err := hex.DecodeString( + "aa768412ceed10bd423c025542ca90071f9fb62d", + ) + if err != nil { + t.Fatal(err) + } + var walletPublicKeyHash [20]byte + copy(walletPublicKeyHash[:], walletPublicKeyHashBytes) + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + msg := &coordinationMessage{ + senderID: group.MemberIndex(10), + coordinationBlock: 900, + walletPublicKeyHash: walletPublicKeyHash, + proposal: test.proposal, + } + unmarshaled := &coordinationMessage{} + + err := pbutils.RoundTrip(msg, unmarshaled) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(msg, unmarshaled) { + t.Fatalf("unexpected content of unmarshaled message") + } + }) + } +} + +func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithHeartbeatProposal(t *testing.T) { + for i := 0; i < 10; i++ { + var ( + senderID group.MemberIndex + coordinationBlock uint64 + walletPublicKeyHash [20]byte + proposal HeartbeatProposal + ) + + f := fuzz.New().NilChance(0.1). + NumElements(0, 512). + Funcs(pbutils.FuzzFuncs()...) + + f.Fuzz(&senderID) + f.Fuzz(&coordinationBlock) + f.Fuzz(&walletPublicKeyHash) + f.Fuzz(&proposal) + + doneMessage := &coordinationMessage{ + senderID: senderID, + coordinationBlock: coordinationBlock, + walletPublicKeyHash: walletPublicKeyHash, + proposal: &proposal, + } + + _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + } +} + +func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithDepositSweepProposal(t *testing.T) { + for i := 0; i < 10; i++ { + var ( + senderID group.MemberIndex + coordinationBlock uint64 + walletPublicKeyHash [20]byte + proposal DepositSweepProposal + ) + + f := fuzz.New().NilChance(0.1). + NumElements(0, 512). + Funcs(pbutils.FuzzFuncs()...) + + f.Fuzz(&senderID) + f.Fuzz(&coordinationBlock) + f.Fuzz(&walletPublicKeyHash) + f.Fuzz(&proposal) + + doneMessage := &coordinationMessage{ + senderID: senderID, + coordinationBlock: coordinationBlock, + walletPublicKeyHash: walletPublicKeyHash, + proposal: &proposal, + } + + _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + } +} + +func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithRedemptionProposal(t *testing.T) { + for i := 0; i < 10; i++ { + var ( + senderID group.MemberIndex + coordinationBlock uint64 + walletPublicKeyHash [20]byte + proposal RedemptionProposal + ) + + f := fuzz.New().NilChance(0.1). + NumElements(0, 512). + Funcs(pbutils.FuzzFuncs()...) + + f.Fuzz(&senderID) + f.Fuzz(&coordinationBlock) + f.Fuzz(&walletPublicKeyHash) + f.Fuzz(&proposal) + + doneMessage := &coordinationMessage{ + senderID: senderID, + coordinationBlock: coordinationBlock, + walletPublicKeyHash: walletPublicKeyHash, + proposal: &proposal, + } + + _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + } +} + +func TestFuzzCoordinationMessage_MarshalingRoundtrip_WithNoopProposal(t *testing.T) { + for i := 0; i < 10; i++ { + var ( + senderID group.MemberIndex + coordinationBlock uint64 + walletPublicKeyHash [20]byte + proposal noopProposal + ) + + f := fuzz.New().NilChance(0.1). + NumElements(0, 512). + Funcs(pbutils.FuzzFuncs()...) + + f.Fuzz(&senderID) + f.Fuzz(&coordinationBlock) + f.Fuzz(&walletPublicKeyHash) + f.Fuzz(&proposal) + + doneMessage := &coordinationMessage{ + senderID: senderID, + coordinationBlock: coordinationBlock, + walletPublicKeyHash: walletPublicKeyHash, + proposal: &proposal, + } + + _ = pbutils.RoundTrip(doneMessage, &coordinationMessage{}) + } +} + +// TODO: Create two unit tests once moving funds is implemented: +// - TestFuzzCoordinationMessage_MarshalingRoundtrip_WithMovingFundsProposal +// - TestFuzzCoordinationMessage_MarshalingRoundtrip_WithMovedFundsSweepProposal + +func TestFuzzCoordinationMessage_Unmarshaler(t *testing.T) { + pbutils.FuzzUnmarshaler(&coordinationMessage{}) +} diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index 200789dff5..89649c286c 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -402,6 +402,14 @@ func (mcp *mockCoordinationProposal) validityBlocks() uint64 { panic("unsupported") } +func (mcp *mockCoordinationProposal) Marshal() ([]byte, error) { + panic("unsupported") +} + +func (mcp *mockCoordinationProposal) Unmarshal(bytes []byte) error { + panic("unsupported") +} + // createMockSigner creates a mock signer instance that can be used for // test cases that needs a placeholder signer. The produced signer cannot // be used to test actual signing scenarios. diff --git a/pkg/tbtc/redemption.go b/pkg/tbtc/redemption.go index 5098caf6eb..9cd9b7b79e 100644 --- a/pkg/tbtc/redemption.go +++ b/pkg/tbtc/redemption.go @@ -54,6 +54,7 @@ const ( // RedemptionProposal represents a redemption proposal issued by a wallet's // coordination leader. type RedemptionProposal struct { + // TODO: Remove WalletPublicKeyHash field. WalletPublicKeyHash [20]byte RedeemersOutputScripts []bitcoin.Script RedemptionTxFee *big.Int diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 33e031ba0d..4f09cc7adb 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -31,6 +31,26 @@ const ( ActionMovedFundsSweep ) +// ParseWalletActionType parses the given value into a WalletActionType. +func ParseWalletActionType(value uint8) (WalletActionType, error) { + switch value { + case 0: + return ActionNoop, nil + case 1: + return ActionHeartbeat, nil + case 2: + return ActionDepositSweep, nil + case 3: + return ActionRedemption, nil + case 4: + return ActionMovingFunds, nil + case 5: + return ActionMovedFundsSweep, nil + default: + return 0, fmt.Errorf("unknown wallet action type [%v]", value) + } +} + func (wat WalletActionType) String() string { switch wat { case ActionNoop: diff --git a/pkg/tbtc/wallet_test.go b/pkg/tbtc/wallet_test.go index ec46fb0a3e..703311eec5 100644 --- a/pkg/tbtc/wallet_test.go +++ b/pkg/tbtc/wallet_test.go @@ -19,6 +19,65 @@ import ( "github.com/keep-network/keep-core/pkg/tecdsa" ) +func TestParseWalletActionType(t *testing.T) { + tests := map[string]struct { + value uint8 + expectedAction WalletActionType + expectedErr error + }{ + "noop": { + value: 0, + expectedAction: ActionNoop, + }, + "heartbeat": { + value: 1, + expectedAction: ActionHeartbeat, + }, + "deposit sweep": { + value: 2, + expectedAction: ActionDepositSweep, + }, + "redemption": { + value: 3, + expectedAction: ActionRedemption, + }, + "moving funds": { + value: 4, + expectedAction: ActionMovingFunds, + }, + "moved funds sweep": { + value: 5, + expectedAction: ActionMovedFundsSweep, + }, + "unknown": { + value: 6, + expectedErr: fmt.Errorf("unknown wallet action type [6]"), + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + action, err := ParseWalletActionType(test.value) + + if !reflect.DeepEqual(test.expectedErr, err) { + t.Errorf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + test.expectedErr, + err, + ) + } + + if test.expectedAction != action { + t.Errorf( + "unexpected action type\nexpected: [%v]\nactual: [%v]", + test.expectedAction, + action, + ) + } + }) + } +} + func TestWalletDispatcher_Dispatch(t *testing.T) { walletDispatcher := newWalletDispatcher() From 1be0521f24f32251a1e7a8c239072c8689e857d5 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 28 Nov 2023 15:35:02 +0100 Subject: [PATCH 06/14] Pass wallet PKH to the proposal generator Proposal generation must have a way to identify the wallet. Code currently used by the wallet maintainer (`pkg/maintainer/wallet`) uses 20-byte wallet public key hashes for that purpose. As we plan to reuse it in the new mechanism, we should use the same identifier. --- pkg/tbtc/coordination.go | 7 +++++-- pkg/tbtc/coordination_test.go | 24 ++++++++++++++---------- pkg/tbtc/node.go | 10 +++++++++- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 3e795ed348..1fd87cd3ff 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -194,6 +194,7 @@ func (cf *coordinationFault) String() string { // wallet's state. If none of the actions are valid, the generator // should return a noopProposal. type coordinationProposalGenerator func( + walletPublicKeyHash [20]byte, actionsChecklist []WalletActionType, ) (coordinationProposal, error) @@ -506,7 +507,9 @@ func (ce *coordinationExecutor) leaderRoutine( coordinationBlock uint64, actionsChecklist []WalletActionType, ) (coordinationProposal, error) { - proposal, err := ce.proposalGenerator(actionsChecklist) + walletPublicKeyHash := ce.walletPublicKeyHash() + + proposal, err := ce.proposalGenerator(walletPublicKeyHash, actionsChecklist) if err != nil { return nil, fmt.Errorf("failed to generate proposal: [%v]", err) } @@ -520,7 +523,7 @@ func (ce *coordinationExecutor) leaderRoutine( message := &coordinationMessage{ senderID: senderID, coordinationBlock: coordinationBlock, - walletPublicKeyHash: ce.walletPublicKeyHash(), + walletPublicKeyHash: walletPublicKeyHash, proposal: proposal, } diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index bbd66829e2..e845b51730 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -392,6 +392,14 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { t.Fatal(err) } + // 20-byte public key hash corresponding to the public key above. + buffer, err := hex.DecodeString("aa768412ceed10bd423c025542ca90071f9fb62d") + if err != nil { + t.Fatal(err) + } + var publicKeyHash [20]byte + copy(publicKeyHash[:], buffer) + coordinatedWallet := wallet{ // Set only relevant fields. publicKey: unmarshalPublicKey(publicKeyHex), @@ -402,12 +410,15 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { // sender. membersIndexes := []group.MemberIndex{77, 5, 10} - proposalGenerator := func(actionsChecklist []WalletActionType) ( + proposalGenerator := func( + walletPublicKeyHash [20]byte, + actionsChecklist []WalletActionType, + ) ( coordinationProposal, error, ) { for _, action := range actionsChecklist { - if action == ActionHeartbeat { + if walletPublicKeyHash == publicKeyHash && action == ActionHeartbeat { return &HeartbeatProposal{ Message: []byte("heartbeat message"), }, nil @@ -482,17 +493,10 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { ) } - buffer, err := hex.DecodeString("aa768412ceed10bd423c025542ca90071f9fb62d") - if err != nil { - t.Fatal(err) - } - var expectedWalletPublicKeyHash [20]byte - copy(expectedWalletPublicKeyHash[:], buffer) - expectedMessage := &coordinationMessage{ senderID: 5, coordinationBlock: 900, - walletPublicKeyHash: expectedWalletPublicKeyHash, + walletPublicKeyHash: publicKeyHash, proposal: expectedProposal, } diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 746dddccce..2fe5847bd3 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -379,12 +379,20 @@ func (n *node) getCoordinationExecutor( return nil, false, fmt.Errorf("failed to get operator address: [%v]", err) } + proposalGenerator := func( + walletPublicKeyHash [20]byte, + actionsChecklist []WalletActionType, + ) (coordinationProposal, error) { + // TODO: Implement proposal generation. + return &noopProposal{}, nil + } + executor := newCoordinationExecutor( n.chain, wallet, membersIndexes, operatorAddress, - nil, // TODO: Set a proper proposal generator. + proposalGenerator, broadcastChannel, membershipValidator, n.protocolLatch, From a35d7b31743cb6cec29ca5c413f044214eb108ff Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 28 Nov 2023 17:24:46 +0100 Subject: [PATCH 07/14] Follower's routine Here we implement the follower's routine along with necessary message validation. --- pkg/tbtc/coordination.go | 95 ++++++++++++++++++++++++++++++++++++++-- pkg/tbtc/wallet.go | 18 ++++++++ pkg/tbtc/wallet_test.go | 68 ++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 4 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 1fd87cd3ff..4833013b47 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -46,6 +46,14 @@ const ( // heartbeat action during the coordination procedure, assuming no other // higher-priority action is proposed. coordinationHeartbeatProbability = float64(0.125) + // coordinationMessageReceiveBuffer is a buffer for messages received from + // the broadcast channel needed when the coordination follower is + // temporarily too slow to handle them. Keep in mind that although we + // expect only 1 coordination message, it may happen that the follower + // receives retransmissions of messages from the coordination protocol, + // and before they are filtered out as not interesting for the follower, + // they are buffered in the channel. + coordinationMessageReceiveBuffer = 512 ) // errCoordinationExecutorBusy is an error returned when the coordination @@ -365,7 +373,12 @@ func (ce *coordinationExecutor) coordinate( ) } } else { - proposal, err = ce.followerRoutine() + proposal, err = ce.followerRoutine( + ctx, + leader, + window.coordinationBlock, + append(actionsChecklist, ActionNoop), + ) if err != nil { return nil, fmt.Errorf( "failed to execute follower's routine: [%v]", @@ -543,7 +556,81 @@ func (ce *coordinationExecutor) leaderRoutine( // window. The routine listens for the coordination message from the leader and // validates it. If the leader's proposal is valid, it returns the received // proposal. Returns an error if the routine failed. -func (ce *coordinationExecutor) followerRoutine() (coordinationProposal, error) { - // TODO: Implement the follower routine. - return nil, nil +func (ce *coordinationExecutor) followerRoutine( + ctx context.Context, + leader chain.Address, + coordinationBlock uint64, + actionsAllowed []WalletActionType, +) (coordinationProposal, error) { + // Cache wallet public key hash to not compute it on every message. + walletPublicKeyHash := ce.walletPublicKeyHash() + // Leader ID is the index of the first (index-wise) member controlled by + // the leader operator. The membersByOperator function returns a list of + // members controlled by the leader operator in the ascending order. + // It is enough to take the first member from the list. No need + // to check for list length as it is guaranteed that the leader operator + // is one of the operators backing the wallet. + leaderID := ce.coordinatedWallet.membersByOperator(leader)[0] + + messagesChan := make(chan net.Message, coordinationMessageReceiveBuffer) + + ce.broadcastChannel.Recv(ctx, func(message net.Message) { + messagesChan <- message + }) + +loop: + for { + select { + case netMessage := <-messagesChan: + // Filter out messages of wrong type. + message, ok := netMessage.Payload().(*coordinationMessage) + if !ok { + continue + } + + // Filter out messages from self. + if slices.Contains(ce.membersIndexes, message.senderID) { + continue + } + + // Filter out messages with invalid membership. + if !ce.membershipValidator.IsValidMembership( + message.senderID, + netMessage.SenderPublicKey(), + ) { + continue + } + + // Filter out messages with wrong coordination block. + if coordinationBlock != message.coordinationBlock { + continue + } + + // Filter out messages with wrong wallet. + if walletPublicKeyHash != message.walletPublicKeyHash { + continue + } + + // Filter out messages from leader's impersonators. + if leaderID != message.senderID { + // TODO: Record coordination fault of type FaultLeaderImpersonation. + continue + } + + // Filter out messages that propose an action that is not allowed + // for the given coordination window. + if !slices.Contains(actionsAllowed, message.proposal.actionType()) { + // TODO: Record coordination fault of type FaultLeaderMistake. + continue + } + + return message.proposal, nil + case <-ctx.Done(): + break loop + } + } + + // TODO: Record coordination fault of type FaultLeaderIdleness. + + return nil, fmt.Errorf("coordination message not received on time") } diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 4f09cc7adb..9c34c69b44 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -7,6 +7,7 @@ import ( "crypto/elliptic" "encoding/hex" "fmt" + "golang.org/x/exp/slices" "math/big" "sync" "time" @@ -386,6 +387,23 @@ func (w *wallet) groupDishonestThreshold(honestThreshold int) int { return w.groupSize() - honestThreshold } +// membersByOperator returns the list of group members' indexes that are +// associated with the given operator address. The returned list is sorted +// in ascending order. +func (w *wallet) membersByOperator(operator chain.Address) []group.MemberIndex { + members := make([]group.MemberIndex, 0) + + for i, signingGroupOperator := range w.signingGroupOperators { + if signingGroupOperator == operator { + members = append(members, group.MemberIndex(i+1)) + } + } + + slices.Sort(members) + + return members +} + func (w *wallet) String() string { publicKey := elliptic.Marshal( w.publicKey.Curve, diff --git a/pkg/tbtc/wallet_test.go b/pkg/tbtc/wallet_test.go index 703311eec5..802e3aed3f 100644 --- a/pkg/tbtc/wallet_test.go +++ b/pkg/tbtc/wallet_test.go @@ -8,6 +8,8 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/protocol/group" "math/big" "reflect" "sync" @@ -318,6 +320,72 @@ func TestDetermineWalletMainUtxo(t *testing.T) { } } +func TestWallet_MembersByOperator(t *testing.T) { + wallet := &wallet{ + // Set only relevant fields. + signingGroupOperators: []chain.Address{ + "0x2", + "0x1", + "0x3", + "0x2", + "0x1", + "0x6", + "0x5", + "0x3", + "0x4", + "0x3", + }, + } + + tests := map[string]struct { + operator chain.Address + expectedMembers []group.MemberIndex + }{ + "operator 1": { + operator: "0x1", + expectedMembers: []group.MemberIndex{2, 5}, + }, + "operator 2": { + operator: "0x2", + expectedMembers: []group.MemberIndex{1, 4}, + }, + "operator 3": { + operator: "0x3", + expectedMembers: []group.MemberIndex{3, 8, 10}, + }, + "operator 4": { + operator: "0x4", + expectedMembers: []group.MemberIndex{9}, + }, + "operator 5": { + operator: "0x5", + expectedMembers: []group.MemberIndex{7}, + }, + "operator 6": { + operator: "0x6", + expectedMembers: []group.MemberIndex{6}, + }, + "operator 7": { + operator: "0x7", + expectedMembers: []group.MemberIndex{}, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + members := wallet.membersByOperator(test.operator) + + if !reflect.DeepEqual(test.expectedMembers, members) { + t.Errorf( + "unexpected members\nexpected: %+v\nactual: %+v\n", + test.expectedMembers, + members, + ) + } + }) + } +} + type mockWalletAction struct { executeFn func() error actionWallet wallet From b4582db447a79af70d7ef36571b7e122a5cb59bf Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 29 Nov 2023 10:53:50 +0100 Subject: [PATCH 08/14] Fix flaky `TestNode_RunCoordinationLayer` test --- pkg/tbtc/node_test.go | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index 89649c286c..45797efc0b 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -324,13 +324,13 @@ func TestNode_RunCoordinationLayer(t *testing.T) { return nil, false } - // Simply add processed results to the list. - var processedResults []*coordinationResult + // Simply pass processed results to the channel. + processedResultsChan := make(chan *coordinationResult, 5) processCoordinationResultFn := func( _ *node, result *coordinationResult, ) { - processedResults = append(processedResults, result) + processedResultsChan <- result } ctx, cancelCtx := context.WithCancel(context.Background()) @@ -347,21 +347,30 @@ func TestNode_RunCoordinationLayer(t *testing.T) { t.Fatal(err) } - // Wait until the second-last coordination window passes. - err = localChain.blockCounter.WaitForBlockHeight(4000) + // Set up a stop signal that will be triggered after the last coordination + // window passes. + waiter, err := localChain.blockCounter.BlockHeightWaiter(5000) if err != nil { t.Fatal(err) } - // Stop coordination layer. As we are between the second-last and the last - // coordination window, the last window should not be processed. This - // allows us to test that the coordination layer's shutdown works as expected. - cancelCtx() - - // Wait until the last coordination window passes. - err = localChain.blockCounter.WaitForBlockHeight(5000) - if err != nil { - t.Fatal(err) + var processedResults []*coordinationResult +loop: + for { + select { + case result := <-processedResultsChan: + processedResults = append(processedResults, result) + + // Once the second-last coordination window is processed, stop the + // coordination layer. In that case, the last window should not be + // processed. This allows us to test that the coordination layer's + // shutdown works as expected. + if len(processedResults) == 3 { + cancelCtx() + } + case <-waiter: + break loop + } } testutils.AssertIntsEqual( From 9745ce2deb0abd9db9d1c57c9908dd3c1af025ef Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 29 Nov 2023 11:54:27 +0100 Subject: [PATCH 09/14] Unit test for follower's routine --- pkg/tbtc/coordination_test.go | 255 ++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index e845b51730..9c4a8ab773 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -5,10 +5,14 @@ import ( "crypto/sha256" "encoding/hex" "github.com/go-test/deep" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/chain/local_v1" "github.com/keep-network/keep-core/pkg/net" netlocal "github.com/keep-network/keep-core/pkg/net/local" + "github.com/keep-network/keep-core/pkg/operator" "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tecdsa" "math/big" "reflect" "testing" @@ -510,3 +514,254 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { ) } } + +func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { + // Uncompressed public key corresponding to the 20-byte public key hash: + // aa768412ceed10bd423c025542ca90071f9fb62d. + publicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + parseScript := func(script string) bitcoin.Script { + parsed, err := hex.DecodeString(script) + if err != nil { + t.Fatal(err) + } + + return parsed + } + + generateOperator := func() struct{ + address chain.Address + channel net.BroadcastChannel + } { + operatorPrivateKey, operatorPublicKey, err := operator.GenerateKeyPair( + local_v1.DefaultCurve, + ) + if err != nil { + t.Fatal(err) + } + + operatorAddress, err := ConnectWithKey(operatorPrivateKey). + Signing(). + PublicKeyToAddress(operatorPublicKey) + if err != nil { + t.Fatal(err) + } + + provider := netlocal.ConnectWithKey(operatorPublicKey) + broadcastChannel, err := provider.BroadcastChannelFor("test") + if err != nil { + t.Fatal(err) + } + + broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &coordinationMessage{} + }) + // Register an unmarshaler for the signingDoneMessage that will + // be uses to test the case with the wrong message type. + broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &signingDoneMessage{} + }) + + return struct{ + address chain.Address + channel net.BroadcastChannel + }{ + address: operatorAddress, + channel: broadcastChannel, + } + } + + leader:= generateOperator() + follower1 := generateOperator() + follower2 := generateOperator() + + coordinatedWallet := wallet{ + // Set only relevant fields. + publicKey: unmarshalPublicKey(publicKeyHex), + signingGroupOperators: []chain.Address{ + follower1.address, + follower2.address, + leader.address, + leader.address, + follower2.address, + follower1.address, + follower1.address, + follower2.address, + leader.address, + leader.address, + }, + } + + leaderID := coordinatedWallet.membersByOperator(leader.address)[0] + + membershipValidator := group.NewMembershipValidator( + &testutils.MockLogger{}, + coordinatedWallet.signingGroupOperators, + Connect().Signing(), + ) + + // Set up the executor for follower 1. + executor := &coordinationExecutor{ + // Set only relevant fields. + coordinatedWallet: coordinatedWallet, + membersIndexes: coordinatedWallet.membersByOperator(follower1.address), + operatorAddress: follower1.address, + broadcastChannel: follower1.channel, + membershipValidator: membershipValidator, + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 10 * time.Second) + defer cancelCtx() + + go func() { + // Give the follower routine some time to start and set up the + // broadcast channel handler. + time.Sleep(1 * time.Second) + + // Send message of wrong type. + err := leader.channel.Send(ctx, &signingDoneMessage{ + senderID: leaderID, + message: big.NewInt(100), + attemptNumber: 2, + signature: &tecdsa.Signature{ + R: big.NewInt(200), + S: big.NewInt(300), + RecoveryID: 3, + }, + endBlock: 4500, + }) + if err != nil { + t.Error(err) + return + } + + // Send message from self. + err = follower1.channel.Send(ctx, &coordinationMessage{ + senderID: coordinatedWallet.membersByOperator(follower1.address)[0], + coordinationBlock: 900, + walletPublicKeyHash: executor.walletPublicKeyHash(), + proposal: &noopProposal{}, + }) + if err != nil { + t.Error(err) + return + } + + // Send message with invalid membership. + err = leader.channel.Send(ctx, &coordinationMessage{ + // Leader operator uses senderID controlled by follower 2. + senderID: coordinatedWallet.membersByOperator(follower2.address)[0], + coordinationBlock: 900, + walletPublicKeyHash: executor.walletPublicKeyHash(), + proposal: &noopProposal{}, + }) + if err != nil { + t.Error(err) + return + } + + // Send message with wrong coordination block. + err = leader.channel.Send(ctx, &coordinationMessage{ + // Proper block is 900. + senderID: leaderID, + coordinationBlock: 901, + walletPublicKeyHash: executor.walletPublicKeyHash(), + proposal: &noopProposal{}, + }) + if err != nil { + t.Error(err) + return + } + + // Send message with wrong wallet. + err = leader.channel.Send(ctx, &coordinationMessage{ + senderID: leaderID, + coordinationBlock: 900, + walletPublicKeyHash: [20]byte{0x01}, + proposal: &noopProposal{}, + }) + if err != nil { + t.Error(err) + return + } + + // Send message that impersonates the leader. + err = follower2.channel.Send(ctx, &coordinationMessage{ + senderID: coordinatedWallet.membersByOperator(follower2.address)[0], + coordinationBlock: 900, + walletPublicKeyHash: executor.walletPublicKeyHash(), + proposal: &noopProposal{}, + }) + if err != nil { + t.Error(err) + return + } + + // Send message with not allowed action proposal. + err = leader.channel.Send(ctx, &coordinationMessage{ + // Heartbeat proposal is not allowed for this window. + senderID: leaderID, + coordinationBlock: 900, + walletPublicKeyHash: executor.walletPublicKeyHash(), + proposal: &HeartbeatProposal{ + Message: []byte("heartbeat message"), + }, + }) + if err != nil { + t.Error(err) + return + } + + // Send a proper message. + err = leader.channel.Send(ctx, &coordinationMessage{ + senderID: leaderID, + coordinationBlock: 900, + walletPublicKeyHash: executor.walletPublicKeyHash(), + proposal: &RedemptionProposal{ + RedeemersOutputScripts: []bitcoin.Script{ + parseScript("00148db50eb52063ea9d98b3eac91489a90f738986f6"), + parseScript("76a9148db50eb52063ea9d98b3eac91489a90f738986f688ac"), + }, + RedemptionTxFee: big.NewInt(10000), + }, + }) + if err != nil { + t.Error(err) + return + } + }() + + proposal, err := executor.followerRoutine( + ctx, + leader.address, + 900, + []WalletActionType{ActionRedemption, ActionNoop}, + ) + if err != nil { + t.Fatal(err) + } + + expectedProposal := &RedemptionProposal{ + RedeemersOutputScripts: []bitcoin.Script{ + parseScript("00148db50eb52063ea9d98b3eac91489a90f738986f6"), + parseScript("76a9148db50eb52063ea9d98b3eac91489a90f738986f688ac"), + }, + RedemptionTxFee: big.NewInt(10000), + } + + if !reflect.DeepEqual(expectedProposal, proposal) { + t.Errorf( + "unexpected proposal: \n"+ + "expected: %v\n"+ + "actual: %v", + expectedProposal, + proposal, + ) + } +} From d4019bfebdef6cc24d889cece2fbeb956fcabce0 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 29 Nov 2023 12:15:52 +0100 Subject: [PATCH 10/14] Re-format code and TODOs adjustments. --- pkg/tbtc/coordination.go | 4 +-- pkg/tbtc/coordination_test.go | 50 +++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 4833013b47..1a16f6eb3f 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -487,7 +487,7 @@ func (ce *coordinationExecutor) actionsChecklist( // frequency is every 16 coordination windows. frequencyWindows := uint64(16) - // TODO: Increase frequency for the active wallet. + // TODO: Consider increasing frequency for the active wallet in the future. if windowIndex%frequencyWindows == 0 { actions = append(actions, ActionDepositSweep) } @@ -496,7 +496,7 @@ func (ce *coordinationExecutor) actionsChecklist( actions = append(actions, ActionMovedFundsSweep) } - // TODO: Increase frequency for old wallets. + // TODO: Consider increasing frequency for old wallets in the future. if windowIndex%frequencyWindows == 0 { actions = append(actions, ActionMovingFunds) } diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index 9c4a8ab773..3e915c975a 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -535,7 +535,7 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { return parsed } - generateOperator := func() struct{ + generateOperator := func() struct { address chain.Address channel net.BroadcastChannel } { @@ -568,7 +568,7 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { return &signingDoneMessage{} }) - return struct{ + return struct { address chain.Address channel net.BroadcastChannel }{ @@ -577,7 +577,7 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { } } - leader:= generateOperator() + leader := generateOperator() follower1 := generateOperator() follower2 := generateOperator() @@ -616,7 +616,7 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { membershipValidator: membershipValidator, } - ctx, cancelCtx := context.WithTimeout(context.Background(), 10 * time.Second) + ctx, cancelCtx := context.WithTimeout(context.Background(), 10*time.Second) defer cancelCtx() go func() { @@ -626,8 +626,8 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { // Send message of wrong type. err := leader.channel.Send(ctx, &signingDoneMessage{ - senderID: leaderID, - message: big.NewInt(100), + senderID: leaderID, + message: big.NewInt(100), attemptNumber: 2, signature: &tecdsa.Signature{ R: big.NewInt(200), @@ -643,10 +643,10 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { // Send message from self. err = follower1.channel.Send(ctx, &coordinationMessage{ - senderID: coordinatedWallet.membersByOperator(follower1.address)[0], - coordinationBlock: 900, + senderID: coordinatedWallet.membersByOperator(follower1.address)[0], + coordinationBlock: 900, walletPublicKeyHash: executor.walletPublicKeyHash(), - proposal: &noopProposal{}, + proposal: &noopProposal{}, }) if err != nil { t.Error(err) @@ -656,10 +656,10 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { // Send message with invalid membership. err = leader.channel.Send(ctx, &coordinationMessage{ // Leader operator uses senderID controlled by follower 2. - senderID: coordinatedWallet.membersByOperator(follower2.address)[0], - coordinationBlock: 900, + senderID: coordinatedWallet.membersByOperator(follower2.address)[0], + coordinationBlock: 900, walletPublicKeyHash: executor.walletPublicKeyHash(), - proposal: &noopProposal{}, + proposal: &noopProposal{}, }) if err != nil { t.Error(err) @@ -669,10 +669,10 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { // Send message with wrong coordination block. err = leader.channel.Send(ctx, &coordinationMessage{ // Proper block is 900. - senderID: leaderID, - coordinationBlock: 901, + senderID: leaderID, + coordinationBlock: 901, walletPublicKeyHash: executor.walletPublicKeyHash(), - proposal: &noopProposal{}, + proposal: &noopProposal{}, }) if err != nil { t.Error(err) @@ -681,10 +681,10 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { // Send message with wrong wallet. err = leader.channel.Send(ctx, &coordinationMessage{ - senderID: leaderID, - coordinationBlock: 900, + senderID: leaderID, + coordinationBlock: 900, walletPublicKeyHash: [20]byte{0x01}, - proposal: &noopProposal{}, + proposal: &noopProposal{}, }) if err != nil { t.Error(err) @@ -693,10 +693,10 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { // Send message that impersonates the leader. err = follower2.channel.Send(ctx, &coordinationMessage{ - senderID: coordinatedWallet.membersByOperator(follower2.address)[0], - coordinationBlock: 900, + senderID: coordinatedWallet.membersByOperator(follower2.address)[0], + coordinationBlock: 900, walletPublicKeyHash: executor.walletPublicKeyHash(), - proposal: &noopProposal{}, + proposal: &noopProposal{}, }) if err != nil { t.Error(err) @@ -706,8 +706,8 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { // Send message with not allowed action proposal. err = leader.channel.Send(ctx, &coordinationMessage{ // Heartbeat proposal is not allowed for this window. - senderID: leaderID, - coordinationBlock: 900, + senderID: leaderID, + coordinationBlock: 900, walletPublicKeyHash: executor.walletPublicKeyHash(), proposal: &HeartbeatProposal{ Message: []byte("heartbeat message"), @@ -720,8 +720,8 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { // Send a proper message. err = leader.channel.Send(ctx, &coordinationMessage{ - senderID: leaderID, - coordinationBlock: 900, + senderID: leaderID, + coordinationBlock: 900, walletPublicKeyHash: executor.walletPublicKeyHash(), proposal: &RedemptionProposal{ RedeemersOutputScripts: []bitcoin.Script{ From 714a27967393f3e9d5d9cb31e42b000317b4d89f Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 29 Nov 2023 12:42:59 +0100 Subject: [PATCH 11/14] Record coordination faults --- pkg/tbtc/coordination.go | 38 +++++++--- pkg/tbtc/coordination_test.go | 129 +++++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 10 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 1a16f6eb3f..3e53adc571 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -360,6 +360,8 @@ func (ce *coordinationExecutor) coordinate( defer cancelCtx() var proposal coordinationProposal + var faults []*coordinationFault + if leader == ce.operatorAddress { proposal, err = ce.leaderRoutine( ctx, @@ -373,7 +375,7 @@ func (ce *coordinationExecutor) coordinate( ) } } else { - proposal, err = ce.followerRoutine( + proposal, faults, err = ce.followerRoutine( ctx, leader, window.coordinationBlock, @@ -397,7 +399,7 @@ func (ce *coordinationExecutor) coordinate( window: window, leader: leader, proposal: proposal, - faults: nil, // TODO: Fill coordination faults. + faults: faults, } return result, nil @@ -561,7 +563,7 @@ func (ce *coordinationExecutor) followerRoutine( leader chain.Address, coordinationBlock uint64, actionsAllowed []WalletActionType, -) (coordinationProposal, error) { +) (coordinationProposal, []*coordinationFault, error) { // Cache wallet public key hash to not compute it on every message. walletPublicKeyHash := ce.walletPublicKeyHash() // Leader ID is the index of the first (index-wise) member controlled by @@ -572,6 +574,8 @@ func (ce *coordinationExecutor) followerRoutine( // is one of the operators backing the wallet. leaderID := ce.coordinatedWallet.membersByOperator(leader)[0] + faults := make([]*coordinationFault, 0) + messagesChan := make(chan net.Message, coordinationMessageReceiveBuffer) ce.broadcastChannel.Recv(ctx, func(message net.Message) { @@ -613,24 +617,42 @@ loop: // Filter out messages from leader's impersonators. if leaderID != message.senderID { - // TODO: Record coordination fault of type FaultLeaderImpersonation. + sender := ce.chain.Signing().PublicKeyBytesToAddress( + netMessage.SenderPublicKey(), + ) + faults = append( + faults, &coordinationFault{ + culprit: sender, + faultType: FaultLeaderImpersonation, + }, + ) continue } // Filter out messages that propose an action that is not allowed // for the given coordination window. if !slices.Contains(actionsAllowed, message.proposal.actionType()) { - // TODO: Record coordination fault of type FaultLeaderMistake. + faults = append( + faults, &coordinationFault{ + culprit: leader, + faultType: FaultLeaderMistake, + }, + ) continue } - return message.proposal, nil + return message.proposal, faults, nil case <-ctx.Done(): break loop } } - // TODO: Record coordination fault of type FaultLeaderIdleness. + faults = append( + faults, &coordinationFault{ + culprit: leader, + faultType: FaultLeaderIdleness, + }, + ) - return nil, fmt.Errorf("coordination message not received on time") + return nil, faults, fmt.Errorf("coordination message not received on time") } diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index 3e915c975a..e2593c371d 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "fmt" "github.com/go-test/deep" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" @@ -600,15 +601,18 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { leaderID := coordinatedWallet.membersByOperator(leader.address)[0] + localChain := Connect() + membershipValidator := group.NewMembershipValidator( &testutils.MockLogger{}, coordinatedWallet.signingGroupOperators, - Connect().Signing(), + localChain.Signing(), ) // Set up the executor for follower 1. executor := &coordinationExecutor{ // Set only relevant fields. + chain: localChain, coordinatedWallet: coordinatedWallet, membersIndexes: coordinatedWallet.membersByOperator(follower1.address), operatorAddress: follower1.address, @@ -737,7 +741,7 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { } }() - proposal, err := executor.followerRoutine( + proposal, faults, err := executor.followerRoutine( ctx, leader.address, 900, @@ -764,4 +768,125 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { proposal, ) } + + expectedFaults := []*coordinationFault{ + { + culprit: follower2.address, + faultType: FaultLeaderImpersonation, + }, + { + culprit: leader.address, + faultType: FaultLeaderMistake, + }, + } + if !reflect.DeepEqual(expectedFaults, faults) { + t.Errorf( + "unexpected faults: \n"+ + "expected: %v\n"+ + "actual: %v", + expectedProposal, + proposal, + ) + } +} + +func TestCoordinationExecutor_FollowerRoutine_WithIdleLeader(t *testing.T) { + // Uncompressed public key corresponding to the 20-byte public key hash: + // aa768412ceed10bd423c025542ca90071f9fb62d. + publicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + generateOperator := func() chain.Address { + operatorPrivateKey, operatorPublicKey, err := operator.GenerateKeyPair( + local_v1.DefaultCurve, + ) + if err != nil { + t.Fatal(err) + } + + operatorAddress, err := ConnectWithKey(operatorPrivateKey). + Signing(). + PublicKeyToAddress(operatorPublicKey) + if err != nil { + t.Fatal(err) + } + + return operatorAddress + } + + leader := generateOperator() + follower1 := generateOperator() + follower2 := generateOperator() + + coordinatedWallet := wallet{ + // Set only relevant fields. + publicKey: unmarshalPublicKey(publicKeyHex), + signingGroupOperators: []chain.Address{ + follower1, + follower2, + leader, + leader, + follower2, + follower1, + follower1, + follower2, + leader, + leader, + }, + } + + provider := netlocal.Connect() + + broadcastChannel, err := provider.BroadcastChannelFor("test") + if err != nil { + t.Fatal(err) + } + + executor := &coordinationExecutor{ + // Set only relevant fields. + coordinatedWallet: coordinatedWallet, + broadcastChannel: broadcastChannel, + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second) + defer cancelCtx() + + _, faults, err := executor.followerRoutine( + ctx, + leader, + 900, + []WalletActionType{ActionRedemption, ActionNoop}, + ) + + expectedErr := fmt.Errorf("coordination message not received on time") + if !reflect.DeepEqual(expectedErr, err) { + t.Errorf( + "unexpected error: \n"+ + "expected: %v\n"+ + "actual: %v", + expectedErr, + err, + ) + } + + expectedFaults := []*coordinationFault{ + { + culprit: leader, + faultType: FaultLeaderIdleness, + }, + } + if !reflect.DeepEqual(expectedFaults, faults) { + t.Errorf( + "unexpected faults: \n"+ + "expected: %v\n"+ + "actual: %v", + expectedFaults, + faults, + ) + } } From d68487b56187127a41c6613762cbded7ef9273ed Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 29 Nov 2023 14:27:15 +0100 Subject: [PATCH 12/14] Cover `coordinationExecutor.coordinate` with unit tests --- pkg/tbtc/coordination.go | 4 +- pkg/tbtc/coordination_test.go | 291 ++++++++++++++++++++++++++++++++-- 2 files changed, 283 insertions(+), 12 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 3e53adc571..c4018398d4 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -321,7 +321,7 @@ func (ce *coordinationExecutor) walletPublicKeyHash() [20]byte { // coordinate executes the coordination procedure for the given coordination // window. // -// TODO: Add logging and cover with unit tests. +// TODO: Add logging. func (ce *coordinationExecutor) coordinate( window *coordinationWindow, ) (*coordinationResult, error) { @@ -574,7 +574,7 @@ func (ce *coordinationExecutor) followerRoutine( // is one of the operators backing the wallet. leaderID := ce.coordinatedWallet.membersByOperator(leader)[0] - faults := make([]*coordinationFault, 0) + var faults []*coordinationFault messagesChan := make(chan net.Message, coordinationMessageReceiveBuffer) diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index e2593c371d..19b556a10f 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -2,6 +2,7 @@ package tbtc import ( "context" + "crypto/ecdsa" "crypto/sha256" "encoding/hex" "fmt" @@ -9,12 +10,15 @@ import ( "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/chain/local_v1" + "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" netlocal "github.com/keep-network/keep-core/pkg/net/local" "github.com/keep-network/keep-core/pkg/operator" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" + "golang.org/x/exp/slices" "math/big" + "math/rand" "reflect" "testing" "time" @@ -172,6 +176,277 @@ func TestWatchCoordinationWindows(t *testing.T) { ) } +func TestCoordinationExecutor_Coordinate(t *testing.T) { + // Uncompressed public key corresponding to the 20-byte public key hash: + // aa768412ceed10bd423c025542ca90071f9fb62d. + publicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + // 20-byte public key hash corresponding to the public key above. + buffer, err := hex.DecodeString("aa768412ceed10bd423c025542ca90071f9fb62d") + if err != nil { + t.Fatal(err) + } + var publicKeyHash [20]byte + copy(publicKeyHash[:], buffer) + + parseScript := func(script string) bitcoin.Script { + parsed, err := hex.DecodeString(script) + if err != nil { + t.Fatal(err) + } + + return parsed + } + + coordinationBlock := uint64(900) + + type operatorFixture struct { + chain Chain + address chain.Address + channel net.BroadcastChannel + waitForBlockHeight func(ctx context.Context, blockHeight uint64) error + } + + generateOperator := func(seed int64) *operatorFixture { + // #nosec G404 (insecure random number source (rand)) + rng := rand.New(rand.NewSource(seed)) + // Generate operators with deterministic addresses that don't change + // between test runs. This is required to assert the leader selection. + generated, err := ecdsa.GenerateKey( + local_v1.DefaultCurve, + rng, + ) + if err != nil { + t.Fatal(err) + } + + localChain := ConnectWithKey( + &operator.PrivateKey{ + PublicKey: operator.PublicKey{ + Curve: operator.Secp256k1, + X: generated.X, + Y: generated.Y, + }, + D: generated.D, + }, + 100*time.Millisecond, + ) + + localChain.setBlockHashByNumber( + coordinationBlock-32, + "1422996cbcbc38fc924a46f4df5f9064279d3ab43396e58386dac9b87440d64f", + ) + + operatorAddress, err := localChain.operatorAddress() + if err != nil { + t.Fatal(err) + } + + _, operatorPublicKey, err := localChain.OperatorKeyPair() + if err != nil { + t.Fatal(err) + } + + broadcastChannel, err := netlocal.ConnectWithKey(operatorPublicKey). + BroadcastChannelFor("test") + if err != nil { + t.Fatal(err) + } + + broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &coordinationMessage{} + }) + + waitForBlockHeight := func(ctx context.Context, blockHeight uint64) error { + blockCounter, err := localChain.BlockCounter() + if err != nil { + return err + } + + wait, err := blockCounter.BlockHeightWaiter(blockHeight) + if err != nil { + return err + } + + select { + case <-wait: + case <-ctx.Done(): + } + + return nil + } + + return &operatorFixture{ + chain: localChain, + address: operatorAddress, + channel: broadcastChannel, + waitForBlockHeight: waitForBlockHeight, + } + } + + operator1 := generateOperator(1) + operator2 := generateOperator(2) + operator3 := generateOperator(3) + + coordinatedWallet := wallet{ + publicKey: unmarshalPublicKey(publicKeyHex), + signingGroupOperators: []chain.Address{ + operator2.address, + operator3.address, + operator1.address, + operator1.address, + operator3.address, + operator2.address, + operator2.address, + operator3.address, + operator1.address, + operator1.address, + }, + } + + proposalGenerator := func( + walletPublicKeyHash [20]byte, + actionsChecklist []WalletActionType, + ) (coordinationProposal, error) { + for _, action := range actionsChecklist { + if walletPublicKeyHash == publicKeyHash && action == ActionRedemption { + return &RedemptionProposal{ + RedeemersOutputScripts: []bitcoin.Script{ + parseScript("00148db50eb52063ea9d98b3eac91489a90f738986f6"), + parseScript("76a9148db50eb52063ea9d98b3eac91489a90f738986f688ac"), + }, + RedemptionTxFee: big.NewInt(10000), + }, nil + } + } + + return &noopProposal{}, nil + } + + membershipValidator := group.NewMembershipValidator( + &testutils.MockLogger{}, + coordinatedWallet.signingGroupOperators, + Connect().Signing(), + ) + + protocolLatch := generator.NewProtocolLatch() + + generateExecutor := func(operator *operatorFixture) *coordinationExecutor { + return newCoordinationExecutor( + operator.chain, + coordinatedWallet, + coordinatedWallet.membersByOperator(operator.address), + operator.address, + proposalGenerator, + operator.channel, + membershipValidator, + protocolLatch, + operator.waitForBlockHeight, + ) + } + + window := newCoordinationWindow(coordinationBlock) + + type report struct { + operatorIndex int + result *coordinationResult + err error + } + + reportChan := make(chan *report, 3) + + for i, currentOperator := range []*operatorFixture{ + operator1, + operator2, + operator3, + } { + go func(operatorIndex int, operator *operatorFixture) { + result, err := generateExecutor(operator).coordinate(window) + + reportChan <- &report{ + operatorIndex: operatorIndex, + result: result, + err: err, + } + }(i+1, currentOperator) + } + + reports := make([]*report, 0) +loop: + //lint:ignore S1000 for-select is used as the channel is not closed by senders. + for { + select { + case r := <-reportChan: + reports = append(reports, r) + + if len(reports) == 3 { + break loop + } + } + } + + slices.SortFunc(reports, func(i, j *report) bool { + return i.operatorIndex < j.operatorIndex + }) + + testutils.AssertIntsEqual(t, "reports count", 3, len(reports)) + + expectedResult := &coordinationResult{ + wallet: coordinatedWallet, + window: window, + leader: operator3.address, + proposal: &RedemptionProposal{ + RedeemersOutputScripts: []bitcoin.Script{ + parseScript("00148db50eb52063ea9d98b3eac91489a90f738986f6"), + parseScript("76a9148db50eb52063ea9d98b3eac91489a90f738986f688ac"), + }, + RedemptionTxFee: big.NewInt(10000), + }, + faults: nil, + } + + expectedReports := []*report{ + { + operatorIndex: 1, + result: expectedResult, + err: nil, + }, + { + operatorIndex: 2, + result: expectedResult, + err: nil, + }, + { + operatorIndex: 3, + result: expectedResult, + err: nil, + }, + } + if !reflect.DeepEqual(expectedReports, reports) { + t.Errorf( + "unexpected reports:\n"+ + "expected: %v\n"+ + "actual: %v", + expectedReports, + reports, + ) + + } + + testutils.AssertBoolsEqual( + t, + "protocol latch state", + false, + protocolLatch.IsExecuting(), + ) +} + func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { coordinationBlock := uint64(900) @@ -540,22 +815,20 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { address chain.Address channel net.BroadcastChannel } { - operatorPrivateKey, operatorPublicKey, err := operator.GenerateKeyPair( - local_v1.DefaultCurve, - ) + localChain := Connect() + + operatorAddress, err := localChain.operatorAddress() if err != nil { t.Fatal(err) } - operatorAddress, err := ConnectWithKey(operatorPrivateKey). - Signing(). - PublicKeyToAddress(operatorPublicKey) + _, operatorPublicKey, err := localChain.OperatorKeyPair() if err != nil { t.Fatal(err) } - provider := netlocal.ConnectWithKey(operatorPublicKey) - broadcastChannel, err := provider.BroadcastChannelFor("test") + broadcastChannel, err := netlocal.ConnectWithKey(operatorPublicKey). + BroadcastChannelFor("test") if err != nil { t.Fatal(err) } @@ -583,7 +856,6 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { follower2 := generateOperator() coordinatedWallet := wallet{ - // Set only relevant fields. publicKey: unmarshalPublicKey(publicKeyHex), signingGroupOperators: []chain.Address{ follower1.address, @@ -824,7 +1096,6 @@ func TestCoordinationExecutor_FollowerRoutine_WithIdleLeader(t *testing.T) { follower2 := generateOperator() coordinatedWallet := wallet{ - // Set only relevant fields. publicKey: unmarshalPublicKey(publicKeyHex), signingGroupOperators: []chain.Address{ follower1, From 4d3b0278c5772140c0e7268653f0d7d0b0cd2558 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 29 Nov 2023 14:42:13 +0100 Subject: [PATCH 13/14] Add logging to `coordinationExecutor.coordinate` function --- pkg/tbtc/coordination.go | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index c4018398d4..4026be1715 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "fmt" "github.com/keep-network/keep-core/pkg/internal/pb" + "go.uber.org/zap" "golang.org/x/exp/slices" "math/rand" "sort" @@ -320,8 +321,6 @@ func (ce *coordinationExecutor) walletPublicKeyHash() [20]byte { // coordinate executes the coordination procedure for the given coordination // window. -// -// TODO: Add logging. func (ce *coordinationExecutor) coordinate( window *coordinationWindow, ) (*coordinationResult, error) { @@ -341,15 +340,33 @@ func (ce *coordinationExecutor) coordinate( ) } + walletPublicKeyBytes, err := marshalPublicKey(ce.coordinatedWallet.publicKey) + if err != nil { + return nil, fmt.Errorf("cannot marshal wallet public key: [%v]", err) + } + + execLogger := logger.With( + zap.Uint64("coordinationBlock", window.coordinationBlock), + zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), + ) + + execLogger.Info("starting coordination") + seed, err := ce.coordinationSeed(window.coordinationBlock) if err != nil { return nil, fmt.Errorf("failed to compute coordination seed: [%v]", err) } + execLogger.Info("coordination seed is: [0x%x]", seed) + leader := ce.coordinationLeader(seed) + execLogger.Info("coordination leader is: [%s]", leader) + actionsChecklist := ce.actionsChecklist(window.index(), seed) + execLogger.Info("actions checklist is: [%v]", actionsChecklist) + // Set up a context that is cancelled when the active phase of the // coordination window ends. ctx, cancelCtx := withCancelOnBlock( @@ -363,6 +380,8 @@ func (ce *coordinationExecutor) coordinate( var faults []*coordinationFault if leader == ce.operatorAddress { + execLogger.Info("executing leader's routine") + proposal, err = ce.leaderRoutine( ctx, window.coordinationBlock, @@ -374,7 +393,11 @@ func (ce *coordinationExecutor) coordinate( err, ) } + + execLogger.Info("broadcasted proposal: [%s]", proposal.actionType()) } else { + execLogger.Info("executing follower's routine") + proposal, faults, err = ce.followerRoutine( ctx, leader, @@ -387,6 +410,12 @@ func (ce *coordinationExecutor) coordinate( err, ) } + + execLogger.Info( + "received proposal: [%s]; observed faults: [%v]", + proposal.actionType(), + faults, + ) } // Just in case, if the proposal is nil, set it to noop. @@ -402,6 +431,8 @@ func (ce *coordinationExecutor) coordinate( faults: faults, } + execLogger.Info("coordination completed with result: [%s]", result) + return result, nil } From 9d65110e7b0d8acc893ff7e4b4953020432df88d Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 29 Nov 2023 14:51:11 +0100 Subject: [PATCH 14/14] Improve functions naming --- pkg/tbtc/coordination.go | 33 ++++++++++++++++----------------- pkg/tbtc/coordination_test.go | 24 ++++++++++++------------ 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/pkg/tbtc/coordination.go b/pkg/tbtc/coordination.go index 4026be1715..b7d1259e7e 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -352,18 +352,18 @@ func (ce *coordinationExecutor) coordinate( execLogger.Info("starting coordination") - seed, err := ce.coordinationSeed(window.coordinationBlock) + seed, err := ce.getSeed(window.coordinationBlock) if err != nil { return nil, fmt.Errorf("failed to compute coordination seed: [%v]", err) } execLogger.Info("coordination seed is: [0x%x]", seed) - leader := ce.coordinationLeader(seed) + leader := ce.getLeader(seed) execLogger.Info("coordination leader is: [%s]", leader) - actionsChecklist := ce.actionsChecklist(window.index(), seed) + actionsChecklist := ce.getActionsChecklist(window.index(), seed) execLogger.Info("actions checklist is: [%v]", actionsChecklist) @@ -382,7 +382,7 @@ func (ce *coordinationExecutor) coordinate( if leader == ce.operatorAddress { execLogger.Info("executing leader's routine") - proposal, err = ce.leaderRoutine( + proposal, err = ce.executeLeaderRoutine( ctx, window.coordinationBlock, actionsChecklist, @@ -398,7 +398,7 @@ func (ce *coordinationExecutor) coordinate( } else { execLogger.Info("executing follower's routine") - proposal, faults, err = ce.followerRoutine( + proposal, faults, err = ce.executeFollowerRoutine( ctx, leader, window.coordinationBlock, @@ -436,9 +436,8 @@ func (ce *coordinationExecutor) coordinate( return result, nil } -// coordinationSeed computes the coordination seed for the given coordination -// window. -func (ce *coordinationExecutor) coordinationSeed( +// getSeed computes the coordination seed for the given coordination window. +func (ce *coordinationExecutor) getSeed( coordinationBlock uint64, ) ([32]byte, error) { walletPublicKeyHash := ce.walletPublicKeyHash() @@ -460,9 +459,9 @@ func (ce *coordinationExecutor) coordinationSeed( ), nil } -// coordinationLeader returns the address of the coordination leader for the -// given coordination seed. -func (ce *coordinationExecutor) coordinationLeader(seed [32]byte) chain.Address { +// getLeader returns the address of the coordination leader for the given +// coordination seed. +func (ce *coordinationExecutor) getLeader(seed [32]byte) chain.Address { // First, take all operators backing the wallet. allOperators := chain.Addresses(ce.coordinatedWallet.signingGroupOperators) @@ -498,10 +497,10 @@ func (ce *coordinationExecutor) coordinationLeader(seed [32]byte) chain.Address return uniqueOperators[0] } -// actionsChecklist returns a list of wallet actions that should be checked +// getActionsChecklist returns a list of wallet actions that should be checked // for the given coordination window. Returns nil for incorrect coordination // windows whose index is 0. -func (ce *coordinationExecutor) actionsChecklist( +func (ce *coordinationExecutor) getActionsChecklist( windowIndex uint64, seed [32]byte, ) []WalletActionType { @@ -545,10 +544,10 @@ func (ce *coordinationExecutor) actionsChecklist( return actions } -// leaderRoutine executes the leader's routine for the given coordination +// executeLeaderRoutine executes the leader's routine for the given coordination // window. The routine generates a proposal and broadcasts it to the followers. // It returns the generated proposal or an error if the routine failed. -func (ce *coordinationExecutor) leaderRoutine( +func (ce *coordinationExecutor) executeLeaderRoutine( ctx context.Context, coordinationBlock uint64, actionsChecklist []WalletActionType, @@ -585,11 +584,11 @@ func (ce *coordinationExecutor) leaderRoutine( return proposal, nil } -// followerRoutine executes the follower's routine for the given coordination +// executeFollowerRoutine executes the follower's routine for the given coordination // window. The routine listens for the coordination message from the leader and // validates it. If the leader's proposal is valid, it returns the received // proposal. Returns an error if the routine failed. -func (ce *coordinationExecutor) followerRoutine( +func (ce *coordinationExecutor) executeFollowerRoutine( ctx context.Context, leader chain.Address, coordinationBlock uint64, diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index 19b556a10f..69c137f0d9 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -447,7 +447,7 @@ loop: ) } -func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { +func TestCoordinationExecutor_GetSeed(t *testing.T) { coordinationBlock := uint64(900) localChain := Connect() @@ -478,7 +478,7 @@ func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { coordinatedWallet: coordinatedWallet, } - seed, err := executor.coordinationSeed(coordinationBlock) + seed, err := executor.getSeed(coordinationBlock) if err != nil { t.Fatal(err) } @@ -494,7 +494,7 @@ func TestCoordinationExecutor_CoordinationSeed(t *testing.T) { ) } -func TestCoordinationExecutor_CoordinationLeader(t *testing.T) { +func TestCoordinationExecutor_GetLeader(t *testing.T) { seedBytes, err := hex.DecodeString( "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", ) @@ -526,7 +526,7 @@ func TestCoordinationExecutor_CoordinationLeader(t *testing.T) { coordinatedWallet: coordinatedWallet, } - leader := executor.coordinationLeader(seed) + leader := executor.getLeader(seed) testutils.AssertStringsEqual( t, @@ -536,7 +536,7 @@ func TestCoordinationExecutor_CoordinationLeader(t *testing.T) { ) } -func TestCoordinationExecutor_ActionsChecklist(t *testing.T) { +func TestCoordinationExecutor_GetActionsChecklist(t *testing.T) { tests := map[string]struct { coordinationBlock uint64 expectedChecklist []WalletActionType @@ -643,7 +643,7 @@ func TestCoordinationExecutor_ActionsChecklist(t *testing.T) { big.NewInt(int64(window.coordinationBlock) + 1).Bytes(), ) - checklist := executor.actionsChecklist(window.index(), seed) + checklist := executor.getActionsChecklist(window.index(), seed) if diff := deep.Equal( checklist, @@ -661,7 +661,7 @@ func TestCoordinationExecutor_ActionsChecklist(t *testing.T) { } } -func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { +func TestCoordinationExecutor_ExecuteLeaderRoutine(t *testing.T) { // Uncompressed public key corresponding to the 20-byte public key hash: // aa768412ceed10bd423c025542ca90071f9fb62d. publicKeyHex, err := hex.DecodeString( @@ -752,7 +752,7 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { cancelCtx() }) - proposal, err := executor.leaderRoutine(ctx, 900, actionsChecklist) + proposal, err := executor.executeLeaderRoutine(ctx, 900, actionsChecklist) if err != nil { t.Fatal(err) } @@ -791,7 +791,7 @@ func TestCoordinationExecutor_LeaderRoutine(t *testing.T) { } } -func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { +func TestCoordinationExecutor_ExecuteFollowerRoutine(t *testing.T) { // Uncompressed public key corresponding to the 20-byte public key hash: // aa768412ceed10bd423c025542ca90071f9fb62d. publicKeyHex, err := hex.DecodeString( @@ -1013,7 +1013,7 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { } }() - proposal, faults, err := executor.followerRoutine( + proposal, faults, err := executor.executeFollowerRoutine( ctx, leader.address, 900, @@ -1062,7 +1062,7 @@ func TestCoordinationExecutor_FollowerRoutine(t *testing.T) { } } -func TestCoordinationExecutor_FollowerRoutine_WithIdleLeader(t *testing.T) { +func TestCoordinationExecutor_ExecuteFollowerRoutine_WithIdleLeader(t *testing.T) { // Uncompressed public key corresponding to the 20-byte public key hash: // aa768412ceed10bd423c025542ca90071f9fb62d. publicKeyHex, err := hex.DecodeString( @@ -1127,7 +1127,7 @@ func TestCoordinationExecutor_FollowerRoutine_WithIdleLeader(t *testing.T) { ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second) defer cancelCtx() - _, faults, err := executor.followerRoutine( + _, faults, err := executor.executeFollowerRoutine( ctx, leader, 900,