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..b7d1259e7e 100644 --- a/pkg/tbtc/coordination.go +++ b/pkg/tbtc/coordination.go @@ -2,7 +2,16 @@ package tbtc import ( "context" + "crypto/sha256" + "encoding/binary" "fmt" + "github.com/keep-network/keep-core/pkg/internal/pb" + "go.uber.org/zap" + "golang.org/x/exp/slices" + "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" @@ -29,6 +38,23 @@ 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 + // 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) + // 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 @@ -71,6 +97,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 @@ -87,11 +132,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. @@ -138,9 +183,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 } @@ -152,13 +195,28 @@ 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( + walletPublicKeyHash [20]byte, + actionsChecklist []WalletActionType, +) (coordinationProposal, error) + // 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 } @@ -195,39 +253,70 @@ 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 { + senderID group.MemberIndex + coordinationBlock uint64 + walletPublicKeyHash [20]byte + proposal coordinationProposal +} + +func (cm *coordinationMessage) Type() string { + return "tbtc/coordination_message" +} + // coordinationExecutor is responsible for executing the coordination // procedure for the given wallet. type coordinationExecutor struct { lock *semaphore.Weighted - signers []*signer // TODO: Do we need whole signers? + chain Chain + + coordinatedWallet wallet + 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 // given wallet. func newCoordinationExecutor( - signers []*signer, + chain Chain, + 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), - signers: signers, + chain: chain, + coordinatedWallet: coordinatedWallet, + membersIndexes: membersIndexes, + operatorAddress: operatorAddress, + proposalGenerator: proposalGenerator, broadcastChannel: broadcastChannel, membershipValidator: membershipValidator, protocolLatch: protocolLatch, + waitForBlockFn: waitForBlockFn, } } -// 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 @@ -240,18 +329,360 @@ 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() + + // Just in case, check if the window is valid. + if window.index() == 0 { + return nil, fmt.Errorf( + "invalid coordination block [%v]", + window.coordinationBlock, + ) + } + + 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.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.getLeader(seed) + + execLogger.Info("coordination leader is: [%s]", leader) + + actionsChecklist := ce.getActionsChecklist(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( + context.Background(), + window.activePhaseEndBlock(), + ce.waitForBlockFn, + ) + defer cancelCtx() + + var proposal coordinationProposal + var faults []*coordinationFault + + if leader == ce.operatorAddress { + execLogger.Info("executing leader's routine") + + proposal, err = ce.executeLeaderRoutine( + ctx, + window.coordinationBlock, + actionsChecklist, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to execute leader's routine: [%v]", + err, + ) + } + + execLogger.Info("broadcasted proposal: [%s]", proposal.actionType()) + } else { + execLogger.Info("executing follower's routine") + + proposal, faults, err = ce.executeFollowerRoutine( + ctx, + leader, + window.coordinationBlock, + append(actionsChecklist, ActionNoop), + ) + if err != nil { + return nil, fmt.Errorf( + "failed to execute follower's routine: [%v]", + err, + ) + } + + execLogger.Info( + "received proposal: [%s]; observed faults: [%v]", + proposal.actionType(), + faults, + ) + } + + // Just in case, if the proposal is nil, set it to noop. + if proposal == nil { + proposal = &noopProposal{} + } + result := &coordinationResult{ - wallet: ce.wallet(), + wallet: ce.coordinatedWallet, window: window, - leader: ce.wallet().signingGroupOperators[0], - proposal: &noopProposal{}, - faults: nil, + leader: leader, + proposal: proposal, + faults: faults, } + execLogger.Info("coordination completed with result: [%s]", result) + return result, nil } + +// getSeed computes the coordination seed for the given coordination window. +func (ce *coordinationExecutor) getSeed( + coordinationBlock uint64, +) ([32]byte, error) { + walletPublicKeyHash := ce.walletPublicKeyHash() + + safeBlockNumber := 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 +} + +// 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) + + // 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] +} + +// 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) getActionsChecklist( + 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 + // 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: Consider increasing frequency for the active wallet in the future. + if windowIndex%frequencyWindows == 0 { + actions = append(actions, ActionDepositSweep) + } + + if windowIndex%frequencyWindows == 0 { + actions = append(actions, ActionMovedFundsSweep) + } + + // TODO: Consider increasing frequency for old wallets in the future. + 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 +} + +// 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) executeLeaderRoutine( + ctx context.Context, + coordinationBlock uint64, + actionsChecklist []WalletActionType, +) (coordinationProposal, error) { + walletPublicKeyHash := ce.walletPublicKeyHash() + + proposal, err := ce.proposalGenerator(walletPublicKeyHash, actionsChecklist) + if err != nil { + 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{ + senderID: senderID, + coordinationBlock: coordinationBlock, + walletPublicKeyHash: walletPublicKeyHash, + proposal: proposal, + } + + 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 +} + +// 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) executeFollowerRoutine( + ctx context.Context, + leader chain.Address, + coordinationBlock uint64, + actionsAllowed []WalletActionType, +) (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 + // 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] + + var faults []*coordinationFault + + 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 { + 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()) { + faults = append( + faults, &coordinationFault{ + culprit: leader, + faultType: FaultLeaderMistake, + }, + ) + continue + } + + return message.proposal, faults, nil + case <-ctx.Done(): + break loop + } + } + + faults = append( + faults, &coordinationFault{ + culprit: leader, + faultType: FaultLeaderIdleness, + }, + ) + + 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 b62a3d7ffe..7ccd0481a9 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -2,6 +2,24 @@ package tbtc import ( "context" + "crypto/ecdsa" + "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" + "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" @@ -63,6 +81,47 @@ func TestCoordinationWindow_IsAfter(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) @@ -116,3 +175,989 @@ func TestWatchCoordinationWindows(t *testing.T) { int(receivedWindows[1].coordinationBlock), ) } + +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_GetSeed(t *testing.T) { + coordinationBlock := uint64(900) + + localChain := Connect() + + localChain.setBlockHashByNumber( + 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{ + // Set only relevant fields. + chain: localChain, + coordinatedWallet: coordinatedWallet, + } + + seed, err := executor.getSeed(coordinationBlock) + 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[:]), + ) +} + +func TestCoordinationExecutor_GetLeader(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.getLeader(seed) + + testutils.AssertStringsEqual( + t, + "coordination leader", + "D2662604f8b4540336fBd3c1F48d7e9cdFbD079c", + leader.String(), + ) +} + +func TestCoordinationExecutor_GetActionsChecklist(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.getActionsChecklist(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_ExecuteLeaderRoutine(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) + + 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( + walletPublicKeyHash [20]byte, + actionsChecklist []WalletActionType, + ) ( + coordinationProposal, + error, + ) { + for _, action := range actionsChecklist { + if walletPublicKeyHash == publicKeyHash && action == ActionHeartbeat { + return &HeartbeatProposal{ + Message: []byte("heartbeat message"), + }, nil + } + } + + 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, + } + + actionsChecklist := []WalletActionType{ + ActionRedemption, + ActionDepositSweep, + ActionMovedFundsSweep, + ActionMovingFunds, + ActionHeartbeat, + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelCtx() + + 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.executeLeaderRoutine(ctx, 900, actionsChecklist) + if err != nil { + t.Fatal(err) + } + + <-ctx.Done() + + expectedProposal := &HeartbeatProposal{ + Message: []byte("heartbeat message"), + } + + if !reflect.DeepEqual(expectedProposal, proposal) { + t.Errorf( + "unexpected proposal: \n"+ + "expected: %v\n"+ + "actual: %v", + expectedProposal, + proposal, + ) + } + + expectedMessage := &coordinationMessage{ + senderID: 5, + coordinationBlock: 900, + walletPublicKeyHash: publicKeyHash, + proposal: expectedProposal, + } + + if !reflect.DeepEqual(expectedMessage, message) { + t.Errorf( + "unexpected message: \n"+ + "expected: %v\n"+ + "actual: %v", + expectedMessage, + message, + ) + } +} + +func TestCoordinationExecutor_ExecuteFollowerRoutine(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 + } { + localChain := Connect() + + 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{} + }) + // 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{ + 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] + + localChain := Connect() + + membershipValidator := group.NewMembershipValidator( + &testutils.MockLogger{}, + coordinatedWallet.signingGroupOperators, + 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, + 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, faults, err := executor.executeFollowerRoutine( + 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, + ) + } + + 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_ExecuteFollowerRoutine_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{ + 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.executeFollowerRoutine( + 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, + ) + } +} 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 93eeb72dcb..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" @@ -125,6 +127,264 @@ func (sdm *signingDoneMessage) Unmarshal(bytes []byte) error { return nil } +// Marshal converts the coordinationMessage to a byte array. +func (cm *coordinationMessage) Marshal() ([]byte, error) { + 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 { + 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 +} + // marshalPublicKey converts an ECDSA public key to a byte // array (uncompressed). func marshalPublicKey(publicKey *ecdsa.PublicKey) ([]byte, error) { 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.go b/pkg/tbtc/node.go index 48303a62ca..2fe5847bd3 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, @@ -365,20 +367,45 @@ 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)) + for i, s := range signers { + membersIndexes[i] = s.signingGroupMemberIndex + } + + operatorAddress, err := n.operatorAddress() + if err != nil { + 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( - signers, + n.chain, + wallet, + membersIndexes, + operatorAddress, + proposalGenerator, broadcastChannel, membershipValidator, n.protocolLatch, + n.waitForBlockHeight, ) n.coordinationExecutors[executorKey] = executor + executorLogger.Infof( + "coordination executor created; controlling [%v] signers", + len(signers), + ) + return executor, true, nil } diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index 5fc3ca15d0..45797efc0b 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") } @@ -321,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()) @@ -344,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( @@ -399,6 +411,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..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" @@ -31,6 +32,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: @@ -366,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 ec46fb0a3e..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" @@ -19,6 +21,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() @@ -259,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