From 82dc68a3f5630af1aeb6f9fa36a2283b25a7cfc5 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 14 May 2024 13:51:49 +0200 Subject: [PATCH] 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 }