diff --git a/CHANGELOG.md b/CHANGELOG.md index bec28b83..42fe4d07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Improvements * [#207](https://github.com/babylonlabs-io/finality-provider/pull/207) create finality provider from JSON file +* [#208](https://github.com/babylonlabs-io/finality-provider/pull/208) Remove sync fp status loop ## v0.13.1 diff --git a/clientcontroller/babylon.go b/clientcontroller/babylon.go index 5c4d9910..191e1fcf 100644 --- a/clientcontroller/babylon.go +++ b/clientcontroller/babylon.go @@ -291,7 +291,7 @@ func (bc *BabylonController) QueryFinalityProviderVotingPower(fpPk *btcec.Public return 0, nil } - return 0, fmt.Errorf("failed to query Finality Voting Power at Height %d: %w", blockHeight, err) + return 0, err } return res.VotingPower, nil diff --git a/finality-provider/config/config.go b/finality-provider/config/config.go index ff6381bf..8040ad64 100644 --- a/finality-provider/config/config.go +++ b/finality-provider/config/config.go @@ -32,7 +32,6 @@ const ( defaultStatusUpdateInterval = 20 * time.Second defaultRandomInterval = 30 * time.Second defaultSubmitRetryInterval = 1 * time.Second - defaultSyncFpStatusInterval = 30 * time.Second defaultSignatureSubmissionInterval = 1 * time.Second defaultMaxSubmissionRetries = 20 defaultBitcoinNetwork = "signet" @@ -65,7 +64,6 @@ type Config struct { 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"` @@ -108,7 +106,6 @@ func DefaultConfigWithHome(homePath string) Config { EOTSManagerAddress: defaultEOTSManagerAddress, RPCListener: DefaultRPCListener, Metrics: metrics.DefaultFpConfig(), - SyncFpStatusInterval: defaultSyncFpStatusInterval, } if err := cfg.Validate(); err != nil { diff --git a/finality-provider/service/app.go b/finality-provider/service/app.go index 9105794e..6074ada0 100644 --- a/finality-provider/service/app.go +++ b/finality-provider/service/app.go @@ -191,60 +191,83 @@ func (app *FinalityProviderApp) StartFinalityProvider(fpPk *bbntypes.BIP340PubKe return nil } -// SyncFinalityProviderStatus syncs the status of the finality-providers with the chain. -func (app *FinalityProviderApp) SyncFinalityProviderStatus() (bool, error) { - var fpInstanceRunning bool - latestBlock, err := app.cc.QueryBestBlock() - if err != nil { - return false, err - } - +// SyncAllFinalityProvidersStatus syncs the status of all the stored finality providers with the chain. +// it should be called before a fp instance is started +func (app *FinalityProviderApp) SyncAllFinalityProvidersStatus() error { fps, err := app.fps.GetAllStoredFinalityProviders() if err != nil { - return false, err + return err } for _, fp := range fps { - vp, err := app.cc.QueryFinalityProviderVotingPower(fp.BtcPk, latestBlock.Height) + latestBlock, err := app.cc.QueryBestBlock() if err != nil { - continue + return err } - bip340PubKey := fp.GetBIP340BTCPK() - if app.IsFinalityProviderRunning(bip340PubKey) { - // there is a instance running, no need to keep syncing - fpInstanceRunning = true - // if it is already running, no need to update status - continue + pkHex := fp.GetBIP340BTCPK().MarshalHex() + power, err := app.cc.QueryFinalityProviderVotingPower(fp.BtcPk, latestBlock.Height) + if err != nil { + return fmt.Errorf("failed to query voting power for finality provider %s at height %d: %w", + fp.GetBIP340BTCPK().MarshalHex(), latestBlock.Height, err) } + // power > 0 (slashed_height must > 0), set status to ACTIVE oldStatus := fp.Status - newStatus, err := app.fps.UpdateFpStatusFromVotingPower(vp, fp) + if power > 0 { + if oldStatus != proto.FinalityProviderStatus_ACTIVE { + fp.Status = proto.FinalityProviderStatus_ACTIVE + app.fps.MustSetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_ACTIVE) + app.logger.Debug( + "the finality-provider status is changed to ACTIVE", + zap.String("fp_btc_pk", pkHex), + zap.String("old_status", oldStatus.String()), + zap.Uint64("power", power), + ) + } + continue + } + slashed, jailed, err := app.cc.QueryFinalityProviderSlashedOrJailed(fp.BtcPk) if err != nil { - return false, err + return err } + if slashed { + app.fps.MustSetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_SLASHED) - if oldStatus != newStatus { - app.logger.Info( - "Update FP status", - zap.String("fp_addr", fp.FPAddr), + app.logger.Debug( + "the finality-provider status is changed to SLAHED", + zap.String("fp_btc_pk", pkHex), zap.String("old_status", oldStatus.String()), - zap.String("new_status", newStatus.String()), ) - fp.Status = newStatus + + continue } + if jailed { + app.fps.MustSetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_JAILED) + + app.logger.Debug( + "the finality-provider status is changed to JAILED", + zap.String("fp_btc_pk", pkHex), + zap.String("old_status", oldStatus.String()), + ) - if !fp.ShouldStart() { continue } + // power == 0 and slashed_height == 0, change to INACTIVE if the current status is ACTIVE + if oldStatus == proto.FinalityProviderStatus_ACTIVE { + app.fps.MustSetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_INACTIVE) + + app.logger.Debug( + "the finality-provider status is changed to INACTIVE", + zap.String("fp_btc_pk", pkHex), + zap.String("old_status", oldStatus.String()), + ) - if err := app.StartFinalityProvider(bip340PubKey, ""); err != nil { - return false, err + continue } - fpInstanceRunning = true } - return fpInstanceRunning, nil + return nil } // Start starts only the finality-provider daemon without any finality-provider instances @@ -253,8 +276,12 @@ func (app *FinalityProviderApp) Start() error { app.startOnce.Do(func() { app.logger.Info("Starting FinalityProviderApp") - app.wg.Add(6) - go app.syncChainFpStatusLoop() + startErr = app.SyncAllFinalityProvidersStatus() + if startErr != nil { + return + } + + app.wg.Add(5) go app.metricsUpdateLoop() go app.monitorCriticalErr() go app.monitorStatusUpdate() diff --git a/finality-provider/service/app_test.go b/finality-provider/service/app_test.go index 78ab04eb..3990d0d7 100644 --- a/finality-provider/service/app_test.go +++ b/finality-provider/service/app_test.go @@ -123,7 +123,7 @@ func FuzzCreateFinalityProvider(f *testing.F) { } func FuzzSyncFinalityProviderStatus(f *testing.F) { - testutil.AddRandomSeedsToFuzzer(f, 14) + testutil.AddRandomSeedsToFuzzer(f, 10) f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) @@ -149,12 +149,24 @@ func FuzzSyncFinalityProviderStatus(f *testing.F) { mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), gomock.Any()).Return(uint64(2), nil).AnyTimes() } mockClientController.EXPECT().QueryFinalityProviderHighestVotedHeight(gomock.Any()).Return(uint64(0), nil).AnyTimes() + var isSlashedOrJailed int + if noVotingPowerTable { + // 0 means is slashed, 1 means is jailed, 2 means neither slashed nor jailed + isSlashedOrJailed = r.Intn(3) + switch isSlashedOrJailed { + case 0: + mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(true, false, nil).AnyTimes() + case 1: + mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, true, nil).AnyTimes() + case 2: + mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, false, nil).AnyTimes() + } + } // Create randomized config pathSuffix := datagen.GenRandomHexStr(r, 10) fpHomeDir := filepath.Join(t.TempDir(), "fp-home", pathSuffix) fpCfg := config.DefaultConfigWithHome(fpHomeDir) - fpCfg.SyncFpStatusInterval = time.Millisecond * 100 // no need for other intervals to run fpCfg.StatusUpdateInterval = time.Minute * 10 fpCfg.SubmissionRetryInterval = time.Minute * 10 @@ -163,26 +175,22 @@ func FuzzSyncFinalityProviderStatus(f *testing.F) { app, fpPk, cleanup := startFPAppWithRegisteredFp(t, r, fpHomeDir, &fpCfg, mockClientController) defer cleanup() - require.Eventually(t, func() bool { - fpInfo, err := app.GetFinalityProviderInfo(fpPk) - if err != nil { - return false - } + fpInfo, err := app.GetFinalityProviderInfo(fpPk) + require.NoError(t, err) - expectedStatus := proto.FinalityProviderStatus_ACTIVE - if noVotingPowerTable { + expectedStatus := proto.FinalityProviderStatus_ACTIVE + if noVotingPowerTable { + switch isSlashedOrJailed { + case 0: + expectedStatus = proto.FinalityProviderStatus_SLASHED + case 1: + expectedStatus = proto.FinalityProviderStatus_JAILED + case 2: expectedStatus = proto.FinalityProviderStatus_REGISTERED } - fpInstance, err := app.GetFinalityProviderInstance() - if err != nil { - return false - } + } - // TODO: verify why mocks are failing - btcPkEqual := fpInstance.GetBtcPk().IsEqual(fpPk.MustToBTCPK()) - statusEqual := strings.EqualFold(fpInfo.Status, expectedStatus.String()) - return statusEqual && btcPkEqual - }, time.Second*5, time.Millisecond*200, "should eventually be registered or active") + require.Equal(t, fpInfo.Status, expectedStatus.String()) }) } @@ -200,7 +208,6 @@ func FuzzUnjailFinalityProvider(f *testing.F) { fpHomeDir := filepath.Join(t.TempDir(), "fp-home", pathSuffix) fpCfg := config.DefaultConfigWithHome(fpHomeDir) // use shorter interval for the test to end faster - fpCfg.SyncFpStatusInterval = time.Millisecond * 10 fpCfg.StatusUpdateInterval = time.Millisecond * 10 fpCfg.SubmissionRetryInterval = time.Millisecond * 10 @@ -249,51 +256,29 @@ func FuzzStatusUpdate(f *testing.F) { mockClientController.EXPECT().QueryFinalityProviderHighestVotedHeight(gomock.Any()).Return(uint64(0), nil).AnyTimes() mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).AnyTimes() mockClientController.EXPECT().SubmitFinalitySig(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.TxResponse{TxHash: ""}, nil).AnyTimes() - var isSlashedOrJailed int - if votingPower == 0 { - // 0 means is slashed, 1 means is jailed, 2 means neither slashed nor jailed - isSlashedOrJailed = r.Intn(3) - switch isSlashedOrJailed { - case 0: - mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(true, false, nil).AnyTimes() - case 1: - mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, true, nil).AnyTimes() - case 2: - mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, false, nil).AnyTimes() - } - } + mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, false, nil).AnyTimes() // Create randomized config pathSuffix := datagen.GenRandomHexStr(r, 10) fpHomeDir := filepath.Join(t.TempDir(), "fp-home", pathSuffix) fpCfg := config.DefaultConfigWithHome(fpHomeDir) // use shorter interval for the test to end faster - fpCfg.SyncFpStatusInterval = time.Millisecond * 10 - fpCfg.StatusUpdateInterval = time.Second * 1 + fpCfg.StatusUpdateInterval = time.Millisecond * 10 fpCfg.SubmissionRetryInterval = time.Millisecond * 10 // Create fp app - app, _, cleanup := startFPAppWithRegisteredFp(t, r, fpHomeDir, &fpCfg, mockClientController) + app, fpPk, cleanup := startFPAppWithRegisteredFp(t, r, fpHomeDir, &fpCfg, mockClientController) defer cleanup() - var fpIns *service.FinalityProviderInstance - var err error - require.Eventually(t, func() bool { - fpIns, err = app.GetFinalityProviderInstance() - return err == nil - }, time.Second*5, time.Millisecond*200, "should eventually be registered or active") + err := app.StartFinalityProvider(fpPk, passphrase) + require.NoError(t, err) + fpIns, err := app.GetFinalityProviderInstance() + require.NoError(t, err) if votingPower > 0 { waitForStatus(t, fpIns, proto.FinalityProviderStatus_ACTIVE) - } else { - switch { - case isSlashedOrJailed == 2 && fpIns.GetStatus() == proto.FinalityProviderStatus_ACTIVE: - waitForStatus(t, fpIns, proto.FinalityProviderStatus_INACTIVE) - case isSlashedOrJailed == 1: - waitForStatus(t, fpIns, proto.FinalityProviderStatus_JAILED) - case isSlashedOrJailed == 0: - waitForStatus(t, fpIns, proto.FinalityProviderStatus_SLASHED) - } + } else if fpIns.GetStatus() == proto.FinalityProviderStatus_ACTIVE { + waitForStatus(t, fpIns, proto.FinalityProviderStatus_INACTIVE) } }) } diff --git a/finality-provider/service/event_loops.go b/finality-provider/service/event_loops.go index b0ce8204..2fcd6281 100644 --- a/finality-provider/service/event_loops.go +++ b/finality-provider/service/event_loops.go @@ -46,10 +46,9 @@ type UnjailFinalityProviderResponse struct { // monitorStatusUpdate periodically check the status of the running finality provider and update // it accordingly. We update the status by querying the latest voting power and the slashed_height. -// In particular, we perform the following status transitions (REGISTERED, ACTIVE, INACTIVE, SLASHED): -// 1. if power == 0 and slashed_height == 0, if status == ACTIVE, change to INACTIVE, otherwise remain the same -// 2. if power == 0 and slashed_height > 0, set status to SLASHED and stop and remove the finality-provider instance -// 3. if power > 0 (slashed_height must > 0), set status to ACTIVE +// In particular, we perform the following status transitions (REGISTERED, ACTIVE, INACTIVE): +// 1. if power == 0 and status == ACTIVE, change to INACTIVE +// 2. if power > 0, change to ACTIVE // NOTE: once error occurs, we log and continue as the status update is not critical to the entire program func (app *FinalityProviderApp) monitorStatusUpdate() { defer app.wg.Done() @@ -99,35 +98,6 @@ func (app *FinalityProviderApp) monitorStatusUpdate() { } continue } - slashed, jailed, err := fpi.GetFinalityProviderSlashedOrJailedWithRetry() - if err != nil { - app.logger.Debug( - "failed to get the slashed or jailed status", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.Error(err), - ) - continue - } - // power == 0 and slashed == true, set status to SLASHED, stop, and remove the finality-provider instance - if slashed { - app.setFinalityProviderSlashed(fpi) - app.logger.Warn( - "the finality-provider is slashed", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.String("old_status", oldStatus.String()), - ) - continue - } - // power == 0 and jailed == true, set status to JAILED, stop, and remove the finality-provider instance - if jailed { - app.setFinalityProviderJailed(fpi) - app.logger.Warn( - "the finality-provider is jailed", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.String("old_status", oldStatus.String()), - ) - continue - } // power == 0 and slashed_height == 0, change to INACTIVE if the current status is ACTIVE if oldStatus == proto.FinalityProviderStatus_ACTIVE { fpi.MustSetStatus(proto.FinalityProviderStatus_INACTIVE) @@ -313,38 +283,3 @@ func (app *FinalityProviderApp) metricsUpdateLoop() { } } } - -// syncChainFpStatusLoop keeps querying the chain for the finality -// provider voting power and update the FP status accordingly. -// If there is some voting power it sets to active, for zero voting power -// it goes from: CREATED -> REGISTERED or ACTIVE -> INACTIVE. -// if there is any node running or a new finality provider instance -// is started, the loop stops. -func (app *FinalityProviderApp) syncChainFpStatusLoop() { - defer app.wg.Done() - - interval := app.config.SyncFpStatusInterval - app.logger.Info( - "starting sync FP status loop", - zap.Float64("interval seconds", interval.Seconds()), - ) - syncFpStatusTicker := time.NewTicker(interval) - defer syncFpStatusTicker.Stop() - - for { - select { - case <-syncFpStatusTicker.C: - fpInstanceStarted, err := app.SyncFinalityProviderStatus() - if err != nil { - app.logger.Error("failed to sync finality-provider status", zap.Error(err)) - } - if fpInstanceStarted { - return - } - - case <-app.quit: - app.logger.Info("exiting sync FP status loop") - return - } - } -} diff --git a/finality-provider/store/fpstore.go b/finality-provider/store/fpstore.go index ffff5334..5d558e06 100644 --- a/finality-provider/store/fpstore.go +++ b/finality-provider/store/fpstore.go @@ -107,6 +107,12 @@ func (s *FinalityProviderStore) SetFpStatus(btcPk *btcec.PublicKey, status proto return s.setFinalityProviderState(btcPk, setFpStatus) } +func (s *FinalityProviderStore) MustSetFpStatus(btcPk *btcec.PublicKey, status proto.FinalityProviderStatus) { + if err := s.SetFpStatus(btcPk, status); err != nil { + panic(err) + } +} + // UpdateFpStatusFromVotingPower based on the current voting power of the finality provider // updates the status, if it has some voting power, sets to active func (s *FinalityProviderStore) UpdateFpStatusFromVotingPower(