From 531d62a2b76f96ccb1aaccb152dada3850131090 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 8 Apr 2024 10:39:04 +0200 Subject: [PATCH 01/40] Prevented heartbeat execution if operator is unstaking --- pkg/chain/ethereum/beacon.go | 5 +++++ pkg/chain/ethereum/tbtc.go | 6 ++++++ pkg/chain/local_v1/local.go | 4 ++++ pkg/sortition/chain.go | 4 ++++ pkg/sortition/internal/local/chain.go | 5 +++++ pkg/tbtc/chain_test.go | 5 +++++ pkg/tbtc/heartbeat.go | 20 ++++++++++++++++---- 7 files changed, 45 insertions(+), 4 deletions(-) diff --git a/pkg/chain/ethereum/beacon.go b/pkg/chain/ethereum/beacon.go index 6f37143e62..953e34342e 100644 --- a/pkg/chain/ethereum/beacon.go +++ b/pkg/chain/ethereum/beacon.go @@ -384,6 +384,11 @@ func (bc *BeaconChain) IsRecognized(operatorPublicKey *operator.PublicKey) (bool return true, nil } +// TODO: Implement a real IsOperatorUnstaking function. +func (bc *BeaconChain) IsOperatorUnstaking() (bool, error) { + return false, errNotImplemented +} + // TODO: Implement a real SubmitRelayEntry function. func (bc *BeaconChain) SubmitRelayEntry( entry []byte, diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 31f99f2e9d..27fff01bad 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -2157,3 +2157,9 @@ func (tc *TbtcChain) GetRedemptionDelay( func (tc *TbtcChain) GetDepositMinAge() (uint32, error) { return tc.walletProposalValidator.DEPOSITMINAGE() } + +func (tc *TbtcChain) IsOperatorUnstaking() (bool, error) { + // TODO: Implement by checking if the operator has deauthorized their entire + // stake. + return false, nil +} diff --git a/pkg/chain/local_v1/local.go b/pkg/chain/local_v1/local.go index 6715634ab2..5eb5859d98 100644 --- a/pkg/chain/local_v1/local.go +++ b/pkg/chain/local_v1/local.go @@ -418,6 +418,10 @@ func (c *localChain) IsEligibleForRewards() (bool, error) { panic("unsupported") } +func (c *localChain) IsOperatorUnstaking() (bool, error) { + panic("unsupported") +} + func (c *localChain) CanRestoreRewardEligibility() (bool, error) { panic("unsupported") } diff --git a/pkg/sortition/chain.go b/pkg/sortition/chain.go index cf53a6ff92..cc7af0f2e8 100644 --- a/pkg/sortition/chain.go +++ b/pkg/sortition/chain.go @@ -51,6 +51,10 @@ type Chain interface { // or not. IsEligibleForRewards() (bool, error) + // IsOperatorUnstaking checks if the operator is unstaking. It returns true + // if the operator has deauthorized their entire stake, false otherwise. + IsOperatorUnstaking() (bool, error) + // Checks whether the operator is able to restore their eligibility for // rewards right away. CanRestoreRewardEligibility() (bool, error) diff --git a/pkg/sortition/internal/local/chain.go b/pkg/sortition/internal/local/chain.go index 1c85c4e519..c325ff5f2c 100644 --- a/pkg/sortition/internal/local/chain.go +++ b/pkg/sortition/internal/local/chain.go @@ -173,6 +173,11 @@ func (c *Chain) IsEligibleForRewards() (bool, error) { return !isIneligible, nil } +func (c *Chain) IsOperatorUnstaking() (bool, error) { + // TODO: Implement and use in unit tests. + return false, nil +} + func (c *Chain) CanRestoreRewardEligibility() (bool, error) { c.ineligibleForRewardsUntilMutex.RLock() defer c.ineligibleForRewardsUntilMutex.RUnlock() diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index be758757cc..98c1792484 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -189,6 +189,11 @@ func (lc *localChain) IsEligibleForRewards() (bool, error) { panic("unsupported") } +func (lc *localChain) IsOperatorUnstaking() (bool, error) { + // TODO: Implement and use in unit tests. + return false, nil +} + func (lc *localChain) CanRestoreRewardEligibility() (bool, error) { panic("unsupported") } diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 5c4141966b..d00faa7cc0 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -3,10 +3,11 @@ package tbtc import ( "context" "fmt" + "math/big" + "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/tecdsa" - "math/big" ) const ( @@ -86,12 +87,23 @@ func newHeartbeatAction( } func (ha *heartbeatAction) execute() error { - // TODO: When implementing the moving funds action we should make sure - // heartbeats are not executed by unstaking clients. + // Do not execute the heartbeat action if the operator is unstaking. + isUnstaking, err := ha.chain.IsOperatorUnstaking() + if err != nil { + return fmt.Errorf("failed to check if the operator is unstaking") + } + + if isUnstaking { + logger.Info( + "quitting the heartbeat action without signing because the " + + "operator is unstaking", + ) + return nil + } walletPublicKeyHash := bitcoin.PublicKeyHash(ha.wallet().publicKey) - err := ha.chain.ValidateHeartbeatProposal(walletPublicKeyHash, ha.proposal) + err = ha.chain.ValidateHeartbeatProposal(walletPublicKeyHash, ha.proposal) if err != nil { return fmt.Errorf("heartbeat proposal is invalid: [%v]", err) } From d3ec5a97d4c9cc792e3adbc040fbdc411a99a166 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 8 Apr 2024 13:06:05 +0200 Subject: [PATCH 02/40] Returned number of active members count when signing --- pkg/tbtc/heartbeat.go | 8 ++++++-- pkg/tbtc/heartbeat_test.go | 12 +++++++----- pkg/tbtc/signing.go | 27 +++++++++++++++------------ pkg/tbtc/signing_loop.go | 7 ++++++- pkg/tbtc/signing_loop_test.go | 8 ++++++++ pkg/tbtc/signing_test.go | 6 +++--- 6 files changed, 45 insertions(+), 23 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index d00faa7cc0..f4e3dc3c8c 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -45,7 +45,7 @@ type heartbeatSigningExecutor interface { ctx context.Context, message *big.Int, startBlock uint64, - ) (*tecdsa.Signature, uint64, error) + ) (*tecdsa.Signature, uint32, uint64, error) } // heartbeatAction is a walletAction implementation handling heartbeat requests @@ -123,7 +123,11 @@ func (ha *heartbeatAction) execute() error { ) defer cancelHeartbeatCtx() - signature, _, err := ha.signingExecutor.sign(heartbeatCtx, messageToSign, ha.startBlock) + signature, _, _, err := ha.signingExecutor.sign( + heartbeatCtx, + messageToSign, + ha.startBlock, + ) if err != nil { return fmt.Errorf("cannot sign heartbeat message: [%v]", err) } diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index 32941e9b59..d42cc0796f 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -4,10 +4,11 @@ import ( "context" "encoding/hex" "fmt" - "github.com/keep-network/keep-core/internal/testutils" - "github.com/keep-network/keep-core/pkg/tecdsa" "math/big" "testing" + + "github.com/keep-network/keep-core/internal/testutils" + "github.com/keep-network/keep-core/pkg/tecdsa" ) func TestHeartbeatAction_HappyPath(t *testing.T) { @@ -136,13 +137,14 @@ func (mhse *mockHeartbeatSigningExecutor) sign( ctx context.Context, message *big.Int, startBlock uint64, -) (*tecdsa.Signature, uint64, error) { +) (*tecdsa.Signature, uint32, uint64, error) { mhse.requestedMessage = message mhse.requestedStartBlock = startBlock if mhse.shouldFail { - return nil, 0, fmt.Errorf("oofta") + return nil, 0, 0, fmt.Errorf("oofta") } - return &tecdsa.Signature{}, startBlock + 1, nil + // TODO: Return the active members count and use it in unit tests. + return &tecdsa.Signature{}, 0, startBlock + 1, nil } diff --git a/pkg/tbtc/signing.go b/pkg/tbtc/signing.go index 532a03942d..b4ab2b069e 100644 --- a/pkg/tbtc/signing.go +++ b/pkg/tbtc/signing.go @@ -145,7 +145,7 @@ func (se *signingExecutor) signBatch( signingStartBlock = endBlocks[i-1] + signingBatchInterludeBlocks } - signature, endBlock, err := se.sign(ctx, message, signingStartBlock) + signature, _, endBlock, err := se.sign(ctx, message, signingStartBlock) if err != nil { return nil, err } @@ -167,15 +167,16 @@ func (se *signingExecutor) signBatch( // triggered according to the given start block. If the message cannot be signed // within a limited time window, an error is returned. If the message was // signed successfully, this function returns the signature along with the -// block at which the signature was calculated. This end block is common for -// all wallet signers so can be used as a synchronization point. +// number of active members that participated in signing, the block at which the +// signature was calculated. The end block is common for all wallet signers so +// can be used as a synchronization point. func (se *signingExecutor) sign( ctx context.Context, message *big.Int, startBlock uint64, -) (*tecdsa.Signature, uint64, error) { +) (*tecdsa.Signature, uint32, uint64, error) { if lockAcquired := se.lock.TryAcquire(1); !lockAcquired { - return nil, 0, errSigningExecutorBusy + return nil, 0, 0, errSigningExecutorBusy } defer se.lock.Release(1) @@ -183,7 +184,7 @@ func (se *signingExecutor) sign( walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) if err != nil { - return nil, 0, fmt.Errorf("cannot marshal wallet public key: [%v]", err) + return nil, 0, 0, fmt.Errorf("cannot marshal wallet public key: [%v]", err) } loopTimeoutBlock := startBlock + @@ -197,8 +198,9 @@ func (se *signingExecutor) sign( ) type signingOutcome struct { - signature *tecdsa.Signature - endBlock uint64 + signature *tecdsa.Signature + activeMembersCount uint32 + endBlock uint64 } wg := sync.WaitGroup{} @@ -365,8 +367,9 @@ func (se *signingExecutor) sign( ) signingOutcomeChan <- &signingOutcome{ - signature: loopResult.result.Signature, - endBlock: loopResult.latestEndBlock, + signature: loopResult.result.Signature, + activeMembersCount: loopResult.activeMembersCount, + endBlock: loopResult.latestEndBlock, } }(currentSigner) } @@ -383,9 +386,9 @@ func (se *signingExecutor) sign( // signer, that means all signers failed and have not produced a signature. select { case outcome := <-signingOutcomeChan: - return outcome.signature, outcome.endBlock, nil + return outcome.signature, outcome.activeMembersCount, outcome.endBlock, nil default: - return nil, 0, fmt.Errorf("all signers failed") + return nil, 0, 0, fmt.Errorf("all signers failed") } } diff --git a/pkg/tbtc/signing_loop.go b/pkg/tbtc/signing_loop.go index f67a585fe7..48f881df03 100644 --- a/pkg/tbtc/signing_loop.go +++ b/pkg/tbtc/signing_loop.go @@ -5,11 +5,12 @@ import ( "crypto/sha256" "encoding/binary" "fmt" - "github.com/keep-network/keep-core/pkg/protocol/announcer" "math/big" "math/rand" "sort" + "github.com/keep-network/keep-core/pkg/protocol/announcer" + "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/protocol/group" @@ -143,6 +144,9 @@ type signingAttemptFn func(*signingAttemptParams) (*signing.Result, uint64, erro type signingRetryLoopResult struct { // result is the outcome of the signing process. result *signing.Result + // activeMembersCount is the number of members that participated in the + // signing process. + activeMembersCount uint32 // latestEndBlock is the block at which the slowest signer of the successful // signing attempt completed signature computation. This block is also // the common end block accepted by all other members of the signing group. @@ -407,6 +411,7 @@ func (srl *signingRetryLoop) start( return &signingRetryLoopResult{ result: result, + activeMembersCount: uint32(len(readyMembersIndexes)), latestEndBlock: latestEndBlock, attemptTimeoutBlock: timeoutBlock, }, nil diff --git a/pkg/tbtc/signing_loop_test.go b/pkg/tbtc/signing_loop_test.go index fbdba89552..cf3b02e716 100644 --- a/pkg/tbtc/signing_loop_test.go +++ b/pkg/tbtc/signing_loop_test.go @@ -101,6 +101,7 @@ func TestSigningRetryLoop(t *testing.T) { expectedErr: nil, expectedResult: &signingRetryLoopResult{ result: testResult, + activeMembersCount: 10, latestEndBlock: 215, // the end block resolved by the done check phase attemptTimeoutBlock: 236, // start block of the first attempt + 30 }, @@ -151,6 +152,7 @@ func TestSigningRetryLoop(t *testing.T) { expectedErr: nil, expectedResult: &signingRetryLoopResult{ result: testResult, + activeMembersCount: 6, latestEndBlock: 215, // the end block resolved by the done check phase attemptTimeoutBlock: 236, // start block of the first attempt + 30 }, @@ -205,6 +207,7 @@ func TestSigningRetryLoop(t *testing.T) { expectedErr: nil, expectedResult: &signingRetryLoopResult{ result: testResult, + activeMembersCount: 10, latestEndBlock: 260, // the end block resolved by the done check phase attemptTimeoutBlock: 277, // start block of the second attempt + 30 }, @@ -261,6 +264,7 @@ func TestSigningRetryLoop(t *testing.T) { expectedErr: nil, expectedResult: &signingRetryLoopResult{ result: testResult, + activeMembersCount: 10, latestEndBlock: 260, // the end block resolved by the done check phase attemptTimeoutBlock: 277, // start block of the second attempt + 30 }, @@ -317,6 +321,7 @@ func TestSigningRetryLoop(t *testing.T) { expectedErr: nil, expectedResult: &signingRetryLoopResult{ result: testResult, + activeMembersCount: 10, latestEndBlock: 260, // the end block resolved by the done check phase attemptTimeoutBlock: 277, // start block of the second attempt + 30 }, @@ -365,6 +370,7 @@ func TestSigningRetryLoop(t *testing.T) { expectedErr: nil, expectedResult: &signingRetryLoopResult{ result: testResult, + activeMembersCount: 10, latestEndBlock: 260, // the end block resolved by the done check phase attemptTimeoutBlock: 277, // start block of the second attempt + 30 }, @@ -436,6 +442,7 @@ func TestSigningRetryLoop(t *testing.T) { expectedErr: nil, expectedResult: &signingRetryLoopResult{ result: testResult, + activeMembersCount: 10, latestEndBlock: 260, // the end block resolved by the done check phase attemptTimeoutBlock: 277, // start block of the second attempt + 30 }, @@ -541,6 +548,7 @@ func TestSigningRetryLoop(t *testing.T) { expectedErr: nil, expectedResult: &signingRetryLoopResult{ result: testResult, + activeMembersCount: 10, latestEndBlock: 260, // the end block resolved by the done check phase attemptTimeoutBlock: 277, // start block of the second attempt + 30 }, diff --git a/pkg/tbtc/signing_test.go b/pkg/tbtc/signing_test.go index 1ace228c72..9bcd42ef6c 100644 --- a/pkg/tbtc/signing_test.go +++ b/pkg/tbtc/signing_test.go @@ -27,7 +27,7 @@ func TestSigningExecutor_Sign(t *testing.T) { message := big.NewInt(100) startBlock := uint64(0) - signature, endBlock, err := executor.sign(ctx, message, startBlock) + signature, _, endBlock, err := executor.sign(ctx, message, startBlock) if err != nil { t.Fatal(err) } @@ -59,13 +59,13 @@ func TestSigningExecutor_Sign_Busy(t *testing.T) { errChan := make(chan error, 1) go func() { - _, _, err := executor.sign(ctx, message, startBlock) + _, _, _, err := executor.sign(ctx, message, startBlock) errChan <- err }() time.Sleep(100 * time.Millisecond) - _, _, err := executor.sign(ctx, message, startBlock) + _, _, _, err := executor.sign(ctx, message, startBlock) testutils.AssertErrorsSame(t, errSigningExecutorBusy, err) err = <-errChan From 6c5710532d82ab1187f417abdcb19d24382600d4 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 8 Apr 2024 19:07:52 +0200 Subject: [PATCH 03/40] Added counter of consecutive heartbeat failure --- pkg/tbtc/heartbeat.go | 74 +++++++++++++++++++++++++++++++++----- pkg/tbtc/heartbeat_test.go | 28 +++++++++------ pkg/tbtc/node.go | 6 ++++ 3 files changed, 90 insertions(+), 18 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index f4e3dc3c8c..0ad1c05cc3 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -24,6 +24,12 @@ const ( // another action has been already requested by the coordinator. // The value of 25 blocks is roughly 5 minutes, assuming 12 seconds per block. heartbeatRequestTimeoutSafetyMarginBlocks = 25 + // heartbeatSigningMinimumActiveOperators determines the minimum number of + // active operators during signing for a heartbeat to be considered valid. + heartbeatSigningMinimumActiveOperators = 70 + // heartbeatConsecutiveFailuresThreshold determines the number of consecutive + // heartbeat failures required to trigger inactivity operator notification. + heartbeatConsecutiveFailureThreshold = 3 ) type HeartbeatProposal struct { @@ -57,7 +63,9 @@ type heartbeatAction struct { executingWallet wallet signingExecutor heartbeatSigningExecutor - proposal *HeartbeatProposal + proposal *HeartbeatProposal + failureCounter *uint + startBlock uint64 expiryBlock uint64 @@ -70,6 +78,7 @@ func newHeartbeatAction( executingWallet wallet, signingExecutor heartbeatSigningExecutor, proposal *HeartbeatProposal, + failureCounter *uint, startBlock uint64, expiryBlock uint64, waitForBlockFn waitForBlockFn, @@ -80,6 +89,7 @@ func newHeartbeatAction( executingWallet: executingWallet, signingExecutor: signingExecutor, proposal: proposal, + failureCounter: failureCounter, startBlock: startBlock, expiryBlock: expiryBlock, waitForBlockFn: waitForBlockFn, @@ -123,21 +133,69 @@ func (ha *heartbeatAction) execute() error { ) defer cancelHeartbeatCtx() - signature, _, _, err := ha.signingExecutor.sign( + signature, activeOperatorsCount, _, err := ha.signingExecutor.sign( heartbeatCtx, messageToSign, ha.startBlock, ) + + // If there was no error and the number of active operators during signing + // was enough, we can consider the heartbeat procedure as successful. + if err == nil && activeOperatorsCount >= heartbeatSigningMinimumActiveOperators { + logger.Infof( + "successfully generated signature [%s] for heartbeat message [0x%x]", + signature, + ha.proposal.Message[:], + ) + + // Reset the counter for consecutive heartbeat failure. + *ha.failureCounter = 0 + + return nil + } + + // If there was an error or the number of active operators during signing + // was not enough, we must consider the heartbeat procedure as a failure. if err != nil { - return fmt.Errorf("cannot sign heartbeat message: [%v]", err) + logger.Infof("error while generating heartbeat signature: [%v]", err) + } else { + logger.Infof( + "not enough active operators during signing; required [%d]: "+ + "actual [%d]", + activeOperatorsCount, + heartbeatSigningMinimumActiveOperators, + ) } - logger.Infof( - "generated signature [%s] for heartbeat message [0x%x]", - signature, - ha.proposal.Message[:], - ) + // Increment the heartbeat failure counter. + *ha.failureCounter++ + + // If the number of consecutive heartbeat failures does not exceed the + // threshold do not notify about operator inactivity. + if *ha.failureCounter < heartbeatConsecutiveFailureThreshold { + logger.Infof( + "leaving without notifying about operator inactivity; current "+ + "heartbeat failure count is [%d]", + *ha.failureCounter, + ) + return nil + } + + // The value of consecutive heartbeat failures exceeds the threshold. + // Proceed with operator inactivity notification. + err = ha.notifyOperatorInactivity() + if err != nil { + return fmt.Errorf( + "error while notifying about operator inactivity [%v]]", + err, + ) + } + + return nil +} +func (ha *heartbeatAction) notifyOperatorInactivity() error { + // TODO: Implement return nil } diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index d42cc0796f..9db3abcc64 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -30,6 +30,8 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { }, } + heartbeatFailureCounter := uint(0) + // sha256(sha256(messageToSign)) sha256d, err := hex.DecodeString("38d30dacec5083c902952ce99fc0287659ad0b1ca2086827a8e78b0bef2c8bc1") if err != nil { @@ -48,6 +50,7 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { }, mockExecutor, proposal, + &heartbeatFailureCounter, startBlock, expiryBlock, func(ctx context.Context, blockHeight uint64) error { @@ -93,6 +96,8 @@ func TestHeartbeatAction_SigningError(t *testing.T) { }, } + heartbeatFailureCounter := uint(0) + hostChain := Connect() hostChain.setHeartbeatProposalValidationResult(proposal, true) @@ -107,6 +112,7 @@ func TestHeartbeatAction_SigningError(t *testing.T) { }, mockExecutor, proposal, + &heartbeatFailureCounter, startBlock, expiryBlock, func(ctx context.Context, blockHeight uint64) error { @@ -114,16 +120,18 @@ func TestHeartbeatAction_SigningError(t *testing.T) { }, ) - err = action.execute() - if err == nil { - t.Fatal("expected error to be returned") - } - testutils.AssertStringsEqual( - t, - "error message", - "cannot sign heartbeat message: [oofta]", - err.Error(), - ) + action.execute() + // TODO: Uncomment + // err = action.execute() + // if err == nil { + // t.Fatal("expected error to be returned") + // } + // testutils.AssertStringsEqual( + // t, + // "error message", + // "cannot sign heartbeat message: [oofta]", + // err.Error(), + // ) } type mockHeartbeatSigningExecutor struct { diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index c36a7174a7..d502c6dfee 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -65,6 +65,11 @@ type node struct { // dkgExecutor MUST NOT be used outside this struct. dkgExecutor *dkgExecutor + // heartbeatFailureCounter is the counter keeping track of consecutive + // heartbeat failure. It reset to zero after each successful heartbeat + // procedure. + heartbeatFailureCounter uint + signingExecutorsMutex sync.Mutex // signingExecutors is the cache holding signing executors for specific wallets. // The cache key is the uncompressed public key (with 04 prefix) of the wallet. @@ -458,6 +463,7 @@ func (n *node) handleHeartbeatProposal( wallet, signingExecutor, proposal, + &n.heartbeatFailureCounter, startBlock, expiryBlock, n.waitForBlockHeight, From ca9ba4e6e1e3fd7d9f2c93e4dcc80f668e962edf Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 16 Apr 2024 18:42:31 +0200 Subject: [PATCH 04/40] Added inactivity operator notifier --- pkg/chain/ethereum/tbtc.go | 11 ++ pkg/tbtc/chain.go | 8 + pkg/tbtc/chain_test.go | 10 ++ pkg/tbtc/heartbeat.go | 35 +++-- pkg/tbtc/heartbeat_test.go | 11 ++ pkg/tbtc/inactivity.go | 80 ++++++++++ pkg/tbtc/inactivity_submit.go | 95 ++++++++++++ pkg/tbtc/node.go | 26 ++++ pkg/tecdsa/inactivity/claim.go | 22 +++ pkg/tecdsa/inactivity/inactivity.go | 79 ++++++++++ pkg/tecdsa/inactivity/marshalling.go | 15 ++ pkg/tecdsa/inactivity/member.go | 136 +++++++++++++++++ pkg/tecdsa/inactivity/message.go | 42 ++++++ pkg/tecdsa/inactivity/states.go | 214 +++++++++++++++++++++++++++ 14 files changed, 768 insertions(+), 16 deletions(-) create mode 100644 pkg/tbtc/inactivity.go create mode 100644 pkg/tbtc/inactivity_submit.go create mode 100644 pkg/tecdsa/inactivity/claim.go create mode 100644 pkg/tecdsa/inactivity/inactivity.go create mode 100644 pkg/tecdsa/inactivity/marshalling.go create mode 100644 pkg/tecdsa/inactivity/member.go create mode 100644 pkg/tecdsa/inactivity/message.go create mode 100644 pkg/tecdsa/inactivity/states.go diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 27fff01bad..96080006ac 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -28,6 +28,7 @@ import ( "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tbtc" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" + "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) // Definitions of contract names. @@ -993,6 +994,16 @@ func (tc *TbtcChain) DKGParameters() (*tbtc.DKGParameters, error) { }, nil } +func (tc *TbtcChain) CalculateInactivityClaimSignatureHash( + nonce *big.Int, + walletPublicKey *ecdsa.PublicKey, + inactiveMembersIndexes []group.MemberIndex, + heartbeatFailed bool, +) (inactivity.ClaimSignatureHash, error) { + // TODO: Implement + return inactivity.ClaimSignatureHash{}, nil +} + func (tc *TbtcChain) PastDepositRevealedEvents( filter *tbtc.DepositRevealedEventFilter, ) ([]*tbtc.DepositRevealedEvent, error) { diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 0a52004b60..b9670bcdd7 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -12,6 +12,7 @@ import ( "github.com/keep-network/keep-core/pkg/sortition" "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" + "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) type DKGState int @@ -106,6 +107,13 @@ type DistributedKeyGenerationChain interface { startBlock uint64, ) (dkg.ResultSignatureHash, error) + CalculateInactivityClaimSignatureHash( + nonce *big.Int, + walletPublicKey *ecdsa.PublicKey, + inactiveMembersIndexes []group.MemberIndex, + heartbeatFailed bool, + ) (inactivity.ClaimSignatureHash, error) + // IsDKGResultValid checks whether the submitted DKG result is valid from // the on-chain contract standpoint. IsDKGResultValid(dkgResult *DKGChainResult) (bool, error) diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 98c1792484..0fa033adeb 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -24,6 +24,7 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" + "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) const localChainOperatorID = chain.OperatorID(1) @@ -551,6 +552,15 @@ func (lc *localChain) DKGParameters() (*DKGParameters, error) { }, nil } +func (lc *localChain) CalculateInactivityClaimSignatureHash( + nonce *big.Int, + walletPublicKey *ecdsa.PublicKey, + inactiveMembersIndexes []group.MemberIndex, + heartbeatFailed bool, +) (inactivity.ClaimSignatureHash, error) { + panic("unsupported") +} + func (lc *localChain) PastDepositRevealedEvents( filter *DepositRevealedEventFilter, ) ([]*DepositRevealedEvent, error) { diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 0ad1c05cc3..695a35c7e9 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -7,6 +7,7 @@ import ( "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" ) @@ -66,6 +67,8 @@ type heartbeatAction struct { proposal *HeartbeatProposal failureCounter *uint + inactivityClaimExecutor *inactivityClaimExecutor + startBlock uint64 expiryBlock uint64 @@ -79,20 +82,22 @@ func newHeartbeatAction( signingExecutor heartbeatSigningExecutor, proposal *HeartbeatProposal, failureCounter *uint, + inactivityClaimExecutor *inactivityClaimExecutor, startBlock uint64, expiryBlock uint64, waitForBlockFn waitForBlockFn, ) *heartbeatAction { return &heartbeatAction{ - logger: logger, - chain: chain, - executingWallet: executingWallet, - signingExecutor: signingExecutor, - proposal: proposal, - failureCounter: failureCounter, - startBlock: startBlock, - expiryBlock: expiryBlock, - waitForBlockFn: waitForBlockFn, + logger: logger, + chain: chain, + executingWallet: executingWallet, + signingExecutor: signingExecutor, + proposal: proposal, + failureCounter: failureCounter, + inactivityClaimExecutor: inactivityClaimExecutor, + startBlock: startBlock, + expiryBlock: expiryBlock, + waitForBlockFn: waitForBlockFn, } } @@ -104,7 +109,7 @@ func (ha *heartbeatAction) execute() error { } if isUnstaking { - logger.Info( + logger.Warn( "quitting the heartbeat action without signing because the " + "operator is unstaking", ) @@ -183,7 +188,10 @@ func (ha *heartbeatAction) execute() error { // The value of consecutive heartbeat failures exceeds the threshold. // Proceed with operator inactivity notification. - err = ha.notifyOperatorInactivity() + err = ha.inactivityClaimExecutor.publishClaim( + []group.MemberIndex{}, + true, + ) if err != nil { return fmt.Errorf( "error while notifying about operator inactivity [%v]]", @@ -194,11 +202,6 @@ func (ha *heartbeatAction) execute() error { return nil } -func (ha *heartbeatAction) notifyOperatorInactivity() error { - // TODO: Implement - return nil -} - func (ha *heartbeatAction) wallet() wallet { return ha.executingWallet } diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index 9db3abcc64..1f4b6bc819 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -42,6 +42,10 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { hostChain.setHeartbeatProposalValidationResult(proposal, true) mockExecutor := &mockHeartbeatSigningExecutor{} + inactivityNotifier := newInactivityClaimExecutor( + hostChain, + []*signer{}, + ) action := newHeartbeatAction( logger, hostChain, @@ -51,6 +55,7 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { mockExecutor, proposal, &heartbeatFailureCounter, + inactivityNotifier, startBlock, expiryBlock, func(ctx context.Context, blockHeight uint64) error { @@ -104,6 +109,11 @@ func TestHeartbeatAction_SigningError(t *testing.T) { mockExecutor := &mockHeartbeatSigningExecutor{} mockExecutor.shouldFail = true + inactivityNotifier := newInactivityClaimExecutor( + hostChain, + []*signer{}, + ) + action := newHeartbeatAction( logger, hostChain, @@ -113,6 +123,7 @@ func TestHeartbeatAction_SigningError(t *testing.T) { mockExecutor, proposal, &heartbeatFailureCounter, + inactivityNotifier, startBlock, expiryBlock, func(ctx context.Context, blockHeight uint64) error { diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go new file mode 100644 index 0000000000..4988bedc1f --- /dev/null +++ b/pkg/tbtc/inactivity.go @@ -0,0 +1,80 @@ +package tbtc + +import ( + "context" + "math/big" + "sync" + + "github.com/ipfs/go-log/v2" + "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" + "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" +) + +type inactivityClaimExecutor struct { + chain Chain + signers []*signer + + protocolLatch *generator.ProtocolLatch +} + +// TODO Consider moving all inactivity-related code to pkg/protocol/inactivity. +func newInactivityClaimExecutor( + chain Chain, + signers []*signer, +) *inactivityClaimExecutor { + return &inactivityClaimExecutor{ + chain: chain, + signers: signers, + } +} + +func (ice *inactivityClaimExecutor) publishClaim( + inactiveMembersIndexes []group.MemberIndex, + heartbeatFailed bool, +) error { + // TODO: Build a claim and launch the publish function for all + // the signers. The value of `heartbeat` should be true and + // `inactiveMembersIndices` should be empty. + + wg := sync.WaitGroup{} + wg.Add(len(ice.signers)) + + for _, currentSigner := range ice.signers { + ice.protocolLatch.Lock() + defer ice.protocolLatch.Unlock() + + go func(signer *signer) { + // TODO: Launch claim publishing for members. + }(currentSigner) + } + + return nil +} + +func (ice *inactivityClaimExecutor) publish( + ctx context.Context, + inactivityLogger log.StandardLogger, + seed *big.Int, + memberIndex group.MemberIndex, + broadcastChannel net.BroadcastChannel, + groupSize int, + dishonestThreshold int, + membershipValidator *group.MembershipValidator, + inactivityClaim *inactivity.Claim, +) error { + return inactivity.Publish( + ctx, + inactivityLogger, + seed.Text(16), + memberIndex, + broadcastChannel, + groupSize, + dishonestThreshold, + membershipValidator, + newInactivityClaimSigner(ice.chain), + newInactivityClaimSubmitter(), + inactivityClaim, + ) +} diff --git a/pkg/tbtc/inactivity_submit.go b/pkg/tbtc/inactivity_submit.go new file mode 100644 index 0000000000..1bbf70e0b8 --- /dev/null +++ b/pkg/tbtc/inactivity_submit.go @@ -0,0 +1,95 @@ +package tbtc + +import ( + "context" + "fmt" + + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" +) + +// inactivityClaimSigner is responsible for signing the inactivity claim and +// verification of signatures generated by other group members. +type inactivityClaimSigner struct { + chain Chain +} + +func newInactivityClaimSigner( + chain Chain, +) *inactivityClaimSigner { + return &inactivityClaimSigner{ + chain: chain, + } +} + +func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.Claim) ( + *inactivity.SignedClaim, + error, +) { + if claim == nil { + return nil, fmt.Errorf("result is nil") + } + + claimHash, err := ics.chain.CalculateInactivityClaimSignatureHash( + claim.Nonce, + claim.WalletPublicKey, + claim.InactiveMembersIndexes, + claim.HeartbeatFailed, + ) + if err != nil { + return nil, fmt.Errorf( + "inactivity claim hash calculation failed [%w]", + err, + ) + } + + signing := ics.chain.Signing() + + signature, err := signing.Sign(claimHash[:]) + if err != nil { + return nil, fmt.Errorf( + "inactivity claim hash signing failed [%w]", + err, + ) + } + + return &inactivity.SignedClaim{ + PublicKey: signing.PublicKey(), + Signature: signature, + ClaimHash: claimHash, + }, nil +} + +// VerifySignature verifies if the signature was generated from the provided +// inactivity claim using the provided public key. +func (ics *inactivityClaimSigner) VerifySignature( + signedClaim *inactivity.SignedClaim, +) ( + bool, + error, +) { + return ics.chain.Signing().VerifyWithPublicKey( + signedClaim.ClaimHash[:], + signedClaim.Signature, + signedClaim.PublicKey, + ) +} + +type inactivityClaimSubmitter struct { + // TODO: Implement +} + +func newInactivityClaimSubmitter() *inactivityClaimSubmitter { + // TODO: Implement + return &inactivityClaimSubmitter{} +} + +func (ics *inactivityClaimSubmitter) SubmitClaim( + ctx context.Context, + memberIndex group.MemberIndex, + claim *inactivity.Claim, + signatures map[group.MemberIndex][]byte, +) error { + // TODO: Implement + return nil +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index d502c6dfee..16abca3cca 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -410,6 +410,25 @@ func (n *node) getCoordinationExecutor( return executor, true, nil } +func (n *node) getInactivityNotifier( + walletPublicKey *ecdsa.PublicKey, +) (*inactivityClaimExecutor, error) { + signers := n.walletRegistry.getSigners(walletPublicKey) + if len(signers) == 0 { + // This is not an error because the node simply does not control + // the given wallet. + return nil, nil + } + + inactivityNotifier := newInactivityClaimExecutor( + n.chain, + signers, + ) + + // TODO: Continue with the implementation. + return inactivityNotifier, nil +} + // handleHeartbeatProposal handles an incoming heartbeat proposal by // orchestrating and dispatching an appropriate wallet action. func (n *node) handleHeartbeatProposal( @@ -442,6 +461,12 @@ func (n *node) handleHeartbeatProposal( return } + inactivityNotifier, err := n.getInactivityNotifier(wallet.publicKey) + if err != nil { + logger.Errorf("cannot get inactivity operator: [%v]", err) + return + } + logger.Infof( "starting orchestration of the heartbeat action for wallet [0x%x]; "+ "20-byte public key hash of that wallet is [0x%x]", @@ -464,6 +489,7 @@ func (n *node) handleHeartbeatProposal( signingExecutor, proposal, &n.heartbeatFailureCounter, + inactivityNotifier, startBlock, expiryBlock, n.waitForBlockHeight, diff --git a/pkg/tecdsa/inactivity/claim.go b/pkg/tecdsa/inactivity/claim.go new file mode 100644 index 0000000000..02ce0a0f8b --- /dev/null +++ b/pkg/tecdsa/inactivity/claim.go @@ -0,0 +1,22 @@ +package inactivity + +import ( + "crypto/ecdsa" + "math/big" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// Claim represents an inactivity claim. +type Claim struct { + Nonce *big.Int + WalletPublicKey *ecdsa.PublicKey + InactiveMembersIndexes []group.MemberIndex + HeartbeatFailed bool +} + +const ClaimSignatureHashByteSize = 32 + +// ClaimSignatureHash is a signature hash of the inactivity claim. The hashing +// algorithm used depends on the client code. +type ClaimSignatureHash [ClaimSignatureHashByteSize]byte diff --git a/pkg/tecdsa/inactivity/inactivity.go b/pkg/tecdsa/inactivity/inactivity.go new file mode 100644 index 0000000000..67e0509e2b --- /dev/null +++ b/pkg/tecdsa/inactivity/inactivity.go @@ -0,0 +1,79 @@ +package inactivity + +import ( + "context" + "fmt" + + "github.com/ipfs/go-log/v2" + + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/protocol/state" +) + +// SignedClaim represents information pertaining to the process of signing +// an inactivity claim: the public key used during signing, the resulting +// signature and the hash of the inactivity claim that was used during signing. +type SignedClaim struct { + PublicKey []byte + Signature []byte + ClaimHash ClaimSignatureHash +} + +type ClaimSigner interface { + SignClaim(claim *Claim) (*SignedClaim, error) + VerifySignature(signedClaim *SignedClaim) (bool, error) +} + +type ClaimSubmitter interface { + SubmitClaim( + ctx context.Context, + memberIndex group.MemberIndex, + claim *Claim, + signatures map[group.MemberIndex][]byte, + ) error +} + +func Publish( + ctx context.Context, + logger log.StandardLogger, + sessionID string, + memberIndex group.MemberIndex, + channel net.BroadcastChannel, + groupSize int, + dishonestThreshold int, + membershipValidator *group.MembershipValidator, + claimSigner ClaimSigner, + claimSubmitter ClaimSubmitter, + claim *Claim, +) error { + initialState := &claimSigningState{ + BaseAsyncState: state.NewBaseAsyncState(), + channel: channel, + claimSigner: claimSigner, + claimSubmitter: claimSubmitter, + member: newSigningMember( + logger, + memberIndex, + groupSize, + dishonestThreshold, + membershipValidator, + sessionID, + ), + claim: claim, + } + + stateMachine := state.NewAsyncMachine(logger, ctx, channel, initialState) + + lastState, err := stateMachine.Execute() + if err != nil { + return err + } + + _, ok := lastState.(*claimSubmissionState) + if !ok { + return fmt.Errorf("execution ended on state %T", lastState) + } + + return nil +} diff --git a/pkg/tecdsa/inactivity/marshalling.go b/pkg/tecdsa/inactivity/marshalling.go new file mode 100644 index 0000000000..e104b9962e --- /dev/null +++ b/pkg/tecdsa/inactivity/marshalling.go @@ -0,0 +1,15 @@ +package inactivity + +// Marshal converts this claimSignatureMessage to a byte array suitable +// for network communication. +func (csm *claimSignatureMessage) Marshal() ([]byte, error) { + // TODO: Implement + return nil, nil +} + +// Unmarshal converts a byte array produced by Marshal to a +// claimSignatureMessage. +func (csm *claimSignatureMessage) Unmarshal(bytes []byte) error { + // TODO: Implement + return nil +} diff --git a/pkg/tecdsa/inactivity/member.go b/pkg/tecdsa/inactivity/member.go new file mode 100644 index 0000000000..2b42977c3a --- /dev/null +++ b/pkg/tecdsa/inactivity/member.go @@ -0,0 +1,136 @@ +package inactivity + +import ( + "context" + "fmt" + + "github.com/ipfs/go-log/v2" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +type signingMember struct { + logger log.StandardLogger + // Index of this group member. + memberIndex group.MemberIndex + // Group to which this member belongs. + group *group.Group + // Validator allowing to check public key and member index against + // group members. + membershipValidator *group.MembershipValidator + // Identifier of the particular operator inactivity notification session + // this member is part of. + sessionID string + // Hash of inactivity claim preferred by the current participant. + preferredInactivityClaimHash ClaimSignatureHash + // Signature over preferredInactivityClaimHash calculated by the member. + selfInactivityClaimSignature []byte +} + +// newSigningMember creates a new signingMember in the initial state. +func newSigningMember( + logger log.StandardLogger, + memberIndex group.MemberIndex, + groupSize int, + dishonestThreshold int, + membershipValidator *group.MembershipValidator, + sessionID string, +) *signingMember { + return &signingMember{ + logger: logger, + memberIndex: memberIndex, + // TODO: Check is this is a correct way to create the group. + group: group.NewGroup(dishonestThreshold, groupSize), + membershipValidator: membershipValidator, + sessionID: sessionID, + } +} + +// shouldAcceptMessage indicates whether the given member should accept +// a message from the given sender. +func (sm *signingMember) shouldAcceptMessage( + senderID group.MemberIndex, + senderPublicKey []byte, +) bool { + isMessageFromSelf := senderID == sm.memberIndex + isSenderValid := sm.membershipValidator.IsValidMembership( + senderID, + senderPublicKey, + ) + isSenderAccepted := sm.group.IsOperating(senderID) + + return !isMessageFromSelf && isSenderValid && isSenderAccepted +} + +// initializeSubmittingMember performs a transition of a member state to the +// next phase of the protocol. +func (sm *signingMember) initializeSubmittingMember() *submittingMember { + return &submittingMember{ + signingMember: sm, + } +} + +func (sm *signingMember) signClaim( + claim *Claim, + claimSigner ClaimSigner, +) (*claimSignatureMessage, error) { + signedClaim, err := claimSigner.SignClaim(claim) + if err != nil { + return nil, fmt.Errorf("failed to sign inactivity claim [%v]", err) + } + + // Register self signature and claim hash. + sm.selfInactivityClaimSignature = signedClaim.Signature + sm.preferredInactivityClaimHash = signedClaim.ClaimHash + + return &claimSignatureMessage{ + senderID: sm.memberIndex, + claimHash: signedClaim.ClaimHash, + signature: signedClaim.Signature, + publicKey: signedClaim.PublicKey, + sessionID: sm.sessionID, + }, nil +} + +// verifyInactivityClaimSignatures verifies signatures received in messages from +// other group members. It collects signatures supporting only the same +// inactivity claim hash as the one preferred by the current member. Each member +// is allowed to broadcast only one signature over a preferred inactivity claim +// hash. The function assumes that the input messages list does not contain a +// message from self and that the public key presented in each message is the +// correct one. This key needs to be compared against the one used by network +// client earlier, before this function is called. +func (sm *signingMember) verifyInactivityClaimSignatures( + messages []*claimSignatureMessage, + resultSigner ClaimSigner, +) map[group.MemberIndex][]byte { + // TODO: Implement + return nil +} + +// submittingMember represents a member submitting an inactivity claim to the +// blockchain along with signatures received from other group members supporting +// the claim. +type submittingMember struct { + *signingMember +} + +// submitClaim submits the inactivity claim along with the supporting signatures +// to the provided claim submitter. +func (sm *submittingMember) submitClaim( + ctx context.Context, + claim *Claim, + signatures map[group.MemberIndex][]byte, + claimSubmitter ClaimSubmitter, +) error { + if err := claimSubmitter.SubmitClaim( + ctx, + sm.memberIndex, + claim, + signatures, + ); err != nil { + return fmt.Errorf("failed to submit inactivity [%v]", err) + } + + return nil +} diff --git a/pkg/tecdsa/inactivity/message.go b/pkg/tecdsa/inactivity/message.go new file mode 100644 index 0000000000..cfedf2a663 --- /dev/null +++ b/pkg/tecdsa/inactivity/message.go @@ -0,0 +1,42 @@ +package inactivity + +import ( + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +const messageTypePrefix = "tecdsa_inactivity/" + +// message holds common traits of all signing protocol messages. +type message interface { + // SenderID returns protocol-level identifier of the message sender. + SenderID() group.MemberIndex + // SessionID returns the session identifier of the message. + SessionID() string + // Type returns the exact type of the message. + Type() string +} + +type claimSignatureMessage struct { + senderID group.MemberIndex + + claimHash ClaimSignatureHash + signature []byte + publicKey []byte + sessionID string +} + +// SenderID returns protocol-level identifier of the message sender. +func (csm *claimSignatureMessage) SenderID() group.MemberIndex { + return csm.senderID +} + +// SessionID returns the session identifier of the message. +func (csm *claimSignatureMessage) SessionID() string { + return csm.sessionID +} + +// Type returns a string describing an claimSignatureMessage type for +// marshaling purposes. +func (csm *claimSignatureMessage) Type() string { + return messageTypePrefix + "claim_signature_message" +} diff --git a/pkg/tecdsa/inactivity/states.go b/pkg/tecdsa/inactivity/states.go new file mode 100644 index 0000000000..6574cf79f9 --- /dev/null +++ b/pkg/tecdsa/inactivity/states.go @@ -0,0 +1,214 @@ +package inactivity + +import ( + "bytes" + "context" + "strconv" + + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/protocol/state" +) + +// claimSigningState is the state during which group members sign their +// preferred inactivity claim (by hashing their inactivity, and then signing the +// result), and share this over the broadcast channel. +type claimSigningState struct { + *state.BaseAsyncState + + channel net.BroadcastChannel + claimSigner ClaimSigner + claimSubmitter ClaimSubmitter + + member *signingMember + + claim *Claim +} + +func (css *claimSigningState) Initiate(ctx context.Context) error { + message, err := css.member.signClaim(css.claim, css.claimSigner) + if err != nil { + return err + } + + if err := css.channel.Send( + ctx, + message, + net.BackoffRetransmissionStrategy, + ); err != nil { + return err + } + + return nil +} + +func (css *claimSigningState) Receive(netMessage net.Message) error { + // The network layer determines the message sender's public key based on + // the network client's pinned identity. The sender can not use any other + // public key than the one it is identified with in the network. + // Furthermore, the sender must possess the associated private key - each + // network message is signed with it. + // + // The network layer rejects any message with an incorrect signature or + // altered public key. By this point, we've conducted enough checks to + // be very certain that the sender' public key presented in the network + // net.Message is the correct one. + // + // In this final step, we compare the pinned network key with one used to + // produce a signature over the inactivity claim hash. If the keys don't + // match, it means that an incorrect key was used to sign inactivity claim + // hash and the message should be rejected. + isValidKeyUsed := func(signatureMessage *claimSignatureMessage) bool { + return bytes.Equal(signatureMessage.publicKey, netMessage.SenderPublicKey()) + } + + // As there is only one message type exchanged during result publication, + // we can simplify the code and cast directly to the concrete type + // `*resultSignatureMessage` instead of casting to the generic `message`. + if signatureMessage, ok := netMessage.Payload().(*claimSignatureMessage); ok { + if css.member.shouldAcceptMessage( + signatureMessage.SenderID(), + netMessage.SenderPublicKey(), + ) && isValidKeyUsed( + signatureMessage, + ) && css.member.sessionID == signatureMessage.sessionID { + css.ReceiveToHistory(netMessage) + } + } + + return nil +} + +func (css *claimSigningState) CanTransition() bool { + // Although there is no hard requirement to expect signature messages + // from all participants, it makes sense to do so because this is an + // additional participant availability check that allows to maximize + // the final count of active participants. Moreover, this check does not + // bound the signing state to a fixed duration and one can move to the + // next state as soon as possible. + messagingDone := len(receivedMessages[*claimSignatureMessage](css.BaseAsyncState)) == + len(css.member.group.OperatingMemberIndexes())-1 + + // TODO: Modify the above code so that only 51 members are needed. Since it + // is executed after a failed heartbeat, we cannot expect all the + // members to sign the claim. In the future consider taking the number + // of active signers from the heartbeat procedure. + + return messagingDone +} + +func (css *claimSigningState) Next() (state.AsyncState, error) { + return &signaturesVerificationState{ + BaseAsyncState: css.BaseAsyncState, + channel: css.channel, + claimSigner: css.claimSigner, + claimSubmitter: css.claimSubmitter, + member: css.member, + claim: css.claim, + validSignatures: make(map[group.MemberIndex][]byte), + }, nil +} + +func (css *claimSigningState) MemberIndex() group.MemberIndex { + return css.member.memberIndex +} + +type signaturesVerificationState struct { + *state.BaseAsyncState + + channel net.BroadcastChannel + claimSigner ClaimSigner + claimSubmitter ClaimSubmitter + + member *signingMember + + claim *Claim + + validSignatures map[group.MemberIndex][]byte +} + +func (svs *signaturesVerificationState) Initiate(ctx context.Context) error { + svs.validSignatures = svs.member.verifyInactivityClaimSignatures( + receivedMessages[*claimSignatureMessage](svs.BaseAsyncState), + svs.claimSigner, + ) + return nil +} + +func (svs *signaturesVerificationState) Receive(msg net.Message) error { + return nil +} + +func (svs *signaturesVerificationState) CanTransition() bool { + return true +} + +func (svs *signaturesVerificationState) Next() (state.AsyncState, error) { + return &claimSubmissionState{ + BaseAsyncState: svs.BaseAsyncState, + channel: svs.channel, + claimSubmitter: svs.claimSubmitter, + member: svs.member.initializeSubmittingMember(), + claim: svs.claim, + signatures: svs.validSignatures, + }, nil +} + +func (svs *signaturesVerificationState) MemberIndex() group.MemberIndex { + return svs.member.memberIndex +} + +type claimSubmissionState struct { + *state.BaseAsyncState + + channel net.BroadcastChannel + claimSubmitter ClaimSubmitter + + member *submittingMember + + claim *Claim + signatures map[group.MemberIndex][]byte +} + +func (css *claimSubmissionState) Initiate(ctx context.Context) error { + return css.member.submitClaim( + ctx, + css.claim, + css.signatures, + css.claimSubmitter, + ) +} + +func (css *claimSubmissionState) Receive(msg net.Message) error { + return nil +} + +func (css *claimSubmissionState) CanTransition() bool { + return true +} + +func (css *claimSubmissionState) Next() (state.AsyncState, error) { + // returning nil represents this is the final state + return nil, nil +} + +func (css *claimSubmissionState) MemberIndex() group.MemberIndex { + return css.member.memberIndex +} + +// receivedMessages returns all messages of type T that have been received +// and validated so far. Returned messages are deduplicated so there is a +// guarantee that only one message of the given type is returned for the +// given sender. +func receivedMessages[T message](base *state.BaseAsyncState) []T { + var messageTemplate T + + payloads := state.ExtractMessagesPayloads[T](base, messageTemplate.Type()) + + return state.DeduplicateMessagesPayloads( + payloads, + func(message T) string { + return strconv.Itoa(int(message.SenderID())) + }, + ) +} From e2b4f3417042e6b81599278e25e5d23abcbe753c Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 19 Apr 2024 13:08:07 +0200 Subject: [PATCH 05/40] Added heartbeat failure counter per wallet --- pkg/tbtc/node.go | 61 ++++++++++++++++++++++-------- pkg/tbtc/node_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 15 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 16abca3cca..f197c28310 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -65,10 +65,11 @@ type node struct { // dkgExecutor MUST NOT be used outside this struct. dkgExecutor *dkgExecutor - // heartbeatFailureCounter is the counter keeping track of consecutive - // heartbeat failure. It reset to zero after each successful heartbeat - // procedure. - heartbeatFailureCounter uint + heartbeatFailureCountersMutex sync.Mutex + // heartbeatFailureCounters holds counters keeping track of consecutive + // heartbeat failures. Each wallet has a separate counter. The key used in + // the map is the uncompressed public key (with 04 prefix) of the wallet. + heartbeatFailureCounters map[string]*uint signingExecutorsMutex sync.Mutex // signingExecutors is the cache holding signing executors for specific wallets. @@ -111,16 +112,17 @@ func newNode( scheduler.RegisterProtocol(latch) node := &node{ - groupParameters: groupParameters, - chain: chain, - btcChain: btcChain, - netProvider: netProvider, - walletRegistry: walletRegistry, - walletDispatcher: newWalletDispatcher(), - protocolLatch: latch, - signingExecutors: make(map[string]*signingExecutor), - coordinationExecutors: make(map[string]*coordinationExecutor), - proposalGenerator: proposalGenerator, + groupParameters: groupParameters, + chain: chain, + btcChain: btcChain, + netProvider: netProvider, + walletRegistry: walletRegistry, + walletDispatcher: newWalletDispatcher(), + protocolLatch: latch, + heartbeatFailureCounters: make(map[string]*uint), + signingExecutors: make(map[string]*signingExecutor), + coordinationExecutors: make(map[string]*coordinationExecutor), + proposalGenerator: proposalGenerator, } // Only the operator address is known at this point and can be pre-fetched. @@ -213,6 +215,29 @@ func (n *node) validateDKG( n.dkgExecutor.executeDkgValidation(seed, submissionBlock, result, resultHash) } +func (n *node) getHeartbeatCounter( + walletPublicKey *ecdsa.PublicKey, +) (*uint, error) { + n.heartbeatFailureCountersMutex.Lock() + defer n.heartbeatFailureCountersMutex.Unlock() + + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + return nil, fmt.Errorf("cannot marshal wallet public key: [%v]", err) + } + + counterKey := hex.EncodeToString(walletPublicKeyBytes) + + if counter, exists := n.heartbeatFailureCounters[counterKey]; exists { + return counter, nil + } + + counterInitialValue := new(uint) // The value is zero-initialized. + n.heartbeatFailureCounters[counterKey] = counterInitialValue + + return counterInitialValue, nil +} + // getSigningExecutor gets the signing executor responsible for executing // signing related to a specific wallet whose part is controlled by this node. // The second boolean return value indicates whether the node controls at least @@ -461,6 +486,12 @@ func (n *node) handleHeartbeatProposal( return } + heartbeatFailureCounter, err := n.getHeartbeatCounter(wallet.publicKey) + if err != nil { + logger.Errorf("cannot get heartbeat failure counter: [%v]", err) + return + } + inactivityNotifier, err := n.getInactivityNotifier(wallet.publicKey) if err != nil { logger.Errorf("cannot get inactivity operator: [%v]", err) @@ -488,7 +519,7 @@ func (n *node) handleHeartbeatProposal( wallet, signingExecutor, proposal, - &n.heartbeatFailureCounter, + heartbeatFailureCounter, inactivityNotifier, startBlock, expiryBlock, diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index b9dbb01992..a3f516fba4 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -19,6 +19,94 @@ import ( "github.com/keep-network/keep-core/pkg/tecdsa" ) +func TestNode_GetHeartbeatCounter(t *testing.T) { + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + localChain := Connect() + localProvider := local.Connect() + + signer := createMockSigner(t) + + // Populate the mock keystore with the mock signer's data. This is + // required to make the node controlling the signer's wallet. + keyStorePersistence := createMockKeyStorePersistence(t, signer) + + node, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + localProvider, + keyStorePersistence, + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{}, + ) + if err != nil { + t.Fatal(err) + } + + walletPublicKey := signer.wallet.publicKey + + testutils.AssertIntsEqual( + t, + "cache size", + 0, + len(node.heartbeatFailureCounters), + ) + + counter, err := node.getHeartbeatCounter(walletPublicKey) + if err != nil { + t.Fatal(err) + } + + testutils.AssertIntsEqual( + t, + "cache size", + 1, + len(node.heartbeatFailureCounters), + ) + + testutils.AssertUintsEqual(t, "counter value", 0, uint64(*counter)) + + // Increment the counter and check the value again + *counter++ + testutils.AssertUintsEqual(t, "counter value", 1, uint64(*counter)) + + // Construct an arbitrary public key representing a different wallet. + x, y := walletPublicKey.Curve.Double(walletPublicKey.X, walletPublicKey.Y) + anotherWalletPublicKey := &ecdsa.PublicKey{ + Curve: walletPublicKey.Curve, + X: x, + Y: y, + } + + anotherCounter, err := node.getHeartbeatCounter(anotherWalletPublicKey) + if err != nil { + t.Fatal(err) + } + + testutils.AssertIntsEqual( + t, + "cache size", + 2, + len(node.heartbeatFailureCounters), + ) + + testutils.AssertUintsEqual(t, "counter value", 0, uint64(*anotherCounter)) + + // Increment one counter and reset another. + *anotherCounter++ + *counter = 0 + + testutils.AssertUintsEqual(t, "counter value", 0, uint64(*counter)) + testutils.AssertUintsEqual(t, "counter value", 1, uint64(*anotherCounter)) +} + func TestNode_GetSigningExecutor(t *testing.T) { groupParameters := &GroupParameters{ GroupSize: 5, From d6e9219985aa1b340d42beeca2166f4ff87224e9 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 22 Apr 2024 14:58:04 +0200 Subject: [PATCH 06/40] Added inactivity claim executor caching --- pkg/tbtc/node.go | 107 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index f197c28310..3d16e609dd 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -71,6 +71,18 @@ type node struct { // the map is the uncompressed public key (with 04 prefix) of the wallet. heartbeatFailureCounters map[string]*uint + inactivityClaimExecutorMutex sync.Mutex + // inactivityClaimExecutors is the cache holding inactivity claim executors + // for specific wallets. The cache key is the uncompressed public key + // (with 04 prefix) of the wallet. + // inactivityClaimExecutor encapsulates the logic of handling inactivity + // claim signing and submitting. + // + // inactivityClaimExecutors MUST NOT be used outside this struct. Please use + // wallet actions and walletDispatcher to execute an action on an existing + // wallet. + inactivityClaimExecutors map[string]*inactivityClaimExecutor + signingExecutorsMutex sync.Mutex // signingExecutors is the cache holding signing executors for specific wallets. // The cache key is the uncompressed public key (with 04 prefix) of the wallet. @@ -435,23 +447,88 @@ func (n *node) getCoordinationExecutor( return executor, true, nil } -func (n *node) getInactivityNotifier( +// getInactivityClaimExecutor gets the inactivity claim executor responsible for +// executing inactivity claim signing and submission related to a specific +// wallet whose part is controlled by this node. The second boolean return value +// indicates whether the node controls at least one signer for the given wallet. +func (n *node) getInactivityClaimExecutor( walletPublicKey *ecdsa.PublicKey, -) (*inactivityClaimExecutor, error) { +) (*inactivityClaimExecutor, bool, error) { + n.inactivityClaimExecutorMutex.Lock() + defer n.inactivityClaimExecutorMutex.Unlock() + + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + return nil, false, fmt.Errorf("cannot marshal wallet public key: [%v]", err) + } + + executorKey := hex.EncodeToString(walletPublicKeyBytes) + + if executor, exists := n.inactivityClaimExecutors[executorKey]; exists { + return executor, true, nil + } + + executorLogger := logger.With( + zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), + ) + signers := n.walletRegistry.getSigners(walletPublicKey) if len(signers) == 0 { // This is not an error because the node simply does not control // the given wallet. - return nil, nil + return nil, false, nil + } + + // All signers belong to one wallet. Take that wallet from the first signer. + wallet := signers[0].wallet + + channelName := fmt.Sprintf( + "%s-%s", + ProtocolName, + hex.EncodeToString(walletPublicKeyBytes), + ) + + broadcastChannel, err := n.netProvider.BroadcastChannelFor(channelName) + if err != nil { + return nil, false, fmt.Errorf("failed to get broadcast channel: [%v]", err) + } + + // TODO: Handle unmarshallers + + // signing.RegisterUnmarshallers(broadcastChannel) + // announcer.RegisterUnmarshaller(broadcastChannel) + // broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { + // return &signingDoneMessage{} + // }) + + membershipValidator := group.NewMembershipValidator( + executorLogger, + wallet.signingGroupOperators, + n.chain.Signing(), + ) + + err = broadcastChannel.SetFilter(membershipValidator.IsInGroup) + if err != nil { + return nil, false, fmt.Errorf( + "could not set filter for channel [%v]: [%v]", + broadcastChannel.Name(), + err, + ) } - inactivityNotifier := newInactivityClaimExecutor( + executorLogger.Infof( + "signing executor created; controlling [%v] signers", + len(signers), + ) + + executor := newInactivityClaimExecutor( n.chain, signers, ) - // TODO: Continue with the implementation. - return inactivityNotifier, nil + n.inactivityClaimExecutors[executorKey] = executor + + return executor, true, nil } // handleHeartbeatProposal handles an incoming heartbeat proposal by @@ -492,9 +569,21 @@ func (n *node) handleHeartbeatProposal( return } - inactivityNotifier, err := n.getInactivityNotifier(wallet.publicKey) + inactivityClaimExecutor, ok, err := n.getInactivityClaimExecutor(wallet.publicKey) if err != nil { - logger.Errorf("cannot get inactivity operator: [%v]", err) + logger.Errorf("cannot get inactivity claim executor: [%v]", err) + return + } + // This check is actually redundant. We know the node controls some + // wallet signers as we just got the wallet from the registry using their + // public key hash. However, we are doing it just in case. The API + // contract of getInactivityClaimExecutor may change one day. + if !ok { + logger.Infof( + "node does not control signers of wallet [0x%x]; "+ + "ignoring the received heartbeat request", + walletPublicKeyBytes, + ) return } @@ -520,7 +609,7 @@ func (n *node) handleHeartbeatProposal( signingExecutor, proposal, heartbeatFailureCounter, - inactivityNotifier, + inactivityClaimExecutor, startBlock, expiryBlock, n.waitForBlockHeight, From 147d82d80065b5a84bed49d91f155fe26f80a10b Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 22 Apr 2024 18:33:17 +0200 Subject: [PATCH 07/40] Handled publishing jobs for signers --- pkg/chain/ethereum/tbtc.go | 12 ++-- pkg/tbtc/chain.go | 20 +++--- pkg/tbtc/chain_test.go | 9 +-- pkg/tbtc/heartbeat.go | 4 ++ pkg/tbtc/heartbeat_test.go | 14 ++--- pkg/tbtc/inactivity.go | 113 ++++++++++++++++++++++++++++++---- pkg/tbtc/inactivity_submit.go | 7 +-- pkg/tbtc/node.go | 5 ++ 8 files changed, 142 insertions(+), 42 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 96080006ac..6b7d365be4 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -995,15 +995,19 @@ func (tc *TbtcChain) DKGParameters() (*tbtc.DKGParameters, error) { } func (tc *TbtcChain) CalculateInactivityClaimSignatureHash( - nonce *big.Int, - walletPublicKey *ecdsa.PublicKey, - inactiveMembersIndexes []group.MemberIndex, - heartbeatFailed bool, + claim *inactivity.Claim, ) (inactivity.ClaimSignatureHash, error) { // TODO: Implement return inactivity.ClaimSignatureHash{}, nil } +func (tc *TbtcChain) GetInactivityClaimNonce( + walletID [32]byte, +) (*big.Int, error) { + // TODO: Implement + return nil, nil +} + func (tc *TbtcChain) PastDepositRevealedEvents( filter *tbtc.DepositRevealedEventFilter, ) ([]*tbtc.DepositRevealedEvent, error) { diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index b9670bcdd7..49fdc59ed2 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -107,13 +107,6 @@ type DistributedKeyGenerationChain interface { startBlock uint64, ) (dkg.ResultSignatureHash, error) - CalculateInactivityClaimSignatureHash( - nonce *big.Int, - walletPublicKey *ecdsa.PublicKey, - inactiveMembersIndexes []group.MemberIndex, - heartbeatFailed bool, - ) (inactivity.ClaimSignatureHash, error) - // IsDKGResultValid checks whether the submitted DKG result is valid from // the on-chain contract standpoint. IsDKGResultValid(dkgResult *DKGChainResult) (bool, error) @@ -128,6 +121,18 @@ type DistributedKeyGenerationChain interface { DKGParameters() (*DKGParameters, error) } +type InactivityClaimChain interface { + // CalculateInactivityClaimSignatureHash calculates hash for the given + // inactivity claim. + CalculateInactivityClaimSignatureHash( + claim *inactivity.Claim, + ) (inactivity.ClaimSignatureHash, error) + + // GetInactivityClaimNonce returns inactivity claim nonce for the given + // wallet. + GetInactivityClaimNonce(walletID [32]byte) (*big.Int, error) +} + // DKGChainResultHash represents a hash of the DKGChainResult. The algorithm // used is specific to the chain. type DKGChainResultHash [32]byte @@ -461,6 +466,7 @@ type Chain interface { sortition.Chain GroupSelectionChain DistributedKeyGenerationChain + InactivityClaimChain BridgeChain WalletProposalValidatorChain } diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 0fa033adeb..707a647e4f 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -553,14 +553,15 @@ func (lc *localChain) DKGParameters() (*DKGParameters, error) { } func (lc *localChain) CalculateInactivityClaimSignatureHash( - nonce *big.Int, - walletPublicKey *ecdsa.PublicKey, - inactiveMembersIndexes []group.MemberIndex, - heartbeatFailed bool, + claim *inactivity.Claim, ) (inactivity.ClaimSignatureHash, error) { panic("unsupported") } +func (lc *localChain) GetInactivityClaimNonce(walletID [32]byte) (*big.Int, error) { + panic("unsupported") +} + func (lc *localChain) PastDepositRevealedEvents( filter *DepositRevealedEventFilter, ) ([]*DepositRevealedEvent, error) { diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 695a35c7e9..ac90cf126d 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -189,8 +189,12 @@ func (ha *heartbeatAction) execute() error { // The value of consecutive heartbeat failures exceeds the threshold. // Proceed with operator inactivity notification. err = ha.inactivityClaimExecutor.publishClaim( + // Leave the list empty. Some operators were inactive during the + // heartbeat because they were simply unstaking and therefore should not + // be punished. []group.MemberIndex{}, true, + messageToSign, ) if err != nil { return fmt.Errorf( diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index 1f4b6bc819..a8eec2b681 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -42,10 +42,7 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { hostChain.setHeartbeatProposalValidationResult(proposal, true) mockExecutor := &mockHeartbeatSigningExecutor{} - inactivityNotifier := newInactivityClaimExecutor( - hostChain, - []*signer{}, - ) + inactivityClaimExecutor := &inactivityClaimExecutor{} action := newHeartbeatAction( logger, hostChain, @@ -55,7 +52,7 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { mockExecutor, proposal, &heartbeatFailureCounter, - inactivityNotifier, + inactivityClaimExecutor, startBlock, expiryBlock, func(ctx context.Context, blockHeight uint64) error { @@ -109,10 +106,7 @@ func TestHeartbeatAction_SigningError(t *testing.T) { mockExecutor := &mockHeartbeatSigningExecutor{} mockExecutor.shouldFail = true - inactivityNotifier := newInactivityClaimExecutor( - hostChain, - []*signer{}, - ) + inactivityClaimExecutor := &inactivityClaimExecutor{} action := newHeartbeatAction( logger, @@ -123,7 +117,7 @@ func TestHeartbeatAction_SigningError(t *testing.T) { mockExecutor, proposal, &heartbeatFailureCounter, - inactivityNotifier, + inactivityClaimExecutor, startBlock, expiryBlock, func(ctx context.Context, blockHeight uint64) error { diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index 4988bedc1f..99827853e3 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -2,41 +2,101 @@ package tbtc import ( "context" + "fmt" "math/big" "sync" "github.com/ipfs/go-log/v2" + "go.uber.org/zap" + "golang.org/x/sync/semaphore" + + "github.com/keep-network/keep-core/pkg/bitcoin" "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" "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) +// errInactivityClaimExecutorBusy is an error returned when the inactivity claim +// executor cannot execute the inactivity claim due to another inactivity claim +// execution in progress. +var errInactivityClaimExecutorBusy = fmt.Errorf("inactivity claim executor is busy") + type inactivityClaimExecutor struct { - chain Chain - signers []*signer + lock *semaphore.Weighted - protocolLatch *generator.ProtocolLatch + chain Chain + signers []*signer + broadcastChannel net.BroadcastChannel + membershipValidator *group.MembershipValidator + groupParameters *GroupParameters + protocolLatch *generator.ProtocolLatch + + waitForBlockFn waitForBlockFn } // TODO Consider moving all inactivity-related code to pkg/protocol/inactivity. func newInactivityClaimExecutor( chain Chain, signers []*signer, + broadcastChannel net.BroadcastChannel, + membershipValidator *group.MembershipValidator, + groupParameters *GroupParameters, + protocolLatch *generator.ProtocolLatch, + waitForBlockFn waitForBlockFn, ) *inactivityClaimExecutor { return &inactivityClaimExecutor{ - chain: chain, - signers: signers, + lock: semaphore.NewWeighted(1), + chain: chain, + signers: signers, + broadcastChannel: broadcastChannel, + membershipValidator: membershipValidator, + groupParameters: groupParameters, + protocolLatch: protocolLatch, + waitForBlockFn: waitForBlockFn, } } func (ice *inactivityClaimExecutor) publishClaim( inactiveMembersIndexes []group.MemberIndex, heartbeatFailed bool, + message *big.Int, ) error { - // TODO: Build a claim and launch the publish function for all - // the signers. The value of `heartbeat` should be true and - // `inactiveMembersIndices` should be empty. + if lockAcquired := ice.lock.TryAcquire(1); !lockAcquired { + return errInactivityClaimExecutorBusy + } + defer ice.lock.Release(1) + + wallet := ice.wallet() + + walletPublicKeyHash := bitcoin.PublicKeyHash(wallet.publicKey) + walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) + if err != nil { + return fmt.Errorf("cannot marshal wallet public key: [%v]", err) + } + + execLogger := logger.With( + zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), + ) + + walletRegistryData, err := ice.chain.GetWallet(walletPublicKeyHash) + if err != nil { + return fmt.Errorf("could not get registry data on wallet: [%v]", err) + } + + nonce, err := ice.chain.GetInactivityClaimNonce( + walletRegistryData.EcdsaWalletID, + ) + if err != nil { + return fmt.Errorf("could not get nonce for wallet: [%v]", err) + } + + claim := &inactivity.Claim{ + Nonce: nonce, + WalletPublicKey: wallet.publicKey, + InactiveMembersIndexes: inactiveMembersIndexes, + HeartbeatFailed: heartbeatFailed, + } wg := sync.WaitGroup{} wg.Add(len(ice.signers)) @@ -45,11 +105,37 @@ func (ice *inactivityClaimExecutor) publishClaim( ice.protocolLatch.Lock() defer ice.protocolLatch.Unlock() + defer wg.Done() + + inactivityClaimTimeoutBlock := uint64(0) // TODO: Set the value of timeout block + go func(signer *signer) { - // TODO: Launch claim publishing for members. + ctx, cancelCtx := withCancelOnBlock( + context.Background(), + inactivityClaimTimeoutBlock, + ice.waitForBlockFn, + ) + defer cancelCtx() + + ice.publish( + ctx, + execLogger, + message, + signer.signingGroupMemberIndex, + wallet.groupSize(), + wallet.groupDishonestThreshold( + ice.groupParameters.HonestThreshold, + ), + ice.membershipValidator, + claim, + ) + }(currentSigner) } + // Wait until all controlled signers complete their routine. + wg.Wait() + return nil } @@ -58,7 +144,6 @@ func (ice *inactivityClaimExecutor) publish( inactivityLogger log.StandardLogger, seed *big.Int, memberIndex group.MemberIndex, - broadcastChannel net.BroadcastChannel, groupSize int, dishonestThreshold int, membershipValidator *group.MembershipValidator, @@ -69,7 +154,7 @@ func (ice *inactivityClaimExecutor) publish( inactivityLogger, seed.Text(16), memberIndex, - broadcastChannel, + ice.broadcastChannel, groupSize, dishonestThreshold, membershipValidator, @@ -78,3 +163,9 @@ func (ice *inactivityClaimExecutor) publish( inactivityClaim, ) } + +func (ice *inactivityClaimExecutor) wallet() wallet { + // All signers belong to one wallet. Take that wallet from the + // first signer. + return ice.signers[0].wallet +} diff --git a/pkg/tbtc/inactivity_submit.go b/pkg/tbtc/inactivity_submit.go index 1bbf70e0b8..8e7ad856a4 100644 --- a/pkg/tbtc/inactivity_submit.go +++ b/pkg/tbtc/inactivity_submit.go @@ -30,12 +30,7 @@ func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.Claim) ( return nil, fmt.Errorf("result is nil") } - claimHash, err := ics.chain.CalculateInactivityClaimSignatureHash( - claim.Nonce, - claim.WalletPublicKey, - claim.InactiveMembersIndexes, - claim.HeartbeatFailed, - ) + claimHash, err := ics.chain.CalculateInactivityClaimSignatureHash(claim) if err != nil { return nil, fmt.Errorf( "inactivity claim hash calculation failed [%w]", diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 3d16e609dd..075992b8cf 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -524,6 +524,11 @@ func (n *node) getInactivityClaimExecutor( executor := newInactivityClaimExecutor( n.chain, signers, + broadcastChannel, + membershipValidator, + n.groupParameters, + n.protocolLatch, + n.waitForBlockHeight, ) n.inactivityClaimExecutors[executorKey] = executor From 36c20d2cf81677f0bd30c01cff91a8a3e93031a5 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 24 Apr 2024 14:02:49 +0200 Subject: [PATCH 08/40] Added claim signature message --- pkg/tecdsa/inactivity/claim.go | 17 ++ pkg/tecdsa/inactivity/gen/pb/message.pb.go | 185 +++++++++++++++++++++ pkg/tecdsa/inactivity/gen/pb/message.proto | 12 ++ pkg/tecdsa/inactivity/marshalling.go | 48 +++++- pkg/tecdsa/inactivity/marshalling_test.go | 65 ++++++++ 5 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 pkg/tecdsa/inactivity/gen/pb/message.pb.go create mode 100644 pkg/tecdsa/inactivity/gen/pb/message.proto create mode 100644 pkg/tecdsa/inactivity/marshalling_test.go diff --git a/pkg/tecdsa/inactivity/claim.go b/pkg/tecdsa/inactivity/claim.go index 02ce0a0f8b..b434fa09c2 100644 --- a/pkg/tecdsa/inactivity/claim.go +++ b/pkg/tecdsa/inactivity/claim.go @@ -2,6 +2,7 @@ package inactivity import ( "crypto/ecdsa" + "fmt" "math/big" "github.com/keep-network/keep-core/pkg/protocol/group" @@ -20,3 +21,19 @@ const ClaimSignatureHashByteSize = 32 // ClaimSignatureHash is a signature hash of the inactivity claim. The hashing // algorithm used depends on the client code. type ClaimSignatureHash [ClaimSignatureHashByteSize]byte + +// ClaimSignatureHashFromBytes converts bytes slice to ClaimSignatureHash. +// It requires provided bytes slice size to be exactly +// ClaimSignatureHashByteSize. +func ClaimSignatureHashFromBytes(bytes []byte) (ClaimSignatureHash, error) { + var hash ClaimSignatureHash + + if len(bytes) != ClaimSignatureHashByteSize { + return hash, fmt.Errorf( + "bytes length is not equal %v", ClaimSignatureHashByteSize, + ) + } + copy(hash[:], bytes[:]) + + return hash, nil +} diff --git a/pkg/tecdsa/inactivity/gen/pb/message.pb.go b/pkg/tecdsa/inactivity/gen/pb/message.pb.go new file mode 100644 index 0000000000..ea4eed0654 --- /dev/null +++ b/pkg/tecdsa/inactivity/gen/pb/message.pb.go @@ -0,0 +1,185 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc v3.19.4 +// source: pkg/tecdsa/inactivity/gen/pb/message.proto + +package pb + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ClaimSignatureMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SenderID uint32 `protobuf:"varint,1,opt,name=senderID,proto3" json:"senderID,omitempty"` + ClaimHash []byte `protobuf:"bytes,2,opt,name=claimHash,proto3" json:"claimHash,omitempty"` + Signature []byte `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` + PublicKey []byte `protobuf:"bytes,4,opt,name=publicKey,proto3" json:"publicKey,omitempty"` + SessionID string `protobuf:"bytes,5,opt,name=sessionID,proto3" json:"sessionID,omitempty"` +} + +func (x *ClaimSignatureMessage) Reset() { + *x = ClaimSignatureMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClaimSignatureMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClaimSignatureMessage) ProtoMessage() {} + +func (x *ClaimSignatureMessage) ProtoReflect() protoreflect.Message { + mi := &file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes[0] + 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 ClaimSignatureMessage.ProtoReflect.Descriptor instead. +func (*ClaimSignatureMessage) Descriptor() ([]byte, []int) { + return file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescGZIP(), []int{0} +} + +func (x *ClaimSignatureMessage) GetSenderID() uint32 { + if x != nil { + return x.SenderID + } + return 0 +} + +func (x *ClaimSignatureMessage) GetClaimHash() []byte { + if x != nil { + return x.ClaimHash + } + return nil +} + +func (x *ClaimSignatureMessage) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +func (x *ClaimSignatureMessage) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *ClaimSignatureMessage) GetSessionID() string { + if x != nil { + return x.SessionID + } + return "" +} + +var File_pkg_tecdsa_inactivity_gen_pb_message_proto protoreflect.FileDescriptor + +var file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDesc = []byte{ + 0x0a, 0x2a, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2f, 0x69, 0x6e, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x62, 0x2f, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x69, 0x6e, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x22, 0xab, 0x01, 0x0a, 0x15, 0x43, 0x6c, 0x61, + 0x69, 0x6d, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 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, 0x1c, + 0x0a, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x48, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, + 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescOnce sync.Once + file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescData = file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDesc +) + +func file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescGZIP() []byte { + file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescOnce.Do(func() { + file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescData) + }) + return file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescData +} + +var file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_pkg_tecdsa_inactivity_gen_pb_message_proto_goTypes = []interface{}{ + (*ClaimSignatureMessage)(nil), // 0: inactivity.ClaimSignatureMessage +} +var file_pkg_tecdsa_inactivity_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 +} + +func init() { file_pkg_tecdsa_inactivity_gen_pb_message_proto_init() } +func file_pkg_tecdsa_inactivity_gen_pb_message_proto_init() { + if File_pkg_tecdsa_inactivity_gen_pb_message_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClaimSignatureMessage); 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{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pkg_tecdsa_inactivity_gen_pb_message_proto_goTypes, + DependencyIndexes: file_pkg_tecdsa_inactivity_gen_pb_message_proto_depIdxs, + MessageInfos: file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes, + }.Build() + File_pkg_tecdsa_inactivity_gen_pb_message_proto = out.File + file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDesc = nil + file_pkg_tecdsa_inactivity_gen_pb_message_proto_goTypes = nil + file_pkg_tecdsa_inactivity_gen_pb_message_proto_depIdxs = nil +} diff --git a/pkg/tecdsa/inactivity/gen/pb/message.proto b/pkg/tecdsa/inactivity/gen/pb/message.proto new file mode 100644 index 0000000000..e0e768a356 --- /dev/null +++ b/pkg/tecdsa/inactivity/gen/pb/message.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option go_package = "./pb"; +package inactivity; + +message ClaimSignatureMessage { + uint32 senderID = 1; + bytes claimHash = 2; + bytes signature = 3; + bytes publicKey = 4; + string sessionID = 5; +} \ No newline at end of file diff --git a/pkg/tecdsa/inactivity/marshalling.go b/pkg/tecdsa/inactivity/marshalling.go index e104b9962e..8e7b568c2c 100644 --- a/pkg/tecdsa/inactivity/marshalling.go +++ b/pkg/tecdsa/inactivity/marshalling.go @@ -1,15 +1,57 @@ package inactivity +import ( + "fmt" + + "google.golang.org/protobuf/proto" + + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tecdsa/inactivity/gen/pb" +) + +func validateMemberIndex(protoIndex uint32) error { + // Protobuf does not have uint8 type, so we are using uint32. When + // unmarshalling message, we need to make sure we do not overflow. + if protoIndex > group.MaxMemberIndex { + return fmt.Errorf("invalid member index value: [%v]", protoIndex) + } + return nil +} + // Marshal converts this claimSignatureMessage to a byte array suitable // for network communication. func (csm *claimSignatureMessage) Marshal() ([]byte, error) { - // TODO: Implement - return nil, nil + return proto.Marshal(&pb.ClaimSignatureMessage{ + SenderID: uint32(csm.senderID), + ClaimHash: csm.claimHash[:], + Signature: csm.signature, + PublicKey: csm.publicKey, + SessionID: csm.sessionID, + }) } // Unmarshal converts a byte array produced by Marshal to a // claimSignatureMessage. func (csm *claimSignatureMessage) Unmarshal(bytes []byte) error { - // TODO: Implement + pbMsg := pb.ClaimSignatureMessage{} + if err := proto.Unmarshal(bytes, &pbMsg); err != nil { + return err + } + + if err := validateMemberIndex(pbMsg.SenderID); err != nil { + return err + } + csm.senderID = group.MemberIndex(pbMsg.SenderID) + + claimHash, err := ClaimSignatureHashFromBytes(pbMsg.ClaimHash) + if err != nil { + return err + } + csm.claimHash = claimHash + + csm.signature = pbMsg.Signature + csm.publicKey = pbMsg.PublicKey + csm.sessionID = pbMsg.SessionID + return nil } diff --git a/pkg/tecdsa/inactivity/marshalling_test.go b/pkg/tecdsa/inactivity/marshalling_test.go new file mode 100644 index 0000000000..1b5ab43e09 --- /dev/null +++ b/pkg/tecdsa/inactivity/marshalling_test.go @@ -0,0 +1,65 @@ +package inactivity + +import ( + "reflect" + "testing" + + fuzz "github.com/google/gofuzz" + + "github.com/keep-network/keep-core/pkg/internal/pbutils" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestClaimSignatureMessage_MarshalingRoundtrip(t *testing.T) { + msg := &claimSignatureMessage{ + senderID: 123, + claimHash: [32]byte{0: 11, 10: 22, 31: 33}, + signature: []byte("signature"), + publicKey: []byte("pubkey"), + sessionID: "session-1", + } + unmarshaled := &claimSignatureMessage{} + + err := pbutils.RoundTrip(msg, unmarshaled) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(msg, unmarshaled) { + t.Fatalf("unexpected content of unmarshaled message") + } +} + +func TestFuzzClaimSignatureMessage_MarshalingRoundtrip(t *testing.T) { + for i := 0; i < 10; i++ { + var ( + senderID group.MemberIndex + claimHash ClaimSignatureHash + signature []byte + publicKey []byte + sessionID string + ) + + f := fuzz.New().NilChance(0.1).NumElements(0, 512) + + f.Fuzz(&senderID) + f.Fuzz(&claimHash) + f.Fuzz(&signature) + f.Fuzz(&publicKey) + f.Fuzz(&sessionID) + + message := &claimSignatureMessage{ + senderID: senderID, + claimHash: claimHash, + signature: signature, + publicKey: publicKey, + sessionID: sessionID, + } + + _ = pbutils.RoundTrip(message, &claimSignatureMessage{}) + } +} + +func TestFuzzClaimSignatureMessage_Unmarshaler(t *testing.T) { + pbutils.FuzzUnmarshaler(&claimSignatureMessage{}) +} From 17df0d875e8f764e0b6233830537f81cea95a13b Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 24 Apr 2024 18:22:02 +0200 Subject: [PATCH 09/40] Added inactivity claim signature hash calculation --- pkg/chain/ethereum/tbtc.go | 96 +++++++++++++++++++++++++++++++-- pkg/chain/ethereum/tbtc_test.go | 39 ++++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 6b7d365be4..2bbe3d2bae 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -997,15 +997,103 @@ func (tc *TbtcChain) DKGParameters() (*tbtc.DKGParameters, error) { func (tc *TbtcChain) CalculateInactivityClaimSignatureHash( claim *inactivity.Claim, ) (inactivity.ClaimSignatureHash, error) { - // TODO: Implement - return inactivity.ClaimSignatureHash{}, nil + walletPublicKeyBytes := elliptic.Marshal( + claim.WalletPublicKey.Curve, + claim.WalletPublicKey.X, + claim.WalletPublicKey.Y, + ) + // Crop the 04 prefix as the calculateInactivityClaimSignatureHash function + // expects an unprefixed 64-byte public key, + unprefixedGroupPublicKeyBytes := walletPublicKeyBytes[1:] + + // Inactive members indexes should be sorted in the ascending order. As the + // claim object may possibly be shared between concurrent code, it is + // safer to copy the indexes into a new slice. Additionally, the type + // representing inactive member index should be `big.Int` as the smart + // contract reading the calculated hash uses `uint256` for inactive member + // indexes. + inactiveMembersIndexes := make([]*big.Int, len(claim.InactiveMembersIndexes)) + for i, index := range claim.InactiveMembersIndexes { + inactiveMembersIndexes[i] = big.NewInt(int64(index)) + } + + sort.Slice(inactiveMembersIndexes, func(i, j int) bool { + return inactiveMembersIndexes[i].Cmp(inactiveMembersIndexes[j]) < 0 + }) + + return calculateInactivityClaimSignatureHash( + tc.chainID, + claim.Nonce, + unprefixedGroupPublicKeyBytes, + inactiveMembersIndexes, + claim.HeartbeatFailed, + ) +} + +func calculateInactivityClaimSignatureHash( + chainID *big.Int, + nonce *big.Int, + walletPublicKey []byte, + inactiveMembersIndexes []*big.Int, + heartbeatFailed bool, +) (inactivity.ClaimSignatureHash, error) { + publicKeySize := 64 + + if len(walletPublicKey) != publicKeySize { + return inactivity.ClaimSignatureHash{}, fmt.Errorf( + "wrong wallet public key length", + ) + } + + uint256Type, err := abi.NewType("uint256", "uint256", nil) + if err != nil { + return inactivity.ClaimSignatureHash{}, err + } + bytesType, err := abi.NewType("bytes", "bytes", nil) + if err != nil { + return inactivity.ClaimSignatureHash{}, err + } + uint256SliceType, err := abi.NewType("uint256[]", "uint256[]", nil) + if err != nil { + return inactivity.ClaimSignatureHash{}, err + } + boolType, err := abi.NewType("bool", "bool", nil) + if err != nil { + return inactivity.ClaimSignatureHash{}, err + } + + bytes, err := abi.Arguments{ + {Type: uint256Type}, + {Type: uint256Type}, + {Type: bytesType}, + {Type: uint256SliceType}, + {Type: boolType}, + }.Pack( + chainID, + nonce, + walletPublicKey, + inactiveMembersIndexes, + heartbeatFailed, + ) + if err != nil { + return inactivity.ClaimSignatureHash{}, err + } + + return inactivity.ClaimSignatureHash(crypto.Keccak256Hash(bytes)), nil } func (tc *TbtcChain) GetInactivityClaimNonce( walletID [32]byte, ) (*big.Int, error) { - // TODO: Implement - return nil, nil + nonce, err := tc.walletRegistry.InactivityClaimNonce(walletID) + if err != nil { + return nil, fmt.Errorf( + "failed to get inactivity claim nonce: [%w]", + err, + ) + } + + return nonce, nil } func (tc *TbtcChain) PastDepositRevealedEvents( diff --git a/pkg/chain/ethereum/tbtc_test.go b/pkg/chain/ethereum/tbtc_test.go index 950dd4e5dc..5f335bebdf 100644 --- a/pkg/chain/ethereum/tbtc_test.go +++ b/pkg/chain/ethereum/tbtc_test.go @@ -240,6 +240,45 @@ func TestCalculateDKGResultSignatureHash(t *testing.T) { ) } +func TestCalculateInactivityClaimSignatureHash(t *testing.T) { + chainID := big.NewInt(31337) + nonce := big.NewInt(3) + + walletPublicKey, err := hex.DecodeString( + "9a0544440cc47779235ccb76d669590c2cd20c7e431f97e17a1093faf03291c473e" + + "661a208a8a565ca1e384059bd2ff7ff6886df081ff1229250099d388c83df", + ) + if err != nil { + t.Fatal(err) + } + + inactiveMembersIndexes := []*big.Int{ + big.NewInt(1), big.NewInt(2), big.NewInt(30), + } + + heartbeatFailed := true + + hash, err := calculateInactivityClaimSignatureHash( + chainID, + nonce, + walletPublicKey, + inactiveMembersIndexes, + heartbeatFailed, + ) + if err != nil { + t.Fatal(err) + } + + expectedHash := "f3210008cba186e90386a1bd0c63b6f29a67666f632350be22ce63ab39fc506e" + + testutils.AssertStringsEqual( + t, + "hash", + expectedHash, + hex.EncodeToString(hash[:]), + ) +} + func TestParseDkgResultValidationOutcome(t *testing.T) { isValid, err := parseDkgResultValidationOutcome( &struct { From bd181a9edefe8797c471ff34363b8c88212c6c16 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 24 Apr 2024 19:47:25 +0200 Subject: [PATCH 10/40] Implemented inactivity claim submitting --- pkg/chain/ethereum/tbtc.go | 92 +++++++++++++++++++--- pkg/tbtc/chain.go | 25 ++++++ pkg/tbtc/chain_test.go | 20 +++++ pkg/tbtc/inactivity.go | 52 ++++++++++++- pkg/tbtc/inactivity_submit.go | 131 ++++++++++++++++++++++++++++++-- pkg/tecdsa/inactivity/claim.go | 16 ++++ pkg/tecdsa/inactivity/member.go | 51 ++++++++++++- 7 files changed, 365 insertions(+), 22 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 2bbe3d2bae..e17151b89d 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -994,6 +994,78 @@ func (tc *TbtcChain) DKGParameters() (*tbtc.DKGParameters, error) { }, nil } +func (tc *TbtcChain) AssembleInactivityClaim( + walletID [32]byte, + inactiveMembersIndices []group.MemberIndex, + signatures map[group.MemberIndex][]byte, + heartbeatFailed bool, +) ( + *tbtc.InactivityChainClaim, + error, +) { + signingMemberIndices, signatureBytes, err := convertSignaturesToChainFormat( + signatures, + ) + if err != nil { + return nil, fmt.Errorf( + "could not convert signatures to chain format: [%v]", + err, + ) + } + + // Sort inactiveMembersIndices slice in ascending order as expected by the + // on-chain contract. + sort.Slice(inactiveMembersIndices[:], func(i, j int) bool { + return inactiveMembersIndices[i] < inactiveMembersIndices[j] + }) + + return &tbtc.InactivityChainClaim{ + WalletID: walletID, + InactiveMembersIndices: inactiveMembersIndices, + HeartbeatFailed: heartbeatFailed, + Signatures: signatureBytes, + SigningMembersIndices: signingMemberIndices, + }, nil +} + +// convertInactivityClaimToAbiType converts the TBTC-specific inactivity claim +// to the format applicable for the WalletRegistry ABI. +func convertInactivityClaimToAbiType( + claim *tbtc.InactivityChainClaim, +) ecdsaabi.EcdsaInactivityClaim { + inactiveMembersIndices := make([]*big.Int, len(claim.InactiveMembersIndices)) + for i, memberIndex := range claim.InactiveMembersIndices { + inactiveMembersIndices[i] = big.NewInt(int64(memberIndex)) + } + + signingMembersIndices := make([]*big.Int, len(claim.SigningMembersIndices)) + for i, memberIndex := range claim.SigningMembersIndices { + signingMembersIndices[i] = big.NewInt(int64(memberIndex)) + } + + return ecdsaabi.EcdsaInactivityClaim{ + WalletID: claim.WalletID, + InactiveMembersIndices: inactiveMembersIndices, + HeartbeatFailed: claim.HeartbeatFailed, + Signatures: claim.Signatures, + SigningMembersIndices: signingMembersIndices, + } +} + +func (tc *TbtcChain) SubmitInactivityClaim( + claim *tbtc.InactivityChainClaim, + nonce *big.Int, + groupMembers []uint32, +) error { + _, err := tc.walletRegistry.NotifyOperatorInactivity( + convertInactivityClaimToAbiType(claim), + nonce, + groupMembers, + ) + + return err +} + func (tc *TbtcChain) CalculateInactivityClaimSignatureHash( claim *inactivity.Claim, ) (inactivity.ClaimSignatureHash, error) { @@ -1006,21 +1078,17 @@ func (tc *TbtcChain) CalculateInactivityClaimSignatureHash( // expects an unprefixed 64-byte public key, unprefixedGroupPublicKeyBytes := walletPublicKeyBytes[1:] - // Inactive members indexes should be sorted in the ascending order. As the - // claim object may possibly be shared between concurrent code, it is - // safer to copy the indexes into a new slice. Additionally, the type - // representing inactive member index should be `big.Int` as the smart - // contract reading the calculated hash uses `uint256` for inactive member - // indexes. - inactiveMembersIndexes := make([]*big.Int, len(claim.InactiveMembersIndexes)) - for i, index := range claim.InactiveMembersIndexes { + // The indexes are already sorted. + sortedIndexes := claim.GetInactiveMembersIndexes() + + // The type representing inactive member index should be `big.Int` as the + // smart contract reading the calculated hash uses `uint256` for inactive + // member indexes. + inactiveMembersIndexes := make([]*big.Int, len(sortedIndexes)) + for i, index := range sortedIndexes { inactiveMembersIndexes[i] = big.NewInt(int64(index)) } - sort.Slice(inactiveMembersIndexes, func(i, j int) bool { - return inactiveMembersIndexes[i].Cmp(inactiveMembersIndexes[j]) < 0 - }) - return calculateInactivityClaimSignatureHash( tc.chainID, claim.Nonce, diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 49fdc59ed2..7f2686fe10 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -121,7 +121,32 @@ type DistributedKeyGenerationChain interface { DKGParameters() (*DKGParameters, error) } +// InactivityChainClaim represents an inactivity claim submitted to the chain. +type InactivityChainClaim struct { + WalletID [32]byte + InactiveMembersIndices []group.MemberIndex + HeartbeatFailed bool + Signatures []byte + SigningMembersIndices []group.MemberIndex +} + type InactivityClaimChain interface { + // AssembleDKGResult assembles the inactivity chain claim according to the + // rules expected by the given chain. + AssembleInactivityClaim( + walletID [32]byte, + inactiveMembersIndices []group.MemberIndex, + signatures map[group.MemberIndex][]byte, + heartbeatFailed bool, + ) (*InactivityChainClaim, error) + + // SubmitInactivityClaim submits the inactivity claim to the chain. + SubmitInactivityClaim( + claim *InactivityChainClaim, + nonce *big.Int, + groupMembers []uint32, + ) error + // CalculateInactivityClaimSignatureHash calculates hash for the given // inactivity claim. CalculateInactivityClaimSignatureHash( diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 707a647e4f..bf622e5f1c 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -552,6 +552,26 @@ func (lc *localChain) DKGParameters() (*DKGParameters, error) { }, nil } +func (lc *localChain) AssembleInactivityClaim( + walletID [32]byte, + inactiveMembersIndices []group.MemberIndex, + signatures map[group.MemberIndex][]byte, + heartbeatFailed bool, +) ( + *InactivityChainClaim, + error, +) { + panic("unsupported") +} + +func (lc *localChain) SubmitInactivityClaim( + claim *InactivityChainClaim, + nonce *big.Int, + groupMembers []uint32, +) error { + panic("unsupported") +} + func (lc *localChain) CalculateInactivityClaimSignatureHash( claim *inactivity.Claim, ) (inactivity.ClaimSignatureHash, error) { diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index 99827853e3..c0a798b6d9 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -11,12 +11,21 @@ import ( "golang.org/x/sync/semaphore" "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" "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) +const ( + // inactivityClaimSubmissionDelayStepBlocks determines the delay step in blocks + // that is used to calculate the submission delay period that should be respected + // by the given member to avoid all members submitting the same inactivity claim + // at the same time. + inactivityClaimSubmissionDelayStepBlocks = 3 +) + // errInactivityClaimExecutorBusy is an error returned when the inactivity claim // executor cannot execute the inactivity claim due to another inactivity claim // execution in progress. @@ -30,7 +39,7 @@ type inactivityClaimExecutor struct { broadcastChannel net.BroadcastChannel membershipValidator *group.MembershipValidator groupParameters *GroupParameters - protocolLatch *generator.ProtocolLatch + protocolLatch *generator.ProtocolLatch waitForBlockFn waitForBlockFn } @@ -98,6 +107,11 @@ func (ice *inactivityClaimExecutor) publishClaim( HeartbeatFailed: heartbeatFailed, } + groupMembers, err := ice.getWalletMembersInfo() + if err != nil { + return fmt.Errorf("could not get wallet members info: [%v]", err) + } + wg := sync.WaitGroup{} wg.Add(len(ice.signers)) @@ -126,6 +140,7 @@ func (ice *inactivityClaimExecutor) publishClaim( wallet.groupDishonestThreshold( ice.groupParameters.HonestThreshold, ), + groupMembers, ice.membershipValidator, claim, ) @@ -139,6 +154,32 @@ func (ice *inactivityClaimExecutor) publishClaim( return nil } +func (ice *inactivityClaimExecutor) getWalletMembersInfo() ([]uint32, error) { + // Cache mapping operator addresses to their wallet member IDs. It helps to + // limit the number of calls to the ETH client if some operator addresses + // occur on the list multiple times. + operatorIDCache := make(map[chain.Address]uint32) + + walletMemberIDs := make([]uint32, 0) + + for _, operatorAddress := range ice.wallet().signingGroupOperators { + // Search for the operator address in the cache. Store the operator + // address in the cache if it's not there. + if operatorID, found := operatorIDCache[operatorAddress]; !found { + fetchedOperatorID, err := ice.chain.GetOperatorID(operatorAddress) + if err != nil { + return nil, fmt.Errorf("could not get operator ID: [%w]", err) + } + operatorIDCache[operatorAddress] = fetchedOperatorID + walletMemberIDs = append(walletMemberIDs, fetchedOperatorID) + } else { + walletMemberIDs = append(walletMemberIDs, operatorID) + } + } + + return walletMemberIDs, nil +} + func (ice *inactivityClaimExecutor) publish( ctx context.Context, inactivityLogger log.StandardLogger, @@ -146,6 +187,7 @@ func (ice *inactivityClaimExecutor) publish( memberIndex group.MemberIndex, groupSize int, dishonestThreshold int, + groupMembers []uint32, membershipValidator *group.MembershipValidator, inactivityClaim *inactivity.Claim, ) error { @@ -159,7 +201,13 @@ func (ice *inactivityClaimExecutor) publish( dishonestThreshold, membershipValidator, newInactivityClaimSigner(ice.chain), - newInactivityClaimSubmitter(), + newInactivityClaimSubmitter( + inactivityLogger, + ice.chain, + ice.groupParameters, + groupMembers, + ice.waitForBlockFn, + ), inactivityClaim, ) } diff --git a/pkg/tbtc/inactivity_submit.go b/pkg/tbtc/inactivity_submit.go index 8e7ad856a4..1dec255ee1 100644 --- a/pkg/tbtc/inactivity_submit.go +++ b/pkg/tbtc/inactivity_submit.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) @@ -71,12 +73,29 @@ func (ics *inactivityClaimSigner) VerifySignature( } type inactivityClaimSubmitter struct { - // TODO: Implement + inactivityLogger log.StandardLogger + + chain Chain + groupParameters *GroupParameters + groupMembers []uint32 + + waitForBlockFn waitForBlockFn } -func newInactivityClaimSubmitter() *inactivityClaimSubmitter { - // TODO: Implement - return &inactivityClaimSubmitter{} +func newInactivityClaimSubmitter( + inactivityLogger log.StandardLogger, + chain Chain, + groupParameters *GroupParameters, + groupMembers []uint32, + waitForBlockFn waitForBlockFn, +) *inactivityClaimSubmitter { + return &inactivityClaimSubmitter{ + inactivityLogger: inactivityLogger, + chain: chain, + groupParameters: groupParameters, + groupMembers: groupMembers, + waitForBlockFn: waitForBlockFn, + } } func (ics *inactivityClaimSubmitter) SubmitClaim( @@ -85,6 +104,106 @@ func (ics *inactivityClaimSubmitter) SubmitClaim( claim *inactivity.Claim, signatures map[group.MemberIndex][]byte, ) error { - // TODO: Implement - return nil + if len(signatures) < ics.groupParameters.HonestThreshold { + return fmt.Errorf( + "could not submit inactivity claim with [%v] signatures for "+ + "group honest threshold [%v]", + len(signatures), + ics.groupParameters.HonestThreshold, + ) + } + + // The inactivity nonce at the beginning of the execution process. + inactivityNonce := claim.Nonce + + walletPublicKeyHash := bitcoin.PublicKeyHash(claim.WalletPublicKey) + + walletRegistryData, err := ics.chain.GetWallet(walletPublicKeyHash) + if err != nil { + return fmt.Errorf("could not get registry data on wallet: [%v]", err) + } + + ecdsaWalletID := walletRegistryData.EcdsaWalletID + + currentNonce, err := ics.chain.GetInactivityClaimNonce( + ecdsaWalletID, + ) + if err != nil { + return fmt.Errorf("could not get nonce for wallet: [%v]", err) + } + + if currentNonce.Cmp(inactivityNonce) > 0 { + // Someone who was ahead of us in the queue submitted the claim. Giving up. + ics.inactivityLogger.Infof( + "[member:%v] inactivity claim already submitted; "+ + "aborting inactivity claim on-chain submission", + memberIndex, + ) + return nil + } + + inactivityClaim, err := ics.chain.AssembleInactivityClaim( + ecdsaWalletID, + claim.GetInactiveMembersIndexes(), + signatures, + claim.HeartbeatFailed, + ) + if err != nil { + return fmt.Errorf("could not assemble inactivity chain claim [%w]", err) + } + + blockCounter, err := ics.chain.BlockCounter() + if err != nil { + return err + } + + // We can't determine a common block at which the publication starts. + // However, all we want here is to ensure the members does not submit + // in the same time. This can be achieved by simply using the index-based + // delay starting from the current block. + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return fmt.Errorf("cannot get current block: [%v]", err) + } + delayBlocks := uint64(memberIndex-1) * inactivityClaimSubmissionDelayStepBlocks + submissionBlock := currentBlock + delayBlocks + + ics.inactivityLogger.Infof( + "[member:%v] waiting for block [%v] to submit inactivity claim", + memberIndex, + submissionBlock, + ) + + err = ics.waitForBlockFn(ctx, submissionBlock) + if err != nil { + return fmt.Errorf( + "error while waiting for DKG result submission block: [%v]", + err, + ) + } + + if ctx.Err() != nil { + // The context was cancelled by the upstream. Regardless of the cause, + // that means the inactivity execution is no longer awaiting the result, + // and we can safely return. + ics.inactivityLogger.Infof( + "[member:%v] inactivity execution is no longer awaiting the "+ + "result; aborting inactivity claim on-chain submission", + memberIndex, + ) + return nil + } + + ics.inactivityLogger.Infof( + "[member:%v] submitting inactivity claim with [%v] supporting "+ + "member signatures", + memberIndex, + len(signatures), + ) + + return ics.chain.SubmitInactivityClaim( + inactivityClaim, + inactivityNonce, + ics.groupMembers, + ) } diff --git a/pkg/tecdsa/inactivity/claim.go b/pkg/tecdsa/inactivity/claim.go index b434fa09c2..a582abe28b 100644 --- a/pkg/tecdsa/inactivity/claim.go +++ b/pkg/tecdsa/inactivity/claim.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "fmt" "math/big" + "sort" "github.com/keep-network/keep-core/pkg/protocol/group" ) @@ -16,6 +17,21 @@ type Claim struct { HeartbeatFailed bool } +// GetInactiveMembersIndexes returns the indexes of inactive members. +// The original slice is copied to avoid concurrency issues if the claim object +// is shared between many goroutines. The returned indexes are sorted. +func (c *Claim) GetInactiveMembersIndexes() []group.MemberIndex { + sortedIndexes := make([]group.MemberIndex, len(c.InactiveMembersIndexes)) + + copy(sortedIndexes, c.InactiveMembersIndexes) + + sort.Slice(sortedIndexes, func(i, j int) bool { + return sortedIndexes[i] < sortedIndexes[j] + }) + + return sortedIndexes +} + const ClaimSignatureHashByteSize = 32 // ClaimSignatureHash is a signature hash of the inactivity claim. The hashing diff --git a/pkg/tecdsa/inactivity/member.go b/pkg/tecdsa/inactivity/member.go index 2b42977c3a..e993a7a7c3 100644 --- a/pkg/tecdsa/inactivity/member.go +++ b/pkg/tecdsa/inactivity/member.go @@ -104,8 +104,55 @@ func (sm *signingMember) verifyInactivityClaimSignatures( messages []*claimSignatureMessage, resultSigner ClaimSigner, ) map[group.MemberIndex][]byte { - // TODO: Implement - return nil + receivedValidClaimSignatures := make(map[group.MemberIndex][]byte) + + for _, message := range messages { + // Sender's preferred inactivity claim hash doesn't match current + // member's preferred inactivity claim hash. + if message.claimHash != sm.preferredInactivityClaimHash { + sm.logger.Infof( + "[member:%v] signature from sender [%d] supports "+ + "result different than preferred", + sm.memberIndex, + message.senderID, + ) + continue + } + + // Check if the signature is valid. + isValid, err := resultSigner.VerifySignature( + &SignedClaim{ + ClaimHash: message.claimHash, + Signature: message.signature, + PublicKey: message.publicKey, + }, + ) + if err != nil { + sm.logger.Infof( + "[member:%v] verification of signature "+ + "from sender [%d] failed: [%v]", + sm.memberIndex, + message.senderID, + err, + ) + continue + } + if !isValid { + sm.logger.Infof( + "[member:%v] sender [%d] provided invalid signature", + sm.memberIndex, + message.senderID, + ) + continue + } + + receivedValidClaimSignatures[message.senderID] = message.signature + } + + // Register member's self signature. + receivedValidClaimSignatures[sm.memberIndex] = sm.selfInactivityClaimSignature + + return receivedValidClaimSignatures } // submittingMember represents a member submitting an inactivity claim to the From abad9adc892ab48c33d38312748549c4097b35d7 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 29 Apr 2024 15:52:17 +0200 Subject: [PATCH 11/40] Improved inactivity claim submission process --- pkg/tbtc/heartbeat.go | 10 ++++++---- pkg/tbtc/inactivity.go | 6 +++++- pkg/tbtc/node.go | 9 ++------- pkg/tecdsa/inactivity/inactivity.go | 9 +++++++++ 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index ac90cf126d..30c9d3d1b4 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -138,7 +138,7 @@ func (ha *heartbeatAction) execute() error { ) defer cancelHeartbeatCtx() - signature, activeOperatorsCount, _, err := ha.signingExecutor.sign( + signature, activeOperatorsCount, signingEndBlock, err := ha.signingExecutor.sign( heartbeatCtx, messageToSign, ha.startBlock, @@ -189,12 +189,14 @@ func (ha *heartbeatAction) execute() error { // The value of consecutive heartbeat failures exceeds the threshold. // Proceed with operator inactivity notification. err = ha.inactivityClaimExecutor.publishClaim( - // Leave the list empty. Some operators were inactive during the - // heartbeat because they were simply unstaking and therefore should not - // be punished. + // Leave the list of inactive operators empty even if some operators + // were inactive during signing heartbeat. The inactive operators could + // simply be in the process of unstaking and therefore should not be + // punished. []group.MemberIndex{}, true, messageToSign, + signingEndBlock, ) if err != nil { return fmt.Errorf( diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index c0a798b6d9..9c7c5c33e0 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -24,6 +24,9 @@ const ( // by the given member to avoid all members submitting the same inactivity claim // at the same time. inactivityClaimSubmissionDelayStepBlocks = 3 + // inactivityClaimMaximumSubmissionBlocks determines the maximum block + // duration of inactivity claim submission procedure. + inactivityClaimMaximumSubmissionBlocks = 60 ) // errInactivityClaimExecutorBusy is an error returned when the inactivity claim @@ -70,6 +73,7 @@ func (ice *inactivityClaimExecutor) publishClaim( inactiveMembersIndexes []group.MemberIndex, heartbeatFailed bool, message *big.Int, + startBlock uint64, ) error { if lockAcquired := ice.lock.TryAcquire(1); !lockAcquired { return errInactivityClaimExecutorBusy @@ -121,7 +125,7 @@ func (ice *inactivityClaimExecutor) publishClaim( defer wg.Done() - inactivityClaimTimeoutBlock := uint64(0) // TODO: Set the value of timeout block + inactivityClaimTimeoutBlock := startBlock + inactivityClaimMaximumSubmissionBlocks go func(signer *signer) { ctx, cancelCtx := withCancelOnBlock( diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 075992b8cf..1bb1bdb8d0 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -18,6 +18,7 @@ import ( "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/announcer" "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" "github.com/keep-network/keep-core/pkg/tecdsa/signing" ) @@ -493,13 +494,7 @@ func (n *node) getInactivityClaimExecutor( return nil, false, fmt.Errorf("failed to get broadcast channel: [%v]", err) } - // TODO: Handle unmarshallers - - // signing.RegisterUnmarshallers(broadcastChannel) - // announcer.RegisterUnmarshaller(broadcastChannel) - // broadcastChannel.SetUnmarshaler(func() net.TaggedUnmarshaler { - // return &signingDoneMessage{} - // }) + inactivity.RegisterUnmarshallers(broadcastChannel) membershipValidator := group.NewMembershipValidator( executorLogger, diff --git a/pkg/tecdsa/inactivity/inactivity.go b/pkg/tecdsa/inactivity/inactivity.go index 67e0509e2b..8a5bc17b04 100644 --- a/pkg/tecdsa/inactivity/inactivity.go +++ b/pkg/tecdsa/inactivity/inactivity.go @@ -77,3 +77,12 @@ func Publish( return nil } + +// RegisterUnmarshallers initializes the given broadcast channel to be able to +// perform inactivity claim interactions by registering all the required +// protocol message unmarshallers. +func RegisterUnmarshallers(channel net.BroadcastChannel) { + channel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &claimSignatureMessage{} + }) +} From 9b1786a4738bf0007fff775d9f42114e24ae026c Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 2 May 2024 17:59:56 +0200 Subject: [PATCH 12/40] Updated package structure --- pkg/chain/ethereum/tbtc.go | 2 +- pkg/{tecdsa => protocol}/inactivity/claim.go | 0 pkg/{tecdsa => protocol}/inactivity/gen/pb/message.pb.go | 0 pkg/{tecdsa => protocol}/inactivity/gen/pb/message.proto | 0 pkg/{tecdsa => protocol}/inactivity/inactivity.go | 0 pkg/{tecdsa => protocol}/inactivity/marshalling.go | 2 +- pkg/{tecdsa => protocol}/inactivity/marshalling_test.go | 0 pkg/{tecdsa => protocol}/inactivity/member.go | 0 pkg/{tecdsa => protocol}/inactivity/message.go | 0 pkg/{tecdsa => protocol}/inactivity/states.go | 0 pkg/tbtc/chain.go | 2 +- pkg/tbtc/chain_test.go | 2 +- pkg/tbtc/inactivity.go | 3 +-- pkg/tbtc/inactivity_submit.go | 2 +- pkg/tbtc/node.go | 2 +- 15 files changed, 7 insertions(+), 8 deletions(-) rename pkg/{tecdsa => protocol}/inactivity/claim.go (100%) rename pkg/{tecdsa => protocol}/inactivity/gen/pb/message.pb.go (100%) rename pkg/{tecdsa => protocol}/inactivity/gen/pb/message.proto (100%) rename pkg/{tecdsa => protocol}/inactivity/inactivity.go (100%) rename pkg/{tecdsa => protocol}/inactivity/marshalling.go (95%) rename pkg/{tecdsa => protocol}/inactivity/marshalling_test.go (100%) rename pkg/{tecdsa => protocol}/inactivity/member.go (100%) rename pkg/{tecdsa => protocol}/inactivity/message.go (100%) rename pkg/{tecdsa => protocol}/inactivity/states.go (100%) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index e17151b89d..65146c71e7 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -25,10 +25,10 @@ import ( "github.com/keep-network/keep-core/pkg/internal/byteutils" "github.com/keep-network/keep-core/pkg/operator" "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/protocol/inactivity" "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tbtc" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" - "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) // Definitions of contract names. diff --git a/pkg/tecdsa/inactivity/claim.go b/pkg/protocol/inactivity/claim.go similarity index 100% rename from pkg/tecdsa/inactivity/claim.go rename to pkg/protocol/inactivity/claim.go diff --git a/pkg/tecdsa/inactivity/gen/pb/message.pb.go b/pkg/protocol/inactivity/gen/pb/message.pb.go similarity index 100% rename from pkg/tecdsa/inactivity/gen/pb/message.pb.go rename to pkg/protocol/inactivity/gen/pb/message.pb.go diff --git a/pkg/tecdsa/inactivity/gen/pb/message.proto b/pkg/protocol/inactivity/gen/pb/message.proto similarity index 100% rename from pkg/tecdsa/inactivity/gen/pb/message.proto rename to pkg/protocol/inactivity/gen/pb/message.proto diff --git a/pkg/tecdsa/inactivity/inactivity.go b/pkg/protocol/inactivity/inactivity.go similarity index 100% rename from pkg/tecdsa/inactivity/inactivity.go rename to pkg/protocol/inactivity/inactivity.go diff --git a/pkg/tecdsa/inactivity/marshalling.go b/pkg/protocol/inactivity/marshalling.go similarity index 95% rename from pkg/tecdsa/inactivity/marshalling.go rename to pkg/protocol/inactivity/marshalling.go index 8e7b568c2c..57c6dbca1a 100644 --- a/pkg/tecdsa/inactivity/marshalling.go +++ b/pkg/protocol/inactivity/marshalling.go @@ -6,7 +6,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa/inactivity/gen/pb" + "github.com/keep-network/keep-core/pkg/protocol/inactivity/gen/pb" ) func validateMemberIndex(protoIndex uint32) error { diff --git a/pkg/tecdsa/inactivity/marshalling_test.go b/pkg/protocol/inactivity/marshalling_test.go similarity index 100% rename from pkg/tecdsa/inactivity/marshalling_test.go rename to pkg/protocol/inactivity/marshalling_test.go diff --git a/pkg/tecdsa/inactivity/member.go b/pkg/protocol/inactivity/member.go similarity index 100% rename from pkg/tecdsa/inactivity/member.go rename to pkg/protocol/inactivity/member.go diff --git a/pkg/tecdsa/inactivity/message.go b/pkg/protocol/inactivity/message.go similarity index 100% rename from pkg/tecdsa/inactivity/message.go rename to pkg/protocol/inactivity/message.go diff --git a/pkg/tecdsa/inactivity/states.go b/pkg/protocol/inactivity/states.go similarity index 100% rename from pkg/tecdsa/inactivity/states.go rename to pkg/protocol/inactivity/states.go diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 7f2686fe10..fd548b7ffd 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -9,10 +9,10 @@ import ( "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/operator" "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/protocol/inactivity" "github.com/keep-network/keep-core/pkg/sortition" "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" - "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) type DKGState int diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index bf622e5f1c..da2efc4ec9 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -22,9 +22,9 @@ import ( "github.com/keep-network/keep-core/pkg/chain/local_v1" "github.com/keep-network/keep-core/pkg/operator" "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/protocol/inactivity" "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" - "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" ) const localChainOperatorID = chain.OperatorID(1) diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index 9c7c5c33e0..b09117abe9 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -15,7 +15,7 @@ import ( "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" - "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" + "github.com/keep-network/keep-core/pkg/protocol/inactivity" ) const ( @@ -47,7 +47,6 @@ type inactivityClaimExecutor struct { waitForBlockFn waitForBlockFn } -// TODO Consider moving all inactivity-related code to pkg/protocol/inactivity. func newInactivityClaimExecutor( chain Chain, signers []*signer, diff --git a/pkg/tbtc/inactivity_submit.go b/pkg/tbtc/inactivity_submit.go index 1dec255ee1..21879ed6d4 100644 --- a/pkg/tbtc/inactivity_submit.go +++ b/pkg/tbtc/inactivity_submit.go @@ -7,7 +7,7 @@ import ( "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" + "github.com/keep-network/keep-core/pkg/protocol/inactivity" ) // inactivityClaimSigner is responsible for signing the inactivity claim and diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 1bb1bdb8d0..9a6121ed93 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -18,7 +18,7 @@ import ( "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/announcer" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa/inactivity" + "github.com/keep-network/keep-core/pkg/protocol/inactivity" "github.com/keep-network/keep-core/pkg/tecdsa/signing" ) From 6c9fad6fe6c898f357d9e318078fce187f4a92ca Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 3 May 2024 14:08:31 +0200 Subject: [PATCH 13/40] Modified the required number of signatures during inactivity claim --- pkg/protocol/inactivity/member.go | 5 ++--- pkg/protocol/inactivity/states.go | 21 ++++++++------------- pkg/tbtc/inactivity.go | 20 +++++++++++++++++++- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/pkg/protocol/inactivity/member.go b/pkg/protocol/inactivity/member.go index e993a7a7c3..7720fc7112 100644 --- a/pkg/protocol/inactivity/member.go +++ b/pkg/protocol/inactivity/member.go @@ -37,9 +37,8 @@ func newSigningMember( sessionID string, ) *signingMember { return &signingMember{ - logger: logger, - memberIndex: memberIndex, - // TODO: Check is this is a correct way to create the group. + logger: logger, + memberIndex: memberIndex, group: group.NewGroup(dishonestThreshold, groupSize), membershipValidator: membershipValidator, sessionID: sessionID, diff --git a/pkg/protocol/inactivity/states.go b/pkg/protocol/inactivity/states.go index 6574cf79f9..ef97e482bb 100644 --- a/pkg/protocol/inactivity/states.go +++ b/pkg/protocol/inactivity/states.go @@ -80,19 +80,14 @@ func (css *claimSigningState) Receive(netMessage net.Message) error { } func (css *claimSigningState) CanTransition() bool { - // Although there is no hard requirement to expect signature messages - // from all participants, it makes sense to do so because this is an - // additional participant availability check that allows to maximize - // the final count of active participants. Moreover, this check does not - // bound the signing state to a fixed duration and one can move to the - // next state as soon as possible. - messagingDone := len(receivedMessages[*claimSignatureMessage](css.BaseAsyncState)) == - len(css.member.group.OperatingMemberIndexes())-1 - - // TODO: Modify the above code so that only 51 members are needed. Since it - // is executed after a failed heartbeat, we cannot expect all the - // members to sign the claim. In the future consider taking the number - // of active signers from the heartbeat procedure. + // Require the number of received signatures to be at least the honest + // threshold. Unlike in the case of DKG, we cannot expect all the members to + // participate in signing as we know we are dealing with some problem + // arising from operator inactivity. + // TODO: Consider passing the number of required signatures from the code + // that launched the inactivity operator execution. + messagingDone := len(receivedMessages[*claimSignatureMessage](css.BaseAsyncState)) >= + css.member.group.HonestThreshold() return messagingDone } diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index b09117abe9..937d02ada9 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -2,6 +2,7 @@ package tbtc import ( "context" + "errors" "fmt" "math/big" "sync" @@ -134,7 +135,7 @@ func (ice *inactivityClaimExecutor) publishClaim( ) defer cancelCtx() - ice.publish( + err := ice.publish( ctx, execLogger, message, @@ -148,6 +149,23 @@ func (ice *inactivityClaimExecutor) publishClaim( claim, ) + if err != nil { + if errors.Is(err, context.Canceled) { + execLogger.Infof( + "[member:%v] inactivity claim is no longer awaiting "+ + "publishing; aborting inactivity claim publishing", + signer.signingGroupMemberIndex, + ) + return + } + + execLogger.Errorf( + "[member:%v] inactivity claim publishing failed [%v]", + signer.signingGroupMemberIndex, + err, + ) + return + } }(currentSigner) } From 4db180404440b74f759559d7589c19f13cefdba8 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 3 May 2024 17:09:15 +0200 Subject: [PATCH 14/40] Fixed build problem --- Dockerfile | 1 + pkg/protocol/inactivity/gen/pb/message.pb.go | 93 ++++++++++---------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/Dockerfile b/Dockerfile index f2422c8d4f..4689baa439 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,7 @@ COPY ./pkg/tecdsa/dkg/gen $APP_DIR/pkg/tecdsa/dkg/gen COPY ./pkg/tecdsa/signing/gen $APP_DIR/pkg/tecdsa/signing/gen COPY ./pkg/tecdsa/gen $APP_DIR/pkg/tecdsa/gen COPY ./pkg/protocol/announcer/gen $APP_DIR/pkg/protocol/announcer/gen +COPY ./pkg/protocol/inactivity/gen $APP_DIR/pkg/protocol/inactivity/gen # Environment is to download published and tagged NPM packages versions. ARG ENVIRONMENT diff --git a/pkg/protocol/inactivity/gen/pb/message.pb.go b/pkg/protocol/inactivity/gen/pb/message.pb.go index ea4eed0654..2990d8e0e5 100644 --- a/pkg/protocol/inactivity/gen/pb/message.pb.go +++ b/pkg/protocol/inactivity/gen/pb/message.pb.go @@ -2,16 +2,15 @@ // versions: // protoc-gen-go v1.28.0 // protoc v3.19.4 -// source: pkg/tecdsa/inactivity/gen/pb/message.proto +// source: pkg/protocol/inactivity/gen/pb/message.proto package pb import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( @@ -36,7 +35,7 @@ type ClaimSignatureMessage struct { func (x *ClaimSignatureMessage) Reset() { *x = ClaimSignatureMessage{} if protoimpl.UnsafeEnabled { - mi := &file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes[0] + mi := &file_pkg_protocol_inactivity_gen_pb_message_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -49,7 +48,7 @@ func (x *ClaimSignatureMessage) String() string { func (*ClaimSignatureMessage) ProtoMessage() {} func (x *ClaimSignatureMessage) ProtoReflect() protoreflect.Message { - mi := &file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes[0] + mi := &file_pkg_protocol_inactivity_gen_pb_message_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -62,7 +61,7 @@ func (x *ClaimSignatureMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use ClaimSignatureMessage.ProtoReflect.Descriptor instead. func (*ClaimSignatureMessage) Descriptor() ([]byte, []int) { - return file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescGZIP(), []int{0} + return file_pkg_protocol_inactivity_gen_pb_message_proto_rawDescGZIP(), []int{0} } func (x *ClaimSignatureMessage) GetSenderID() uint32 { @@ -100,44 +99,44 @@ func (x *ClaimSignatureMessage) GetSessionID() string { return "" } -var File_pkg_tecdsa_inactivity_gen_pb_message_proto protoreflect.FileDescriptor - -var file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDesc = []byte{ - 0x0a, 0x2a, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2f, 0x69, 0x6e, 0x61, - 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x62, 0x2f, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x69, 0x6e, - 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x22, 0xab, 0x01, 0x0a, 0x15, 0x43, 0x6c, 0x61, - 0x69, 0x6d, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 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, 0x1c, - 0x0a, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x48, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, - 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, - 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +var File_pkg_protocol_inactivity_gen_pb_message_proto protoreflect.FileDescriptor + +var file_pkg_protocol_inactivity_gen_pb_message_proto_rawDesc = []byte{ + 0x0a, 0x2c, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, 0x69, + 0x6e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x62, + 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, + 0x69, 0x6e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x22, 0xab, 0x01, 0x0a, 0x15, 0x43, + 0x6c, 0x61, 0x69, 0x6d, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 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, 0x1c, 0x0a, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x48, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1c, + 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( - file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescOnce sync.Once - file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescData = file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDesc + file_pkg_protocol_inactivity_gen_pb_message_proto_rawDescOnce sync.Once + file_pkg_protocol_inactivity_gen_pb_message_proto_rawDescData = file_pkg_protocol_inactivity_gen_pb_message_proto_rawDesc ) -func file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescGZIP() []byte { - file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescOnce.Do(func() { - file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescData) +func file_pkg_protocol_inactivity_gen_pb_message_proto_rawDescGZIP() []byte { + file_pkg_protocol_inactivity_gen_pb_message_proto_rawDescOnce.Do(func() { + file_pkg_protocol_inactivity_gen_pb_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_protocol_inactivity_gen_pb_message_proto_rawDescData) }) - return file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDescData + return file_pkg_protocol_inactivity_gen_pb_message_proto_rawDescData } -var file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_pkg_tecdsa_inactivity_gen_pb_message_proto_goTypes = []interface{}{ +var file_pkg_protocol_inactivity_gen_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_pkg_protocol_inactivity_gen_pb_message_proto_goTypes = []interface{}{ (*ClaimSignatureMessage)(nil), // 0: inactivity.ClaimSignatureMessage } -var file_pkg_tecdsa_inactivity_gen_pb_message_proto_depIdxs = []int32{ +var file_pkg_protocol_inactivity_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 @@ -145,13 +144,13 @@ var file_pkg_tecdsa_inactivity_gen_pb_message_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for field type_name } -func init() { file_pkg_tecdsa_inactivity_gen_pb_message_proto_init() } -func file_pkg_tecdsa_inactivity_gen_pb_message_proto_init() { - if File_pkg_tecdsa_inactivity_gen_pb_message_proto != nil { +func init() { file_pkg_protocol_inactivity_gen_pb_message_proto_init() } +func file_pkg_protocol_inactivity_gen_pb_message_proto_init() { + if File_pkg_protocol_inactivity_gen_pb_message_proto != nil { return } if !protoimpl.UnsafeEnabled { - file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_pkg_protocol_inactivity_gen_pb_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ClaimSignatureMessage); i { case 0: return &v.state @@ -168,18 +167,18 @@ func file_pkg_tecdsa_inactivity_gen_pb_message_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDesc, + RawDescriptor: file_pkg_protocol_inactivity_gen_pb_message_proto_rawDesc, NumEnums: 0, NumMessages: 1, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_pkg_tecdsa_inactivity_gen_pb_message_proto_goTypes, - DependencyIndexes: file_pkg_tecdsa_inactivity_gen_pb_message_proto_depIdxs, - MessageInfos: file_pkg_tecdsa_inactivity_gen_pb_message_proto_msgTypes, + GoTypes: file_pkg_protocol_inactivity_gen_pb_message_proto_goTypes, + DependencyIndexes: file_pkg_protocol_inactivity_gen_pb_message_proto_depIdxs, + MessageInfos: file_pkg_protocol_inactivity_gen_pb_message_proto_msgTypes, }.Build() - File_pkg_tecdsa_inactivity_gen_pb_message_proto = out.File - file_pkg_tecdsa_inactivity_gen_pb_message_proto_rawDesc = nil - file_pkg_tecdsa_inactivity_gen_pb_message_proto_goTypes = nil - file_pkg_tecdsa_inactivity_gen_pb_message_proto_depIdxs = nil + File_pkg_protocol_inactivity_gen_pb_message_proto = out.File + file_pkg_protocol_inactivity_gen_pb_message_proto_rawDesc = nil + file_pkg_protocol_inactivity_gen_pb_message_proto_goTypes = nil + file_pkg_protocol_inactivity_gen_pb_message_proto_depIdxs = nil } From c4b504e97ffb0d1eed733418a1638fba43711035 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Fri, 3 May 2024 18:10:10 +0200 Subject: [PATCH 15/40] Fixed issues related to launching inactivity submission --- pkg/chain/ethereum/tbtc.go | 20 ++++++++++++++++++++ pkg/tbtc/chain.go | 19 +++++++++++++++++-- pkg/tbtc/chain_test.go | 6 ++++++ pkg/tbtc/inactivity.go | 31 +++++++++++++++++++++++++++---- pkg/tbtc/inactivity_submit.go | 2 +- pkg/tbtc/node.go | 3 ++- 6 files changed, 73 insertions(+), 8 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 65146c71e7..99af445809 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -994,6 +994,26 @@ func (tc *TbtcChain) DKGParameters() (*tbtc.DKGParameters, error) { }, nil } +func (tc *TbtcChain) OnInactivityClaimed( + handler func(event *tbtc.InactivityClaimedEvent), +) subscription.EventSubscription { + onEvent := func( + walletID [32]byte, + nonce *big.Int, + notifier common.Address, + blockNumber uint64, + ) { + handler(&tbtc.InactivityClaimedEvent{ + WalletID: walletID, + Nonce: nonce, + Notifier: chain.Address(notifier.Hex()), + BlockNumber: blockNumber, + }) + } + + return tc.walletRegistry.InactivityClaimedEvent(nil, nil).OnEvent(onEvent) +} + func (tc *TbtcChain) AssembleInactivityClaim( walletID [32]byte, inactiveMembersIndices []group.MemberIndex, diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index fd548b7ffd..f156ae68d4 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -121,6 +121,15 @@ type DistributedKeyGenerationChain interface { DKGParameters() (*DKGParameters, error) } +// InactivityClaimedEvent represents an inactivity claimed event. It is emitted +// after a submitted inactivity claim lands on the chain. +type InactivityClaimedEvent struct { + WalletID [32]byte + Nonce *big.Int + Notifier chain.Address + BlockNumber uint64 +} + // InactivityChainClaim represents an inactivity claim submitted to the chain. type InactivityChainClaim struct { WalletID [32]byte @@ -131,8 +140,14 @@ type InactivityChainClaim struct { } type InactivityClaimChain interface { - // AssembleDKGResult assembles the inactivity chain claim according to the - // rules expected by the given chain. + // OnInactivityClaimed registers a callback that is invoked when an on-chain + // notification of the inactivity claim submission is seen. + OnInactivityClaimed( + func(event *InactivityClaimedEvent), + ) subscription.EventSubscription + + // AssembleInactivityClaim assembles the inactivity chain claim according to + // the rules expected by the given chain. AssembleInactivityClaim( walletID [32]byte, inactiveMembersIndices []group.MemberIndex, diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index da2efc4ec9..f4b7463243 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -552,6 +552,12 @@ func (lc *localChain) DKGParameters() (*DKGParameters, error) { }, nil } +func (lc *localChain) OnInactivityClaimed( + func(event *InactivityClaimedEvent), +) subscription.EventSubscription { + panic("unsupported") +} + func (lc *localChain) AssembleInactivityClaim( walletID [32]byte, inactiveMembersIndices []group.MemberIndex, diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index 937d02ada9..fd5a175457 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -88,8 +88,12 @@ func (ice *inactivityClaimExecutor) publishClaim( return fmt.Errorf("cannot marshal wallet public key: [%v]", err) } + timeoutBlock := startBlock + inactivityClaimMaximumSubmissionBlocks + execLogger := logger.With( zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), + zap.Uint64("inactivityClaimStartBlock", startBlock), + zap.Uint64("inactivityClaimTimeoutBlock", timeoutBlock), ) walletRegistryData, err := ice.chain.GetWallet(walletPublicKeyHash) @@ -125,16 +129,36 @@ func (ice *inactivityClaimExecutor) publishClaim( defer wg.Done() - inactivityClaimTimeoutBlock := startBlock + inactivityClaimMaximumSubmissionBlocks - go func(signer *signer) { + execLogger.Info( + "[member:%v] starting inactivity claim publishing", + signer.signingGroupMemberIndex, + ) + ctx, cancelCtx := withCancelOnBlock( context.Background(), - inactivityClaimTimeoutBlock, + timeoutBlock, ice.waitForBlockFn, ) defer cancelCtx() + subscription := ice.chain.OnInactivityClaimed( + func(event *InactivityClaimedEvent) { + defer cancelCtx() + + execLogger.Infof( + "[member:%v] Inactivity claim submitted for wallet "+ + "with ID [0x%x] and nonce [%v] by notifier [%v] "+ + "at block [%v]", + signer.signingGroupMemberIndex, + event.WalletID, + event.Nonce, + event.Notifier, + event.BlockNumber, + ) + }) + defer subscription.Unsubscribe() + err := ice.publish( ctx, execLogger, @@ -148,7 +172,6 @@ func (ice *inactivityClaimExecutor) publishClaim( ice.membershipValidator, claim, ) - if err != nil { if errors.Is(err, context.Canceled) { execLogger.Infof( diff --git a/pkg/tbtc/inactivity_submit.go b/pkg/tbtc/inactivity_submit.go index 21879ed6d4..2058142948 100644 --- a/pkg/tbtc/inactivity_submit.go +++ b/pkg/tbtc/inactivity_submit.go @@ -177,7 +177,7 @@ func (ics *inactivityClaimSubmitter) SubmitClaim( err = ics.waitForBlockFn(ctx, submissionBlock) if err != nil { return fmt.Errorf( - "error while waiting for DKG result submission block: [%v]", + "error while waiting for inactivity claim submission block: [%v]", err, ) } diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 9a6121ed93..f916ae4159 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -134,6 +134,7 @@ func newNode( protocolLatch: latch, heartbeatFailureCounters: make(map[string]*uint), signingExecutors: make(map[string]*signingExecutor), + inactivityClaimExecutors: make(map[string]*inactivityClaimExecutor), coordinationExecutors: make(map[string]*coordinationExecutor), proposalGenerator: proposalGenerator, } @@ -512,7 +513,7 @@ func (n *node) getInactivityClaimExecutor( } executorLogger.Infof( - "signing executor created; controlling [%v] signers", + "inactivity executor created; controlling [%v] signers", len(signers), ) From ae51571c43bc4d40bb8ffa0a17b9ce223266c4d2 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 6 May 2024 16:40:47 +0200 Subject: [PATCH 16/40] Improved logging --- pkg/tbtc/heartbeat.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 30c9d3d1b4..c1f9cf4e7e 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -161,16 +161,13 @@ func (ha *heartbeatAction) execute() error { // If there was an error or the number of active operators during signing // was not enough, we must consider the heartbeat procedure as a failure. - if err != nil { - logger.Infof("error while generating heartbeat signature: [%v]", err) - } else { - logger.Infof( - "not enough active operators during signing; required [%d]: "+ - "actual [%d]", - activeOperatorsCount, - heartbeatSigningMinimumActiveOperators, - ) - } + ha.logger.Warnf( + "heartbeat failed; [%d/%d] operators participated; the process "+ + "returned [%v] as error", + activeOperatorsCount, + heartbeatSigningMinimumActiveOperators, + err, + ) // Increment the heartbeat failure counter. *ha.failureCounter++ From 2f3f7df5a6692e0f5bd773ef5ec530369ba6f7c6 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 6 May 2024 16:47:59 +0200 Subject: [PATCH 17/40] Improved logging --- pkg/tbtc/heartbeat.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index c1f9cf4e7e..570ddecb6c 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -109,7 +109,7 @@ func (ha *heartbeatAction) execute() error { } if isUnstaking { - logger.Warn( + ha.logger.Warn( "quitting the heartbeat action without signing because the " + "operator is unstaking", ) @@ -147,7 +147,7 @@ func (ha *heartbeatAction) execute() error { // If there was no error and the number of active operators during signing // was enough, we can consider the heartbeat procedure as successful. if err == nil && activeOperatorsCount >= heartbeatSigningMinimumActiveOperators { - logger.Infof( + ha.logger.Infof( "successfully generated signature [%s] for heartbeat message [0x%x]", signature, ha.proposal.Message[:], @@ -175,7 +175,7 @@ func (ha *heartbeatAction) execute() error { // If the number of consecutive heartbeat failures does not exceed the // threshold do not notify about operator inactivity. if *ha.failureCounter < heartbeatConsecutiveFailureThreshold { - logger.Infof( + ha.logger.Warnf( "leaving without notifying about operator inactivity; current "+ "heartbeat failure count is [%d]", *ha.failureCounter, From 2695f771739aa688487a89ce29122765342709de Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 6 May 2024 19:18:32 +0200 Subject: [PATCH 18/40] Refactored wallet heartbeat failure counters --- pkg/tbtc/heartbeat.go | 22 +++-- pkg/tbtc/heartbeat_failure.go | 41 +++++++++ pkg/tbtc/heartbeat_failure_test.go | 128 +++++++++++++++++++++++++++++ pkg/tbtc/heartbeat_test.go | 8 +- pkg/tbtc/node.go | 41 ++------- pkg/tbtc/node_test.go | 88 -------------------- 6 files changed, 193 insertions(+), 135 deletions(-) create mode 100644 pkg/tbtc/heartbeat_failure.go create mode 100644 pkg/tbtc/heartbeat_failure_test.go diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 570ddecb6c..7b02f51061 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -2,6 +2,7 @@ package tbtc import ( "context" + "encoding/hex" "fmt" "math/big" @@ -65,7 +66,7 @@ type heartbeatAction struct { signingExecutor heartbeatSigningExecutor proposal *HeartbeatProposal - failureCounter *uint + failureCounter *heartbeatFailureCounter inactivityClaimExecutor *inactivityClaimExecutor @@ -81,7 +82,7 @@ func newHeartbeatAction( executingWallet wallet, signingExecutor heartbeatSigningExecutor, proposal *HeartbeatProposal, - failureCounter *uint, + failureCounter *heartbeatFailureCounter, inactivityClaimExecutor *inactivityClaimExecutor, startBlock uint64, expiryBlock uint64, @@ -116,7 +117,14 @@ func (ha *heartbeatAction) execute() error { return nil } - walletPublicKeyHash := bitcoin.PublicKeyHash(ha.wallet().publicKey) + walletPublicKey := ha.wallet().publicKey + walletPublicKeyHash := bitcoin.PublicKeyHash(walletPublicKey) + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + return fmt.Errorf("failed to unmarshal wallet public key: [%v]", err) + } + + walletKey := hex.EncodeToString(walletPublicKeyBytes) err = ha.chain.ValidateHeartbeatProposal(walletPublicKeyHash, ha.proposal) if err != nil { @@ -154,7 +162,7 @@ func (ha *heartbeatAction) execute() error { ) // Reset the counter for consecutive heartbeat failure. - *ha.failureCounter = 0 + ha.failureCounter.reset(walletKey) return nil } @@ -170,15 +178,15 @@ func (ha *heartbeatAction) execute() error { ) // Increment the heartbeat failure counter. - *ha.failureCounter++ + ha.failureCounter.increment(walletKey) // If the number of consecutive heartbeat failures does not exceed the // threshold do not notify about operator inactivity. - if *ha.failureCounter < heartbeatConsecutiveFailureThreshold { + if ha.failureCounter.get(walletKey) < heartbeatConsecutiveFailureThreshold { ha.logger.Warnf( "leaving without notifying about operator inactivity; current "+ "heartbeat failure count is [%d]", - *ha.failureCounter, + ha.failureCounter.get(walletKey), ) return nil } diff --git a/pkg/tbtc/heartbeat_failure.go b/pkg/tbtc/heartbeat_failure.go new file mode 100644 index 0000000000..f00458fa86 --- /dev/null +++ b/pkg/tbtc/heartbeat_failure.go @@ -0,0 +1,41 @@ +package tbtc + +import ( + "sync" +) + +// heartbeatFailureCounter holds counters keeping track of consecutive +// heartbeat failures. Each wallet has a separate counter. The key used in +// the map is the uncompressed public key (with 04 prefix) of the wallet. +type heartbeatFailureCounter struct { + mutex sync.Mutex + counters map[string]uint +} + +func newHeartbeatFailureCounter() *heartbeatFailureCounter { + return &heartbeatFailureCounter{ + counters: make(map[string]uint), + } +} + +func (hfc *heartbeatFailureCounter) increment(walletPublicKey string) { + hfc.mutex.Lock() + defer hfc.mutex.Unlock() + + hfc.counters[walletPublicKey]++ + +} + +func (hfc *heartbeatFailureCounter) reset(walletPublicKey string) { + hfc.mutex.Lock() + defer hfc.mutex.Unlock() + + hfc.counters[walletPublicKey] = 0 +} + +func (hfc *heartbeatFailureCounter) get(walletPublicKey string) uint { + hfc.mutex.Lock() + defer hfc.mutex.Unlock() + + return hfc.counters[walletPublicKey] +} diff --git a/pkg/tbtc/heartbeat_failure_test.go b/pkg/tbtc/heartbeat_failure_test.go new file mode 100644 index 0000000000..364ed60ddc --- /dev/null +++ b/pkg/tbtc/heartbeat_failure_test.go @@ -0,0 +1,128 @@ +package tbtc + +import ( + "crypto/ecdsa" + "encoding/hex" + "testing" + + "github.com/keep-network/keep-core/internal/testutils" +) + +func TestHeartbeatFailureCounter_Increment(t *testing.T) { + walletPublicKey := createMockSigner(t).wallet.publicKey + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + t.Fatal(t) + } + + heartbeatFailureCounter := newHeartbeatFailureCounter() + + counterKey := hex.EncodeToString(walletPublicKeyBytes) + + // Check first increment. + heartbeatFailureCounter.increment(counterKey) + count := heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 1, + uint64(count), + ) + + // Check second increment. + heartbeatFailureCounter.increment(counterKey) + count = heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 2, + uint64(count), + ) +} + +func TestHeartbeatFailureCounter_Reset(t *testing.T) { + walletPublicKey := createMockSigner(t).wallet.publicKey + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + t.Fatal(t) + } + + heartbeatFailureCounter := newHeartbeatFailureCounter() + + counterKey := hex.EncodeToString(walletPublicKeyBytes) + + // Check reset works as the first operation. + heartbeatFailureCounter.reset(counterKey) + count := heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 0, + uint64(count), + ) + + // Check reset works after an increment. + heartbeatFailureCounter.increment(counterKey) + heartbeatFailureCounter.reset(counterKey) + + count = heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 0, + uint64(count), + ) +} + +func TestHeartbeatFailureCounter_Get(t *testing.T) { + walletPublicKey := createMockSigner(t).wallet.publicKey + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + t.Fatal(t) + } + + heartbeatFailureCounter := newHeartbeatFailureCounter() + + counterKey := hex.EncodeToString(walletPublicKeyBytes) + + // Check get works as the first operation. + count := heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 0, + uint64(count), + ) + + // Check get works after an increment. + heartbeatFailureCounter.increment(counterKey) + count = heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 1, + uint64(count), + ) + + // Construct an arbitrary public key representing a different wallet. + x, y := walletPublicKey.Curve.Double(walletPublicKey.X, walletPublicKey.Y) + anotherWalletPublicKey := &ecdsa.PublicKey{ + Curve: walletPublicKey.Curve, + X: x, + Y: y, + } + anotherWalletPublicKeyBytes, err := marshalPublicKey(anotherWalletPublicKey) + if err != nil { + t.Fatal(t) + } + anotherCounterKey := hex.EncodeToString(anotherWalletPublicKeyBytes) + + // Check get works on another wallet. + count = heartbeatFailureCounter.get(anotherCounterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 0, + uint64(count), + ) +} diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index a8eec2b681..84bb40f747 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -30,7 +30,7 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { }, } - heartbeatFailureCounter := uint(0) + heartbeatFailureCounter := newHeartbeatFailureCounter() // sha256(sha256(messageToSign)) sha256d, err := hex.DecodeString("38d30dacec5083c902952ce99fc0287659ad0b1ca2086827a8e78b0bef2c8bc1") @@ -51,7 +51,7 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { }, mockExecutor, proposal, - &heartbeatFailureCounter, + heartbeatFailureCounter, inactivityClaimExecutor, startBlock, expiryBlock, @@ -98,7 +98,7 @@ func TestHeartbeatAction_SigningError(t *testing.T) { }, } - heartbeatFailureCounter := uint(0) + heartbeatFailureCounter := newHeartbeatFailureCounter() hostChain := Connect() hostChain.setHeartbeatProposalValidationResult(proposal, true) @@ -116,7 +116,7 @@ func TestHeartbeatAction_SigningError(t *testing.T) { }, mockExecutor, proposal, - &heartbeatFailureCounter, + heartbeatFailureCounter, inactivityClaimExecutor, startBlock, expiryBlock, diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index f916ae4159..1401f13992 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -66,11 +66,9 @@ type node struct { // dkgExecutor MUST NOT be used outside this struct. dkgExecutor *dkgExecutor - heartbeatFailureCountersMutex sync.Mutex - // heartbeatFailureCounters holds counters keeping track of consecutive - // heartbeat failures. Each wallet has a separate counter. The key used in - // the map is the uncompressed public key (with 04 prefix) of the wallet. - heartbeatFailureCounters map[string]*uint + // heartbeatFailureCounter stores the counters of consecutive heartbeat + // failures for each wallet. + heartbeatFailureCounter *heartbeatFailureCounter inactivityClaimExecutorMutex sync.Mutex // inactivityClaimExecutors is the cache holding inactivity claim executors @@ -132,7 +130,7 @@ func newNode( walletRegistry: walletRegistry, walletDispatcher: newWalletDispatcher(), protocolLatch: latch, - heartbeatFailureCounters: make(map[string]*uint), + heartbeatFailureCounter: newHeartbeatFailureCounter(), signingExecutors: make(map[string]*signingExecutor), inactivityClaimExecutors: make(map[string]*inactivityClaimExecutor), coordinationExecutors: make(map[string]*coordinationExecutor), @@ -229,29 +227,6 @@ func (n *node) validateDKG( n.dkgExecutor.executeDkgValidation(seed, submissionBlock, result, resultHash) } -func (n *node) getHeartbeatCounter( - walletPublicKey *ecdsa.PublicKey, -) (*uint, error) { - n.heartbeatFailureCountersMutex.Lock() - defer n.heartbeatFailureCountersMutex.Unlock() - - walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) - if err != nil { - return nil, fmt.Errorf("cannot marshal wallet public key: [%v]", err) - } - - counterKey := hex.EncodeToString(walletPublicKeyBytes) - - if counter, exists := n.heartbeatFailureCounters[counterKey]; exists { - return counter, nil - } - - counterInitialValue := new(uint) // The value is zero-initialized. - n.heartbeatFailureCounters[counterKey] = counterInitialValue - - return counterInitialValue, nil -} - // getSigningExecutor gets the signing executor responsible for executing // signing related to a specific wallet whose part is controlled by this node. // The second boolean return value indicates whether the node controls at least @@ -564,12 +539,6 @@ func (n *node) handleHeartbeatProposal( return } - heartbeatFailureCounter, err := n.getHeartbeatCounter(wallet.publicKey) - if err != nil { - logger.Errorf("cannot get heartbeat failure counter: [%v]", err) - return - } - inactivityClaimExecutor, ok, err := n.getInactivityClaimExecutor(wallet.publicKey) if err != nil { logger.Errorf("cannot get inactivity claim executor: [%v]", err) @@ -609,7 +578,7 @@ func (n *node) handleHeartbeatProposal( wallet, signingExecutor, proposal, - heartbeatFailureCounter, + n.heartbeatFailureCounter, inactivityClaimExecutor, startBlock, expiryBlock, diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index a3f516fba4..b9dbb01992 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -19,94 +19,6 @@ import ( "github.com/keep-network/keep-core/pkg/tecdsa" ) -func TestNode_GetHeartbeatCounter(t *testing.T) { - groupParameters := &GroupParameters{ - GroupSize: 5, - GroupQuorum: 4, - HonestThreshold: 3, - } - - localChain := Connect() - localProvider := local.Connect() - - signer := createMockSigner(t) - - // Populate the mock keystore with the mock signer's data. This is - // required to make the node controlling the signer's wallet. - keyStorePersistence := createMockKeyStorePersistence(t, signer) - - node, err := newNode( - groupParameters, - localChain, - newLocalBitcoinChain(), - localProvider, - keyStorePersistence, - &mockPersistenceHandle{}, - generator.StartScheduler(), - &mockCoordinationProposalGenerator{}, - Config{}, - ) - if err != nil { - t.Fatal(err) - } - - walletPublicKey := signer.wallet.publicKey - - testutils.AssertIntsEqual( - t, - "cache size", - 0, - len(node.heartbeatFailureCounters), - ) - - counter, err := node.getHeartbeatCounter(walletPublicKey) - if err != nil { - t.Fatal(err) - } - - testutils.AssertIntsEqual( - t, - "cache size", - 1, - len(node.heartbeatFailureCounters), - ) - - testutils.AssertUintsEqual(t, "counter value", 0, uint64(*counter)) - - // Increment the counter and check the value again - *counter++ - testutils.AssertUintsEqual(t, "counter value", 1, uint64(*counter)) - - // Construct an arbitrary public key representing a different wallet. - x, y := walletPublicKey.Curve.Double(walletPublicKey.X, walletPublicKey.Y) - anotherWalletPublicKey := &ecdsa.PublicKey{ - Curve: walletPublicKey.Curve, - X: x, - Y: y, - } - - anotherCounter, err := node.getHeartbeatCounter(anotherWalletPublicKey) - if err != nil { - t.Fatal(err) - } - - testutils.AssertIntsEqual( - t, - "cache size", - 2, - len(node.heartbeatFailureCounters), - ) - - testutils.AssertUintsEqual(t, "counter value", 0, uint64(*anotherCounter)) - - // Increment one counter and reset another. - *anotherCounter++ - *counter = 0 - - testutils.AssertUintsEqual(t, "counter value", 0, uint64(*counter)) - testutils.AssertUintsEqual(t, "counter value", 1, uint64(*anotherCounter)) -} - func TestNode_GetSigningExecutor(t *testing.T) { groupParameters := &GroupParameters{ GroupSize: 5, From 78960d24708a6606d455edd1027a01afd61fd068 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 7 May 2024 11:25:17 +0200 Subject: [PATCH 19/40] Improved inactivity-related logging --- pkg/protocol/inactivity/message.go | 4 ++-- pkg/tbtc/node.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/protocol/inactivity/message.go b/pkg/protocol/inactivity/message.go index cfedf2a663..b66b0c80af 100644 --- a/pkg/protocol/inactivity/message.go +++ b/pkg/protocol/inactivity/message.go @@ -4,9 +4,9 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/group" ) -const messageTypePrefix = "tecdsa_inactivity/" +const messageTypePrefix = "protocol_inactivity/" -// message holds common traits of all signing protocol messages. +// message holds common traits of all inactivity protocol messages. type message interface { // SenderID returns protocol-level identifier of the message sender. SenderID() group.MemberIndex diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 1401f13992..af0c37293f 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -460,7 +460,7 @@ func (n *node) getInactivityClaimExecutor( wallet := signers[0].wallet channelName := fmt.Sprintf( - "%s-%s", + "%s-%s-inactivity", ProtocolName, hex.EncodeToString(walletPublicKeyBytes), ) From e9e6b0fa9df8e703d669947aff4bfc21cdf9ba0f Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 7 May 2024 11:30:55 +0200 Subject: [PATCH 20/40] Adjusted received messages condition --- pkg/protocol/inactivity/states.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/protocol/inactivity/states.go b/pkg/protocol/inactivity/states.go index ef97e482bb..dcabe04e93 100644 --- a/pkg/protocol/inactivity/states.go +++ b/pkg/protocol/inactivity/states.go @@ -84,10 +84,8 @@ func (css *claimSigningState) CanTransition() bool { // threshold. Unlike in the case of DKG, we cannot expect all the members to // participate in signing as we know we are dealing with some problem // arising from operator inactivity. - // TODO: Consider passing the number of required signatures from the code - // that launched the inactivity operator execution. messagingDone := len(receivedMessages[*claimSignatureMessage](css.BaseAsyncState)) >= - css.member.group.HonestThreshold() + css.member.group.HonestThreshold()-1 return messagingDone } From c0ec7f7416bec6d6495bf2c3ba9b06ffdeaaef52 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 7 May 2024 11:41:45 +0200 Subject: [PATCH 21/40] Renames --- pkg/tbtc/inactivity.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index fd5a175457..58d34cd729 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -72,7 +72,7 @@ func newInactivityClaimExecutor( func (ice *inactivityClaimExecutor) publishClaim( inactiveMembersIndexes []group.MemberIndex, heartbeatFailed bool, - message *big.Int, + sessionId *big.Int, startBlock uint64, ) error { if lockAcquired := ice.lock.TryAcquire(1); !lockAcquired { @@ -115,7 +115,7 @@ func (ice *inactivityClaimExecutor) publishClaim( HeartbeatFailed: heartbeatFailed, } - groupMembers, err := ice.getWalletMembersInfo() + groupMembers, err := ice.getWalletOperatorsIDs() if err != nil { return fmt.Errorf("could not get wallet members info: [%v]", err) } @@ -162,7 +162,7 @@ func (ice *inactivityClaimExecutor) publishClaim( err := ice.publish( ctx, execLogger, - message, + sessionId, signer.signingGroupMemberIndex, wallet.groupSize(), wallet.groupDishonestThreshold( @@ -198,7 +198,7 @@ func (ice *inactivityClaimExecutor) publishClaim( return nil } -func (ice *inactivityClaimExecutor) getWalletMembersInfo() ([]uint32, error) { +func (ice *inactivityClaimExecutor) getWalletOperatorsIDs() ([]uint32, error) { // Cache mapping operator addresses to their wallet member IDs. It helps to // limit the number of calls to the ETH client if some operator addresses // occur on the list multiple times. From acd49ee1ec5326a0134c1554ea6c3f16be04dd9c Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 7 May 2024 11:54:23 +0200 Subject: [PATCH 22/40] Moved heartbeat failure counter --- pkg/tbtc/heartbeat.go | 37 +++++++++ pkg/tbtc/heartbeat_failure.go | 41 --------- pkg/tbtc/heartbeat_failure_test.go | 128 ----------------------------- pkg/tbtc/heartbeat_test.go | 120 +++++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 169 deletions(-) delete mode 100644 pkg/tbtc/heartbeat_failure.go delete mode 100644 pkg/tbtc/heartbeat_failure_test.go diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 7b02f51061..df5be6a933 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "math/big" + "sync" "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/bitcoin" @@ -220,3 +221,39 @@ func (ha *heartbeatAction) wallet() wallet { func (ha *heartbeatAction) actionType() WalletActionType { return ActionHeartbeat } + +// heartbeatFailureCounter holds counters keeping track of consecutive +// heartbeat failures. Each wallet has a separate counter. The key used in +// the map is the uncompressed public key (with 04 prefix) of the wallet. +type heartbeatFailureCounter struct { + mutex sync.Mutex + counters map[string]uint +} + +func newHeartbeatFailureCounter() *heartbeatFailureCounter { + return &heartbeatFailureCounter{ + counters: make(map[string]uint), + } +} + +func (hfc *heartbeatFailureCounter) increment(walletPublicKey string) { + hfc.mutex.Lock() + defer hfc.mutex.Unlock() + + hfc.counters[walletPublicKey]++ + +} + +func (hfc *heartbeatFailureCounter) reset(walletPublicKey string) { + hfc.mutex.Lock() + defer hfc.mutex.Unlock() + + hfc.counters[walletPublicKey] = 0 +} + +func (hfc *heartbeatFailureCounter) get(walletPublicKey string) uint { + hfc.mutex.Lock() + defer hfc.mutex.Unlock() + + return hfc.counters[walletPublicKey] +} diff --git a/pkg/tbtc/heartbeat_failure.go b/pkg/tbtc/heartbeat_failure.go deleted file mode 100644 index f00458fa86..0000000000 --- a/pkg/tbtc/heartbeat_failure.go +++ /dev/null @@ -1,41 +0,0 @@ -package tbtc - -import ( - "sync" -) - -// heartbeatFailureCounter holds counters keeping track of consecutive -// heartbeat failures. Each wallet has a separate counter. The key used in -// the map is the uncompressed public key (with 04 prefix) of the wallet. -type heartbeatFailureCounter struct { - mutex sync.Mutex - counters map[string]uint -} - -func newHeartbeatFailureCounter() *heartbeatFailureCounter { - return &heartbeatFailureCounter{ - counters: make(map[string]uint), - } -} - -func (hfc *heartbeatFailureCounter) increment(walletPublicKey string) { - hfc.mutex.Lock() - defer hfc.mutex.Unlock() - - hfc.counters[walletPublicKey]++ - -} - -func (hfc *heartbeatFailureCounter) reset(walletPublicKey string) { - hfc.mutex.Lock() - defer hfc.mutex.Unlock() - - hfc.counters[walletPublicKey] = 0 -} - -func (hfc *heartbeatFailureCounter) get(walletPublicKey string) uint { - hfc.mutex.Lock() - defer hfc.mutex.Unlock() - - return hfc.counters[walletPublicKey] -} diff --git a/pkg/tbtc/heartbeat_failure_test.go b/pkg/tbtc/heartbeat_failure_test.go deleted file mode 100644 index 364ed60ddc..0000000000 --- a/pkg/tbtc/heartbeat_failure_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package tbtc - -import ( - "crypto/ecdsa" - "encoding/hex" - "testing" - - "github.com/keep-network/keep-core/internal/testutils" -) - -func TestHeartbeatFailureCounter_Increment(t *testing.T) { - walletPublicKey := createMockSigner(t).wallet.publicKey - walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) - if err != nil { - t.Fatal(t) - } - - heartbeatFailureCounter := newHeartbeatFailureCounter() - - counterKey := hex.EncodeToString(walletPublicKeyBytes) - - // Check first increment. - heartbeatFailureCounter.increment(counterKey) - count := heartbeatFailureCounter.get(counterKey) - testutils.AssertUintsEqual( - t, - "counter value", - 1, - uint64(count), - ) - - // Check second increment. - heartbeatFailureCounter.increment(counterKey) - count = heartbeatFailureCounter.get(counterKey) - testutils.AssertUintsEqual( - t, - "counter value", - 2, - uint64(count), - ) -} - -func TestHeartbeatFailureCounter_Reset(t *testing.T) { - walletPublicKey := createMockSigner(t).wallet.publicKey - walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) - if err != nil { - t.Fatal(t) - } - - heartbeatFailureCounter := newHeartbeatFailureCounter() - - counterKey := hex.EncodeToString(walletPublicKeyBytes) - - // Check reset works as the first operation. - heartbeatFailureCounter.reset(counterKey) - count := heartbeatFailureCounter.get(counterKey) - testutils.AssertUintsEqual( - t, - "counter value", - 0, - uint64(count), - ) - - // Check reset works after an increment. - heartbeatFailureCounter.increment(counterKey) - heartbeatFailureCounter.reset(counterKey) - - count = heartbeatFailureCounter.get(counterKey) - testutils.AssertUintsEqual( - t, - "counter value", - 0, - uint64(count), - ) -} - -func TestHeartbeatFailureCounter_Get(t *testing.T) { - walletPublicKey := createMockSigner(t).wallet.publicKey - walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) - if err != nil { - t.Fatal(t) - } - - heartbeatFailureCounter := newHeartbeatFailureCounter() - - counterKey := hex.EncodeToString(walletPublicKeyBytes) - - // Check get works as the first operation. - count := heartbeatFailureCounter.get(counterKey) - testutils.AssertUintsEqual( - t, - "counter value", - 0, - uint64(count), - ) - - // Check get works after an increment. - heartbeatFailureCounter.increment(counterKey) - count = heartbeatFailureCounter.get(counterKey) - testutils.AssertUintsEqual( - t, - "counter value", - 1, - uint64(count), - ) - - // Construct an arbitrary public key representing a different wallet. - x, y := walletPublicKey.Curve.Double(walletPublicKey.X, walletPublicKey.Y) - anotherWalletPublicKey := &ecdsa.PublicKey{ - Curve: walletPublicKey.Curve, - X: x, - Y: y, - } - anotherWalletPublicKeyBytes, err := marshalPublicKey(anotherWalletPublicKey) - if err != nil { - t.Fatal(t) - } - anotherCounterKey := hex.EncodeToString(anotherWalletPublicKeyBytes) - - // Check get works on another wallet. - count = heartbeatFailureCounter.get(anotherCounterKey) - testutils.AssertUintsEqual( - t, - "counter value", - 0, - uint64(count), - ) -} diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index 84bb40f747..22334f535c 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -2,6 +2,7 @@ package tbtc import ( "context" + "crypto/ecdsa" "encoding/hex" "fmt" "math/big" @@ -139,6 +140,125 @@ func TestHeartbeatAction_SigningError(t *testing.T) { // ) } +func TestHeartbeatFailureCounter_Increment(t *testing.T) { + walletPublicKey := createMockSigner(t).wallet.publicKey + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + t.Fatal(t) + } + + heartbeatFailureCounter := newHeartbeatFailureCounter() + + counterKey := hex.EncodeToString(walletPublicKeyBytes) + + // Check first increment. + heartbeatFailureCounter.increment(counterKey) + count := heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 1, + uint64(count), + ) + + // Check second increment. + heartbeatFailureCounter.increment(counterKey) + count = heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 2, + uint64(count), + ) +} + +func TestHeartbeatFailureCounter_Reset(t *testing.T) { + walletPublicKey := createMockSigner(t).wallet.publicKey + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + t.Fatal(t) + } + + heartbeatFailureCounter := newHeartbeatFailureCounter() + + counterKey := hex.EncodeToString(walletPublicKeyBytes) + + // Check reset works as the first operation. + heartbeatFailureCounter.reset(counterKey) + count := heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 0, + uint64(count), + ) + + // Check reset works after an increment. + heartbeatFailureCounter.increment(counterKey) + heartbeatFailureCounter.reset(counterKey) + + count = heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 0, + uint64(count), + ) +} + +func TestHeartbeatFailureCounter_Get(t *testing.T) { + walletPublicKey := createMockSigner(t).wallet.publicKey + walletPublicKeyBytes, err := marshalPublicKey(walletPublicKey) + if err != nil { + t.Fatal(t) + } + + heartbeatFailureCounter := newHeartbeatFailureCounter() + + counterKey := hex.EncodeToString(walletPublicKeyBytes) + + // Check get works as the first operation. + count := heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 0, + uint64(count), + ) + + // Check get works after an increment. + heartbeatFailureCounter.increment(counterKey) + count = heartbeatFailureCounter.get(counterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 1, + uint64(count), + ) + + // Construct an arbitrary public key representing a different wallet. + x, y := walletPublicKey.Curve.Double(walletPublicKey.X, walletPublicKey.Y) + anotherWalletPublicKey := &ecdsa.PublicKey{ + Curve: walletPublicKey.Curve, + X: x, + Y: y, + } + anotherWalletPublicKeyBytes, err := marshalPublicKey(anotherWalletPublicKey) + if err != nil { + t.Fatal(t) + } + anotherCounterKey := hex.EncodeToString(anotherWalletPublicKeyBytes) + + // Check get works on another wallet. + count = heartbeatFailureCounter.get(anotherCounterKey) + testutils.AssertUintsEqual( + t, + "counter value", + 0, + uint64(count), + ) +} + type mockHeartbeatSigningExecutor struct { shouldFail bool From 6fb3bb7db3bf28fb2225929d09b07cf59927db39 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 7 May 2024 12:00:08 +0200 Subject: [PATCH 23/40] Simplified code --- pkg/protocol/inactivity/claim.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/protocol/inactivity/claim.go b/pkg/protocol/inactivity/claim.go index a582abe28b..95651e3596 100644 --- a/pkg/protocol/inactivity/claim.go +++ b/pkg/protocol/inactivity/claim.go @@ -49,7 +49,7 @@ func ClaimSignatureHashFromBytes(bytes []byte) (ClaimSignatureHash, error) { "bytes length is not equal %v", ClaimSignatureHashByteSize, ) } - copy(hash[:], bytes[:]) + copy(hash[:], bytes) return hash, nil } From e25efaa96deb4512b86b369fc1e2f67e857bae10 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 7 May 2024 12:01:24 +0200 Subject: [PATCH 24/40] Updated error message --- pkg/tbtc/inactivity_submit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tbtc/inactivity_submit.go b/pkg/tbtc/inactivity_submit.go index 2058142948..8ce93ecc8c 100644 --- a/pkg/tbtc/inactivity_submit.go +++ b/pkg/tbtc/inactivity_submit.go @@ -29,7 +29,7 @@ func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.Claim) ( error, ) { if claim == nil { - return nil, fmt.Errorf("result is nil") + return nil, fmt.Errorf("claim is nil") } claimHash, err := ics.chain.CalculateInactivityClaimSignatureHash(claim) From 278c2eb60841cd0c350a206dd2b936af60a5ce20 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 7 May 2024 18:48:41 +0200 Subject: [PATCH 25/40] Added unit tests for inactivity claim signer --- pkg/tbtc/chain_test.go | 15 +- pkg/tbtc/inactivity_submit_test.go | 242 +++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 pkg/tbtc/inactivity_submit_test.go diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index f4b7463243..e7291c5c0f 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -581,7 +581,20 @@ func (lc *localChain) SubmitInactivityClaim( func (lc *localChain) CalculateInactivityClaimSignatureHash( claim *inactivity.Claim, ) (inactivity.ClaimSignatureHash, error) { - panic("unsupported") + if claim.WalletPublicKey == nil { + return inactivity.ClaimSignatureHash{}, fmt.Errorf( + "wallet public key is nil", + ) + } + + encoded := fmt.Sprint( + claim.Nonce, + claim.WalletPublicKey, + claim.InactiveMembersIndexes, + claim.HeartbeatFailed, + ) + + return sha3.Sum256([]byte(encoded)), nil } func (lc *localChain) GetInactivityClaimNonce(walletID [32]byte) (*big.Int, error) { diff --git a/pkg/tbtc/inactivity_submit_test.go b/pkg/tbtc/inactivity_submit_test.go new file mode 100644 index 0000000000..16b66b4ffa --- /dev/null +++ b/pkg/tbtc/inactivity_submit_test.go @@ -0,0 +1,242 @@ +package tbtc + +import ( + "fmt" + "math/big" + "reflect" + "testing" + + "golang.org/x/crypto/sha3" + + "github.com/keep-network/keep-core/pkg/internal/tecdsatest" + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/protocol/inactivity" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestSignClaim_SigningSuccessful(t *testing.T) { + chain := Connect() + inactivityClaimSigner := newInactivityClaimSigner(chain) + + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + claim := &inactivity.Claim{ + Nonce: big.NewInt(5), + WalletPublicKey: privateKeyShare.PublicKey(), + InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, + HeartbeatFailed: true, + } + + signedClaim, err := inactivityClaimSigner.SignClaim(claim) + if err != nil { + t.Fatal(err) + } + + expectedPublicKey := chain.Signing().PublicKey() + if !reflect.DeepEqual( + expectedPublicKey, + signedClaim.PublicKey, + ) { + t.Errorf( + "unexpected public key\n"+ + "expected: %v\n"+ + "actual: %v\n", + expectedPublicKey, + signedClaim.PublicKey, + ) + } + + expectedInactivityClaimHash := inactivity.ClaimSignatureHash( + sha3.Sum256( + []byte(fmt.Sprint( + claim.Nonce, + claim.WalletPublicKey, + claim.InactiveMembersIndexes, + claim.HeartbeatFailed, + )), + ), + ) + if expectedInactivityClaimHash != signedClaim.ClaimHash { + t.Errorf( + "unexpected claim hash\n"+ + "expected: %v\n"+ + "actual: %v\n", + expectedInactivityClaimHash, + signedClaim.ClaimHash, + ) + } + + // Since signature is different on every run (even if the same private key + // and claim hash are used), simply verify if it's correct + signatureVerification, err := chain.Signing().Verify( + signedClaim.ClaimHash[:], + signedClaim.Signature, + ) + if err != nil { + t.Fatal(err) + } + + if !signatureVerification { + t.Errorf( + "Signature [0x%x] was not generated properly for the claim hash "+ + "[0x%x]", + signedClaim.Signature, + signedClaim.ClaimHash, + ) + } +} + +func TestSignClaim_ErrorDuringInactivityClaimHashCalculation(t *testing.T) { + chain := Connect() + inactivityClaimSigner := newInactivityClaimSigner(chain) + + // Use nil as the claim to cause hash calculation error. + _, err := inactivityClaimSigner.SignClaim(nil) + + expectedError := fmt.Errorf("claim is nil") + if !reflect.DeepEqual(expectedError, err) { + t.Errorf( + "unexpected error\nexpected: %v\nactual: %v\n", + expectedError, + err, + ) + } +} + +func TestVerifySignature_VerifySuccessful(t *testing.T) { + chain := Connect() + inactivityClaimSigner := newInactivityClaimSigner(chain) + + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + claim := &inactivity.Claim{ + Nonce: big.NewInt(5), + WalletPublicKey: privateKeyShare.PublicKey(), + InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, + HeartbeatFailed: true, + } + + signedClaim, err := inactivityClaimSigner.SignClaim(claim) + if err != nil { + t.Fatal(err) + } + + verificationSuccessful, err := inactivityClaimSigner.VerifySignature( + signedClaim, + ) + if err != nil { + t.Fatal(err) + } + + if !verificationSuccessful { + t.Fatal( + "Expected successful verification of signature, but it was " + + "unsuccessful", + ) + } +} + +func TestVerifySignature_VerifyFailure(t *testing.T) { + chain := Connect() + inactivityClaimSigner := newInactivityClaimSigner(chain) + + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + claim := &inactivity.Claim{ + Nonce: big.NewInt(5), + WalletPublicKey: privateKeyShare.PublicKey(), + InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, + HeartbeatFailed: true, + } + + signedClaim, err := inactivityClaimSigner.SignClaim(claim) + if err != nil { + t.Fatal(err) + } + + anotherClaim := &inactivity.Claim{ + Nonce: big.NewInt(6), + WalletPublicKey: privateKeyShare.PublicKey(), + InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, + HeartbeatFailed: true, + } + + anotherSignedClaim, err := inactivityClaimSigner.SignClaim(anotherClaim) + if err != nil { + t.Fatal(err) + } + + // Assign signature from another claim to cause a signature verification + // failure. + signedClaim.Signature = anotherSignedClaim.Signature + + verificationSuccessful, err := inactivityClaimSigner.VerifySignature( + signedClaim, + ) + if err != nil { + t.Fatal(err) + } + + if verificationSuccessful { + t.Fatal( + "Expected unsuccessful verification of signature, but it was " + + "successful", + ) + } +} + +func TestVerifySignature_VerifyError(t *testing.T) { + chain := Connect() + inactivityClaimSigner := newInactivityClaimSigner(chain) + + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + claim := &inactivity.Claim{ + Nonce: big.NewInt(5), + WalletPublicKey: privateKeyShare.PublicKey(), + InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, + HeartbeatFailed: true, + } + + signedClaim, err := inactivityClaimSigner.SignClaim(claim) + if err != nil { + t.Fatal(err) + } + + // Drop the last byte of the signature to cause an error during signature + // verification. + signedClaim.Signature = signedClaim.Signature[:len(signedClaim.Signature)-1] + + _, err = inactivityClaimSigner.VerifySignature(signedClaim) + + expectedError := fmt.Errorf( + "failed to unmarshal signature: [asn1: syntax error: data truncated]", + ) + if !reflect.DeepEqual(expectedError, err) { + t.Errorf( + "unexpected error\n"+ + "expected: [%+v]\n"+ + "actual: [%+v]", + expectedError, + err, + ) + } +} + +// TODO: Continue with unit tests. From 0e3b9e5a5e7bfe2b010ecf77509bdfe5536713a2 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 8 May 2024 11:30:49 +0200 Subject: [PATCH 26/40] Renamed variables related to inactivity claim --- pkg/chain/ethereum/tbtc.go | 38 ++++++++++----------- pkg/chain/ethereum/tbtc_test.go | 4 +-- pkg/protocol/inactivity/claim.go | 27 +++++++-------- pkg/protocol/inactivity/inactivity.go | 16 ++++----- pkg/protocol/inactivity/marshalling.go | 2 +- pkg/protocol/inactivity/marshalling_test.go | 2 +- pkg/protocol/inactivity/member.go | 8 ++--- pkg/protocol/inactivity/message.go | 2 +- pkg/protocol/inactivity/states.go | 6 ++-- pkg/tbtc/chain.go | 19 ++++++----- pkg/tbtc/chain_test.go | 12 +++---- pkg/tbtc/inactivity.go | 6 ++-- pkg/tbtc/inactivity_submit.go | 16 ++++----- pkg/tbtc/inactivity_submit_test.go | 12 +++---- 14 files changed, 85 insertions(+), 85 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 99af445809..2c1bc7cf30 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1020,7 +1020,7 @@ func (tc *TbtcChain) AssembleInactivityClaim( signatures map[group.MemberIndex][]byte, heartbeatFailed bool, ) ( - *tbtc.InactivityChainClaim, + *tbtc.InactivityClaim, error, ) { signingMemberIndices, signatureBytes, err := convertSignaturesToChainFormat( @@ -1039,7 +1039,7 @@ func (tc *TbtcChain) AssembleInactivityClaim( return inactiveMembersIndices[i] < inactiveMembersIndices[j] }) - return &tbtc.InactivityChainClaim{ + return &tbtc.InactivityClaim{ WalletID: walletID, InactiveMembersIndices: inactiveMembersIndices, HeartbeatFailed: heartbeatFailed, @@ -1051,7 +1051,7 @@ func (tc *TbtcChain) AssembleInactivityClaim( // convertInactivityClaimToAbiType converts the TBTC-specific inactivity claim // to the format applicable for the WalletRegistry ABI. func convertInactivityClaimToAbiType( - claim *tbtc.InactivityChainClaim, + claim *tbtc.InactivityClaim, ) ecdsaabi.EcdsaInactivityClaim { inactiveMembersIndices := make([]*big.Int, len(claim.InactiveMembersIndices)) for i, memberIndex := range claim.InactiveMembersIndices { @@ -1073,7 +1073,7 @@ func convertInactivityClaimToAbiType( } func (tc *TbtcChain) SubmitInactivityClaim( - claim *tbtc.InactivityChainClaim, + claim *tbtc.InactivityClaim, nonce *big.Int, groupMembers []uint32, ) error { @@ -1086,16 +1086,16 @@ func (tc *TbtcChain) SubmitInactivityClaim( return err } -func (tc *TbtcChain) CalculateInactivityClaimSignatureHash( - claim *inactivity.Claim, -) (inactivity.ClaimSignatureHash, error) { +func (tc *TbtcChain) CalculateInactivityClaimHash( + claim *inactivity.ClaimPreimage, +) (inactivity.ClaimHash, error) { walletPublicKeyBytes := elliptic.Marshal( claim.WalletPublicKey.Curve, claim.WalletPublicKey.X, claim.WalletPublicKey.Y, ) - // Crop the 04 prefix as the calculateInactivityClaimSignatureHash function - // expects an unprefixed 64-byte public key, + // Crop the 04 prefix as the calculateInactivityClaimHash function expects + // an unprefixed 64-byte public key, unprefixedGroupPublicKeyBytes := walletPublicKeyBytes[1:] // The indexes are already sorted. @@ -1109,7 +1109,7 @@ func (tc *TbtcChain) CalculateInactivityClaimSignatureHash( inactiveMembersIndexes[i] = big.NewInt(int64(index)) } - return calculateInactivityClaimSignatureHash( + return calculateInactivityClaimHash( tc.chainID, claim.Nonce, unprefixedGroupPublicKeyBytes, @@ -1118,36 +1118,36 @@ func (tc *TbtcChain) CalculateInactivityClaimSignatureHash( ) } -func calculateInactivityClaimSignatureHash( +func calculateInactivityClaimHash( chainID *big.Int, nonce *big.Int, walletPublicKey []byte, inactiveMembersIndexes []*big.Int, heartbeatFailed bool, -) (inactivity.ClaimSignatureHash, error) { +) (inactivity.ClaimHash, error) { publicKeySize := 64 if len(walletPublicKey) != publicKeySize { - return inactivity.ClaimSignatureHash{}, fmt.Errorf( + return inactivity.ClaimHash{}, fmt.Errorf( "wrong wallet public key length", ) } uint256Type, err := abi.NewType("uint256", "uint256", nil) if err != nil { - return inactivity.ClaimSignatureHash{}, err + return inactivity.ClaimHash{}, err } bytesType, err := abi.NewType("bytes", "bytes", nil) if err != nil { - return inactivity.ClaimSignatureHash{}, err + return inactivity.ClaimHash{}, err } uint256SliceType, err := abi.NewType("uint256[]", "uint256[]", nil) if err != nil { - return inactivity.ClaimSignatureHash{}, err + return inactivity.ClaimHash{}, err } boolType, err := abi.NewType("bool", "bool", nil) if err != nil { - return inactivity.ClaimSignatureHash{}, err + return inactivity.ClaimHash{}, err } bytes, err := abi.Arguments{ @@ -1164,10 +1164,10 @@ func calculateInactivityClaimSignatureHash( heartbeatFailed, ) if err != nil { - return inactivity.ClaimSignatureHash{}, err + return inactivity.ClaimHash{}, err } - return inactivity.ClaimSignatureHash(crypto.Keccak256Hash(bytes)), nil + return inactivity.ClaimHash(crypto.Keccak256Hash(bytes)), nil } func (tc *TbtcChain) GetInactivityClaimNonce( diff --git a/pkg/chain/ethereum/tbtc_test.go b/pkg/chain/ethereum/tbtc_test.go index 5f335bebdf..996e06a199 100644 --- a/pkg/chain/ethereum/tbtc_test.go +++ b/pkg/chain/ethereum/tbtc_test.go @@ -240,7 +240,7 @@ func TestCalculateDKGResultSignatureHash(t *testing.T) { ) } -func TestCalculateInactivityClaimSignatureHash(t *testing.T) { +func TestCalculateInactivityClaimHash(t *testing.T) { chainID := big.NewInt(31337) nonce := big.NewInt(3) @@ -258,7 +258,7 @@ func TestCalculateInactivityClaimSignatureHash(t *testing.T) { heartbeatFailed := true - hash, err := calculateInactivityClaimSignatureHash( + hash, err := calculateInactivityClaimHash( chainID, nonce, walletPublicKey, diff --git a/pkg/protocol/inactivity/claim.go b/pkg/protocol/inactivity/claim.go index 95651e3596..ee5f687e11 100644 --- a/pkg/protocol/inactivity/claim.go +++ b/pkg/protocol/inactivity/claim.go @@ -9,8 +9,8 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/group" ) -// Claim represents an inactivity claim. -type Claim struct { +// ClaimPreimage represents an inactivity claim preimage. +type ClaimPreimage struct { Nonce *big.Int WalletPublicKey *ecdsa.PublicKey InactiveMembersIndexes []group.MemberIndex @@ -20,7 +20,7 @@ type Claim struct { // GetInactiveMembersIndexes returns the indexes of inactive members. // The original slice is copied to avoid concurrency issues if the claim object // is shared between many goroutines. The returned indexes are sorted. -func (c *Claim) GetInactiveMembersIndexes() []group.MemberIndex { +func (c *ClaimPreimage) GetInactiveMembersIndexes() []group.MemberIndex { sortedIndexes := make([]group.MemberIndex, len(c.InactiveMembersIndexes)) copy(sortedIndexes, c.InactiveMembersIndexes) @@ -32,21 +32,20 @@ func (c *Claim) GetInactiveMembersIndexes() []group.MemberIndex { return sortedIndexes } -const ClaimSignatureHashByteSize = 32 +const ClaimHashByteSize = 32 -// ClaimSignatureHash is a signature hash of the inactivity claim. The hashing -// algorithm used depends on the client code. -type ClaimSignatureHash [ClaimSignatureHashByteSize]byte +// ClaimHash is a hash of the inactivity claim. The hashing algorithm used +// depends on the client code. +type ClaimHash [ClaimHashByteSize]byte -// ClaimSignatureHashFromBytes converts bytes slice to ClaimSignatureHash. -// It requires provided bytes slice size to be exactly -// ClaimSignatureHashByteSize. -func ClaimSignatureHashFromBytes(bytes []byte) (ClaimSignatureHash, error) { - var hash ClaimSignatureHash +// ClaimHashFromBytes converts bytes slice to ClaimHash. It requires provided +// bytes slice size to be exactly ClaimHashByteSize. +func ClaimHashFromBytes(bytes []byte) (ClaimHash, error) { + var hash ClaimHash - if len(bytes) != ClaimSignatureHashByteSize { + if len(bytes) != ClaimHashByteSize { return hash, fmt.Errorf( - "bytes length is not equal %v", ClaimSignatureHashByteSize, + "bytes length is not equal %v", ClaimHashByteSize, ) } copy(hash[:], bytes) diff --git a/pkg/protocol/inactivity/inactivity.go b/pkg/protocol/inactivity/inactivity.go index 8a5bc17b04..3e136f308d 100644 --- a/pkg/protocol/inactivity/inactivity.go +++ b/pkg/protocol/inactivity/inactivity.go @@ -11,30 +11,30 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/state" ) -// SignedClaim represents information pertaining to the process of signing +// SignedClaimHash represents information pertaining to the process of signing // an inactivity claim: the public key used during signing, the resulting // signature and the hash of the inactivity claim that was used during signing. -type SignedClaim struct { +type SignedClaimHash struct { PublicKey []byte Signature []byte - ClaimHash ClaimSignatureHash + ClaimHash ClaimHash } type ClaimSigner interface { - SignClaim(claim *Claim) (*SignedClaim, error) - VerifySignature(signedClaim *SignedClaim) (bool, error) + SignClaim(claim *ClaimPreimage) (*SignedClaimHash, error) + VerifySignature(signedClaim *SignedClaimHash) (bool, error) } type ClaimSubmitter interface { SubmitClaim( ctx context.Context, memberIndex group.MemberIndex, - claim *Claim, + claim *ClaimPreimage, signatures map[group.MemberIndex][]byte, ) error } -func Publish( +func PublishClaim( ctx context.Context, logger log.StandardLogger, sessionID string, @@ -45,7 +45,7 @@ func Publish( membershipValidator *group.MembershipValidator, claimSigner ClaimSigner, claimSubmitter ClaimSubmitter, - claim *Claim, + claim *ClaimPreimage, ) error { initialState := &claimSigningState{ BaseAsyncState: state.NewBaseAsyncState(), diff --git a/pkg/protocol/inactivity/marshalling.go b/pkg/protocol/inactivity/marshalling.go index 57c6dbca1a..f117f015a4 100644 --- a/pkg/protocol/inactivity/marshalling.go +++ b/pkg/protocol/inactivity/marshalling.go @@ -43,7 +43,7 @@ func (csm *claimSignatureMessage) Unmarshal(bytes []byte) error { } csm.senderID = group.MemberIndex(pbMsg.SenderID) - claimHash, err := ClaimSignatureHashFromBytes(pbMsg.ClaimHash) + claimHash, err := ClaimHashFromBytes(pbMsg.ClaimHash) if err != nil { return err } diff --git a/pkg/protocol/inactivity/marshalling_test.go b/pkg/protocol/inactivity/marshalling_test.go index 1b5ab43e09..5e0a2d771d 100644 --- a/pkg/protocol/inactivity/marshalling_test.go +++ b/pkg/protocol/inactivity/marshalling_test.go @@ -34,7 +34,7 @@ func TestFuzzClaimSignatureMessage_MarshalingRoundtrip(t *testing.T) { for i := 0; i < 10; i++ { var ( senderID group.MemberIndex - claimHash ClaimSignatureHash + claimHash ClaimHash signature []byte publicKey []byte sessionID string diff --git a/pkg/protocol/inactivity/member.go b/pkg/protocol/inactivity/member.go index 7720fc7112..9343f48ece 100644 --- a/pkg/protocol/inactivity/member.go +++ b/pkg/protocol/inactivity/member.go @@ -22,7 +22,7 @@ type signingMember struct { // this member is part of. sessionID string // Hash of inactivity claim preferred by the current participant. - preferredInactivityClaimHash ClaimSignatureHash + preferredInactivityClaimHash ClaimHash // Signature over preferredInactivityClaimHash calculated by the member. selfInactivityClaimSignature []byte } @@ -70,7 +70,7 @@ func (sm *signingMember) initializeSubmittingMember() *submittingMember { } func (sm *signingMember) signClaim( - claim *Claim, + claim *ClaimPreimage, claimSigner ClaimSigner, ) (*claimSignatureMessage, error) { signedClaim, err := claimSigner.SignClaim(claim) @@ -120,7 +120,7 @@ func (sm *signingMember) verifyInactivityClaimSignatures( // Check if the signature is valid. isValid, err := resultSigner.VerifySignature( - &SignedClaim{ + &SignedClaimHash{ ClaimHash: message.claimHash, Signature: message.signature, PublicKey: message.publicKey, @@ -165,7 +165,7 @@ type submittingMember struct { // to the provided claim submitter. func (sm *submittingMember) submitClaim( ctx context.Context, - claim *Claim, + claim *ClaimPreimage, signatures map[group.MemberIndex][]byte, claimSubmitter ClaimSubmitter, ) error { diff --git a/pkg/protocol/inactivity/message.go b/pkg/protocol/inactivity/message.go index b66b0c80af..693722251c 100644 --- a/pkg/protocol/inactivity/message.go +++ b/pkg/protocol/inactivity/message.go @@ -19,7 +19,7 @@ type message interface { type claimSignatureMessage struct { senderID group.MemberIndex - claimHash ClaimSignatureHash + claimHash ClaimHash signature []byte publicKey []byte sessionID string diff --git a/pkg/protocol/inactivity/states.go b/pkg/protocol/inactivity/states.go index dcabe04e93..d5d6a1059a 100644 --- a/pkg/protocol/inactivity/states.go +++ b/pkg/protocol/inactivity/states.go @@ -22,7 +22,7 @@ type claimSigningState struct { member *signingMember - claim *Claim + claim *ClaimPreimage } func (css *claimSigningState) Initiate(ctx context.Context) error { @@ -115,7 +115,7 @@ type signaturesVerificationState struct { member *signingMember - claim *Claim + claim *ClaimPreimage validSignatures map[group.MemberIndex][]byte } @@ -159,7 +159,7 @@ type claimSubmissionState struct { member *submittingMember - claim *Claim + claim *ClaimPreimage signatures map[group.MemberIndex][]byte } diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index f156ae68d4..ddcf0d93b5 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -130,8 +130,8 @@ type InactivityClaimedEvent struct { BlockNumber uint64 } -// InactivityChainClaim represents an inactivity claim submitted to the chain. -type InactivityChainClaim struct { +// InactivityClaim represents an inactivity claim submitted to the chain. +type InactivityClaim struct { WalletID [32]byte InactiveMembersIndices []group.MemberIndex HeartbeatFailed bool @@ -153,20 +153,21 @@ type InactivityClaimChain interface { inactiveMembersIndices []group.MemberIndex, signatures map[group.MemberIndex][]byte, heartbeatFailed bool, - ) (*InactivityChainClaim, error) + ) (*InactivityClaim, error) // SubmitInactivityClaim submits the inactivity claim to the chain. SubmitInactivityClaim( - claim *InactivityChainClaim, + claim *InactivityClaim, nonce *big.Int, groupMembers []uint32, ) error - // CalculateInactivityClaimSignatureHash calculates hash for the given - // inactivity claim. - CalculateInactivityClaimSignatureHash( - claim *inactivity.Claim, - ) (inactivity.ClaimSignatureHash, error) + // CalculateInactivityClaimHash calculates hash for the given inactivity + // claim. + CalculateInactivityClaimHash(claim *inactivity.ClaimPreimage) ( + inactivity.ClaimHash, + error, + ) // GetInactivityClaimNonce returns inactivity claim nonce for the given // wallet. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index e7291c5c0f..97eef9e0c8 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -564,25 +564,25 @@ func (lc *localChain) AssembleInactivityClaim( signatures map[group.MemberIndex][]byte, heartbeatFailed bool, ) ( - *InactivityChainClaim, + *InactivityClaim, error, ) { panic("unsupported") } func (lc *localChain) SubmitInactivityClaim( - claim *InactivityChainClaim, + claim *InactivityClaim, nonce *big.Int, groupMembers []uint32, ) error { panic("unsupported") } -func (lc *localChain) CalculateInactivityClaimSignatureHash( - claim *inactivity.Claim, -) (inactivity.ClaimSignatureHash, error) { +func (lc *localChain) CalculateInactivityClaimHash( + claim *inactivity.ClaimPreimage, +) (inactivity.ClaimHash, error) { if claim.WalletPublicKey == nil { - return inactivity.ClaimSignatureHash{}, fmt.Errorf( + return inactivity.ClaimHash{}, fmt.Errorf( "wallet public key is nil", ) } diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index 58d34cd729..e2a368e788 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -108,7 +108,7 @@ func (ice *inactivityClaimExecutor) publishClaim( return fmt.Errorf("could not get nonce for wallet: [%v]", err) } - claim := &inactivity.Claim{ + claim := &inactivity.ClaimPreimage{ Nonce: nonce, WalletPublicKey: wallet.publicKey, InactiveMembersIndexes: inactiveMembersIndexes, @@ -233,9 +233,9 @@ func (ice *inactivityClaimExecutor) publish( dishonestThreshold int, groupMembers []uint32, membershipValidator *group.MembershipValidator, - inactivityClaim *inactivity.Claim, + inactivityClaim *inactivity.ClaimPreimage, ) error { - return inactivity.Publish( + return inactivity.PublishClaim( ctx, inactivityLogger, seed.Text(16), diff --git a/pkg/tbtc/inactivity_submit.go b/pkg/tbtc/inactivity_submit.go index 8ce93ecc8c..063fed8690 100644 --- a/pkg/tbtc/inactivity_submit.go +++ b/pkg/tbtc/inactivity_submit.go @@ -24,15 +24,15 @@ func newInactivityClaimSigner( } } -func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.Claim) ( - *inactivity.SignedClaim, +func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.ClaimPreimage) ( + *inactivity.SignedClaimHash, error, ) { if claim == nil { return nil, fmt.Errorf("claim is nil") } - claimHash, err := ics.chain.CalculateInactivityClaimSignatureHash(claim) + claimHash, err := ics.chain.CalculateInactivityClaimHash(claim) if err != nil { return nil, fmt.Errorf( "inactivity claim hash calculation failed [%w]", @@ -50,7 +50,7 @@ func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.Claim) ( ) } - return &inactivity.SignedClaim{ + return &inactivity.SignedClaimHash{ PublicKey: signing.PublicKey(), Signature: signature, ClaimHash: claimHash, @@ -60,7 +60,7 @@ func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.Claim) ( // VerifySignature verifies if the signature was generated from the provided // inactivity claim using the provided public key. func (ics *inactivityClaimSigner) VerifySignature( - signedClaim *inactivity.SignedClaim, + signedClaim *inactivity.SignedClaimHash, ) ( bool, error, @@ -101,7 +101,7 @@ func newInactivityClaimSubmitter( func (ics *inactivityClaimSubmitter) SubmitClaim( ctx context.Context, memberIndex group.MemberIndex, - claim *inactivity.Claim, + claim *inactivity.ClaimPreimage, signatures map[group.MemberIndex][]byte, ) error { if len(signatures) < ics.groupParameters.HonestThreshold { @@ -142,7 +142,7 @@ func (ics *inactivityClaimSubmitter) SubmitClaim( return nil } - inactivityClaim, err := ics.chain.AssembleInactivityClaim( + chainClaim, err := ics.chain.AssembleInactivityClaim( ecdsaWalletID, claim.GetInactiveMembersIndexes(), signatures, @@ -202,7 +202,7 @@ func (ics *inactivityClaimSubmitter) SubmitClaim( ) return ics.chain.SubmitInactivityClaim( - inactivityClaim, + chainClaim, inactivityNonce, ics.groupMembers, ) diff --git a/pkg/tbtc/inactivity_submit_test.go b/pkg/tbtc/inactivity_submit_test.go index 16b66b4ffa..d30bccde24 100644 --- a/pkg/tbtc/inactivity_submit_test.go +++ b/pkg/tbtc/inactivity_submit_test.go @@ -24,7 +24,7 @@ func TestSignClaim_SigningSuccessful(t *testing.T) { } privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) - claim := &inactivity.Claim{ + claim := &inactivity.ClaimPreimage{ Nonce: big.NewInt(5), WalletPublicKey: privateKeyShare.PublicKey(), InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, @@ -50,7 +50,7 @@ func TestSignClaim_SigningSuccessful(t *testing.T) { ) } - expectedInactivityClaimHash := inactivity.ClaimSignatureHash( + expectedInactivityClaimHash := inactivity.ClaimHash( sha3.Sum256( []byte(fmt.Sprint( claim.Nonce, @@ -117,7 +117,7 @@ func TestVerifySignature_VerifySuccessful(t *testing.T) { } privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) - claim := &inactivity.Claim{ + claim := &inactivity.ClaimPreimage{ Nonce: big.NewInt(5), WalletPublicKey: privateKeyShare.PublicKey(), InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, @@ -154,7 +154,7 @@ func TestVerifySignature_VerifyFailure(t *testing.T) { } privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) - claim := &inactivity.Claim{ + claim := &inactivity.ClaimPreimage{ Nonce: big.NewInt(5), WalletPublicKey: privateKeyShare.PublicKey(), InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, @@ -166,7 +166,7 @@ func TestVerifySignature_VerifyFailure(t *testing.T) { t.Fatal(err) } - anotherClaim := &inactivity.Claim{ + anotherClaim := &inactivity.ClaimPreimage{ Nonce: big.NewInt(6), WalletPublicKey: privateKeyShare.PublicKey(), InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, @@ -207,7 +207,7 @@ func TestVerifySignature_VerifyError(t *testing.T) { } privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) - claim := &inactivity.Claim{ + claim := &inactivity.ClaimPreimage{ Nonce: big.NewInt(5), WalletPublicKey: privateKeyShare.PublicKey(), InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, From 4105cd1c1416d1c0395a8fe021f42e200506ffda Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 8 May 2024 12:56:11 +0200 Subject: [PATCH 27/40] Cleaned file structure for inactivity package --- pkg/protocol/inactivity/claim.go | 54 ----- pkg/protocol/inactivity/inactivity.go | 47 ++++ pkg/tbtc/inactivity.go | 198 +++++++++++++++++ pkg/tbtc/inactivity_submit.go | 209 ------------------ ...vity_submit_test.go => inactivity_test.go} | 0 5 files changed, 245 insertions(+), 263 deletions(-) delete mode 100644 pkg/protocol/inactivity/claim.go delete mode 100644 pkg/tbtc/inactivity_submit.go rename pkg/tbtc/{inactivity_submit_test.go => inactivity_test.go} (100%) diff --git a/pkg/protocol/inactivity/claim.go b/pkg/protocol/inactivity/claim.go deleted file mode 100644 index ee5f687e11..0000000000 --- a/pkg/protocol/inactivity/claim.go +++ /dev/null @@ -1,54 +0,0 @@ -package inactivity - -import ( - "crypto/ecdsa" - "fmt" - "math/big" - "sort" - - "github.com/keep-network/keep-core/pkg/protocol/group" -) - -// ClaimPreimage represents an inactivity claim preimage. -type ClaimPreimage struct { - Nonce *big.Int - WalletPublicKey *ecdsa.PublicKey - InactiveMembersIndexes []group.MemberIndex - HeartbeatFailed bool -} - -// GetInactiveMembersIndexes returns the indexes of inactive members. -// The original slice is copied to avoid concurrency issues if the claim object -// is shared between many goroutines. The returned indexes are sorted. -func (c *ClaimPreimage) GetInactiveMembersIndexes() []group.MemberIndex { - sortedIndexes := make([]group.MemberIndex, len(c.InactiveMembersIndexes)) - - copy(sortedIndexes, c.InactiveMembersIndexes) - - sort.Slice(sortedIndexes, func(i, j int) bool { - return sortedIndexes[i] < sortedIndexes[j] - }) - - return sortedIndexes -} - -const ClaimHashByteSize = 32 - -// ClaimHash is a hash of the inactivity claim. The hashing algorithm used -// depends on the client code. -type ClaimHash [ClaimHashByteSize]byte - -// ClaimHashFromBytes converts bytes slice to ClaimHash. It requires provided -// bytes slice size to be exactly ClaimHashByteSize. -func ClaimHashFromBytes(bytes []byte) (ClaimHash, error) { - var hash ClaimHash - - if len(bytes) != ClaimHashByteSize { - return hash, fmt.Errorf( - "bytes length is not equal %v", ClaimHashByteSize, - ) - } - copy(hash[:], bytes) - - return hash, nil -} diff --git a/pkg/protocol/inactivity/inactivity.go b/pkg/protocol/inactivity/inactivity.go index 3e136f308d..7a57a450c7 100644 --- a/pkg/protocol/inactivity/inactivity.go +++ b/pkg/protocol/inactivity/inactivity.go @@ -2,7 +2,10 @@ package inactivity import ( "context" + "crypto/ecdsa" "fmt" + "math/big" + "sort" "github.com/ipfs/go-log/v2" @@ -11,6 +14,50 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/state" ) +// ClaimPreimage represents an inactivity claim preimage. +type ClaimPreimage struct { + Nonce *big.Int + WalletPublicKey *ecdsa.PublicKey + InactiveMembersIndexes []group.MemberIndex + HeartbeatFailed bool +} + +// GetInactiveMembersIndexes returns the indexes of inactive members. +// The original slice is copied to avoid concurrency issues if the claim object +// is shared between many goroutines. The returned indexes are sorted. +func (c *ClaimPreimage) GetInactiveMembersIndexes() []group.MemberIndex { + sortedIndexes := make([]group.MemberIndex, len(c.InactiveMembersIndexes)) + + copy(sortedIndexes, c.InactiveMembersIndexes) + + sort.Slice(sortedIndexes, func(i, j int) bool { + return sortedIndexes[i] < sortedIndexes[j] + }) + + return sortedIndexes +} + +const ClaimHashByteSize = 32 + +// ClaimHash is a hash of the inactivity claim. The hashing algorithm used +// depends on the client code. +type ClaimHash [ClaimHashByteSize]byte + +// ClaimHashFromBytes converts bytes slice to ClaimHash. It requires provided +// bytes slice size to be exactly ClaimHashByteSize. +func ClaimHashFromBytes(bytes []byte) (ClaimHash, error) { + var hash ClaimHash + + if len(bytes) != ClaimHashByteSize { + return hash, fmt.Errorf( + "bytes length is not equal %v", ClaimHashByteSize, + ) + } + copy(hash[:], bytes) + + return hash, nil +} + // SignedClaimHash represents information pertaining to the process of signing // an inactivity claim: the public key used during signing, the resulting // signature and the hash of the inactivity claim that was used during signing. diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index e2a368e788..d1209de5c6 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -261,3 +261,201 @@ func (ice *inactivityClaimExecutor) wallet() wallet { // first signer. return ice.signers[0].wallet } + +// inactivityClaimSigner is responsible for signing the inactivity claim and +// verification of signatures generated by other group members. +type inactivityClaimSigner struct { + chain Chain +} + +func newInactivityClaimSigner( + chain Chain, +) *inactivityClaimSigner { + return &inactivityClaimSigner{ + chain: chain, + } +} + +func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.ClaimPreimage) ( + *inactivity.SignedClaimHash, + error, +) { + if claim == nil { + return nil, fmt.Errorf("claim is nil") + } + + claimHash, err := ics.chain.CalculateInactivityClaimHash(claim) + if err != nil { + return nil, fmt.Errorf( + "inactivity claim hash calculation failed [%w]", + err, + ) + } + + signing := ics.chain.Signing() + + signature, err := signing.Sign(claimHash[:]) + if err != nil { + return nil, fmt.Errorf( + "inactivity claim hash signing failed [%w]", + err, + ) + } + + return &inactivity.SignedClaimHash{ + PublicKey: signing.PublicKey(), + Signature: signature, + ClaimHash: claimHash, + }, nil +} + +// VerifySignature verifies if the signature was generated from the provided +// inactivity claim using the provided public key. +func (ics *inactivityClaimSigner) VerifySignature( + signedClaim *inactivity.SignedClaimHash, +) ( + bool, + error, +) { + return ics.chain.Signing().VerifyWithPublicKey( + signedClaim.ClaimHash[:], + signedClaim.Signature, + signedClaim.PublicKey, + ) +} + +type inactivityClaimSubmitter struct { + inactivityLogger log.StandardLogger + + chain Chain + groupParameters *GroupParameters + groupMembers []uint32 + + waitForBlockFn waitForBlockFn +} + +func newInactivityClaimSubmitter( + inactivityLogger log.StandardLogger, + chain Chain, + groupParameters *GroupParameters, + groupMembers []uint32, + waitForBlockFn waitForBlockFn, +) *inactivityClaimSubmitter { + return &inactivityClaimSubmitter{ + inactivityLogger: inactivityLogger, + chain: chain, + groupParameters: groupParameters, + groupMembers: groupMembers, + waitForBlockFn: waitForBlockFn, + } +} + +func (ics *inactivityClaimSubmitter) SubmitClaim( + ctx context.Context, + memberIndex group.MemberIndex, + claim *inactivity.ClaimPreimage, + signatures map[group.MemberIndex][]byte, +) error { + if len(signatures) < ics.groupParameters.HonestThreshold { + return fmt.Errorf( + "could not submit inactivity claim with [%v] signatures for "+ + "group honest threshold [%v]", + len(signatures), + ics.groupParameters.HonestThreshold, + ) + } + + // The inactivity nonce at the beginning of the execution process. + inactivityNonce := claim.Nonce + + walletPublicKeyHash := bitcoin.PublicKeyHash(claim.WalletPublicKey) + + walletRegistryData, err := ics.chain.GetWallet(walletPublicKeyHash) + if err != nil { + return fmt.Errorf("could not get registry data on wallet: [%v]", err) + } + + ecdsaWalletID := walletRegistryData.EcdsaWalletID + + currentNonce, err := ics.chain.GetInactivityClaimNonce( + ecdsaWalletID, + ) + if err != nil { + return fmt.Errorf("could not get nonce for wallet: [%v]", err) + } + + if currentNonce.Cmp(inactivityNonce) > 0 { + // Someone who was ahead of us in the queue submitted the claim. Giving up. + ics.inactivityLogger.Infof( + "[member:%v] inactivity claim already submitted; "+ + "aborting inactivity claim on-chain submission", + memberIndex, + ) + return nil + } + + chainClaim, err := ics.chain.AssembleInactivityClaim( + ecdsaWalletID, + claim.GetInactiveMembersIndexes(), + signatures, + claim.HeartbeatFailed, + ) + if err != nil { + return fmt.Errorf("could not assemble inactivity chain claim [%w]", err) + } + + blockCounter, err := ics.chain.BlockCounter() + if err != nil { + return err + } + + // We can't determine a common block at which the publication starts. + // However, all we want here is to ensure the members does not submit + // in the same time. This can be achieved by simply using the index-based + // delay starting from the current block. + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return fmt.Errorf("cannot get current block: [%v]", err) + } + delayBlocks := uint64(memberIndex-1) * inactivityClaimSubmissionDelayStepBlocks + submissionBlock := currentBlock + delayBlocks + + ics.inactivityLogger.Infof( + "[member:%v] waiting for block [%v] to submit inactivity claim", + memberIndex, + submissionBlock, + ) + + err = ics.waitForBlockFn(ctx, submissionBlock) + if err != nil { + return fmt.Errorf( + "error while waiting for inactivity claim submission block: [%v]", + err, + ) + } + + if ctx.Err() != nil { + // The context was cancelled by the upstream. Regardless of the cause, + // that means the inactivity execution is no longer awaiting the result, + // and we can safely return. + ics.inactivityLogger.Infof( + "[member:%v] inactivity execution is no longer awaiting the "+ + "result; aborting inactivity claim on-chain submission", + memberIndex, + ) + return nil + } + + ics.inactivityLogger.Infof( + "[member:%v] submitting inactivity claim with [%v] supporting "+ + "member signatures", + memberIndex, + len(signatures), + ) + + return ics.chain.SubmitInactivityClaim( + chainClaim, + inactivityNonce, + ics.groupMembers, + ) +} diff --git a/pkg/tbtc/inactivity_submit.go b/pkg/tbtc/inactivity_submit.go deleted file mode 100644 index 063fed8690..0000000000 --- a/pkg/tbtc/inactivity_submit.go +++ /dev/null @@ -1,209 +0,0 @@ -package tbtc - -import ( - "context" - "fmt" - - "github.com/ipfs/go-log/v2" - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/protocol/inactivity" -) - -// inactivityClaimSigner is responsible for signing the inactivity claim and -// verification of signatures generated by other group members. -type inactivityClaimSigner struct { - chain Chain -} - -func newInactivityClaimSigner( - chain Chain, -) *inactivityClaimSigner { - return &inactivityClaimSigner{ - chain: chain, - } -} - -func (ics *inactivityClaimSigner) SignClaim(claim *inactivity.ClaimPreimage) ( - *inactivity.SignedClaimHash, - error, -) { - if claim == nil { - return nil, fmt.Errorf("claim is nil") - } - - claimHash, err := ics.chain.CalculateInactivityClaimHash(claim) - if err != nil { - return nil, fmt.Errorf( - "inactivity claim hash calculation failed [%w]", - err, - ) - } - - signing := ics.chain.Signing() - - signature, err := signing.Sign(claimHash[:]) - if err != nil { - return nil, fmt.Errorf( - "inactivity claim hash signing failed [%w]", - err, - ) - } - - return &inactivity.SignedClaimHash{ - PublicKey: signing.PublicKey(), - Signature: signature, - ClaimHash: claimHash, - }, nil -} - -// VerifySignature verifies if the signature was generated from the provided -// inactivity claim using the provided public key. -func (ics *inactivityClaimSigner) VerifySignature( - signedClaim *inactivity.SignedClaimHash, -) ( - bool, - error, -) { - return ics.chain.Signing().VerifyWithPublicKey( - signedClaim.ClaimHash[:], - signedClaim.Signature, - signedClaim.PublicKey, - ) -} - -type inactivityClaimSubmitter struct { - inactivityLogger log.StandardLogger - - chain Chain - groupParameters *GroupParameters - groupMembers []uint32 - - waitForBlockFn waitForBlockFn -} - -func newInactivityClaimSubmitter( - inactivityLogger log.StandardLogger, - chain Chain, - groupParameters *GroupParameters, - groupMembers []uint32, - waitForBlockFn waitForBlockFn, -) *inactivityClaimSubmitter { - return &inactivityClaimSubmitter{ - inactivityLogger: inactivityLogger, - chain: chain, - groupParameters: groupParameters, - groupMembers: groupMembers, - waitForBlockFn: waitForBlockFn, - } -} - -func (ics *inactivityClaimSubmitter) SubmitClaim( - ctx context.Context, - memberIndex group.MemberIndex, - claim *inactivity.ClaimPreimage, - signatures map[group.MemberIndex][]byte, -) error { - if len(signatures) < ics.groupParameters.HonestThreshold { - return fmt.Errorf( - "could not submit inactivity claim with [%v] signatures for "+ - "group honest threshold [%v]", - len(signatures), - ics.groupParameters.HonestThreshold, - ) - } - - // The inactivity nonce at the beginning of the execution process. - inactivityNonce := claim.Nonce - - walletPublicKeyHash := bitcoin.PublicKeyHash(claim.WalletPublicKey) - - walletRegistryData, err := ics.chain.GetWallet(walletPublicKeyHash) - if err != nil { - return fmt.Errorf("could not get registry data on wallet: [%v]", err) - } - - ecdsaWalletID := walletRegistryData.EcdsaWalletID - - currentNonce, err := ics.chain.GetInactivityClaimNonce( - ecdsaWalletID, - ) - if err != nil { - return fmt.Errorf("could not get nonce for wallet: [%v]", err) - } - - if currentNonce.Cmp(inactivityNonce) > 0 { - // Someone who was ahead of us in the queue submitted the claim. Giving up. - ics.inactivityLogger.Infof( - "[member:%v] inactivity claim already submitted; "+ - "aborting inactivity claim on-chain submission", - memberIndex, - ) - return nil - } - - chainClaim, err := ics.chain.AssembleInactivityClaim( - ecdsaWalletID, - claim.GetInactiveMembersIndexes(), - signatures, - claim.HeartbeatFailed, - ) - if err != nil { - return fmt.Errorf("could not assemble inactivity chain claim [%w]", err) - } - - blockCounter, err := ics.chain.BlockCounter() - if err != nil { - return err - } - - // We can't determine a common block at which the publication starts. - // However, all we want here is to ensure the members does not submit - // in the same time. This can be achieved by simply using the index-based - // delay starting from the current block. - currentBlock, err := blockCounter.CurrentBlock() - if err != nil { - return fmt.Errorf("cannot get current block: [%v]", err) - } - delayBlocks := uint64(memberIndex-1) * inactivityClaimSubmissionDelayStepBlocks - submissionBlock := currentBlock + delayBlocks - - ics.inactivityLogger.Infof( - "[member:%v] waiting for block [%v] to submit inactivity claim", - memberIndex, - submissionBlock, - ) - - err = ics.waitForBlockFn(ctx, submissionBlock) - if err != nil { - return fmt.Errorf( - "error while waiting for inactivity claim submission block: [%v]", - err, - ) - } - - if ctx.Err() != nil { - // The context was cancelled by the upstream. Regardless of the cause, - // that means the inactivity execution is no longer awaiting the result, - // and we can safely return. - ics.inactivityLogger.Infof( - "[member:%v] inactivity execution is no longer awaiting the "+ - "result; aborting inactivity claim on-chain submission", - memberIndex, - ) - return nil - } - - ics.inactivityLogger.Infof( - "[member:%v] submitting inactivity claim with [%v] supporting "+ - "member signatures", - memberIndex, - len(signatures), - ) - - return ics.chain.SubmitInactivityClaim( - chainClaim, - inactivityNonce, - ics.groupMembers, - ) -} diff --git a/pkg/tbtc/inactivity_submit_test.go b/pkg/tbtc/inactivity_test.go similarity index 100% rename from pkg/tbtc/inactivity_submit_test.go rename to pkg/tbtc/inactivity_test.go From b2fcba0ad3fd07397312b04c820de1c7dcdf7daf Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 8 May 2024 15:01:37 +0200 Subject: [PATCH 28/40] Renamed functions related to publishing inactivity claim --- pkg/tbtc/heartbeat.go | 2 +- pkg/tbtc/inactivity.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index df5be6a933..c016da8a30 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -194,7 +194,7 @@ func (ha *heartbeatAction) execute() error { // The value of consecutive heartbeat failures exceeds the threshold. // Proceed with operator inactivity notification. - err = ha.inactivityClaimExecutor.publishClaim( + err = ha.inactivityClaimExecutor.claimInactivity( // Leave the list of inactive operators empty even if some operators // were inactive during signing heartbeat. The inactive operators could // simply be in the process of unstaking and therefore should not be diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index d1209de5c6..7e4ae0bf64 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -69,10 +69,10 @@ func newInactivityClaimExecutor( } } -func (ice *inactivityClaimExecutor) publishClaim( +func (ice *inactivityClaimExecutor) claimInactivity( inactiveMembersIndexes []group.MemberIndex, heartbeatFailed bool, - sessionId *big.Int, + sessionID *big.Int, startBlock uint64, ) error { if lockAcquired := ice.lock.TryAcquire(1); !lockAcquired { @@ -159,10 +159,10 @@ func (ice *inactivityClaimExecutor) publishClaim( }) defer subscription.Unsubscribe() - err := ice.publish( + err := ice.publishInactivityClaim( ctx, execLogger, - sessionId, + sessionID, signer.signingGroupMemberIndex, wallet.groupSize(), wallet.groupDishonestThreshold( @@ -224,10 +224,10 @@ func (ice *inactivityClaimExecutor) getWalletOperatorsIDs() ([]uint32, error) { return walletMemberIDs, nil } -func (ice *inactivityClaimExecutor) publish( +func (ice *inactivityClaimExecutor) publishInactivityClaim( ctx context.Context, inactivityLogger log.StandardLogger, - seed *big.Int, + sessionID *big.Int, memberIndex group.MemberIndex, groupSize int, dishonestThreshold int, @@ -238,7 +238,7 @@ func (ice *inactivityClaimExecutor) publish( return inactivity.PublishClaim( ctx, inactivityLogger, - seed.Text(16), + sessionID.Text(16), memberIndex, ice.broadcastChannel, groupSize, From 18fe3627a919ba2dc446d17ee205de19effa96ec Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 9 May 2024 12:15:05 +0200 Subject: [PATCH 29/40] Added tbtc chain interface for staking --- pkg/chain/ethereum/beacon.go | 5 ----- pkg/chain/local_v1/local.go | 4 ---- pkg/sortition/chain.go | 4 ---- pkg/sortition/internal/local/chain.go | 5 ----- pkg/tbtc/chain.go | 9 +++++++++ 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/pkg/chain/ethereum/beacon.go b/pkg/chain/ethereum/beacon.go index 953e34342e..6f37143e62 100644 --- a/pkg/chain/ethereum/beacon.go +++ b/pkg/chain/ethereum/beacon.go @@ -384,11 +384,6 @@ func (bc *BeaconChain) IsRecognized(operatorPublicKey *operator.PublicKey) (bool return true, nil } -// TODO: Implement a real IsOperatorUnstaking function. -func (bc *BeaconChain) IsOperatorUnstaking() (bool, error) { - return false, errNotImplemented -} - // TODO: Implement a real SubmitRelayEntry function. func (bc *BeaconChain) SubmitRelayEntry( entry []byte, diff --git a/pkg/chain/local_v1/local.go b/pkg/chain/local_v1/local.go index 5eb5859d98..6715634ab2 100644 --- a/pkg/chain/local_v1/local.go +++ b/pkg/chain/local_v1/local.go @@ -418,10 +418,6 @@ func (c *localChain) IsEligibleForRewards() (bool, error) { panic("unsupported") } -func (c *localChain) IsOperatorUnstaking() (bool, error) { - panic("unsupported") -} - func (c *localChain) CanRestoreRewardEligibility() (bool, error) { panic("unsupported") } diff --git a/pkg/sortition/chain.go b/pkg/sortition/chain.go index cc7af0f2e8..cf53a6ff92 100644 --- a/pkg/sortition/chain.go +++ b/pkg/sortition/chain.go @@ -51,10 +51,6 @@ type Chain interface { // or not. IsEligibleForRewards() (bool, error) - // IsOperatorUnstaking checks if the operator is unstaking. It returns true - // if the operator has deauthorized their entire stake, false otherwise. - IsOperatorUnstaking() (bool, error) - // Checks whether the operator is able to restore their eligibility for // rewards right away. CanRestoreRewardEligibility() (bool, error) diff --git a/pkg/sortition/internal/local/chain.go b/pkg/sortition/internal/local/chain.go index c325ff5f2c..1c85c4e519 100644 --- a/pkg/sortition/internal/local/chain.go +++ b/pkg/sortition/internal/local/chain.go @@ -173,11 +173,6 @@ func (c *Chain) IsEligibleForRewards() (bool, error) { return !isIneligible, nil } -func (c *Chain) IsOperatorUnstaking() (bool, error) { - // TODO: Implement and use in unit tests. - return false, nil -} - func (c *Chain) CanRestoreRewardEligibility() (bool, error) { c.ineligibleForRewardsUntilMutex.RLock() defer c.ineligibleForRewardsUntilMutex.RUnlock() diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index ddcf0d93b5..7a0e4e1793 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -24,6 +24,14 @@ const ( Challenge ) +// StakingChain defines the subset of the TBTC chain interface that pertains to +// the staking activities. +type StakingChain interface { + // IsOperatorUnstaking checks if the operator is unstaking. It returns true + // if the operator has deauthorized their entire stake, false otherwise. + IsOperatorUnstaking() (bool, error) +} + // GroupSelectionChain defines the subset of the TBTC chain interface that // pertains to the group selection activities. type GroupSelectionChain interface { @@ -505,6 +513,7 @@ type Chain interface { GetBlockHashByNumber(blockNumber uint64) ([32]byte, error) sortition.Chain + StakingChain GroupSelectionChain DistributedKeyGenerationChain InactivityClaimChain From 90b3d91654eed380467a8de33e50418b33caedf2 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 9 May 2024 12:59:50 +0200 Subject: [PATCH 30/40] Added index sorting at inactivity claim creation --- pkg/chain/ethereum/tbtc.go | 13 ++-------- pkg/protocol/inactivity/inactivity.go | 36 +++++++++++++++++++-------- pkg/tbtc/inactivity.go | 14 +++++------ 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 2c1bc7cf30..5e0561cc1b 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1033,12 +1033,6 @@ func (tc *TbtcChain) AssembleInactivityClaim( ) } - // Sort inactiveMembersIndices slice in ascending order as expected by the - // on-chain contract. - sort.Slice(inactiveMembersIndices[:], func(i, j int) bool { - return inactiveMembersIndices[i] < inactiveMembersIndices[j] - }) - return &tbtc.InactivityClaim{ WalletID: walletID, InactiveMembersIndices: inactiveMembersIndices, @@ -1098,14 +1092,11 @@ func (tc *TbtcChain) CalculateInactivityClaimHash( // an unprefixed 64-byte public key, unprefixedGroupPublicKeyBytes := walletPublicKeyBytes[1:] - // The indexes are already sorted. - sortedIndexes := claim.GetInactiveMembersIndexes() - // The type representing inactive member index should be `big.Int` as the // smart contract reading the calculated hash uses `uint256` for inactive // member indexes. - inactiveMembersIndexes := make([]*big.Int, len(sortedIndexes)) - for i, index := range sortedIndexes { + inactiveMembersIndexes := make([]*big.Int, len(claim.InactiveMembersIndexes)) + for i, index := range claim.InactiveMembersIndexes { inactiveMembersIndexes[i] = big.NewInt(int64(index)) } diff --git a/pkg/protocol/inactivity/inactivity.go b/pkg/protocol/inactivity/inactivity.go index 7a57a450c7..97ab07b00c 100644 --- a/pkg/protocol/inactivity/inactivity.go +++ b/pkg/protocol/inactivity/inactivity.go @@ -22,19 +22,35 @@ type ClaimPreimage struct { HeartbeatFailed bool } -// GetInactiveMembersIndexes returns the indexes of inactive members. -// The original slice is copied to avoid concurrency issues if the claim object -// is shared between many goroutines. The returned indexes are sorted. -func (c *ClaimPreimage) GetInactiveMembersIndexes() []group.MemberIndex { - sortedIndexes := make([]group.MemberIndex, len(c.InactiveMembersIndexes)) - - copy(sortedIndexes, c.InactiveMembersIndexes) +func NewClaimPreimage( + nonce *big.Int, + walletPublicKey *ecdsa.PublicKey, + inactiveMembersIndexes []group.MemberIndex, + heartbeatFailed bool, +) *ClaimPreimage { + // Made the inactive member indexes unique as expected by the on-chain + // contract. + indexesCache := make(map[group.MemberIndex]bool) + uniqueIndexes := []group.MemberIndex{} + + for _, index := range inactiveMembersIndexes { + if _, exists := indexesCache[index]; !exists { + indexesCache[index] = true + uniqueIndexes = append(uniqueIndexes, index) + } + } - sort.Slice(sortedIndexes, func(i, j int) bool { - return sortedIndexes[i] < sortedIndexes[j] + // Sort the inactive member indexes as expected by the on-chain contract. + sort.Slice(uniqueIndexes, func(i, j int) bool { + return uniqueIndexes[i] < uniqueIndexes[j] }) - return sortedIndexes + return &ClaimPreimage{ + Nonce: nonce, + WalletPublicKey: walletPublicKey, + InactiveMembersIndexes: uniqueIndexes, + HeartbeatFailed: heartbeatFailed, + } } const ClaimHashByteSize = 32 diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index 7e4ae0bf64..0ebd041409 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -108,12 +108,12 @@ func (ice *inactivityClaimExecutor) claimInactivity( return fmt.Errorf("could not get nonce for wallet: [%v]", err) } - claim := &inactivity.ClaimPreimage{ - Nonce: nonce, - WalletPublicKey: wallet.publicKey, - InactiveMembersIndexes: inactiveMembersIndexes, - HeartbeatFailed: heartbeatFailed, - } + claim := inactivity.NewClaimPreimage( + nonce, + wallet.publicKey, + inactiveMembersIndexes, + heartbeatFailed, + ) groupMembers, err := ice.getWalletOperatorsIDs() if err != nil { @@ -396,7 +396,7 @@ func (ics *inactivityClaimSubmitter) SubmitClaim( chainClaim, err := ics.chain.AssembleInactivityClaim( ecdsaWalletID, - claim.GetInactiveMembersIndexes(), + claim.InactiveMembersIndexes, signatures, claim.HeartbeatFailed, ) From e76d60d7bda9782f12b71cfd6f91fbafa8c07682 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 9 May 2024 15:31:47 +0200 Subject: [PATCH 31/40] Fixed concurrency error --- pkg/tbtc/inactivity.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index 0ebd041409..bdac468fe4 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -124,12 +124,12 @@ func (ice *inactivityClaimExecutor) claimInactivity( wg.Add(len(ice.signers)) for _, currentSigner := range ice.signers { - ice.protocolLatch.Lock() - defer ice.protocolLatch.Unlock() + go func(signer *signer) { + ice.protocolLatch.Lock() + defer ice.protocolLatch.Unlock() - defer wg.Done() + defer wg.Done() - go func(signer *signer) { execLogger.Info( "[member:%v] starting inactivity claim publishing", signer.signingGroupMemberIndex, From 9ad6371c9f14856f97900d95e627f05ed29dc344 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Thu, 9 May 2024 19:38:06 +0200 Subject: [PATCH 32/40] Improved inactivity time synchronization --- pkg/tbtc/heartbeat.go | 49 ++++++++++++++++++++++++-------------- pkg/tbtc/heartbeat_test.go | 4 ++-- pkg/tbtc/inactivity.go | 18 ++++---------- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index c016da8a30..75ae226dd2 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -14,17 +14,23 @@ import ( ) const ( - // heartbeatProposalValidityBlocks determines the wallet heartbeat proposal - // validity time expressed in blocks. In other words, this is the worst-case - // time for a wallet heartbeat during which the wallet is busy and cannot - // take another actions. The value of 300 blocks is roughly 1 hour, assuming - // 12 seconds per block. - heartbeatProposalValidityBlocks = 300 + // heartbeatTotalProposalValidityBlocks determines the total wallet + // heartbeat proposal validity time expressed in blocks. In other words, + // this is the worst-case time for a wallet heartbeat during which the + // wallet is busy and cannot take another actions. It includes the total + // duration need to perform both both signing the heartbeat message and + // optionally notifying about operator inactivity if the heartbeat failed. + // The value of 600 blocks is roughly 2 hours, assuming 12 seconds per block. + heartbeatTotalProposalValidityBlocks = 600 + // heartbeatInactivityNotificationValidityBlocks determines the duration + // that needs to be preserved for the optional notification about operator + // inactivity that follows a failed heartbeat signing. + heartbeatInactivityNotificationValidityBlocks = 300 // heartbeatRequestTimeoutSafetyMarginBlocks determines the duration of the - // safety margin that must be preserved between the signing timeout - // and the timeout of the entire heartbeat action. This safety - // margin prevents against the case where signing completes too late and - // another action has been already requested by the coordinator. + // safety margin that must be preserved between the timeout of operator + // inactivity notification and the timeout of the entire heartbeat action. + // This safety margin prevents against the case where signing completes too + // late and another action has been already requested by the coordinator. // The value of 25 blocks is roughly 5 minutes, assuming 12 seconds per block. heartbeatRequestTimeoutSafetyMarginBlocks = 25 // heartbeatSigningMinimumActiveOperators determines the minimum number of @@ -44,7 +50,7 @@ func (hp *HeartbeatProposal) ActionType() WalletActionType { } func (hp *HeartbeatProposal) ValidityBlocks() uint64 { - return heartbeatProposalValidityBlocks + return heartbeatTotalProposalValidityBlocks } // heartbeatSigningExecutor is an interface meant to decouple the specific @@ -136,19 +142,19 @@ func (ha *heartbeatAction) execute() error { messageToSign := new(big.Int).SetBytes(messageBytes[:]) // Just in case. This should never happen. - if ha.expiryBlock < heartbeatRequestTimeoutSafetyMarginBlocks { + if ha.expiryBlock < heartbeatInactivityNotificationValidityBlocks { return fmt.Errorf("invalid proposal expiry block") } - heartbeatCtx, cancelHeartbeatCtx := withCancelOnBlock( + heartbeatSigningCtx, cancelHeartbeatSigningCtx := withCancelOnBlock( context.Background(), - ha.expiryBlock-heartbeatRequestTimeoutSafetyMarginBlocks, + ha.expiryBlock-heartbeatInactivityNotificationValidityBlocks, ha.waitForBlockFn, ) - defer cancelHeartbeatCtx() + defer cancelHeartbeatSigningCtx() - signature, activeOperatorsCount, signingEndBlock, err := ha.signingExecutor.sign( - heartbeatCtx, + signature, activeOperatorsCount, _, err := ha.signingExecutor.sign( + heartbeatSigningCtx, messageToSign, ha.startBlock, ) @@ -192,9 +198,17 @@ func (ha *heartbeatAction) execute() error { return nil } + heartbeatInactivityCtx, cancelHeartbeatInactivityCtx := withCancelOnBlock( + context.Background(), + ha.expiryBlock-heartbeatRequestTimeoutSafetyMarginBlocks, + ha.waitForBlockFn, + ) + defer cancelHeartbeatInactivityCtx() + // The value of consecutive heartbeat failures exceeds the threshold. // Proceed with operator inactivity notification. err = ha.inactivityClaimExecutor.claimInactivity( + heartbeatInactivityCtx, // Leave the list of inactive operators empty even if some operators // were inactive during signing heartbeat. The inactive operators could // simply be in the process of unstaking and therefore should not be @@ -202,7 +216,6 @@ func (ha *heartbeatAction) execute() error { []group.MemberIndex{}, true, messageToSign, - signingEndBlock, ) if err != nil { return fmt.Errorf( diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index 22334f535c..1bbab80b88 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -22,7 +22,7 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { } startBlock := uint64(10) - expiryBlock := startBlock + heartbeatProposalValidityBlocks + expiryBlock := startBlock + heartbeatTotalProposalValidityBlocks proposal := &HeartbeatProposal{ Message: [16]byte{ @@ -90,7 +90,7 @@ func TestHeartbeatAction_SigningError(t *testing.T) { } startBlock := uint64(10) - expiryBlock := startBlock + heartbeatProposalValidityBlocks + expiryBlock := startBlock + heartbeatTotalProposalValidityBlocks proposal := &HeartbeatProposal{ Message: [16]byte{ diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index bdac468fe4..bd5aaaa87c 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -24,10 +24,7 @@ const ( // that is used to calculate the submission delay period that should be respected // by the given member to avoid all members submitting the same inactivity claim // at the same time. - inactivityClaimSubmissionDelayStepBlocks = 3 - // inactivityClaimMaximumSubmissionBlocks determines the maximum block - // duration of inactivity claim submission procedure. - inactivityClaimMaximumSubmissionBlocks = 60 + inactivityClaimSubmissionDelayStepBlocks = 2 ) // errInactivityClaimExecutorBusy is an error returned when the inactivity claim @@ -70,10 +67,10 @@ func newInactivityClaimExecutor( } func (ice *inactivityClaimExecutor) claimInactivity( + parentCtx context.Context, inactiveMembersIndexes []group.MemberIndex, heartbeatFailed bool, sessionID *big.Int, - startBlock uint64, ) error { if lockAcquired := ice.lock.TryAcquire(1); !lockAcquired { return errInactivityClaimExecutorBusy @@ -88,12 +85,9 @@ func (ice *inactivityClaimExecutor) claimInactivity( return fmt.Errorf("cannot marshal wallet public key: [%v]", err) } - timeoutBlock := startBlock + inactivityClaimMaximumSubmissionBlocks - execLogger := logger.With( zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), - zap.Uint64("inactivityClaimStartBlock", startBlock), - zap.Uint64("inactivityClaimTimeoutBlock", timeoutBlock), + zap.String("sessionID", fmt.Sprintf("0x%x", sessionID)), ) walletRegistryData, err := ice.chain.GetWallet(walletPublicKeyHash) @@ -135,11 +129,7 @@ func (ice *inactivityClaimExecutor) claimInactivity( signer.signingGroupMemberIndex, ) - ctx, cancelCtx := withCancelOnBlock( - context.Background(), - timeoutBlock, - ice.waitForBlockFn, - ) + ctx, cancelCtx := context.WithCancel(parentCtx) defer cancelCtx() subscription := ice.chain.OnInactivityClaimed( From 4eafccb91562035b542a8bcc42407427af9fa515 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 13 May 2024 10:29:08 +0200 Subject: [PATCH 33/40] Context-related renames --- pkg/tbtc/inactivity.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/tbtc/inactivity.go b/pkg/tbtc/inactivity.go index bd5aaaa87c..39bcfc00f8 100644 --- a/pkg/tbtc/inactivity.go +++ b/pkg/tbtc/inactivity.go @@ -67,7 +67,7 @@ func newInactivityClaimExecutor( } func (ice *inactivityClaimExecutor) claimInactivity( - parentCtx context.Context, + ctx context.Context, inactiveMembersIndexes []group.MemberIndex, heartbeatFailed bool, sessionID *big.Int, @@ -129,12 +129,12 @@ func (ice *inactivityClaimExecutor) claimInactivity( signer.signingGroupMemberIndex, ) - ctx, cancelCtx := context.WithCancel(parentCtx) - defer cancelCtx() + signerCtx, cancelSignerCtx := context.WithCancel(ctx) + defer cancelSignerCtx() subscription := ice.chain.OnInactivityClaimed( func(event *InactivityClaimedEvent) { - defer cancelCtx() + defer cancelSignerCtx() execLogger.Infof( "[member:%v] Inactivity claim submitted for wallet "+ @@ -150,7 +150,7 @@ func (ice *inactivityClaimExecutor) claimInactivity( defer subscription.Unsubscribe() err := ice.publishInactivityClaim( - ctx, + signerCtx, execLogger, sessionID, signer.signingGroupMemberIndex, From 7137d4f01fb7a99807664d61f4d62b8407d3dee1 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 13 May 2024 10:36:43 +0200 Subject: [PATCH 34/40] Fixed typos --- pkg/tbtc/heartbeat.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 75ae226dd2..2aa9599afb 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -18,21 +18,21 @@ const ( // heartbeat proposal validity time expressed in blocks. In other words, // this is the worst-case time for a wallet heartbeat during which the // wallet is busy and cannot take another actions. It includes the total - // duration need to perform both both signing the heartbeat message and + // duration needed to perform both signing the heartbeat message and // optionally notifying about operator inactivity if the heartbeat failed. // The value of 600 blocks is roughly 2 hours, assuming 12 seconds per block. heartbeatTotalProposalValidityBlocks = 600 - // heartbeatInactivityNotificationValidityBlocks determines the duration - // that needs to be preserved for the optional notification about operator - // inactivity that follows a failed heartbeat signing. - heartbeatInactivityNotificationValidityBlocks = 300 - // heartbeatRequestTimeoutSafetyMarginBlocks determines the duration of the - // safety margin that must be preserved between the timeout of operator - // inactivity notification and the timeout of the entire heartbeat action. - // This safety margin prevents against the case where signing completes too - // late and another action has been already requested by the coordinator. - // The value of 25 blocks is roughly 5 minutes, assuming 12 seconds per block. - heartbeatRequestTimeoutSafetyMarginBlocks = 25 + // heartbeatInactivityClaimValidityBlocks determines the duration that needs + // to be preserved for the optional notification about operator inactivity + // that follows a failed heartbeat signing. + heartbeatInactivityClaimValidityBlocks = 300 + // heartbeatTimeoutSafetyMarginBlocks determines the duration of the safety + // margin that must be preserved between the timeout of operator inactivity + // notification and the timeout of the entire heartbeat action. This safety + // margin prevents against the case where signing completes too late and + // another action has been already requested by the coordinator. The value + // of 25 blocks is roughly 5 minutes, assuming 12 seconds per block. + heartbeatTimeoutSafetyMarginBlocks = 25 // heartbeatSigningMinimumActiveOperators determines the minimum number of // active operators during signing for a heartbeat to be considered valid. heartbeatSigningMinimumActiveOperators = 70 @@ -142,13 +142,13 @@ func (ha *heartbeatAction) execute() error { messageToSign := new(big.Int).SetBytes(messageBytes[:]) // Just in case. This should never happen. - if ha.expiryBlock < heartbeatInactivityNotificationValidityBlocks { + if ha.expiryBlock < heartbeatInactivityClaimValidityBlocks { return fmt.Errorf("invalid proposal expiry block") } heartbeatSigningCtx, cancelHeartbeatSigningCtx := withCancelOnBlock( context.Background(), - ha.expiryBlock-heartbeatInactivityNotificationValidityBlocks, + ha.expiryBlock-heartbeatInactivityClaimValidityBlocks, ha.waitForBlockFn, ) defer cancelHeartbeatSigningCtx() @@ -200,7 +200,7 @@ func (ha *heartbeatAction) execute() error { heartbeatInactivityCtx, cancelHeartbeatInactivityCtx := withCancelOnBlock( context.Background(), - ha.expiryBlock-heartbeatRequestTimeoutSafetyMarginBlocks, + ha.expiryBlock-heartbeatTimeoutSafetyMarginBlocks, ha.waitForBlockFn, ) defer cancelHeartbeatInactivityCtx() From 8b92d0e746ae38e06fd86169d195db82ec15e5d4 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 13 May 2024 15:33:45 +0200 Subject: [PATCH 35/40] Added unit tests for inactivity executor --- pkg/tbtc/chain_test.go | 74 ++++++++++++++- pkg/tbtc/inactivity_test.go | 180 ++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 5 deletions(-) diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 97eef9e0c8..d8ad9bd2e4 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -41,6 +41,9 @@ type localChain struct { dkgResultChallengeHandlersMutex sync.Mutex dkgResultChallengeHandlers map[int]func(submission *DKGResultChallengedEvent) + inactivityClaimedHandlersMutex sync.Mutex + inactivityClaimedHandlers map[int]func(submission *InactivityClaimedEvent) + dkgMutex sync.Mutex dkgState DKGState dkgResult *DKGChainResult @@ -49,6 +52,9 @@ type localChain struct { walletsMutex sync.Mutex wallets map[[20]byte]*WalletChainData + inactivityNonceMutex sync.Mutex + inactivityNonces map[[32]byte]uint64 + blocksByTimestampMutex sync.Mutex blocksByTimestamp map[uint64]uint64 @@ -553,9 +559,20 @@ func (lc *localChain) DKGParameters() (*DKGParameters, error) { } func (lc *localChain) OnInactivityClaimed( - func(event *InactivityClaimedEvent), + handler func(event *InactivityClaimedEvent), ) subscription.EventSubscription { - panic("unsupported") + lc.inactivityClaimedHandlersMutex.Lock() + defer lc.inactivityClaimedHandlersMutex.Unlock() + + handlerID := generateHandlerID() + lc.inactivityClaimedHandlers[handlerID] = handler + + return subscription.NewEventSubscription(func() { + lc.inactivityClaimedHandlersMutex.Lock() + defer lc.inactivityClaimedHandlersMutex.Unlock() + + delete(lc.inactivityClaimedHandlers, handlerID) + }) } func (lc *localChain) AssembleInactivityClaim( @@ -567,7 +584,20 @@ func (lc *localChain) AssembleInactivityClaim( *InactivityClaim, error, ) { - panic("unsupported") + signingMembersIndexes := make([]group.MemberIndex, 0) + signaturesConcatenation := make([]byte, 0) + for memberIndex, signature := range signatures { + signingMembersIndexes = append(signingMembersIndexes, memberIndex) + signaturesConcatenation = append(signaturesConcatenation, signature...) + } + + return &InactivityClaim{ + WalletID: walletID, + InactiveMembersIndices: inactiveMembersIndices, + HeartbeatFailed: heartbeatFailed, + Signatures: signaturesConcatenation, + SigningMembersIndices: signingMembersIndexes, + }, nil } func (lc *localChain) SubmitInactivityClaim( @@ -575,7 +605,33 @@ func (lc *localChain) SubmitInactivityClaim( nonce *big.Int, groupMembers []uint32, ) error { - panic("unsupported") + lc.inactivityClaimedHandlersMutex.Lock() + defer lc.inactivityClaimedHandlersMutex.Unlock() + + lc.inactivityNonceMutex.Lock() + defer lc.inactivityNonceMutex.Unlock() + + if nonce.Uint64() != lc.inactivityNonces[claim.WalletID] { + return fmt.Errorf("wrong inactivity claim nonce") + } + + blockNumber, err := lc.blockCounter.CurrentBlock() + if err != nil { + return fmt.Errorf("failed to get the current block") + } + + for _, handler := range lc.inactivityClaimedHandlers { + handler(&InactivityClaimedEvent{ + WalletID: claim.WalletID, + Nonce: nonce, + Notifier: "", + BlockNumber: blockNumber, + }) + } + + lc.inactivityNonces[claim.WalletID]++ + + return nil } func (lc *localChain) CalculateInactivityClaimHash( @@ -598,7 +654,11 @@ func (lc *localChain) CalculateInactivityClaimHash( } func (lc *localChain) GetInactivityClaimNonce(walletID [32]byte) (*big.Int, error) { - panic("unsupported") + lc.inactivityNonceMutex.Lock() + defer lc.inactivityNonceMutex.Unlock() + + nonce := lc.inactivityNonces[walletID] + return big.NewInt(int64(nonce)), nil } func (lc *localChain) PastDepositRevealedEvents( @@ -1182,7 +1242,11 @@ func ConnectWithKey( dkgResultChallengeHandlers: make( map[int]func(submission *DKGResultChallengedEvent), ), + inactivityClaimedHandlers: make( + map[int]func(submission *InactivityClaimedEvent), + ), wallets: make(map[[20]byte]*WalletChainData), + inactivityNonces: make(map[[32]byte]uint64), blocksByTimestamp: make(map[uint64]uint64), blocksHashesByNumber: make(map[uint64][32]byte), pastDepositRevealedEvents: make(map[[32]byte][]*DepositRevealedEvent), diff --git a/pkg/tbtc/inactivity_test.go b/pkg/tbtc/inactivity_test.go index d30bccde24..da3e56f08a 100644 --- a/pkg/tbtc/inactivity_test.go +++ b/pkg/tbtc/inactivity_test.go @@ -1,19 +1,199 @@ package tbtc import ( + "context" "fmt" "math/big" "reflect" "testing" + "time" "golang.org/x/crypto/sha3" + "github.com/keep-network/keep-core/internal/testutils" + "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/internal/tecdsatest" + "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/protocol/inactivity" "github.com/keep-network/keep-core/pkg/tecdsa" ) +func TestInactivityClaimExecutor_ClaimInactivity(t *testing.T) { + executor, walletEcdsaID, chain := setupInactivityClaimExecutorScenario(t) + + initialNonce, err := chain.GetInactivityClaimNonce(walletEcdsaID) + if err != nil { + t.Fatal(err) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + message := big.NewInt(100) + inactiveMembersIndexes := []group.MemberIndex{1, 4} + + err = executor.claimInactivity( + ctx, + inactiveMembersIndexes, + true, + message, + ) + if err != nil { + t.Fatal(err) + } + + currentNonce, err := chain.GetInactivityClaimNonce(walletEcdsaID) + if err != nil { + t.Fatal(err) + } + + expectedNonceDiff := uint64(1) + nonceDiff := currentNonce.Uint64() - initialNonce.Uint64() + + testutils.AssertUintsEqual( + t, + "inactivity nonce difference", + expectedNonceDiff, + nonceDiff, + ) +} + +func TestInactivityClaimExecutor_ClaimInactivity_Busy(t *testing.T) { + executor, _, _ := setupInactivityClaimExecutorScenario(t) + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + message := big.NewInt(100) + inactiveMembersIndexes := []group.MemberIndex{1, 4} + + errChan := make(chan error, 1) + go func() { + err := executor.claimInactivity( + ctx, + inactiveMembersIndexes, + true, + message, + ) + errChan <- err + }() + + time.Sleep(100 * time.Millisecond) + + err := executor.claimInactivity( + ctx, + inactiveMembersIndexes, + true, + message, + ) + testutils.AssertErrorsSame(t, errInactivityClaimExecutorBusy, err) + + err = <-errChan + if err != nil { + t.Errorf("unexpected error: [%v]", err) + } +} + +func setupInactivityClaimExecutorScenario(t *testing.T) ( + *inactivityClaimExecutor, + [32]byte, + *localChain, +) { + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + operatorPrivateKey, operatorPublicKey, err := operator.GenerateKeyPair( + local_v1.DefaultCurve, + ) + if err != nil { + t.Fatal(err) + } + + localChain := ConnectWithKey(operatorPrivateKey) + + localProvider := local.ConnectWithKey(operatorPublicKey) + + operatorAddress, err := localChain.Signing().PublicKeyToAddress( + operatorPublicKey, + ) + if err != nil { + t.Fatal(err) + } + + var operators []chain.Address + for i := 0; i < groupParameters.GroupSize; i++ { + operators = append(operators, operatorAddress) + } + + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures( + groupParameters.GroupSize, + ) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + + signers := make([]*signer, len(testData)) + for i := range testData { + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[i]) + + signers[i] = &signer{ + wallet: wallet{ + publicKey: privateKeyShare.PublicKey(), + signingGroupOperators: operators, + }, + signingGroupMemberIndex: group.MemberIndex(i + 1), + privateKeyShare: privateKeyShare, + } + } + + keyStorePersistence := createMockKeyStorePersistence(t, signers...) + + walletPublicKeyHash := bitcoin.PublicKeyHash(signers[0].wallet.publicKey) + ecdsaWalletID := [32]byte{1, 2, 3} + + localChain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + EcdsaWalletID: ecdsaWalletID, + }, + ) + + node, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + localProvider, + keyStorePersistence, + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{}, + ) + if err != nil { + t.Fatal(err) + } + + executor, ok, err := node.getInactivityClaimExecutor( + signers[0].wallet.publicKey, + ) + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("node is supposed to control wallet signers") + } + + return executor, ecdsaWalletID, localChain +} + func TestSignClaim_SigningSuccessful(t *testing.T) { chain := Connect() inactivityClaimSigner := newInactivityClaimSigner(chain) From 60da8cc8955b7bfdc7f529f39b46e8dadaec5afc Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Mon, 13 May 2024 18:12:54 +0200 Subject: [PATCH 36/40] Added unit tests for inactivity claim submitter --- pkg/tbtc/inactivity_test.go | 482 +++++++++++++++++++++++++++++++++--- 1 file changed, 451 insertions(+), 31 deletions(-) diff --git a/pkg/tbtc/inactivity_test.go b/pkg/tbtc/inactivity_test.go index da3e56f08a..b1065e4607 100644 --- a/pkg/tbtc/inactivity_test.go +++ b/pkg/tbtc/inactivity_test.go @@ -204,12 +204,12 @@ func TestSignClaim_SigningSuccessful(t *testing.T) { } privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) - claim := &inactivity.ClaimPreimage{ - Nonce: big.NewInt(5), - WalletPublicKey: privateKeyShare.PublicKey(), - InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, - HeartbeatFailed: true, - } + claim := inactivity.NewClaimPreimage( + big.NewInt(5), + privateKeyShare.PublicKey(), + []group.MemberIndex{11, 22, 33}, + true, + ) signedClaim, err := inactivityClaimSigner.SignClaim(claim) if err != nil { @@ -297,12 +297,12 @@ func TestVerifySignature_VerifySuccessful(t *testing.T) { } privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) - claim := &inactivity.ClaimPreimage{ - Nonce: big.NewInt(5), - WalletPublicKey: privateKeyShare.PublicKey(), - InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, - HeartbeatFailed: true, - } + claim := inactivity.NewClaimPreimage( + big.NewInt(5), + privateKeyShare.PublicKey(), + []group.MemberIndex{11, 22, 33}, + true, + ) signedClaim, err := inactivityClaimSigner.SignClaim(claim) if err != nil { @@ -334,24 +334,24 @@ func TestVerifySignature_VerifyFailure(t *testing.T) { } privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) - claim := &inactivity.ClaimPreimage{ - Nonce: big.NewInt(5), - WalletPublicKey: privateKeyShare.PublicKey(), - InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, - HeartbeatFailed: true, - } + claim := inactivity.NewClaimPreimage( + big.NewInt(5), + privateKeyShare.PublicKey(), + []group.MemberIndex{11, 22, 33}, + true, + ) signedClaim, err := inactivityClaimSigner.SignClaim(claim) if err != nil { t.Fatal(err) } - anotherClaim := &inactivity.ClaimPreimage{ - Nonce: big.NewInt(6), - WalletPublicKey: privateKeyShare.PublicKey(), - InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, - HeartbeatFailed: true, - } + anotherClaim := inactivity.NewClaimPreimage( + big.NewInt(6), + privateKeyShare.PublicKey(), + []group.MemberIndex{11, 22, 33}, + true, + ) anotherSignedClaim, err := inactivityClaimSigner.SignClaim(anotherClaim) if err != nil { @@ -387,12 +387,12 @@ func TestVerifySignature_VerifyError(t *testing.T) { } privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) - claim := &inactivity.ClaimPreimage{ - Nonce: big.NewInt(5), - WalletPublicKey: privateKeyShare.PublicKey(), - InactiveMembersIndexes: []group.MemberIndex{11, 22, 33}, - HeartbeatFailed: true, - } + claim := inactivity.NewClaimPreimage( + big.NewInt(5), + privateKeyShare.PublicKey(), + []group.MemberIndex{11, 22, 33}, + true, + ) signedClaim, err := inactivityClaimSigner.SignClaim(claim) if err != nil { @@ -419,4 +419,424 @@ func TestVerifySignature_VerifyError(t *testing.T) { } } -// TODO: Continue with unit tests. +func TestSubmitClaim_MemberSubmitsClaim(t *testing.T) { + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + publicKey := privateKeyShare.PublicKey() + walletPublicKeyHash := bitcoin.PublicKeyHash(publicKey) + ecdsaWalletID := [32]byte{1, 2, 3} + + chain := Connect() + + chain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + EcdsaWalletID: ecdsaWalletID, + }, + ) + + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + groupMembers := []uint32{1, 2, 2, 3, 5} + + inactivityClaimSubmitter := newInactivityClaimSubmitter( + &testutils.MockLogger{}, + chain, + groupParameters, + groupMembers, + testWaitForBlockFn(chain), + ) + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + memberIndex := group.MemberIndex(1) + + claim := inactivity.NewClaimPreimage( + big.NewInt(0), + publicKey, + []group.MemberIndex{11, 22, 33}, + true, + ) + + signatures := map[group.MemberIndex][]byte{ + 1: []byte("signature 1"), + 2: []byte("signature 2"), + 3: []byte("signature 3"), + 4: []byte("signature 4"), + } + + err = inactivityClaimSubmitter.SubmitClaim( + ctx, + memberIndex, + claim, + signatures, + ) + if err != nil { + t.Fatal(err) + } + + expectedNonce := big.NewInt(1) + + nonce, err := chain.GetInactivityClaimNonce(ecdsaWalletID) + if err != nil { + t.Fatal(err) + } + + testutils.AssertBigIntsEqual( + t, + "inactivity nonce", + expectedNonce, + nonce, + ) +} + +func TestSubmitClaim_AnotherMemberSubmitsClaim(t *testing.T) { + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + publicKey := privateKeyShare.PublicKey() + walletPublicKeyHash := bitcoin.PublicKeyHash(publicKey) + ecdsaWalletID := [32]byte{1, 2, 3} + + chain := Connect() + + chain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + EcdsaWalletID: ecdsaWalletID, + }, + ) + + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + groupMembers := []uint32{1, 2, 2, 3, 5} + + inactivityClaimSubmitter := newInactivityClaimSubmitter( + &testutils.MockLogger{}, + chain, + groupParameters, + groupMembers, + testWaitForBlockFn(chain), + ) + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + claim := inactivity.NewClaimPreimage( + big.NewInt(0), + publicKey, + []group.MemberIndex{11, 22, 33}, + true, + ) + + signatures := map[group.MemberIndex][]byte{ + 1: []byte("signature 1"), + 2: []byte("signature 2"), + 3: []byte("signature 3"), + 4: []byte("signature 4"), + } + + // Set up a global listener that will cancel the common context upon claim + // submission. That mimics the real-world scenario. + chain.OnInactivityClaimed( + func(event *InactivityClaimedEvent) { + cancelCtx() + }, + ) + + secondMemberSubmissionChannel := make(chan error) + // Attempt to submit claim for the second member on a separate goroutine. + go func() { + secondMemberIndex := group.MemberIndex(2) + secondMemberErr := inactivityClaimSubmitter.SubmitClaim( + ctx, + secondMemberIndex, + claim, + signatures, + ) + secondMemberSubmissionChannel <- secondMemberErr + }() + + // This sleep is needed to give enough time for the second member to + // register their claim submission event handler and act properly on the + // claim submitted by the first member. + time.Sleep(1 * time.Second) + + // While the second member is waiting for submission eligibility, submit the + // claim with the first member. + firstMemberIndex := group.MemberIndex(1) + firstMemberErr := inactivityClaimSubmitter.SubmitClaim( + ctx, + firstMemberIndex, + claim, + signatures, + ) + if err != nil { + t.Fatal(firstMemberErr) + } + + // Check that the second member returned without errors + secondMemberErr := <-secondMemberSubmissionChannel + if secondMemberErr != nil { + t.Fatal(secondMemberErr) + } + + expectedNonce := big.NewInt(1) + + nonce, err := chain.GetInactivityClaimNonce(ecdsaWalletID) + if err != nil { + t.Fatal(err) + } + + testutils.AssertBigIntsEqual( + t, + "inactivity nonce", + expectedNonce, + nonce, + ) +} + +func TestSubmitClaim_InvalidResult(t *testing.T) { + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + publicKey := privateKeyShare.PublicKey() + walletPublicKeyHash := bitcoin.PublicKeyHash(publicKey) + ecdsaWalletID := [32]byte{1, 2, 3} + + chain := Connect() + + chain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + EcdsaWalletID: ecdsaWalletID, + }, + ) + + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + groupMembers := []uint32{1, 2, 2, 3, 5} + + inactivityClaimSubmitter := newInactivityClaimSubmitter( + &testutils.MockLogger{}, + chain, + groupParameters, + groupMembers, + testWaitForBlockFn(chain), + ) + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + memberIndex := group.MemberIndex(1) + + claim := inactivity.NewClaimPreimage( + big.NewInt(12345), // Use wrong nonce. + publicKey, + []group.MemberIndex{11, 22, 33}, + true, + ) + + signatures := map[group.MemberIndex][]byte{ + 1: []byte("signature 1"), + 2: []byte("signature 2"), + 3: []byte("signature 3"), + 4: []byte("signature 4"), + } + + err = inactivityClaimSubmitter.SubmitClaim( + ctx, + memberIndex, + claim, + signatures, + ) + + expectedErr := fmt.Errorf("wrong inactivity claim nonce") + if !reflect.DeepEqual(expectedErr, err) { + t.Errorf( + "unexpected error \nexpected: [%v]\nactual: [%v]\n", + expectedErr, + err, + ) + } +} + +func TestSubmitClaim_ContextCancelled(t *testing.T) { + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + publicKey := privateKeyShare.PublicKey() + walletPublicKeyHash := bitcoin.PublicKeyHash(publicKey) + ecdsaWalletID := [32]byte{1, 2, 3} + + chain := Connect() + + chain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + EcdsaWalletID: ecdsaWalletID, + }, + ) + + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + groupMembers := []uint32{1, 2, 2, 3, 5} + + inactivityClaimSubmitter := newInactivityClaimSubmitter( + &testutils.MockLogger{}, + chain, + groupParameters, + groupMembers, + testWaitForBlockFn(chain), + ) + + ctx, cancelCtx := context.WithCancel(context.Background()) + + // Simulate the case when timeout occurs and the context gets cancelled. + cancelCtx() + + memberIndex := group.MemberIndex(1) + + claim := inactivity.NewClaimPreimage( + big.NewInt(0), + publicKey, + []group.MemberIndex{11, 22, 33}, + true, + ) + + signatures := map[group.MemberIndex][]byte{ + 1: []byte("signature 1"), + 2: []byte("signature 2"), + 3: []byte("signature 3"), + 4: []byte("signature 4"), + } + + err = inactivityClaimSubmitter.SubmitClaim( + ctx, + memberIndex, + claim, + signatures, + ) + if err != nil { + t.Errorf("unexpected error [%v]", err) + } + + // Check the inactivity nonce is still 0. + expectedNonce := big.NewInt(0) + + nonce, err := chain.GetInactivityClaimNonce(ecdsaWalletID) + if err != nil { + t.Fatal(err) + } + + testutils.AssertBigIntsEqual( + t, + "inactivity nonce", + expectedNonce, + nonce, + ) +} + +func TestSubmitClaim_TooFewSignatures(t *testing.T) { + testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) + if err != nil { + t.Fatalf("failed to load test data: [%v]", err) + } + privateKeyShare := tecdsa.NewPrivateKeyShare(testData[0]) + + publicKey := privateKeyShare.PublicKey() + walletPublicKeyHash := bitcoin.PublicKeyHash(publicKey) + ecdsaWalletID := [32]byte{1, 2, 3} + + chain := Connect() + + chain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + EcdsaWalletID: ecdsaWalletID, + }, + ) + + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + groupMembers := []uint32{1, 2, 2, 3, 5} + + inactivityClaimSubmitter := newInactivityClaimSubmitter( + &testutils.MockLogger{}, + chain, + groupParameters, + groupMembers, + testWaitForBlockFn(chain), + ) + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + memberIndex := group.MemberIndex(1) + + claim := inactivity.NewClaimPreimage( + big.NewInt(0), + publicKey, + []group.MemberIndex{11, 22, 33}, + true, + ) + + signatures := map[group.MemberIndex][]byte{ + 1: []byte("signature 1"), + 2: []byte("signature 2"), + } + + err = inactivityClaimSubmitter.SubmitClaim( + ctx, + memberIndex, + claim, + signatures, + ) + + expectedError := fmt.Errorf( + "could not submit inactivity claim with [2] signatures for group honest threshold [3]", + ) + if !reflect.DeepEqual(expectedError, err) { + t.Errorf( + "unexpected error\n"+ + "expected: [%+v]\n"+ + "actual: [%+v]", + expectedError, + err, + ) + } +} From 82dc68a3f5630af1aeb6f9fa36a2283b25a7cfc5 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 14 May 2024 13:51:49 +0200 Subject: [PATCH 37/40] Adjusted existing unit tests for heartbeats --- pkg/tbtc/heartbeat.go | 16 +- pkg/tbtc/heartbeat_test.go | 323 ++++++++++++++++++++++++++++++++++--- 2 files changed, 319 insertions(+), 20 deletions(-) diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 2aa9599afb..65821f65a4 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -63,6 +63,18 @@ type heartbeatSigningExecutor interface { ) (*tecdsa.Signature, uint32, uint64, error) } +// heartbeatInactivityClaimExecutor is an interface meant to decouple the +// specific implementation of the inactivity claim executor from the heartbeat +// action. +type heartbeatInactivityClaimExecutor interface { + claimInactivity( + ctx context.Context, + inactiveMembersIndexes []group.MemberIndex, + heartbeatFailed bool, + sessionID *big.Int, + ) error +} + // heartbeatAction is a walletAction implementation handling heartbeat requests // from the wallet coordinator. type heartbeatAction struct { @@ -75,7 +87,7 @@ type heartbeatAction struct { proposal *HeartbeatProposal failureCounter *heartbeatFailureCounter - inactivityClaimExecutor *inactivityClaimExecutor + inactivityClaimExecutor heartbeatInactivityClaimExecutor startBlock uint64 expiryBlock uint64 @@ -90,7 +102,7 @@ func newHeartbeatAction( signingExecutor heartbeatSigningExecutor, proposal *HeartbeatProposal, failureCounter *heartbeatFailureCounter, - inactivityClaimExecutor *inactivityClaimExecutor, + inactivityClaimExecutor heartbeatInactivityClaimExecutor, startBlock uint64, expiryBlock uint64, waitForBlockFn waitForBlockFn, diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index 1bbab80b88..92cd2e0d3c 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/keep-network/keep-core/internal/testutils" + "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" ) @@ -21,6 +22,8 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { t.Fatal(err) } + walletPublicKeyStr := hex.EncodeToString(walletPublicKeyHex) + startBlock := uint64(10) expiryBlock := startBlock + heartbeatTotalProposalValidityBlocks @@ -31,7 +34,10 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { }, } + // Set the heartbeat failure counter to `1` for the given wallet. The value + // of the counter should be reset to `0` after executing the action. heartbeatFailureCounter := newHeartbeatFailureCounter() + heartbeatFailureCounter.increment(walletPublicKeyStr) // sha256(sha256(messageToSign)) sha256d, err := hex.DecodeString("38d30dacec5083c902952ce99fc0287659ad0b1ca2086827a8e78b0bef2c8bc1") @@ -42,8 +48,12 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { hostChain := Connect() hostChain.setHeartbeatProposalValidationResult(proposal, true) + // Set the active operators count to the minimum required value. mockExecutor := &mockHeartbeatSigningExecutor{} - inactivityClaimExecutor := &inactivityClaimExecutor{} + mockExecutor.activeOperatorsCount = heartbeatSigningMinimumActiveOperators + + inactivityClaimExecutor := &mockInactivityClaimExecutor{} + action := newHeartbeatAction( logger, hostChain, @@ -66,6 +76,12 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { t.Fatal(err) } + testutils.AssertUintsEqual( + t, + "heartbeat failure count", + 0, + uint64(heartbeatFailureCounter.get(walletPublicKeyStr)), + ) testutils.AssertBigIntsEqual( t, "message to sign", @@ -78,9 +94,15 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { startBlock, mockExecutor.requestedStartBlock, ) + testutils.AssertBigIntsEqual( + t, + "inactivity claim executor session ID", + nil, // executor not called. + inactivityClaimExecutor.sessionID, + ) } -func TestHeartbeatAction_SigningError(t *testing.T) { +func TestHeartbeatAction_Failure_SigningError(t *testing.T) { walletPublicKeyHex, err := hex.DecodeString( "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", @@ -89,6 +111,8 @@ func TestHeartbeatAction_SigningError(t *testing.T) { t.Fatal(err) } + walletPublicKeyStr := hex.EncodeToString(walletPublicKeyHex) + startBlock := uint64(10) expiryBlock := startBlock + heartbeatTotalProposalValidityBlocks @@ -106,8 +130,9 @@ func TestHeartbeatAction_SigningError(t *testing.T) { mockExecutor := &mockHeartbeatSigningExecutor{} mockExecutor.shouldFail = true + mockExecutor.activeOperatorsCount = heartbeatSigningMinimumActiveOperators - inactivityClaimExecutor := &inactivityClaimExecutor{} + inactivityClaimExecutor := &mockInactivityClaimExecutor{} action := newHeartbeatAction( logger, @@ -126,18 +151,259 @@ func TestHeartbeatAction_SigningError(t *testing.T) { }, ) - action.execute() - // TODO: Uncomment - // err = action.execute() - // if err == nil { - // t.Fatal("expected error to be returned") - // } - // testutils.AssertStringsEqual( - // t, - // "error message", - // "cannot sign heartbeat message: [oofta]", - // err.Error(), - // ) + // Do not expect the execution to result in an error. Signing error does not + // mean the procedure failure. + err = action.execute() + if err != nil { + t.Fatal(err) + } + + testutils.AssertUintsEqual( + t, + "heartbeat failure count", + 1, + uint64(heartbeatFailureCounter.get(walletPublicKeyStr)), + ) + testutils.AssertBigIntsEqual( + t, + "inactivity claim executor session ID", + nil, // executor not called. + inactivityClaimExecutor.sessionID, + ) +} + +func TestHeartbeatAction_Failure_TooFewActiveOperators(t *testing.T) { + walletPublicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + walletPublicKeyStr := hex.EncodeToString(walletPublicKeyHex) + + startBlock := uint64(10) + expiryBlock := startBlock + heartbeatTotalProposalValidityBlocks + + proposal := &HeartbeatProposal{ + Message: [16]byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, + } + + heartbeatFailureCounter := newHeartbeatFailureCounter() + + hostChain := Connect() + hostChain.setHeartbeatProposalValidationResult(proposal, true) + + // Set the active operators count just below the required number. + mockExecutor := &mockHeartbeatSigningExecutor{} + mockExecutor.activeOperatorsCount = heartbeatSigningMinimumActiveOperators - 1 + + inactivityClaimExecutor := &mockInactivityClaimExecutor{} + + action := newHeartbeatAction( + logger, + hostChain, + wallet{ + publicKey: unmarshalPublicKey(walletPublicKeyHex), + }, + mockExecutor, + proposal, + heartbeatFailureCounter, + inactivityClaimExecutor, + startBlock, + expiryBlock, + func(ctx context.Context, blockHeight uint64) error { + return nil + }, + ) + + // Do not expect the execution to result in an error. Signing error does not + // mean the procedure failure. + err = action.execute() + if err != nil { + t.Fatal(err) + } + + testutils.AssertUintsEqual( + t, + "heartbeat failure count", + 1, + uint64(heartbeatFailureCounter.get(walletPublicKeyStr)), + ) + testutils.AssertBigIntsEqual( + t, + "inactivity claim executor session ID", + nil, // executor not called. + inactivityClaimExecutor.sessionID, + ) +} + +func TestHeartbeatAction_Failure_CounterExceeded(t *testing.T) { + walletPublicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + walletPublicKeyStr := hex.EncodeToString(walletPublicKeyHex) + + startBlock := uint64(10) + expiryBlock := startBlock + heartbeatTotalProposalValidityBlocks + + proposal := &HeartbeatProposal{ + Message: [16]byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, + } + + // sha256(sha256(messageToSign)) + sha256d, err := hex.DecodeString("38d30dacec5083c902952ce99fc0287659ad0b1ca2086827a8e78b0bef2c8bc1") + if err != nil { + t.Fatal(err) + } + + // Set the heartbeat failure counter to `2` so that the next failure will + // trigger operator inactivity claim execution. + heartbeatFailureCounter := newHeartbeatFailureCounter() + heartbeatFailureCounter.increment(walletPublicKeyStr) + heartbeatFailureCounter.increment(walletPublicKeyStr) + + hostChain := Connect() + hostChain.setHeartbeatProposalValidationResult(proposal, true) + + mockExecutor := &mockHeartbeatSigningExecutor{} + mockExecutor.shouldFail = true + + inactivityClaimExecutor := &mockInactivityClaimExecutor{} + + action := newHeartbeatAction( + logger, + hostChain, + wallet{ + publicKey: unmarshalPublicKey(walletPublicKeyHex), + }, + mockExecutor, + proposal, + heartbeatFailureCounter, + inactivityClaimExecutor, + startBlock, + expiryBlock, + func(ctx context.Context, blockHeight uint64) error { + return nil + }, + ) + + // Do not expect the execution to result in an error. Signing error does not + // mean the procedure failure. + err = action.execute() + if err != nil { + t.Fatal(err) + } + + testutils.AssertUintsEqual( + t, + "heartbeat failure count", + 3, + uint64(heartbeatFailureCounter.get(walletPublicKeyStr)), + ) + testutils.AssertBigIntsEqual( + t, + "inactivity claim executor session ID", + new(big.Int).SetBytes(sha256d), + inactivityClaimExecutor.sessionID, + ) +} + +func TestHeartbeatAction_Failure_InactivityExecutionFailure(t *testing.T) { + walletPublicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + walletPublicKeyStr := hex.EncodeToString(walletPublicKeyHex) + + startBlock := uint64(10) + expiryBlock := startBlock + heartbeatTotalProposalValidityBlocks + + proposal := &HeartbeatProposal{ + Message: [16]byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, + } + + // sha256(sha256(messageToSign)) + sha256d, err := hex.DecodeString("38d30dacec5083c902952ce99fc0287659ad0b1ca2086827a8e78b0bef2c8bc1") + if err != nil { + t.Fatal(err) + } + + // Set the heartbeat failure counter to `2` so that the next failure will + // trigger operator inactivity claim execution. + heartbeatFailureCounter := newHeartbeatFailureCounter() + heartbeatFailureCounter.increment(walletPublicKeyStr) + heartbeatFailureCounter.increment(walletPublicKeyStr) + + hostChain := Connect() + hostChain.setHeartbeatProposalValidationResult(proposal, true) + + mockExecutor := &mockHeartbeatSigningExecutor{} + mockExecutor.shouldFail = true + + inactivityClaimExecutor := &mockInactivityClaimExecutor{} + inactivityClaimExecutor.shouldFail = true + + action := newHeartbeatAction( + logger, + hostChain, + wallet{ + publicKey: unmarshalPublicKey(walletPublicKeyHex), + }, + mockExecutor, + proposal, + heartbeatFailureCounter, + inactivityClaimExecutor, + startBlock, + expiryBlock, + func(ctx context.Context, blockHeight uint64) error { + return nil + }, + ) + + err = action.execute() + if err == nil { + t.Fatal("expected error to be returned") + } + testutils.AssertStringsEqual( + t, + "error message", + "error while notifying about operator inactivity [mock inactivity "+ + "claim executor error]]", + err.Error(), + ) + + testutils.AssertUintsEqual( + t, + "heartbeat failure count", + 3, + uint64(heartbeatFailureCounter.get(walletPublicKeyStr)), + ) + testutils.AssertBigIntsEqual( + t, + "inactivity claim executor session ID", + new(big.Int).SetBytes(sha256d), + inactivityClaimExecutor.sessionID, + ) } func TestHeartbeatFailureCounter_Increment(t *testing.T) { @@ -260,7 +526,8 @@ func TestHeartbeatFailureCounter_Get(t *testing.T) { } type mockHeartbeatSigningExecutor struct { - shouldFail bool + shouldFail bool + activeOperatorsCount uint32 requestedMessage *big.Int requestedStartBlock uint64 @@ -278,6 +545,26 @@ func (mhse *mockHeartbeatSigningExecutor) sign( return nil, 0, 0, fmt.Errorf("oofta") } - // TODO: Return the active members count and use it in unit tests. - return &tecdsa.Signature{}, 0, startBlock + 1, nil + return &tecdsa.Signature{}, mhse.activeOperatorsCount, startBlock + 1, nil +} + +type mockInactivityClaimExecutor struct { + shouldFail bool + + sessionID *big.Int +} + +func (mice *mockInactivityClaimExecutor) claimInactivity( + ctx context.Context, + inactiveMembersIndexes []group.MemberIndex, + heartbeatFailed bool, + sessionID *big.Int, +) error { + mice.sessionID = sessionID + + if mice.shouldFail { + return fmt.Errorf("mock inactivity claim executor error") + } + + return nil } From 64c02b42da222a71a689243a33b4bcc7f8c87d66 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 14 May 2024 18:19:42 +0200 Subject: [PATCH 38/40] Added unit tests for inactivity claim signing and submitting members --- pkg/protocol/inactivity/member_test.go | 571 +++++++++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 pkg/protocol/inactivity/member_test.go diff --git a/pkg/protocol/inactivity/member_test.go b/pkg/protocol/inactivity/member_test.go new file mode 100644 index 0000000000..d65f7d61ea --- /dev/null +++ b/pkg/protocol/inactivity/member_test.go @@ -0,0 +1,571 @@ +package inactivity + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/hex" + "fmt" + "math/big" + "reflect" + "testing" + + "github.com/keep-network/keep-core/internal/testutils" + "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/operator" + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestShouldAcceptMessage(t *testing.T) { + groupSize := 5 + honestThreshold := 3 + + localChain := local_v1.Connect(groupSize, honestThreshold) + + operatorsAddresses := make([]chain.Address, groupSize) + operatorsPublicKeys := make([][]byte, groupSize) + for i := range operatorsAddresses { + _, operatorPublicKey, err := operator.GenerateKeyPair( + local_v1.DefaultCurve, + ) + if err != nil { + t.Fatal(err) + } + + operatorAddress, err := localChain.Signing().PublicKeyToAddress( + operatorPublicKey, + ) + if err != nil { + t.Fatal(err) + } + + operatorsAddresses[i] = operatorAddress + operatorsPublicKeys[i] = operator.MarshalUncompressed(operatorPublicKey) + } + + tests := map[string]struct { + senderIndex group.MemberIndex + senderPublicKey []byte + inactiveMembersIDs []group.MemberIndex + expectedResult bool + }{ + "message from another valid and operating member": { + senderIndex: group.MemberIndex(2), + senderPublicKey: operatorsPublicKeys[1], + inactiveMembersIDs: []group.MemberIndex{}, + expectedResult: true, + }, + "message from another valid but non-operating member": { + senderIndex: group.MemberIndex(2), + senderPublicKey: operatorsPublicKeys[1], + inactiveMembersIDs: []group.MemberIndex{2}, + expectedResult: false, + }, + "message from self": { + senderIndex: group.MemberIndex(1), + senderPublicKey: operatorsPublicKeys[0], + inactiveMembersIDs: []group.MemberIndex{}, + expectedResult: false, + }, + "message from another invalid member": { + senderIndex: group.MemberIndex(2), + senderPublicKey: operatorsPublicKeys[3], + inactiveMembersIDs: []group.MemberIndex{}, + expectedResult: false, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + membershipValdator := group.NewMembershipValidator( + &testutils.MockLogger{}, + operatorsAddresses, + localChain.Signing(), + ) + + member := newSigningMember( + &testutils.MockLogger{}, + group.MemberIndex(1), + groupSize, + groupSize-honestThreshold, + membershipValdator, + "session_1", + ) + + for _, inactiveMemberID := range test.inactiveMembersIDs { + member.group.MarkMemberAsInactive(inactiveMemberID) + } + + result := member.shouldAcceptMessage(test.senderIndex, test.senderPublicKey) + + testutils.AssertBoolsEqual( + t, + "result from message validator", + test.expectedResult, + result, + ) + }) + } +} + +func TestSignClaim(t *testing.T) { + signingMember := initializeSigningMember(t) + + walletPublicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + walletPublicKey := unmarshalPublicKey(walletPublicKeyHex) + + claim := NewClaimPreimage( + big.NewInt(3), + walletPublicKey, + []group.MemberIndex{1, 3}, + true, + ) + + publicKey := []byte("publicKey") + signature := []byte("signature") + claimHash := ClaimHash{0: 11, 6: 22, 31: 33} + sessionID := signingMember.sessionID + + claimSigner := newMockClaimSigner(publicKey) + claimSigner.setSigningOutcome(claim, &signingOutcome{ + signature: signature, + claimHash: claimHash, + err: nil, + }) + + actualSignatureMessage, err := signingMember.signClaim( + claim, + claimSigner, + ) + if err != nil { + t.Fatal(err) + } + + expectedSignatureMessage := &claimSignatureMessage{ + senderID: signingMember.memberIndex, + claimHash: claimHash, + signature: signature, + publicKey: publicKey, + sessionID: sessionID, + } + + if !reflect.DeepEqual( + expectedSignatureMessage, + actualSignatureMessage, + ) { + t.Errorf( + "unexpected signature message \nexpected: %v\nactual: %v\n", + expectedSignatureMessage, + actualSignatureMessage, + ) + } + + if !bytes.Equal(signature, signingMember.selfInactivityClaimSignature) { + t.Errorf( + "unexpected self inactivity claim signature\nexpected: %v\nactual: %v\n", + signature, + signingMember.selfInactivityClaimSignature, + ) + } + + if claimHash != signingMember.preferredInactivityClaimHash { + t.Errorf( + "unexpected preferred inactivity claim hash\nexpected: %v\nactual: %v\n", + claimHash, + signingMember.preferredInactivityClaimHash, + ) + } +} + +func TestSignClaim_ErrorDuringSigning(t *testing.T) { + signingMember := initializeSigningMember(t) + + walletPublicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + walletPublicKey := unmarshalPublicKey(walletPublicKeyHex) + + claim := NewClaimPreimage( + big.NewInt(3), + walletPublicKey, + []group.MemberIndex{1, 3}, + true, + ) + + claimSigner := newMockClaimSigner([]byte("publicKey")) + claimSigner.setSigningOutcome(claim, &signingOutcome{ + signature: []byte("signature"), + claimHash: ClaimHash{0: 11, 6: 22, 31: 33}, + err: fmt.Errorf("dummy error"), + }) + + _, err = signingMember.signClaim( + claim, + claimSigner, + ) + + expectedErr := fmt.Errorf("failed to sign inactivity claim [dummy error]") + if !reflect.DeepEqual(expectedErr, err) { + t.Errorf( + "unexpected error\nexpected: %v\nactual: %v\n", + expectedErr, + err, + ) + } +} + +func TestVerifyInactivityClaimSignatures(t *testing.T) { + signingMember := initializeSigningMember(t) + signingMember.preferredInactivityClaimHash = ClaimHash{11: 11} + signingMember.selfInactivityClaimSignature = []byte("sign 1") + + type messageWithOutcome struct { + message *claimSignatureMessage + outcome *verificationOutcome + } + + tests := map[string]struct { + messagesWithOutcomes []messageWithOutcome + expectedValidSignatures map[group.MemberIndex][]byte + }{ + "messages from other members with valid signatures for the preferred claim": { + messagesWithOutcomes: []messageWithOutcome{ + { + &claimSignatureMessage{ + senderID: 2, + claimHash: ClaimHash{11: 11}, + signature: []byte("sign 2"), + publicKey: []byte("pubKey 2"), + sessionID: "session-1", + }, + &verificationOutcome{ + isValid: true, + err: nil, + }, + }, + { + &claimSignatureMessage{ + senderID: 3, + claimHash: ClaimHash{11: 11}, + signature: []byte("sign 3"), + publicKey: []byte("pubKey 3"), + sessionID: "session-1", + }, + &verificationOutcome{ + isValid: true, + err: nil, + }, + }, + }, + expectedValidSignatures: map[group.MemberIndex][]byte{ + signingMember.memberIndex: signingMember.selfInactivityClaimSignature, + 2: []byte("sign 2"), + 3: []byte("sign 3"), + }, + }, + "received a message from other member with signature for claim " + + "different than preferred": { + messagesWithOutcomes: []messageWithOutcome{ + { + &claimSignatureMessage{ + senderID: 2, + claimHash: ClaimHash{12: 12}, + signature: []byte("sign 2"), + publicKey: []byte("pubKey 2"), + sessionID: "session-1", + }, + &verificationOutcome{ + isValid: true, + err: nil, + }, + }, + }, + expectedValidSignatures: map[group.MemberIndex][]byte{ + signingMember.memberIndex: signingMember.selfInactivityClaimSignature, + }, + }, + "message from other member that causes an error during signature " + + "verification": { + messagesWithOutcomes: []messageWithOutcome{ + { + &claimSignatureMessage{ + senderID: 2, + claimHash: ClaimHash{11: 11}, + signature: []byte("sign 2"), + publicKey: []byte("pubKey 2"), + sessionID: "session-1", + }, + &verificationOutcome{ + isValid: false, + err: fmt.Errorf("dummy error"), + }, + }, + }, + expectedValidSignatures: map[group.MemberIndex][]byte{ + signingMember.memberIndex: signingMember.selfInactivityClaimSignature, + }, + }, + "message from other member with invalid signature": { + messagesWithOutcomes: []messageWithOutcome{ + { + &claimSignatureMessage{ + senderID: 2, + claimHash: ClaimHash{11: 11}, + signature: []byte("bad sign"), + publicKey: []byte("pubKey 2"), + sessionID: "session-1", + }, + &verificationOutcome{ + isValid: false, + err: nil, + }, + }, + }, + expectedValidSignatures: map[group.MemberIndex][]byte{ + signingMember.memberIndex: signingMember.selfInactivityClaimSignature, + }, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + claimSigner := newMockClaimSigner([]byte("publicKey")) + + var messages []*claimSignatureMessage + for _, messageWithOutcome := range test.messagesWithOutcomes { + messages = append(messages, messageWithOutcome.message) + claimSigner.setVerificationOutcome( + messageWithOutcome.message, + messageWithOutcome.outcome, + ) + } + + validSignatures := signingMember.verifyInactivityClaimSignatures( + messages, + claimSigner, + ) + if !reflect.DeepEqual(validSignatures, test.expectedValidSignatures) { + t.Errorf( + "unexpected valid signatures\nexpected: %v\nactual: %v\n", + test.expectedValidSignatures, + validSignatures, + ) + } + }) + } +} + +func TestSubmitClaim(t *testing.T) { + submittingMember := initializeSubmittingMember(t) + + claim := &ClaimPreimage{} + signatures := map[group.MemberIndex][]byte{ + 11: []byte("signature 11"), + 22: []byte("signature 22"), + 33: []byte("signature 33"), + } + + claimSubmitter := newMockClaimSubmitter() + claimSubmitter.setSubmittingOutcome(claim, nil) + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + err := submittingMember.submitClaim( + ctx, + claim, + signatures, + claimSubmitter, + ) + if err != nil { + t.Fatal(err) + } +} + +func initializeSigningMember(t *testing.T) *signingMember { + groupSize := 5 + honestThreshold := 3 + + localChain := local_v1.Connect(groupSize, honestThreshold) + + operatorsAddresses := make([]chain.Address, groupSize) + operatorsPublicKeys := make([][]byte, groupSize) + for i := range operatorsAddresses { + _, operatorPublicKey, err := operator.GenerateKeyPair( + local_v1.DefaultCurve, + ) + if err != nil { + t.Fatal(err) + } + + operatorAddress, err := localChain.Signing().PublicKeyToAddress( + operatorPublicKey, + ) + if err != nil { + t.Fatal(err) + } + + operatorsAddresses[i] = operatorAddress + operatorsPublicKeys[i] = operator.MarshalUncompressed(operatorPublicKey) + } + + membershipValidator := group.NewMembershipValidator( + &testutils.MockLogger{}, + operatorsAddresses, + localChain.Signing(), + ) + + return newSigningMember( + &testutils.MockLogger{}, + group.MemberIndex(1), + groupSize, + groupSize-honestThreshold, + membershipValidator, + "session_1", + ) +} + +func initializeSubmittingMember(t *testing.T) *submittingMember { + signingMember := initializeSigningMember(t) + return signingMember.initializeSubmittingMember() +} + +type signingOutcome struct { + signature []byte + claimHash ClaimHash + err error +} + +type verificationOutcome struct { + isValid bool + err error +} + +type mockClaimSigner struct { + publicKey []byte + signingOutcomes map[*ClaimPreimage]*signingOutcome + verificationOutcomes map[string]*verificationOutcome +} + +func newMockClaimSigner(publicKey []byte) *mockClaimSigner { + return &mockClaimSigner{ + publicKey: publicKey, + signingOutcomes: make(map[*ClaimPreimage]*signingOutcome), + verificationOutcomes: make(map[string]*verificationOutcome), + } +} + +func (mrs *mockClaimSigner) setSigningOutcome( + claim *ClaimPreimage, + outcome *signingOutcome, +) { + mrs.signingOutcomes[claim] = outcome +} + +func (mrs *mockClaimSigner) setVerificationOutcome( + message *claimSignatureMessage, + outcome *verificationOutcome, +) { + key := signatureVerificationKey( + message.publicKey, + message.signature, + message.claimHash, + ) + mrs.verificationOutcomes[key] = outcome +} + +func (mrs *mockClaimSigner) SignClaim(claim *ClaimPreimage) (*SignedClaimHash, error) { + if outcome, ok := mrs.signingOutcomes[claim]; ok { + return &SignedClaimHash{ + PublicKey: mrs.publicKey, + Signature: outcome.signature, + ClaimHash: outcome.claimHash, + }, outcome.err + } + + return nil, fmt.Errorf( + "could not find singing outcome for the inactivity claim", + ) +} + +func (mrs *mockClaimSigner) VerifySignature(signedClaimHash *SignedClaimHash) (bool, error) { + key := signatureVerificationKey( + signedClaimHash.PublicKey, + signedClaimHash.Signature, + signedClaimHash.ClaimHash, + ) + if outcome, ok := mrs.verificationOutcomes[key]; ok { + return outcome.isValid, outcome.err + } + + return false, fmt.Errorf( + "could not find signature verification outcome for the signed claim", + ) +} + +func signatureVerificationKey( + publicKey []byte, + signature []byte, + claimHash ClaimHash, +) string { + return fmt.Sprintf("%s-%s-%s", publicKey, signature, claimHash[:]) +} + +type mockClaimSubmitter struct { + submittingOutcomes map[*ClaimPreimage]error +} + +func newMockClaimSubmitter() *mockClaimSubmitter { + return &mockClaimSubmitter{ + submittingOutcomes: make(map[*ClaimPreimage]error), + } +} + +func (mrs *mockClaimSubmitter) setSubmittingOutcome( + claim *ClaimPreimage, + err error, +) { + mrs.submittingOutcomes[claim] = err +} + +func (mrs *mockClaimSubmitter) SubmitClaim( + ctx context.Context, + memberIndex group.MemberIndex, + claim *ClaimPreimage, + signatures map[group.MemberIndex][]byte, +) error { + if err, ok := mrs.submittingOutcomes[claim]; ok { + return err + } + return fmt.Errorf( + "could not find submitting outcome for the claim", + ) +} + +func unmarshalPublicKey(bytes []byte) *ecdsa.PublicKey { + x, y := elliptic.Unmarshal( + tecdsa.Curve, + bytes, + ) + + return &ecdsa.PublicKey{ + Curve: tecdsa.Curve, + X: x, + Y: y, + } +} From 01d69fc960a15b4f21f7e5270e7f353a202cef85 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 14 May 2024 19:56:01 +0200 Subject: [PATCH 39/40] Added unit tests for inactivity submission states --- pkg/protocol/inactivity/states_test.go | 70 ++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 pkg/protocol/inactivity/states_test.go diff --git a/pkg/protocol/inactivity/states_test.go b/pkg/protocol/inactivity/states_test.go new file mode 100644 index 0000000000..61d2d5c205 --- /dev/null +++ b/pkg/protocol/inactivity/states_test.go @@ -0,0 +1,70 @@ +package inactivity + +import ( + "reflect" + "testing" + + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/state" +) + +func TestReceivedMessages(t *testing.T) { + state := state.NewBaseAsyncState() + + message1 := &claimSignatureMessage{senderID: 1} + message2 := &claimSignatureMessage{senderID: 2} + message3 := &claimSignatureMessage{senderID: 1} + message4 := &claimSignatureMessage{senderID: 3} + message5 := &claimSignatureMessage{senderID: 3} + + state.ReceiveToHistory(newMockNetMessage(message1)) + state.ReceiveToHistory(newMockNetMessage(message2)) + state.ReceiveToHistory(newMockNetMessage(message3)) + state.ReceiveToHistory(newMockNetMessage(message4)) + state.ReceiveToHistory(newMockNetMessage(message5)) + + expectedMessages := []*claimSignatureMessage{message1, message2, message4} + actualType1Messages := receivedMessages[*claimSignatureMessage](state) + if !reflect.DeepEqual(expectedMessages, actualType1Messages) { + t.Errorf( + "unexpected messages\n"+ + "expected: [%v]\n"+ + "actual: [%v]", + expectedMessages, + actualType1Messages, + ) + } +} + +type mockNetMessage struct { + payload interface{} +} + +func newMockNetMessage(payload interface{}) *mockNetMessage { + return &mockNetMessage{payload} +} + +func (mnm *mockNetMessage) TransportSenderID() net.TransportIdentifier { + panic("not implemented") +} + +func (mnm *mockNetMessage) SenderPublicKey() []byte { + panic("not implemented") +} + +func (mnm *mockNetMessage) Payload() interface{} { + return mnm.payload +} + +func (mnm *mockNetMessage) Type() string { + payload, ok := mnm.payload.(message) + if !ok { + panic("wrong payload type") + } + + return payload.Type() +} + +func (mnm *mockNetMessage) Seqno() uint64 { + panic("not implemented") +} From e5d61e7a845704ff478ef6ef1a071784a2e9d0cb Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Wed, 15 May 2024 14:04:34 +0200 Subject: [PATCH 40/40] Added check for unstaking operators --- pkg/chain/ethereum/tbtc.go | 6 ---- pkg/tbtc/chain.go | 9 ------ pkg/tbtc/chain_test.go | 33 ++++++++++++++----- pkg/tbtc/heartbeat.go | 34 ++++++++++++++++++-- pkg/tbtc/heartbeat_test.go | 66 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 25 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 9cce628ce0..3fd3ce6172 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -2363,9 +2363,3 @@ func (tc *TbtcChain) GetRedemptionDelay( func (tc *TbtcChain) GetDepositMinAge() (uint32, error) { return tc.walletProposalValidator.DEPOSITMINAGE() } - -func (tc *TbtcChain) IsOperatorUnstaking() (bool, error) { - // TODO: Implement by checking if the operator has deauthorized their entire - // stake. - return false, nil -} diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 1b0a0c7c2a..1b579e326b 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -24,14 +24,6 @@ const ( Challenge ) -// StakingChain defines the subset of the TBTC chain interface that pertains to -// the staking activities. -type StakingChain interface { - // IsOperatorUnstaking checks if the operator is unstaking. It returns true - // if the operator has deauthorized their entire stake, false otherwise. - IsOperatorUnstaking() (bool, error) -} - // GroupSelectionChain defines the subset of the TBTC chain interface that // pertains to the group selection activities. type GroupSelectionChain interface { @@ -539,7 +531,6 @@ type Chain interface { GetBlockHashByNumber(blockNumber uint64) ([32]byte, error) sortition.Chain - StakingChain GroupSelectionChain DistributedKeyGenerationChain InactivityClaimChain diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 95e250bd5e..6e293f23dc 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -27,7 +27,10 @@ import ( "github.com/keep-network/keep-core/pkg/tecdsa/dkg" ) -const localChainOperatorID = chain.OperatorID(1) +const ( + localChainOperatorID = chain.OperatorID(1) + stakingProvider = chain.Address("0x1111111111111111111111111111111111111111") +) type movingFundsParameters = struct { txMaxTotalFee uint64 @@ -108,6 +111,9 @@ type localChain struct { movingFundsParametersMutex sync.Mutex movingFundsParameters movingFundsParameters + eligibleStakesMutex sync.Mutex + eligibleStakes map[chain.Address]*big.Int + blockCounter chain.BlockCounter operatorPrivateKey *operator.PrivateKey } @@ -185,11 +191,26 @@ func (lc *localChain) setBlockHashByNumber( } func (lc *localChain) OperatorToStakingProvider() (chain.Address, bool, error) { - panic("unsupported") + return stakingProvider, true, nil } func (lc *localChain) EligibleStake(stakingProvider chain.Address) (*big.Int, error) { - panic("unsupported") + lc.eligibleStakesMutex.Lock() + defer lc.eligibleStakesMutex.Unlock() + + eligibleStake, ok := lc.eligibleStakes[stakingProvider] + if !ok { + return nil, fmt.Errorf("eligible stake not found") + } + + return eligibleStake, nil +} + +func (lc *localChain) setOperatorsEligibleStake(stake *big.Int) { + lc.eligibleStakesMutex.Lock() + defer lc.eligibleStakesMutex.Unlock() + + lc.eligibleStakes[stakingProvider] = stake } func (lc *localChain) IsPoolLocked() (bool, error) { @@ -216,11 +237,6 @@ func (lc *localChain) IsEligibleForRewards() (bool, error) { panic("unsupported") } -func (lc *localChain) IsOperatorUnstaking() (bool, error) { - // TODO: Implement and use in unit tests. - return false, nil -} - func (lc *localChain) CanRestoreRewardEligibility() (bool, error) { panic("unsupported") } @@ -1402,6 +1418,7 @@ func ConnectWithKey( movedFundsSweepProposalValidations: make(map[[32]byte]bool), heartbeatProposalValidations: make(map[[16]byte]bool), depositRequests: make(map[[32]byte]*DepositChainRequest), + eligibleStakes: make(map[chain.Address]*big.Int), blockCounter: blockCounter, operatorPrivateKey: operatorPrivateKey, } diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index 65821f65a4..770d055879 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -123,9 +123,12 @@ func newHeartbeatAction( func (ha *heartbeatAction) execute() error { // Do not execute the heartbeat action if the operator is unstaking. - isUnstaking, err := ha.chain.IsOperatorUnstaking() + isUnstaking, err := ha.isOperatorUnstaking() if err != nil { - return fmt.Errorf("failed to check if the operator is unstaking") + return fmt.Errorf( + "failed to check if the operator is unstaking [%v]", + err, + ) } if isUnstaking { @@ -247,6 +250,33 @@ func (ha *heartbeatAction) actionType() WalletActionType { return ActionHeartbeat } +func (ha *heartbeatAction) isOperatorUnstaking() (bool, error) { + stakingProvider, isRegistered, err := ha.chain.OperatorToStakingProvider() + if err != nil { + return false, fmt.Errorf( + "failed to get staking provider for operator [%v]", + err, + ) + } + + if !isRegistered { + return false, fmt.Errorf("staking provider not registered for operator") + } + + // Eligible stake is defined as the currently authorized stake minus the + // pending authorization decrease. + eligibleStake, err := ha.chain.EligibleStake(stakingProvider) + if err != nil { + return false, fmt.Errorf( + "failed to check eligible stake for operator [%v]", + err, + ) + } + + // The operator is considered unstaking if their eligible stake is `0`. + return eligibleStake.Cmp(big.NewInt(0)) == 0, nil +} + // heartbeatFailureCounter holds counters keeping track of consecutive // heartbeat failures. Each wallet has a separate counter. The key used in // the map is the uncompressed public key (with 04 prefix) of the wallet. diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index 92cd2e0d3c..6946904201 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -46,6 +46,7 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { } hostChain := Connect() + hostChain.setOperatorsEligibleStake(big.NewInt(100000)) hostChain.setHeartbeatProposalValidationResult(proposal, true) // Set the active operators count to the minimum required value. @@ -102,6 +103,67 @@ func TestHeartbeatAction_HappyPath(t *testing.T) { ) } +func TestHeartbeatAction_OperatorUnstaking(t *testing.T) { + walletPublicKeyHex, err := hex.DecodeString( + "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + + "aa0d2053a91a050a6797d85c38f2909cb7027f2344a01986aa2f9f8ca7a0c289", + ) + if err != nil { + t.Fatal(err) + } + + startBlock := uint64(10) + expiryBlock := startBlock + heartbeatTotalProposalValidityBlocks + + proposal := &HeartbeatProposal{ + Message: [16]byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, + } + + heartbeatFailureCounter := newHeartbeatFailureCounter() + + hostChain := Connect() + hostChain.setOperatorsEligibleStake(big.NewInt(0)) + hostChain.setHeartbeatProposalValidationResult(proposal, true) + + // Set the active operators count to the minimum required value. + mockExecutor := &mockHeartbeatSigningExecutor{} + mockExecutor.activeOperatorsCount = heartbeatSigningMinimumActiveOperators + + inactivityClaimExecutor := &mockInactivityClaimExecutor{} + + action := newHeartbeatAction( + logger, + hostChain, + wallet{ + publicKey: unmarshalPublicKey(walletPublicKeyHex), + }, + mockExecutor, + proposal, + heartbeatFailureCounter, + inactivityClaimExecutor, + startBlock, + expiryBlock, + func(ctx context.Context, blockHeight uint64) error { + return nil + }, + ) + + err = action.execute() + if err != nil { + t.Fatal(err) + } + + testutils.AssertBigIntsEqual( + t, + "message to sign", + nil, // sign not called + mockExecutor.requestedMessage, + ) +} + func TestHeartbeatAction_Failure_SigningError(t *testing.T) { walletPublicKeyHex, err := hex.DecodeString( "0471e30bca60f6548d7b42582a478ea37ada63b402af7b3ddd57f0c95bb6843175" + @@ -126,6 +188,7 @@ func TestHeartbeatAction_Failure_SigningError(t *testing.T) { heartbeatFailureCounter := newHeartbeatFailureCounter() hostChain := Connect() + hostChain.setOperatorsEligibleStake(big.NewInt(100000)) hostChain.setHeartbeatProposalValidationResult(proposal, true) mockExecutor := &mockHeartbeatSigningExecutor{} @@ -196,6 +259,7 @@ func TestHeartbeatAction_Failure_TooFewActiveOperators(t *testing.T) { heartbeatFailureCounter := newHeartbeatFailureCounter() hostChain := Connect() + hostChain.setOperatorsEligibleStake(big.NewInt(100000)) hostChain.setHeartbeatProposalValidationResult(proposal, true) // Set the active operators count just below the required number. @@ -276,6 +340,7 @@ func TestHeartbeatAction_Failure_CounterExceeded(t *testing.T) { heartbeatFailureCounter.increment(walletPublicKeyStr) hostChain := Connect() + hostChain.setOperatorsEligibleStake(big.NewInt(100000)) hostChain.setHeartbeatProposalValidationResult(proposal, true) mockExecutor := &mockHeartbeatSigningExecutor{} @@ -355,6 +420,7 @@ func TestHeartbeatAction_Failure_InactivityExecutionFailure(t *testing.T) { heartbeatFailureCounter.increment(walletPublicKeyStr) hostChain := Connect() + hostChain.setOperatorsEligibleStake(big.NewInt(100000)) hostChain.setHeartbeatProposalValidationResult(proposal, true) mockExecutor := &mockHeartbeatSigningExecutor{}