diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 9f99230656..2e8ed655da 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -373,6 +373,43 @@ func (tc *TbtcChain) OnDKGStarted( return tc.walletRegistry.DkgStartedEvent(nil, nil).OnEvent(onEvent) } +func (tc *TbtcChain) PastDKGStartedEvents( + filter *tbtc.DKGStartedEventFilter, +) ([]*tbtc.DKGStartedEvent, error) { + var startBlock uint64 + var endBlock *uint64 + var seed []*big.Int + + if filter != nil { + startBlock = filter.StartBlock + endBlock = filter.EndBlock + seed = filter.Seed + } + + events, err := tc.walletRegistry.PastDkgStartedEvents( + startBlock, + endBlock, + seed, + ) + if err != nil { + return nil, err + } + + dkgStartedEvents := make([]*tbtc.DKGStartedEvent, len(events)) + for i, event := range events { + dkgStartedEvents[i] = &tbtc.DKGStartedEvent{ + Seed: event.Seed, + BlockNumber: event.Raw.BlockNumber, + } + } + + sort.SliceStable(dkgStartedEvents, func(i, j int) bool { + return dkgStartedEvents[i].BlockNumber < dkgStartedEvents[j].BlockNumber + }) + + return dkgStartedEvents, err +} + func (tc *TbtcChain) OnDKGResultSubmitted( handler func(event *tbtc.DKGResultSubmittedEvent), ) subscription.EventSubscription { diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 2cebe013c9..7a870bbbeb 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -49,6 +49,14 @@ type DistributedKeyGenerationChain interface { func(event *DKGStartedEvent), ) subscription.EventSubscription + // PastDKGStartedEvents fetches past DKG started events according to the + // provided filter or unfiltered if the filter is nil. Returned events + // are sorted by the block number in the ascending order, i.e. the latest + // event is at the end of the slice. + PastDKGStartedEvents( + filter *DKGStartedEventFilter, + ) ([]*DKGStartedEvent, error) + // OnDKGResultSubmitted registers a callback that is invoked when an on-chain // notification of the DKG result submission is seen. OnDKGResultSubmitted( @@ -131,6 +139,13 @@ type DKGStartedEvent struct { BlockNumber uint64 } +// DKGStartedEventFilter is a component allowing to filter DKGStartedEvent. +type DKGStartedEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + Seed []*big.Int +} + // DKGResultSubmittedEvent represents a DKG result submission event. It is // emitted after a submitted DKG result lands on the chain. type DKGResultSubmittedEvent struct { diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 522c80150f..fbc92fe863 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -131,6 +131,12 @@ func (lc *localChain) OnDKGStarted( panic("unsupported") } +func (lc *localChain) PastDKGStartedEvents( + filter *DKGStartedEventFilter, +) ([]*DKGStartedEvent, error) { + panic("unsupported") +} + func (lc *localChain) OnDKGResultSubmitted( handler func(event *DKGResultSubmittedEvent), ) subscription.EventSubscription { diff --git a/pkg/tbtc/dkg.go b/pkg/tbtc/dkg.go index e1e066cd40..3c33876776 100644 --- a/pkg/tbtc/dkg.go +++ b/pkg/tbtc/dkg.go @@ -20,6 +20,10 @@ import ( ) const ( + // dkgStartedConfirmationBlocks determines the block length of the + // confirmation period that is preserved after a DKG start. Once the period + // elapses, the DKG state is checked to confirm the protocol can be started. + dkgStartedConfirmationBlocks = 20 // dkgResultSubmissionDelayStep 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 DKG result @@ -104,10 +108,14 @@ func (de *dkgExecutor) preParamsCount() int { // executeDkgIfEligible is the main function of dkgExecutor. It performs the // full execution of ECDSA Distributed Key Generation: determining members // selected to the signing group, executing off-chain protocol, and publishing -// the result to the chain. +// the result to the chain. The execution can be delayed by an arbitrary number +// of blocks using the delayBlocks argument. This allows confirming the state +// on-chain - e.g. wait for the required number of confirming blocks - before +//executing the off-chain action. func (de *dkgExecutor) executeDkgIfEligible( seed *big.Int, startBlock uint64, + delayBlocks uint64, ) { dkgLogger := logger.With( zap.String("seed", fmt.Sprintf("0x%x", seed)), @@ -145,6 +153,7 @@ func (de *dkgExecutor) executeDkgIfEligible( memberIndexes, groupSelectionResult, startBlock, + delayBlocks, ) } else { dkgLogger.Infof("not eligible for DKG") @@ -224,13 +233,19 @@ func (de *dkgExecutor) setupBroadcastChannel( // generateSigningGroup executes off-chain protocol for each member controlled // by the current operator and upon successful execution of the protocol -// publishes the result to the chain. +// publishes the result to the chain. The execution can be delayed by an +// arbitrary number of blocks using the delayBlocks argument. This allows +// confirming the state on-chain - e.g. wait for the required number of +// confirming blocks - before executing the off-chain action. Note that the +// startBlock represents the block at which DKG started on-chain. This is +// important for the result submission. func (de *dkgExecutor) generateSigningGroup( dkgLogger *zap.SugaredLogger, seed *big.Int, memberIndexes []uint8, groupSelectionResult *GroupSelectionResult, startBlock uint64, + delayBlocks uint64, ) { membershipValidator := group.NewMembershipValidator( dkgLogger, @@ -296,7 +311,7 @@ func (de *dkgExecutor) generateSigningGroup( retryLoop := newDkgRetryLoop( dkgLogger, seed, - startBlock, + startBlock+delayBlocks, memberIndex, groupSelectionResult.OperatorsAddresses, de.groupParameters, diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 4338c5970f..5a34abc182 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -143,9 +143,16 @@ func (n *node) operatorID() (chain.OperatorID, error) { // distributed key generation if this node's operator proves to be eligible for // the group generated by that seed. This is an interactive on-chain process, // and joinDKGIfEligible can block for an extended period of time while it -// completes the on-chain operation. -func (n *node) joinDKGIfEligible(seed *big.Int, startBlock uint64) { - n.dkgExecutor.executeDkgIfEligible(seed, startBlock) +// completes the on-chain operation. The execution can be delayed by an +// arbitrary number of blocks using the delayBlocks argument. This allows +// confirming the state on-chain - e.g. wait for the required number of +// confirming blocks - before executing the off-chain action. +func (n *node) joinDKGIfEligible( + seed *big.Int, + startBlock uint64, + delayBlocks uint64, +) { + n.dkgExecutor.executeDkgIfEligible(seed, startBlock, delayBlocks) } // validateDKG performs the submitted DKG result validation process. diff --git a/pkg/tbtc/tbtc.go b/pkg/tbtc/tbtc.go index 324d94af54..bb326feec3 100644 --- a/pkg/tbtc/tbtc.go +++ b/pkg/tbtc/tbtc.go @@ -137,25 +137,86 @@ func Initialize( if ok := deduplicator.notifyDKGStarted( event.Seed, ); !ok { - logger.Warnf( - "DKG started event with seed [0x%x] and "+ - "starting block [%v] has been already processed", + logger.Infof( + "DKG started event with seed [0x%x] has been "+ + "already processed", event.Seed, - event.BlockNumber, ) return } + confirmationBlock := event.BlockNumber + dkgStartedConfirmationBlocks + logger.Infof( - "DKG started with seed [0x%x] at block [%v]", + "observed DKG started event with seed [0x%x] and "+ + "starting block [%v]; waiting for block [%v] to confirm", event.Seed, event.BlockNumber, + confirmationBlock, ) - node.joinDKGIfEligible( - event.Seed, - event.BlockNumber, - ) + err := node.waitForBlockHeight(ctx, confirmationBlock) + if err != nil { + logger.Errorf("failed to confirm DKG started event: [%v]", err) + return + } + + dkgState, err := chain.GetDKGState() + if err != nil { + logger.Errorf("failed to check DKG state: [%v]", err) + return + } + + if dkgState == AwaitingResult { + // Fetch all past DKG started events starting from one + // confirmation period before the original event's block. + // If there was a chain reorg, the event we received could be + // moved to a block with a lower number than the one + // we received. + pastEvents, err := chain.PastDKGStartedEvents( + &DKGStartedEventFilter{ + StartBlock: event.BlockNumber - dkgStartedConfirmationBlocks, + }, + ) + if err != nil { + logger.Errorf("failed to get past DKG started events: [%v]", err) + return + } + + // Should not happen but just in case. + if len(pastEvents) == 0 { + logger.Errorf("no past DKG started events") + return + } + + lastEvent := pastEvents[len(pastEvents)-1] + + logger.Infof( + "DKG started with seed [0x%x] at block [%v]", + lastEvent.Seed, + lastEvent.BlockNumber, + ) + + // The off-chain protocol should be started as close as possible + // to the current block or even further. Starting the off-chain + // protocol with a past block will likely cause a failure of the + // first attempt as the start block is used to synchronize + // the announcements and the state machine. Here we ensure + // a proper start point by delaying the execution by the + // confirmation period length. + node.joinDKGIfEligible( + lastEvent.Seed, + lastEvent.BlockNumber, + dkgStartedConfirmationBlocks, + ) + } else { + logger.Infof( + "DKG started event with seed [0x%x] and starting "+ + "block [%v] was not confirmed", + event.Seed, + event.BlockNumber, + ) + } }() })