Skip to content

Commit

Permalink
Heartbeats: Notify operator inactivity on failure (#3800)
Browse files Browse the repository at this point in the history
#Refs: #3796.
This PR enables notifying of operator inactivity failure after multiple
failed heartbeats.
The inactivity notification is executed after three consecutive
heartbeat failures.
A heartbeat is considered as failed if there was an error during message
signing execution or the number of active signers was below a certain
threshold.
The operator inactivity notification is performed by
`inactivityClaimExecutor` and is similar to the DKG result submission
process.
  • Loading branch information
lukasz-zimnoch authored May 15, 2024
2 parents a3904dd + e5d61e7 commit 56755f6
Show file tree
Hide file tree
Showing 24 changed files with 4,133 additions and 79 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
185 changes: 184 additions & 1 deletion pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
"crypto/elliptic"
"encoding/binary"
"fmt"
"github.com/keep-network/keep-common/pkg/cache"
"math/big"
"reflect"
"sort"
"time"

"github.com/keep-network/keep-common/pkg/cache"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
Expand All @@ -26,6 +27,7 @@ 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"
Expand Down Expand Up @@ -1001,6 +1003,187 @@ 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,
signatures map[group.MemberIndex][]byte,
heartbeatFailed bool,
) (
*tbtc.InactivityClaim,
error,
) {
signingMemberIndices, signatureBytes, err := convertSignaturesToChainFormat(
signatures,
)
if err != nil {
return nil, fmt.Errorf(
"could not convert signatures to chain format: [%v]",
err,
)
}

return &tbtc.InactivityClaim{
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.InactivityClaim,
) 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.InactivityClaim,
nonce *big.Int,
groupMembers []uint32,
) error {
_, err := tc.walletRegistry.NotifyOperatorInactivity(
convertInactivityClaimToAbiType(claim),
nonce,
groupMembers,
)

return err
}

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 calculateInactivityClaimHash function expects
// an unprefixed 64-byte public key,
unprefixedGroupPublicKeyBytes := walletPublicKeyBytes[1:]

// 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))
}

return calculateInactivityClaimHash(
tc.chainID,
claim.Nonce,
unprefixedGroupPublicKeyBytes,
inactiveMembersIndexes,
claim.HeartbeatFailed,
)
}

func calculateInactivityClaimHash(
chainID *big.Int,
nonce *big.Int,
walletPublicKey []byte,
inactiveMembersIndexes []*big.Int,
heartbeatFailed bool,
) (inactivity.ClaimHash, error) {
publicKeySize := 64

if len(walletPublicKey) != publicKeySize {
return inactivity.ClaimHash{}, fmt.Errorf(
"wrong wallet public key length",
)
}

uint256Type, err := abi.NewType("uint256", "uint256", nil)
if err != nil {
return inactivity.ClaimHash{}, err
}
bytesType, err := abi.NewType("bytes", "bytes", nil)
if err != nil {
return inactivity.ClaimHash{}, err
}
uint256SliceType, err := abi.NewType("uint256[]", "uint256[]", nil)
if err != nil {
return inactivity.ClaimHash{}, err
}
boolType, err := abi.NewType("bool", "bool", nil)
if err != nil {
return inactivity.ClaimHash{}, 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.ClaimHash{}, err
}

return inactivity.ClaimHash(crypto.Keccak256Hash(bytes)), nil
}

func (tc *TbtcChain) GetInactivityClaimNonce(
walletID [32]byte,
) (*big.Int, error) {
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(
filter *tbtc.DepositRevealedEventFilter,
) ([]*tbtc.DepositRevealedEvent, error) {
Expand Down
39 changes: 39 additions & 0 deletions pkg/chain/ethereum/tbtc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,45 @@ func TestCalculateDKGResultSignatureHash(t *testing.T) {
)
}

func TestCalculateInactivityClaimHash(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 := calculateInactivityClaimHash(
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 {
Expand Down
Loading

0 comments on commit 56755f6

Please sign in to comment.