From 5c7b2caeb4dd13a094937988a7bb99b745e4f2c9 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 16 Apr 2024 18:42:31 +0200 Subject: [PATCH] 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 | 33 +++-- pkg/tbtc/heartbeat_test.go | 11 ++ pkg/tbtc/inactivity.go | 62 ++++++++ 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 | 209 +++++++++++++++++++++++++++ 14 files changed, 744 insertions(+), 15 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..1d7a95aee9 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 + inactivityNotifier *operatorInactivityNotifier + startBlock uint64 expiryBlock uint64 @@ -79,20 +82,22 @@ func newHeartbeatAction( signingExecutor heartbeatSigningExecutor, proposal *HeartbeatProposal, failureCounter *uint, + inactivityNotifier *operatorInactivityNotifier, 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, + inactivityNotifier: inactivityNotifier, + startBlock: startBlock, + expiryBlock: expiryBlock, + waitForBlockFn: waitForBlockFn, } } @@ -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.inactivityNotifier.publishInactivityClaim( + []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..9be202184d 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 := newOperatorInactivityNotifier( + 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 := newOperatorInactivityNotifier( + 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..7933d6e00c --- /dev/null +++ b/pkg/tbtc/inactivity.go @@ -0,0 +1,62 @@ +package tbtc + +import ( + "context" + "math/big" + + "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/tecdsa/inactivity" +) + +type operatorInactivityNotifier struct { + chain Chain + signers []*signer +} + +func newOperatorInactivityNotifier( + chain Chain, + signers []*signer, +) *operatorInactivityNotifier { + return &operatorInactivityNotifier{ + chain: chain, + signers: signers, + } +} + +func (oin *operatorInactivityNotifier) publishInactivityClaim( + inactiveMembersIndexes []group.MemberIndex, + heartbeatFailed bool, +) error { + // TODO: Build a claim and launch the publish function for all + // the signers. + + return nil +} + +func (oin *operatorInactivityNotifier) 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(oin.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..a114fb8e0c 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, +) (*operatorInactivityNotifier, 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 := newOperatorInactivityNotifier( + 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..9af7d90c84 --- /dev/null +++ b/pkg/tecdsa/inactivity/states.go @@ -0,0 +1,209 @@ +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 + + 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())) + }, + ) +}