diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dee8d4a..bdcec56d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## Unreleased +### Improvements + +* [#132](https://github.com/babylonlabs-io/finality-provider/pull/132) Replace fast sync with batch processing + ### Documentation [#120](https://github.com/babylonlabs-io/finality-provider/pull/120) Spec of diff --git a/clientcontroller/babylon.go b/clientcontroller/babylon.go index ac4ec401..12608b9b 100644 --- a/clientcontroller/babylon.go +++ b/clientcontroller/babylon.go @@ -186,41 +186,10 @@ func (bc *BabylonController) SubmitFinalitySig( proof []byte, // TODO: have a type for proof sig *btcec.ModNScalar, ) (*types.TxResponse, error) { - cmtProof := cmtcrypto.Proof{} - if err := cmtProof.Unmarshal(proof); err != nil { - return nil, err - } - - msg := &finalitytypes.MsgAddFinalitySig{ - Signer: bc.mustGetTxSigner(), - FpBtcPk: bbntypes.NewBIP340PubKeyFromBTCPK(fpPk), - BlockHeight: block.Height, - PubRand: bbntypes.NewSchnorrPubRandFromFieldVal(pubRand), - Proof: &cmtProof, - BlockAppHash: block.Hash, - FinalitySig: bbntypes.NewSchnorrEOTSSigFromModNScalar(sig), - } - - unrecoverableErrs := []*sdkErr.Error{ - finalitytypes.ErrInvalidFinalitySig, - finalitytypes.ErrPubRandNotFound, - btcstakingtypes.ErrFpAlreadySlashed, - } - - expectedErrs := []*sdkErr.Error{ - finalitytypes.ErrDuplicatedFinalitySig, - } - - res, err := bc.reliablySendMsg(msg, expectedErrs, unrecoverableErrs) - if err != nil { - return nil, err - } - - if res == nil { - return &types.TxResponse{}, nil - } - - return &types.TxResponse{TxHash: res.TxHash, Events: res.Events}, nil + return bc.SubmitBatchFinalitySigs( + fpPk, []*types.BlockInfo{block}, []*btcec.FieldVal{pubRand}, + [][]byte{proof}, []*btcec.ModNScalar{sig}, + ) } // SubmitBatchFinalitySigs submits a batch of finality signatures to Babylon diff --git a/finality-provider/config/config.go b/finality-provider/config/config.go index 009d9030..ff6381bf 100644 --- a/finality-provider/config/config.go +++ b/finality-provider/config/config.go @@ -18,26 +18,25 @@ import ( ) const ( - defaultChainType = "babylon" - defaultLogLevel = zapcore.InfoLevel - defaultLogDirname = "logs" - defaultLogFilename = "fpd.log" - defaultFinalityProviderKeyName = "finality-provider" - DefaultRPCPort = 12581 - defaultConfigFileName = "fpd.conf" - defaultNumPubRand = 70000 // support running of 1 week with block production time as 10s - defaultNumPubRandMax = 100000 - defaultMinRandHeightGap = 35000 - defaultStatusUpdateInterval = 20 * time.Second - defaultRandomInterval = 30 * time.Second - defaultSubmitRetryInterval = 1 * time.Second - defaultFastSyncInterval = 10 * time.Second - defaultSyncFpStatusInterval = 30 * time.Second - defaultFastSyncLimit = 10 - defaultFastSyncGap = 3 - defaultMaxSubmissionRetries = 20 - defaultBitcoinNetwork = "signet" - defaultDataDirname = "data" + defaultChainType = "babylon" + defaultLogLevel = zapcore.InfoLevel + defaultLogDirname = "logs" + defaultLogFilename = "fpd.log" + defaultFinalityProviderKeyName = "finality-provider" + DefaultRPCPort = 12581 + defaultConfigFileName = "fpd.conf" + defaultNumPubRand = 70000 // support running of 1 week with block production time as 10s + defaultNumPubRandMax = 100000 + defaultMinRandHeightGap = 35000 + defaultBatchSubmissionSize = 1000 + defaultStatusUpdateInterval = 20 * time.Second + defaultRandomInterval = 30 * time.Second + defaultSubmitRetryInterval = 1 * time.Second + defaultSyncFpStatusInterval = 30 * time.Second + defaultSignatureSubmissionInterval = 1 * time.Second + defaultMaxSubmissionRetries = 20 + defaultBitcoinNetwork = "signet" + defaultDataDirname = "data" ) var ( @@ -56,19 +55,18 @@ var ( type Config struct { LogLevel string `long:"loglevel" description:"Logging level for all subsystems" choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal"` // ChainType and ChainID (if any) of the chain config identify a consumer chain - ChainType string `long:"chaintype" description:"the type of the consumer chain" choice:"babylon"` - NumPubRand uint32 `long:"numPubRand" description:"The number of Schnorr public randomness for each commitment"` - NumPubRandMax uint32 `long:"numpubrandmax" description:"The upper bound of the number of Schnorr public randomness for each commitment"` - MinRandHeightGap uint32 `long:"minrandheightgap" description:"The minimum gap between the last committed rand height and the current Babylon block height"` - StatusUpdateInterval time.Duration `long:"statusupdateinterval" description:"The interval between each update of finality-provider status"` - RandomnessCommitInterval time.Duration `long:"randomnesscommitinterval" description:"The interval between each attempt to commit public randomness"` - SubmissionRetryInterval time.Duration `long:"submissionretryinterval" description:"The interval between each attempt to submit finality signature or public randomness after a failure"` - MaxSubmissionRetries uint32 `long:"maxsubmissionretries" description:"The maximum number of retries to submit finality signature or public randomness"` - FastSyncInterval time.Duration `long:"fastsyncinterval" description:"The interval between each try of fast sync, which is disabled if the value is 0"` - FastSyncLimit uint32 `long:"fastsynclimit" description:"The maximum number of blocks to catch up for each fast sync"` - FastSyncGap uint64 `long:"fastsyncgap" description:"The block gap that will trigger the fast sync"` - EOTSManagerAddress string `long:"eotsmanageraddress" description:"The address of the remote EOTS manager; Empty if the EOTS manager is running locally"` - SyncFpStatusInterval time.Duration `long:"syncfpstatusinterval" description:"The duration of time that it should sync FP status with the client blockchain"` + ChainType string `long:"chaintype" description:"the type of the consumer chain" choice:"babylon"` + NumPubRand uint32 `long:"numPubRand" description:"The number of Schnorr public randomness for each commitment"` + NumPubRandMax uint32 `long:"numpubrandmax" description:"The upper bound of the number of Schnorr public randomness for each commitment"` + MinRandHeightGap uint32 `long:"minrandheightgap" description:"The minimum gap between the last committed rand height and the current Babylon block height"` + MaxSubmissionRetries uint32 `long:"maxsubmissionretries" description:"The maximum number of retries to submit finality signature or public randomness"` + EOTSManagerAddress string `long:"eotsmanageraddress" description:"The address of the remote EOTS manager; Empty if the EOTS manager is running locally"` + BatchSubmissionSize uint32 `long:"batchsubmissionsize" description:"The size of a batch in one submission"` + StatusUpdateInterval time.Duration `long:"statusupdateinterval" description:"The interval between each update of finality-provider status"` + RandomnessCommitInterval time.Duration `long:"randomnesscommitinterval" description:"The interval between each attempt to commit public randomness"` + SubmissionRetryInterval time.Duration `long:"submissionretryinterval" description:"The interval between each attempt to submit finality signature or public randomness after a failure"` + SyncFpStatusInterval time.Duration `long:"syncfpstatusinterval" description:"The duration of time that it should sync FP status with the client blockchain"` + SignatureSubmissionInterval time.Duration `long:"signaturesubmissioninterval" description:"The interval between each finality signature(s) submission"` BitcoinNetwork string `long:"bitcoinnetwork" description:"Bitcoin network to run on" choise:"mainnet" choice:"regtest" choice:"testnet" choice:"simnet" choice:"signet"` @@ -91,27 +89,26 @@ func DefaultConfigWithHome(homePath string) Config { bbnCfg.KeyDirectory = homePath pollerCfg := DefaultChainPollerConfig() cfg := Config{ - ChainType: defaultChainType, - LogLevel: defaultLogLevel.String(), - DatabaseConfig: DefaultDBConfigWithHomePath(homePath), - BabylonConfig: &bbnCfg, - PollerConfig: &pollerCfg, - NumPubRand: defaultNumPubRand, - NumPubRandMax: defaultNumPubRandMax, - MinRandHeightGap: defaultMinRandHeightGap, - StatusUpdateInterval: defaultStatusUpdateInterval, - RandomnessCommitInterval: defaultRandomInterval, - SubmissionRetryInterval: defaultSubmitRetryInterval, - FastSyncInterval: defaultFastSyncInterval, - FastSyncLimit: defaultFastSyncLimit, - FastSyncGap: defaultFastSyncGap, - MaxSubmissionRetries: defaultMaxSubmissionRetries, - BitcoinNetwork: defaultBitcoinNetwork, - BTCNetParams: defaultBTCNetParams, - EOTSManagerAddress: defaultEOTSManagerAddress, - RPCListener: DefaultRPCListener, - Metrics: metrics.DefaultFpConfig(), - SyncFpStatusInterval: defaultSyncFpStatusInterval, + ChainType: defaultChainType, + LogLevel: defaultLogLevel.String(), + DatabaseConfig: DefaultDBConfigWithHomePath(homePath), + BabylonConfig: &bbnCfg, + PollerConfig: &pollerCfg, + NumPubRand: defaultNumPubRand, + NumPubRandMax: defaultNumPubRandMax, + MinRandHeightGap: defaultMinRandHeightGap, + BatchSubmissionSize: defaultBatchSubmissionSize, + StatusUpdateInterval: defaultStatusUpdateInterval, + RandomnessCommitInterval: defaultRandomInterval, + SubmissionRetryInterval: defaultSubmitRetryInterval, + SignatureSubmissionInterval: defaultSignatureSubmissionInterval, + MaxSubmissionRetries: defaultMaxSubmissionRetries, + BitcoinNetwork: defaultBitcoinNetwork, + BTCNetParams: defaultBTCNetParams, + EOTSManagerAddress: defaultEOTSManagerAddress, + RPCListener: DefaultRPCListener, + Metrics: metrics.DefaultFpConfig(), + SyncFpStatusInterval: defaultSyncFpStatusInterval, } if err := cfg.Validate(); err != nil { diff --git a/finality-provider/config/poller.go b/finality-provider/config/poller.go index 71bdeb15..d0e1e0f1 100644 --- a/finality-provider/config/poller.go +++ b/finality-provider/config/poller.go @@ -4,13 +4,13 @@ import "time" var ( defaultBufferSize = uint32(1000) - defaultPollingInterval = 20 * time.Second + defaultPollingInterval = 1 * time.Second defaultStaticStartHeight = uint64(1) ) type ChainPollerConfig struct { BufferSize uint32 `long:"buffersize" description:"The maximum number of Babylon blocks that can be stored in the buffer"` - PollInterval time.Duration `long:"pollinterval" description:"The interval between each polling of Babylon blocks"` + PollInterval time.Duration `long:"pollinterval" description:"The interval between each polling of blocks; the value should be set depending on the block production time but could be set smaller for quick catching up"` StaticChainScanningStartHeight uint64 `long:"staticchainscanningstartheight" description:"The static height from which we start polling the chain"` AutoChainScanningMode bool `long:"autochainscanningmode" description:"Automatically discover the height from which to start polling the chain"` } diff --git a/finality-provider/service/fastsync.go b/finality-provider/service/fastsync.go deleted file mode 100644 index aede1a3d..00000000 --- a/finality-provider/service/fastsync.go +++ /dev/null @@ -1,108 +0,0 @@ -package service - -import ( - "fmt" - - "go.uber.org/zap" - - "github.com/babylonlabs-io/finality-provider/lib/math" - "github.com/babylonlabs-io/finality-provider/types" -) - -type FastSyncResult struct { - Responses []*types.TxResponse - SyncedHeight uint64 - LastProcessedHeight uint64 -} - -// FastSync attempts to send a batch of finality signatures -// from the maximum of the last voted height and the last finalized height -// to the current height -func (fp *FinalityProviderInstance) FastSync(startHeight, endHeight uint64) (*FastSyncResult, error) { - if fp.inSync.Swap(true) { - return nil, fmt.Errorf("the finality-provider has already been in fast sync") - } - defer fp.inSync.Store(false) - - if startHeight > endHeight { - return nil, fmt.Errorf("the start height %v should not be higher than the end height %v", - startHeight, endHeight) - } - - activationBlkHeight, err := fp.cc.QueryFinalityActivationBlockHeight() - if err != nil { - return nil, fmt.Errorf("failed to get activation height during fast sync %w", err) - } - - responses := make([]*types.TxResponse, 0) - // make sure it starts at least at the finality activation height - startHeight = math.MaxUint64(startHeight, activationBlkHeight) - // the syncedHeight is at least the starting point - syncedHeight := startHeight - // we may need several rounds to catch-up as we need to limit - // the catch-up distance for each round to avoid memory overflow - for startHeight <= endHeight { - blocks, err := fp.cc.QueryBlocks(startHeight, endHeight, fp.cfg.FastSyncLimit) - if err != nil { - return nil, err - } - - if len(blocks) < 1 { - break - } - - startHeight = blocks[len(blocks)-1].Height + 1 - - // Note: not all the blocks in the range will have votes cast - // due to lack of voting power or public randomness, so we may - // have gaps during sync - catchUpBlocks := make([]*types.BlockInfo, 0, len(blocks)) - for _, b := range blocks { - // check whether the block has been processed before - if fp.hasProcessed(b) { - continue - } - // check whether the finality provider has voting power - hasVp, err := fp.hasVotingPower(b) - if err != nil { - return nil, err - } - if !hasVp { - fp.metrics.IncrementFpTotalBlocksWithoutVotingPower(fp.GetBtcPkHex()) - continue - } - // all good, add the block for catching up - catchUpBlocks = append(catchUpBlocks, b) - } - - if len(catchUpBlocks) < 1 { - continue - } - - syncedHeight = catchUpBlocks[len(catchUpBlocks)-1].Height - - res, err := fp.SubmitBatchFinalitySignatures(catchUpBlocks) - if err != nil { - return nil, err - } - fp.metrics.AddToFpTotalVotedBlocks(fp.GetBtcPkHex(), float64(len(catchUpBlocks))) - - responses = append(responses, res) - - fp.logger.Debug( - "the finality-provider is catching up by sending finality signatures in a batch", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("start_height", catchUpBlocks[0].Height), - zap.Uint64("synced_height", syncedHeight), - ) - } - - // update the processed height - fp.MustSetLastProcessedHeight(syncedHeight) - - return &FastSyncResult{ - Responses: responses, - SyncedHeight: syncedHeight, - LastProcessedHeight: fp.GetLastProcessedHeight(), - }, nil -} diff --git a/finality-provider/service/fastsync_test.go b/finality-provider/service/fastsync_test.go deleted file mode 100644 index f014337d..00000000 --- a/finality-provider/service/fastsync_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package service_test - -import ( - "math/rand" - "testing" - "time" - - "github.com/babylonlabs-io/babylon/testutil/datagen" - ftypes "github.com/babylonlabs-io/babylon/x/finality/types" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" - - "github.com/babylonlabs-io/finality-provider/testutil" - "github.com/babylonlabs-io/finality-provider/types" -) - -// FuzzFastSync_SufficientRandomness tests a case where we have sufficient -// randomness and voting power when the finality provider enters fast-sync -// it is expected that the finality provider could catch up to the current -// height through fast-sync -func FuzzFastSync_SufficientRandomness(f *testing.F) { - testutil.AddRandomSeedsToFuzzer(f, 10) - f.Fuzz(func(t *testing.T, seed int64) { - r := rand.New(rand.NewSource(seed)) - - randomStartingHeight := uint64(r.Int63n(100) + 1) - finalizedHeight := randomStartingHeight + uint64(r.Int63n(10)+2) - currentHeight := finalizedHeight + uint64(r.Int63n(10)+1) - mockClientController := testutil.PrepareMockedClientController(t, r, randomStartingHeight, currentHeight, 0) - mockClientController.EXPECT().QueryLatestFinalizedBlocks(uint64(1)).Return(nil, nil).AnyTimes() - _, fpIns, cleanUp := startFinalityProviderAppWithRegisteredFp(t, r, mockClientController, randomStartingHeight) - defer cleanUp() - - // commit pub rand - mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).Times(1) - mockClientController.EXPECT().CommitPubRandList(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).Times(1) - _, err := fpIns.CommitPubRand(randomStartingHeight) - require.NoError(t, err) - - mockClientController.EXPECT().QueryFinalityProviderVotingPower(fpIns.GetBtcPk(), gomock.Any()). - Return(uint64(1), nil).AnyTimes() - // the last committed height is higher than the current height - // to make sure the randomness is sufficient - lastCommittedHeight := randomStartingHeight + testutil.TestPubRandNum - lastCommittedPubRandMap := make(map[uint64]*ftypes.PubRandCommitResponse) - lastCommittedPubRandMap[lastCommittedHeight] = &ftypes.PubRandCommitResponse{ - NumPubRand: 1000, - Commitment: datagen.GenRandomByteArray(r, 32), - } - mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(lastCommittedPubRandMap, nil).AnyTimes() - - catchUpBlocks := testutil.GenBlocks(r, finalizedHeight+1, currentHeight) - expectedTxHash := testutil.GenRandomHexStr(r, 32) - finalizedBlock := &types.BlockInfo{Height: finalizedHeight, Hash: testutil.GenRandomByteArray(r, 32)} - mockClientController.EXPECT().QueryLatestFinalizedBlocks(uint64(1)).Return([]*types.BlockInfo{finalizedBlock}, nil).AnyTimes() - mockClientController.EXPECT().QueryBlocks(finalizedHeight+1, currentHeight, uint32(10)). - Return(catchUpBlocks, nil) - mockClientController.EXPECT().SubmitBatchFinalitySigs(fpIns.GetBtcPk(), catchUpBlocks, gomock.Any(), gomock.Any(), gomock.Any()). - Return(&types.TxResponse{TxHash: expectedTxHash}, nil).AnyTimes() - result, err := fpIns.FastSync(finalizedHeight+1, currentHeight) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, expectedTxHash, result.Responses[0].TxHash) - require.Equal(t, currentHeight, fpIns.GetLastVotedHeight()) - require.Equal(t, currentHeight, fpIns.GetLastProcessedHeight()) - }) -} - -// FuzzFastSync_NoRandomness tests a case where we have insufficient -// randomness but with voting power when the finality provider enters fast-sync -// it is expected that the finality provider could catch up to the last -// committed height -func FuzzFastSync_NoRandomness(f *testing.F) { - testutil.AddRandomSeedsToFuzzer(f, 10) - f.Fuzz(func(t *testing.T, seed int64) { - r := rand.New(rand.NewSource(seed)) - - randomStartingHeight := uint64(r.Int63n(100) + 100) - finalizedHeight := randomStartingHeight + uint64(r.Int63n(10)+2) - currentHeight := finalizedHeight + uint64(r.Int63n(10)+1) - mockClientController := testutil.PrepareMockedClientController(t, r, randomStartingHeight, currentHeight, 0) - mockClientController.EXPECT().QueryLatestFinalizedBlocks(uint64(1)).Return(nil, nil).AnyTimes() - _, fpIns, cleanUp := startFinalityProviderAppWithRegisteredFp(t, r, mockClientController, randomStartingHeight) - defer cleanUp() - - // commit pub rand - mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).Times(1) - mockClientController.EXPECT().CommitPubRandList(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).Times(1) - _, err := fpIns.CommitPubRand(randomStartingHeight) - require.NoError(t, err) - - // the last height with pub rand is a random value inside [finalizedHeight+1, currentHeight] - lastHeightWithPubRand := uint64(rand.Intn(int(currentHeight)-int(finalizedHeight))) + finalizedHeight + 1 - for i := randomStartingHeight; i <= currentHeight; i++ { - if i <= lastHeightWithPubRand { - mockClientController.EXPECT().QueryFinalityProviderVotingPower(fpIns.GetBtcPk(), i). - Return(uint64(1), nil).AnyTimes() - } else { - mockClientController.EXPECT().QueryFinalityProviderVotingPower(fpIns.GetBtcPk(), i). - Return(uint64(0), nil).AnyTimes() - } - } - lastCommittedPubRandMap := make(map[uint64]*ftypes.PubRandCommitResponse) - lastCommittedPubRandMap[lastHeightWithPubRand-10] = &ftypes.PubRandCommitResponse{ - NumPubRand: 10 + 1, - Commitment: datagen.GenRandomByteArray(r, 32), - } - mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(lastCommittedPubRandMap, nil).AnyTimes() - - catchUpBlocks := testutil.GenBlocks(r, finalizedHeight+1, currentHeight) - expectedTxHash := testutil.GenRandomHexStr(r, 32) - finalizedBlock := &types.BlockInfo{Height: finalizedHeight, Hash: testutil.GenRandomByteArray(r, 32)} - mockClientController.EXPECT().QueryLatestFinalizedBlocks(uint64(1)).Return([]*types.BlockInfo{finalizedBlock}, nil).AnyTimes() - mockClientController.EXPECT().QueryBlocks(finalizedHeight+1, currentHeight, uint32(10)). - Return(catchUpBlocks, nil) - mockClientController.EXPECT().SubmitBatchFinalitySigs(fpIns.GetBtcPk(), catchUpBlocks[:lastHeightWithPubRand-finalizedHeight], gomock.Any(), gomock.Any(), gomock.Any()). - Return(&types.TxResponse{TxHash: expectedTxHash}, nil).AnyTimes() - result, err := fpIns.FastSync(finalizedHeight+1, currentHeight) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, expectedTxHash, result.Responses[0].TxHash) - require.Equal(t, lastHeightWithPubRand, fpIns.GetLastVotedHeight()) - require.Equal(t, lastHeightWithPubRand, fpIns.GetLastProcessedHeight()) - }) -} - -func TestFinalityActivationBlockHeight(t *testing.T) { - t.Parallel() - r := rand.New(rand.NewSource(time.Now().Unix())) - - randomStartingHeight := uint64(r.Int63n(100) + 100) - finalizedHeight := randomStartingHeight + uint64(r.Int63n(10)+2) - currentHeight := finalizedHeight + uint64(r.Int63n(10)+1) - finalityActvationBlockHeight := randomStartingHeight + 10 - - mockClientController := testutil.PrepareMockedClientController(t, r, randomStartingHeight, currentHeight, finalityActvationBlockHeight) - mockClientController.EXPECT().QueryLatestFinalizedBlocks(uint64(1)).Return(nil, nil).AnyTimes() - - mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(make(map[uint64]*ftypes.PubRandCommitResponse), nil).AnyTimes() - mockClientController.EXPECT().CommitPubRandList(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).Times(1) - - _, fpIns, cleanUp := startFinalityProviderAppWithRegisteredFp(t, r, mockClientController, randomStartingHeight) - defer cleanUp() - - _, err := fpIns.CommitPubRand(randomStartingHeight) - require.NoError(t, err) -} diff --git a/finality-provider/service/fp_instance.go b/finality-provider/service/fp_instance.go index 543dcec2..ad2c6750 100644 --- a/finality-provider/service/fp_instance.go +++ b/finality-provider/service/fp_instance.go @@ -43,8 +43,7 @@ type FinalityProviderInstance struct { // passphrase is used to unlock private keys passphrase string - laggingTargetChan chan *types.BlockInfo - criticalErrChan chan<- *CriticalError + criticalErrChan chan<- *CriticalError isStarted *atomic.Bool inSync *atomic.Bool @@ -105,62 +104,30 @@ func (fp *FinalityProviderInstance) Start() error { fp.logger.Info("Starting finality-provider instance", zap.String("pk", fp.GetBtcPkHex())) - startHeight, err := fp.bootstrap() + startHeight, err := fp.getPollerStartingHeight() if err != nil { - return fmt.Errorf("failed to bootstrap the finality-provider %s: %w", fp.GetBtcPkHex(), err) + return fmt.Errorf("failed to get the start height: %w", err) } - fp.logger.Info("the finality-provider has been bootstrapped", + fp.logger.Info("starting the finality provider", zap.String("pk", fp.GetBtcPkHex()), zap.Uint64("height", startHeight)) poller := NewChainPoller(fp.logger, fp.cfg.PollerConfig, fp.cc, fp.metrics) - if err := poller.Start(startHeight + 1); err != nil { - return fmt.Errorf("failed to start the poller: %w", err) + if err := poller.Start(startHeight); err != nil { + return fmt.Errorf("failed to start the poller with start height %d: %w", startHeight, err) } fp.poller = poller - - fp.laggingTargetChan = make(chan *types.BlockInfo, 1) - fp.quit = make(chan struct{}) - fp.wg.Add(1) go fp.finalitySigSubmissionLoop() fp.wg.Add(1) go fp.randomnessCommitmentLoop() - fp.wg.Add(1) - go fp.checkLaggingLoop() return nil } -func (fp *FinalityProviderInstance) bootstrap() (uint64, error) { - latestBlock, err := fp.getLatestBlockWithRetry() - if err != nil { - return 0, err - } - - if fp.checkLagging(latestBlock) { - _, err := fp.tryFastSync(latestBlock) - if err != nil { - if errors.Is(err, ErrFinalityProviderJailed) { - fp.MustSetStatus(proto.FinalityProviderStatus_JAILED) - } - if !clientcontroller.IsExpected(err) { - return 0, err - } - } - } - - startHeight, err := fp.getPollerStartingHeight() - if err != nil { - return 0, err - } - - return startHeight, nil -} - func (fp *FinalityProviderInstance) Stop() error { if !fp.isStarted.Swap(false) { return fmt.Errorf("the finality-provider %s has already stopped", fp.GetBtcPkHex()) @@ -193,45 +160,18 @@ func (fp *FinalityProviderInstance) finalitySigSubmissionLoop() { for { select { - case b := <-fp.poller.GetBlockInfoChan(): - fp.logger.Debug( - "the finality-provider received a new block, start processing", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("height", b.Height), - ) - - // check whether the block has been processed before - if fp.hasProcessed(b) { - continue - } - - activationBlkHeight, err := fp.cc.QueryFinalityActivationBlockHeight() - if err != nil { - fp.reportCriticalErr(fmt.Errorf("failed to get activation height during fast sync %w", err)) - continue - } - - // check if it is allowed to send finality - if b.Height < activationBlkHeight { - continue - } - - // check whether the finality provider has voting power - hasVp, err := fp.hasVotingPower(b) - if err != nil { - fp.reportCriticalErr(err) - continue - } - if !hasVp { - // the finality provider does not have voting power - // and it will never will at this block - fp.MustSetLastProcessedHeight(b.Height) - fp.metrics.IncrementFpTotalBlocksWithoutVotingPower(fp.GetBtcPkHex()) + case <-time.After(fp.cfg.SignatureSubmissionInterval): + pollerBlocks := fp.getAllBlocksFromChan() + if len(pollerBlocks) == 0 { continue } - // use the copy of the block to avoid the impact to other receivers - nextBlock := *b - res, err := fp.retrySubmitFinalitySignatureUntilBlockFinalized(&nextBlock) + targetHeight := pollerBlocks[len(pollerBlocks)-1].Height + fp.logger.Debug("the finality-provider received new block(s), start processing", + zap.String("pk", fp.GetBtcPkHex()), + zap.Uint64("start_height", pollerBlocks[0].Height), + zap.Uint64("end_height", targetHeight), + ) + res, err := fp.retrySubmitSigsUntilFinalized(pollerBlocks) if err != nil { fp.metrics.IncrementFpTotalFailedVotes(fp.GetBtcPkHex()) if !errors.Is(err, ErrFinalityProviderShutDown) { @@ -246,53 +186,71 @@ func (fp *FinalityProviderInstance) finalitySigSubmissionLoop() { continue } fp.logger.Info( - "successfully submitted a finality signature to the consumer chain", + "successfully submitted the finality signature to the consumer chain", + zap.String("consumer_id", string(fp.GetChainID())), zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("height", b.Height), + zap.Uint64("start_height", pollerBlocks[0].Height), + zap.Uint64("end_height", targetHeight), zap.String("tx_hash", res.TxHash), ) - case targetBlock := <-fp.laggingTargetChan: - res, err := fp.tryFastSync(targetBlock) - fp.isLagging.Store(false) + case <-fp.quit: + fp.logger.Info("the finality signature submission loop is closing") + return + } + } +} + +func (fp *FinalityProviderInstance) getAllBlocksFromChan() []*types.BlockInfo { + var pollerBlocks []*types.BlockInfo + for { + select { + case b := <-fp.poller.GetBlockInfoChan(): + // TODO: in cases of catching up, this could issue frequent RPC calls + shouldProcess, err := fp.shouldProcessBlock(b) if err != nil { - if errors.Is(err, ErrFinalityProviderSlashed) { + if !errors.Is(err, ErrFinalityProviderShutDown) { fp.reportCriticalErr(err) - continue } - fp.logger.Debug( - "failed to sync up, will try again later", - zap.String("pk", fp.GetBtcPkHex()), - zap.Error(err), - ) - continue + break } - // response might be nil if sync is not needed - if res != nil { - fp.logger.Info( - "fast sync is finished", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("synced_height", res.SyncedHeight), - zap.Uint64("last_processed_height", res.LastProcessedHeight), - ) - - // inform the poller to skip to the next block of the last - // processed one - err := fp.poller.SkipToHeight(fp.GetLastProcessedHeight() + 1) - if err != nil { - fp.logger.Debug( - "failed to skip heights from the poller", - zap.Error(err), - ) - } + if shouldProcess { + pollerBlocks = append(pollerBlocks, b) + } + if len(pollerBlocks) == int(fp.cfg.BatchSubmissionSize) { + return pollerBlocks } case <-fp.quit: - fp.logger.Info("the finality signature submission loop is closing") - return + fp.logger.Info("the get all blocks loop is closing") + return nil + default: + return pollerBlocks } } } +func (fp *FinalityProviderInstance) shouldProcessBlock(b *types.BlockInfo) (bool, error) { + // check whether the block has been processed before + if fp.hasProcessed(b) { + return false, nil + } + + // check whether the finality provider has voting power + hasVp, err := fp.hasVotingPower(b) + if err != nil { + return false, err + } + if !hasVp { + // the finality provider does not have voting power + // and it will never will at this block + fp.MustSetLastProcessedHeight(b.Height) + fp.metrics.IncrementFpTotalBlocksWithoutVotingPower(fp.GetBtcPkHex()) + return false, nil + } + + return true, nil +} + func (fp *FinalityProviderInstance) randomnessCommitmentLoop() { defer fp.wg.Done() @@ -329,91 +287,6 @@ func (fp *FinalityProviderInstance) randomnessCommitmentLoop() { } } -func (fp *FinalityProviderInstance) checkLaggingLoop() { - defer fp.wg.Done() - - if fp.cfg.FastSyncInterval == 0 { - fp.logger.Info("the fast sync is disabled") - return - } - - fastSyncTicker := time.NewTicker(fp.cfg.FastSyncInterval) - defer fastSyncTicker.Stop() - - for { - select { - case <-fastSyncTicker.C: - if fp.isLagging.Load() { - // we are in fast sync mode, skip do not do checks - continue - } - - latestBlock, err := fp.getLatestBlockWithRetry() - if err != nil { - fp.logger.Debug( - "failed to get the latest block of the consumer chain", - zap.String("pk", fp.GetBtcPkHex()), - zap.Error(err), - ) - continue - } - - if fp.checkLagging(latestBlock) { - fp.isLagging.Store(true) - fp.laggingTargetChan <- latestBlock - } - case <-fp.quit: - fp.logger.Debug("the fast sync loop is closing") - return - } - } -} - -func (fp *FinalityProviderInstance) tryFastSync(targetBlock *types.BlockInfo) (*FastSyncResult, error) { - if fp.inSync.Load() { - return nil, fmt.Errorf("the finality-provider %s is already in sync", fp.GetBtcPkHex()) - } - - // get the last finalized height - lastFinalizedBlocks, err := fp.cc.QueryLatestFinalizedBlocks(1) - if err != nil { - return nil, err - } - if lastFinalizedBlocks == nil { - fp.logger.Debug( - "no finalized blocks yet, no need to catch up", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("height", targetBlock.Height), - ) - return nil, nil - } - - startHeight, err := fp.fastSyncStartHeight(lastFinalizedBlocks[0].Height) - if err != nil { - return nil, err - } - - if startHeight > targetBlock.Height { - return nil, fmt.Errorf("the start height %v should not be higher than the current block %v", startHeight, targetBlock.Height) - } - - fp.logger.Debug("the finality-provider is entering fast sync") - - return fp.FastSync(startHeight, targetBlock.Height) -} - -func (fp *FinalityProviderInstance) fastSyncStartHeight(lastFinalizedHeight uint64) (uint64, error) { - lastProcessedHeight := fp.GetLastProcessedHeight() - - finalityActivationBlkHeight, err := fp.cc.QueryFinalityActivationBlockHeight() - if err != nil { - return 0, err - } - - // return the max start height by checking the finality activation block height - return fppath.MaxUint64(lastProcessedHeight+1, lastFinalizedHeight+1, finalityActivationBlkHeight), nil -} - func (fp *FinalityProviderInstance) hasProcessed(b *types.BlockInfo) bool { if b.Height <= fp.GetLastProcessedHeight() { fp.logger.Debug( @@ -453,15 +326,15 @@ func (fp *FinalityProviderInstance) reportCriticalErr(err error) { } } -// checkLagging returns true if the lasted voted height is behind by a configured gap -func (fp *FinalityProviderInstance) checkLagging(currentBlock *types.BlockInfo) bool { - return currentBlock.Height >= fp.GetLastProcessedHeight()+fp.cfg.FastSyncGap -} - -// retrySubmitFinalitySignatureUntilBlockFinalized periodically tries to submit finality signature until success or the block is finalized +// retrySubmitSigsUntilFinalized periodically tries to submit finality signature until success or the block is finalized // error will be returned if maximum retries have been reached or the query to the consumer chain fails -func (fp *FinalityProviderInstance) retrySubmitFinalitySignatureUntilBlockFinalized(targetBlock *types.BlockInfo) (*types.TxResponse, error) { +func (fp *FinalityProviderInstance) retrySubmitSigsUntilFinalized(targetBlocks []*types.BlockInfo) (*types.TxResponse, error) { + if len(targetBlocks) == 0 { + return nil, fmt.Errorf("cannot send signatures for empty blocks") + } + var failedCycles uint32 + targetHeight := targetBlocks[len(targetBlocks)-1].Height // we break the for loop if the block is finalized or the signature is successfully submitted // error will be returned if maximum retries have been reached or the query to the consumer chain fails @@ -469,13 +342,16 @@ func (fp *FinalityProviderInstance) retrySubmitFinalitySignatureUntilBlockFinali select { case <-time.After(fp.cfg.SubmissionRetryInterval): // error will be returned if max retries have been reached - res, err := fp.SubmitFinalitySignature(targetBlock) + var res *types.TxResponse + var err error + res, err = fp.SubmitBatchFinalitySignatures(targetBlocks) if err != nil { fp.logger.Debug( "failed to submit finality signature to the consumer chain", zap.String("pk", fp.GetBtcPkHex()), zap.Uint32("current_failures", failedCycles), - zap.Uint64("target_block_height", targetBlock.Height), + zap.Uint64("target_start_height", targetBlocks[0].Height), + zap.Uint64("target_end_height", targetHeight), zap.Error(err), ) @@ -497,15 +373,15 @@ func (fp *FinalityProviderInstance) retrySubmitFinalitySignatureUntilBlockFinali } // periodically query the index block to be later checked whether it is Finalized - finalized, err := fp.checkBlockFinalization(targetBlock.Height) + finalized, err := fp.checkBlockFinalization(targetHeight) if err != nil { - return nil, fmt.Errorf("failed to query block finalization at height %v: %w", targetBlock.Height, err) + return nil, fmt.Errorf("failed to query block finalization at height %v: %w", targetHeight, err) } if finalized { fp.logger.Debug( "the block is already finalized, skip submission", zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("target_height", targetBlock.Height), + zap.Uint64("target_height", targetHeight), ) // TODO: returning nil here is to safely break the loop // the error still exists @@ -664,47 +540,7 @@ func (fp *FinalityProviderInstance) CommitPubRand(tipHeight uint64) (*types.TxRe // SubmitFinalitySignature builds and sends a finality signature over the given block to the consumer chain func (fp *FinalityProviderInstance) SubmitFinalitySignature(b *types.BlockInfo) (*types.TxResponse, error) { - sig, err := fp.signFinalitySig(b) - if err != nil { - return nil, err - } - - // get public randomness at the height - prList, err := fp.getPubRandList(b.Height, 1) - if err != nil { - return nil, fmt.Errorf("failed to get public randomness list: %w", err) - } - pubRand := prList[0] - - // get inclusion proof - proofBytes, err := fp.pubRandState.getPubRandProof(pubRand) - if err != nil { - return nil, fmt.Errorf( - "failed to get inclusion proof of public randomness %s for FP %s for block %d: %w", - pubRand.String(), - fp.btcPk.MarshalHex(), - b.Height, - err, - ) - } - - // send finality signature to the consumer chain - res, err := fp.cc.SubmitFinalitySig(fp.GetBtcPk(), b, pubRand, proofBytes, sig.ToModNScalar()) - if err != nil { - return nil, fmt.Errorf("failed to send finality signature to the consumer chain: %w", err) - } - - // it is possible that the vote is duplicate so the metrics do need to update - if res.TxHash != "" { - // update DB - fp.MustUpdateStateAfterFinalitySigSubmission(b.Height) - - // update metrics - fp.metrics.RecordFpVoteTime(fp.GetBtcPkHex()) - fp.metrics.IncrementFpTotalVotedBlocks(fp.GetBtcPkHex()) - } - - return res, nil + return fp.SubmitBatchFinalitySignatures([]*types.BlockInfo{b}) } // SubmitBatchFinalitySignatures builds and sends a finality signature over the given block to the consumer chain @@ -761,7 +597,7 @@ func (fp *FinalityProviderInstance) SubmitBatchFinalitySignatures(blocks []*type } // TestSubmitFinalitySignatureAndExtractPrivKey is exposed for presentation/testing purpose to allow manual sending finality signature -// this API is the same as SubmitFinalitySignature except that we don't constraint the voting height and update status +// this API is the same as SubmitBatchFinalitySignatures except that we don't constraint the voting height and update status // Note: this should not be used in the submission loop func (fp *FinalityProviderInstance) TestSubmitFinalitySignatureAndExtractPrivKey(b *types.BlockInfo) (*types.TxResponse, *btcec.PrivateKey, error) { // get public randomness @@ -814,36 +650,46 @@ func (fp *FinalityProviderInstance) TestSubmitFinalitySignatureAndExtractPrivKey return res, privKey, nil } +// getPollerStartingHeight gets the starting height of the poller with +// max(lastProcessedHeight+1, lastFinalizedHeight+1, params.FinalityActivationHeight) +// this ensures that: +// (1) the fp will not vote for a height lower than params.FinalityActivationHeight +// (2) the fp will not miss for any non-finalized blocks +// (3) the fp will not process any blocks that have been already processed +// Note: if the fp starting from the last finalized height with a gap to the last +// processed height, the fp might miss some rewards due to not sending the votes +// depending on the consumer chain's reward distribution mechanism +// TODO: provide an option to start from the last processed height in case +// the consumer chain distributes rewards for late voters func (fp *FinalityProviderInstance) getPollerStartingHeight() (uint64, error) { if !fp.cfg.PollerConfig.AutoChainScanningMode { return fp.cfg.PollerConfig.StaticChainScanningStartHeight, nil } - // Set initial block to the maximum of - // - last processed height - // - the latest Babylon finalised height - // The above is to ensure that: - // - // (1) Any finality-provider that is eligible to vote for a block, - // doesn't miss submitting a vote for it. - // (2) The finality providers do not submit signatures for any already - // finalised blocks. - initialBlockToGet := fp.GetLastProcessedHeight() - latestFinalisedBlock, err := fp.latestFinalizedBlocksWithRetry(1) + // TODO: query last voted height and update local height + finalityActivationHeight, err := fp.getFinalityActivationHeightWithRetry() if err != nil { - return 0, err + return 0, fmt.Errorf("failed to get finality activation height: %w", err) } - if len(latestFinalisedBlock) != 0 { - if latestFinalisedBlock[0].Height > initialBlockToGet { - initialBlockToGet = latestFinalisedBlock[0].Height - } + + // start from finality activation height + startHeight := finalityActivationHeight + + latestFinalisedBlocks, err := fp.latestFinalizedBlocksWithRetry(1) + if err != nil { + return 0, fmt.Errorf("failed to get the last finalized block: %w", err) } - // ensure that initialBlockToGet is at least 1 - if initialBlockToGet == 0 { - initialBlockToGet = 1 + // if we have finalized blocks, consider the height after the latest finalized block + if len(latestFinalisedBlocks) > 0 { + startHeight = fppath.MaxUint64(startHeight, latestFinalisedBlocks[0].Height+1) } - return initialBlockToGet, nil + + // consider the height after the last processed block + lastProcessedHeight := fp.GetLastProcessedHeight() + startHeight = fppath.MaxUint64(startHeight, lastProcessedHeight+1) + + return startHeight, nil } func (fp *FinalityProviderInstance) GetLastCommittedHeight() (uint64, error) { @@ -912,6 +758,28 @@ func (fp *FinalityProviderInstance) latestFinalizedBlocksWithRetry(count uint64) return response, nil } +func (fp *FinalityProviderInstance) getFinalityActivationHeightWithRetry() (uint64, error) { + var response uint64 + if err := retry.Do(func() error { + finalityActivationHeight, err := fp.cc.QueryFinalityActivationBlockHeight() + if err != nil { + return err + } + response = finalityActivationHeight + return nil + }, RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) { + fp.logger.Debug( + "failed to query babylon for the finality activation height", + zap.Uint("attempt", n+1), + zap.Uint("max_attempts", RtyAttNum), + zap.Error(err), + ) + })); err != nil { + return 0, err + } + return response, nil +} + func (fp *FinalityProviderInstance) getLatestBlockWithRetry() (*types.BlockInfo, error) { var ( latestBlock *types.BlockInfo diff --git a/finality-provider/service/fp_instance_test.go b/finality-provider/service/fp_instance_test.go index 297fa7e6..c08a185e 100644 --- a/finality-provider/service/fp_instance_test.go +++ b/finality-provider/service/fp_instance_test.go @@ -49,7 +49,7 @@ func FuzzCommitPubRandList(f *testing.F) { }) } -func FuzzSubmitFinalitySig(f *testing.F) { +func FuzzSubmitFinalitySigs(f *testing.F) { testutil.AddRandomSeedsToFuzzer(f, 10) f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) @@ -87,9 +87,9 @@ func FuzzSubmitFinalitySig(f *testing.F) { } expectedTxHash := testutil.GenRandomHexStr(r, 32) mockClientController.EXPECT(). - SubmitFinalitySig(fpIns.GetBtcPk(), nextBlock, gomock.Any(), gomock.Any(), gomock.Any()). + SubmitBatchFinalitySigs(fpIns.GetBtcPk(), []*types.BlockInfo{nextBlock}, gomock.Any(), gomock.Any(), gomock.Any()). Return(&types.TxResponse{TxHash: expectedTxHash}, nil).AnyTimes() - providerRes, err := fpIns.SubmitFinalitySignature(nextBlock) + providerRes, err := fpIns.SubmitBatchFinalitySignatures([]*types.BlockInfo{nextBlock}) require.NoError(t, err) require.Equal(t, expectedTxHash, providerRes.TxHash) diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 0741902c..06ef609f 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -109,8 +109,8 @@ func TestDoubleSigning(t *testing.T) { t.Logf("the equivocation attack is successful") } -// TestFastSync tests the fast sync process where the finality-provider is terminated and restarted with fast sync -func TestFastSync(t *testing.T) { +// TestCatchingUp tests if a fp can catch up after restarted +func TestCatchingUp(t *testing.T) { tm, fpIns := StartManagerWithFinalityProvider(t) defer tm.Stop(t) @@ -142,7 +142,6 @@ func TestFastSync(t *testing.T) { n := 3 // stop the finality-provider for a few blocks then restart to trigger the fast sync - tm.FpConfig.FastSyncGap = uint64(n) tm.StopAndRestartFpAfterNBlocks(t, n, fpIns) // check there are n+1 blocks finalized diff --git a/itest/test_manager.go b/itest/test_manager.go index a5f44522..fd8f2dd7 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -830,7 +830,7 @@ func defaultFpConfig(keyringDir, homeDir string) *fpcfg.Config { cfg.BitcoinNetwork = "simnet" cfg.BTCNetParams = chaincfg.SimNetParams - cfg.PollerConfig.AutoChainScanningMode = false + cfg.PollerConfig.PollInterval = 1 * time.Millisecond // babylon configs for sending transactions cfg.BabylonConfig.KeyDirectory = keyringDir // need to use this one to send otherwise we will have account sequence mismatch