diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aed4b62..f9aa08f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * [#175](https://github.com/babylonlabs-io/finality-provider/pull/175) adds: `eotsd version` command * [#179](https://github.com/babylonlabs-io/finality-provider/pull/179) Change `btc_pk` text to `eots_pk` in CLI +* [#182](https://github.com/babylonlabs-io/finality-provider/pull/182) Remove fp manager ### Bug Fixes diff --git a/finality-provider/cmd/fpd/daemon/start.go b/finality-provider/cmd/fpd/daemon/start.go index 94ad0ac6..271205a2 100644 --- a/finality-provider/cmd/fpd/daemon/start.go +++ b/finality-provider/cmd/fpd/daemon/start.go @@ -1,7 +1,6 @@ package daemon import ( - "errors" "fmt" "net" "path/filepath" @@ -138,12 +137,7 @@ func startApp( return fmt.Errorf("invalid finality provider public key %s: %w", fpPkStr, err) } - if err := fpApp.StartHandlingFinalityProvider(fpPk, passphrase); err != nil { - if errors.Is(err, service.ErrFinalityProviderJailed) { - fpApp.Logger().Error("failed to start finality provider", zap.Error(err)) - // do not return error as we still want the service to start - return nil - } + if err := fpApp.StartFinalityProvider(fpPk, passphrase); err != nil { return fmt.Errorf("failed to start the finality-provider instance %s: %w", fpPkStr, err) } diff --git a/finality-provider/service/app.go b/finality-provider/service/app.go index 077f7a1e..93e5653e 100644 --- a/finality-provider/service/app.go +++ b/finality-provider/service/app.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" "sync" - "time" sdkmath "cosmossdk.io/math" + "github.com/avast/retry-go/v4" bbntypes "github.com/babylonlabs-io/babylon/types" bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" "github.com/btcsuite/btcd/btcec/v2" @@ -32,9 +32,8 @@ import ( type FinalityProviderApp struct { startOnce sync.Once stopOnce sync.Once - - wg sync.WaitGroup - quit chan struct{} + wg sync.WaitGroup + quit chan struct{} cc clientcontroller.ClientController kr keyring.Keyring @@ -44,7 +43,7 @@ type FinalityProviderApp struct { logger *zap.Logger input *strings.Reader - fpManager *FinalityProviderManager + fpIns *FinalityProviderInstance eotsManager eotsmanager.EOTSManager metrics *metrics.FpMetrics @@ -52,6 +51,7 @@ type FinalityProviderApp struct { createFinalityProviderRequestChan chan *createFinalityProviderRequest registerFinalityProviderRequestChan chan *registerFinalityProviderRequest finalityProviderRegisteredEventChan chan *finalityProviderRegisteredEvent + criticalErrChan chan *CriticalError } func NewFinalityProviderAppFromConfig( @@ -104,11 +104,6 @@ func NewFinalityProviderApp( fpMetrics := metrics.NewFpMetrics() - fpm, err := NewFinalityProviderManager(fpStore, pubRandStore, config, cc, em, fpMetrics, logger) - if err != nil { - return nil, fmt.Errorf("failed to create finality-provider manager: %w", err) - } - return &FinalityProviderApp{ cc: cc, fps: fpStore, @@ -117,13 +112,14 @@ func NewFinalityProviderApp( config: config, logger: logger, input: input, - fpManager: fpm, + fpIns: nil, eotsManager: em, metrics: fpMetrics, quit: make(chan struct{}), createFinalityProviderRequestChan: make(chan *createFinalityProviderRequest), registerFinalityProviderRequestChan: make(chan *registerFinalityProviderRequest), finalityProviderRegisteredEventChan: make(chan *finalityProviderRegisteredEvent), + criticalErrChan: make(chan *CriticalError), }, nil } @@ -139,30 +135,48 @@ func (app *FinalityProviderApp) GetPubRandProofStore() *store.PubRandProofStore return app.pubRandStore } -func (app *FinalityProviderApp) GetKeyring() keyring.Keyring { - return app.kr -} +func (app *FinalityProviderApp) GetFinalityProviderInfo(fpPk *bbntypes.BIP340PubKey) (*proto.FinalityProviderInfo, error) { + storedFp, err := app.fps.GetFinalityProvider(fpPk.MustToBTCPK()) + if err != nil { + return nil, err + } -func (app *FinalityProviderApp) GetInput() *strings.Reader { - return app.input -} + fpInfo := storedFp.ToFinalityProviderInfo() + + if app.IsFinalityProviderRunning(fpPk) { + fpInfo.IsRunning = true + } -// Logger returns the current logger of FP app. -func (app *FinalityProviderApp) Logger() *zap.Logger { - return app.logger + return fpInfo, nil } func (app *FinalityProviderApp) ListAllFinalityProvidersInfo() ([]*proto.FinalityProviderInfo, error) { - return app.fpManager.AllFinalityProviders() -} + storedFps, err := app.fps.GetAllStoredFinalityProviders() + if err != nil { + return nil, err + } -func (app *FinalityProviderApp) GetFinalityProviderInfo(fpPk *bbntypes.BIP340PubKey) (*proto.FinalityProviderInfo, error) { - return app.fpManager.FinalityProviderInfo(fpPk) + fpsInfo := make([]*proto.FinalityProviderInfo, 0, len(storedFps)) + for _, fp := range storedFps { + fpInfo := fp.ToFinalityProviderInfo() + + if app.IsFinalityProviderRunning(fp.GetBIP340BTCPK()) { + fpInfo.IsRunning = true + } + + fpsInfo = append(fpsInfo, fpInfo) + } + + return fpsInfo, nil } // GetFinalityProviderInstance returns the finality-provider instance with the given Babylon public key func (app *FinalityProviderApp) GetFinalityProviderInstance() (*FinalityProviderInstance, error) { - return app.fpManager.GetFinalityProviderInstance() + if app.fpIns == nil { + return nil, fmt.Errorf("finality provider does not exist") + } + + return app.fpIns, nil } func (app *FinalityProviderApp) RegisterFinalityProvider(fpPkStr string) (*RegisterFinalityProviderResponse, error) { @@ -217,20 +231,18 @@ func (app *FinalityProviderApp) RegisterFinalityProvider(fpPkStr string) (*Regis } } -// StartHandlingFinalityProvider starts a finality provider instance with the given EOTS public key +// StartFinalityProvider starts a finality provider instance with the given EOTS public key // Note: this should be called right after the finality-provider is registered -func (app *FinalityProviderApp) StartHandlingFinalityProvider(fpPk *bbntypes.BIP340PubKey, passphrase string) error { - return app.fpManager.StartFinalityProvider(fpPk, passphrase) -} +func (app *FinalityProviderApp) StartFinalityProvider(fpPk *bbntypes.BIP340PubKey, passphrase string) error { + app.logger.Info("starting finality provider", zap.String("pk", fpPk.MarshalHex())) -// NOTE: this is not safe in production, so only used for testing purpose -func (app *FinalityProviderApp) getFpPrivKey(fpPk []byte) (*btcec.PrivateKey, error) { - record, err := app.eotsManager.KeyRecord(fpPk, "") - if err != nil { - return nil, err + if err := app.startFinalityProviderInstance(fpPk, passphrase); err != nil { + return err } - return record.PrivKey, nil + app.logger.Info("finality provider is started", zap.String("pk", fpPk.MarshalHex())) + + return nil } // SyncFinalityProviderStatus syncs the status of the finality-providers with the chain. @@ -253,7 +265,7 @@ func (app *FinalityProviderApp) SyncFinalityProviderStatus() (bool, error) { } bip340PubKey := fp.GetBIP340BTCPK() - if app.fpManager.IsFinalityProviderRunning(bip340PubKey) { + 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 @@ -280,7 +292,7 @@ func (app *FinalityProviderApp) SyncFinalityProviderStatus() (bool, error) { continue } - if err := app.fpManager.StartFinalityProvider(bip340PubKey, ""); err != nil { + if err := app.StartFinalityProvider(bip340PubKey, ""); err != nil { return false, err } fpInstanceRunning = true @@ -295,11 +307,13 @@ func (app *FinalityProviderApp) Start() error { app.startOnce.Do(func() { app.logger.Info("Starting FinalityProviderApp") - app.wg.Add(4) + app.wg.Add(6) go app.syncChainFpStatusLoop() go app.eventLoop() go app.registrationLoop() go app.metricsUpdateLoop() + go app.monitorCriticalErr() + go app.monitorStatusUpdate() }) return startErr @@ -310,15 +324,19 @@ func (app *FinalityProviderApp) Stop() error { app.stopOnce.Do(func() { app.logger.Info("Stopping FinalityProviderApp") - // Always stop the submission loop first to not generate additional events and actions - app.logger.Debug("Stopping submission loop") close(app.quit) app.wg.Wait() - app.logger.Debug("Stopping finality providers") - if err := app.fpManager.Stop(); err != nil { - stopErr = err - return + if app.fpIns != nil && app.fpIns.IsRunning() { + pkHex := app.fpIns.GetBtcPkHex() + app.logger.Info("stopping finality provider", zap.String("pk", pkHex)) + + if err := app.fpIns.Stop(); err != nil { + stopErr = err + return + } + + app.logger.Info("finality provider is stopped", zap.String("pk", pkHex)) } app.logger.Debug("Stopping EOTS manager") @@ -385,7 +403,7 @@ func (app *FinalityProviderApp) UnjailFinalityProvider(fpPk *bbntypes.BIP340PubK return "", fmt.Errorf("failed to update finality-provider status after unjailing: %w", err) } - app.fpManager.metrics.RecordFpStatus(fpPk.MarshalHex(), proto.FinalityProviderStatus_INACTIVE) + app.metrics.RecordFpStatus(fpPk.MarshalHex(), proto.FinalityProviderStatus_INACTIVE) app.logger.Info("successfully unjailed finality-provider", zap.String("btc_pk", fpPk.MarshalHex()), @@ -423,7 +441,7 @@ func (app *FinalityProviderApp) handleCreateFinalityProviderRequest(req *createF } pkHex := req.eotsPk.MarshalHex() - app.fpManager.metrics.RecordFpStatus(pkHex, proto.FinalityProviderStatus_CREATED) + app.metrics.RecordFpStatus(pkHex, proto.FinalityProviderStatus_CREATED) app.logger.Info("successfully created a finality-provider", zap.String("eots_pk", pkHex), @@ -495,187 +513,100 @@ func (app *FinalityProviderApp) loadChainKeyring( return kr, chainSk, nil } -// UpdateClientController sets a new client controoller in the App. -// Useful for testing with multiples PKs with different keys, it needs -// to update who is the signer -func (app *FinalityProviderApp) UpdateClientController(cc clientcontroller.ClientController) { - app.cc = cc -} +func (app *FinalityProviderApp) startFinalityProviderInstance( + pk *bbntypes.BIP340PubKey, + passphrase string, +) error { + pkHex := pk.MarshalHex() + if app.fpIns == nil { + fpIns, err := NewFinalityProviderInstance( + pk, app.config, app.fps, app.pubRandStore, app.cc, app.eotsManager, + app.metrics, passphrase, app.criticalErrChan, app.logger, + ) + if err != nil { + return fmt.Errorf("failed to create finality provider instance %s: %w", pkHex, err) + } -func CreateChainKey(keyringDir, chainID, keyName, backend, passphrase, hdPath, mnemonic string) (*types.ChainKeyInfo, error) { - sdkCtx, err := fpkr.CreateClientCtx( - keyringDir, chainID, - ) - if err != nil { - return nil, err + app.fpIns = fpIns } - krController, err := fpkr.NewChainKeyringController( - sdkCtx, - keyName, - backend, - ) - if err != nil { - return nil, err - } - - return krController.CreateChainKey(passphrase, hdPath, mnemonic) + return app.fpIns.Start() } -// main event loop for the finality-provider app -func (app *FinalityProviderApp) eventLoop() { - defer app.wg.Done() - - for { - select { - case req := <-app.createFinalityProviderRequestChan: - res, err := app.handleCreateFinalityProviderRequest(req) - if err != nil { - req.errResponse <- err - continue - } - - req.successResponse <- &createFinalityProviderResponse{FpInfo: res.FpInfo} +func (app *FinalityProviderApp) IsFinalityProviderRunning(fpPk *bbntypes.BIP340PubKey) bool { + if app.fpIns == nil { + return false + } - case ev := <-app.finalityProviderRegisteredEventChan: - // change the status of the finality-provider to registered - err := app.fps.SetFpStatus(ev.btcPubKey.MustToBTCPK(), proto.FinalityProviderStatus_REGISTERED) - if err != nil { - app.logger.Fatal("failed to set finality-provider status to REGISTERED", - zap.String("pk", ev.btcPubKey.MarshalHex()), - zap.Error(err), - ) - } - app.fpManager.metrics.RecordFpStatus(ev.btcPubKey.MarshalHex(), proto.FinalityProviderStatus_REGISTERED) + if app.fpIns.GetBtcPkHex() != fpPk.MarshalHex() { + return false + } - // return to the caller - ev.successResponse <- &RegisterFinalityProviderResponse{ - bbnAddress: ev.bbnAddress, - btcPubKey: ev.btcPubKey, - TxHash: ev.txHash, - } + return app.fpIns.IsRunning() +} - case <-app.quit: - app.logger.Debug("exiting main event loop") - return +func (app *FinalityProviderApp) removeFinalityProviderInstance() error { + fpi := app.fpIns + if fpi == nil { + return fmt.Errorf("the finality provider instance does not exist") + } + if fpi.IsRunning() { + if err := fpi.Stop(); err != nil { + return fmt.Errorf("failed to stop the finality provider instance %s", fpi.GetBtcPkHex()) } } -} - -func (app *FinalityProviderApp) registrationLoop() { - defer app.wg.Done() - for { - select { - case req := <-app.registerFinalityProviderRequestChan: - // we won't do any retries here to not block the loop for more important messages. - // Most probably it fails due so some user error so we just return the error to the user. - // TODO: need to start passing context here to be able to cancel the request in case of app quiting - popBytes, err := req.pop.Marshal() - if err != nil { - req.errResponse <- err - continue - } - desBytes, err := req.description.Marshal() - if err != nil { - req.errResponse <- err - continue - } - res, err := app.cc.RegisterFinalityProvider( - req.btcPubKey.MustToBTCPK(), - popBytes, - req.commission, - desBytes, - ) + app.fpIns = nil - if err != nil { - app.logger.Error( - "failed to register finality-provider", - zap.String("pk", req.btcPubKey.MarshalHex()), - zap.Error(err), - ) - req.errResponse <- err - continue - } - - app.logger.Info( - "successfully registered finality-provider on babylon", - zap.String("btc_pk", req.btcPubKey.MarshalHex()), - zap.String("fp_addr", req.fpAddr.String()), - zap.String("txHash", res.TxHash), - ) + return nil +} - app.finalityProviderRegisteredEventChan <- &finalityProviderRegisteredEvent{ - btcPubKey: req.btcPubKey, - bbnAddress: req.fpAddr, - txHash: res.TxHash, - // pass the channel to the event so that we can send the response to the user which requested - // the registration - successResponse: req.successResponse, - } - case <-app.quit: - app.logger.Debug("exiting registration loop") - return - } +func (app *FinalityProviderApp) setFinalityProviderSlashed(fpi *FinalityProviderInstance) { + fpi.MustSetStatus(proto.FinalityProviderStatus_SLASHED) + if err := app.removeFinalityProviderInstance(); err != nil { + panic(fmt.Errorf("failed to terminate a slashed finality-provider %s: %w", fpi.GetBtcPkHex(), err)) } } -func (app *FinalityProviderApp) metricsUpdateLoop() { - defer app.wg.Done() - - interval := app.config.Metrics.UpdateInterval - app.logger.Info("starting metrics update loop", - zap.Float64("interval seconds", interval.Seconds())) - updateTicker := time.NewTicker(interval) - - for { - select { - case <-updateTicker.C: - fps, err := app.fps.GetAllStoredFinalityProviders() - if err != nil { - app.logger.Error("failed to get finality-providers from the store", zap.Error(err)) - continue - } - app.metrics.UpdateFpMetrics(fps) - case <-app.quit: - updateTicker.Stop() - app.logger.Info("exiting metrics update loop") - return - } +func (app *FinalityProviderApp) setFinalityProviderJailed(fpi *FinalityProviderInstance) { + fpi.MustSetStatus(proto.FinalityProviderStatus_JAILED) + if err := app.removeFinalityProviderInstance(); err != nil { + panic(fmt.Errorf("failed to terminate a jailed finality-provider %s: %w", fpi.GetBtcPkHex(), err)) } } -// 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()), +func (app *FinalityProviderApp) getLatestBlockWithRetry() (*types.BlockInfo, error) { + var ( + latestBlock *types.BlockInfo + err error ) - 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 + if err := retry.Do(func() error { + latestBlock, err = app.cc.QueryBestBlock() + if err != nil { + return err } + return nil + }, RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) { + app.logger.Debug( + "failed to query the consumer chain for the latest block", + zap.Uint("attempt", n+1), + zap.Uint("max_attempts", RtyAttNum), + zap.Error(err), + ) + })); err != nil { + return nil, err } + + return latestBlock, nil +} + +// NOTE: this is not safe in production, so only used for testing purpose +func (app *FinalityProviderApp) getFpPrivKey(fpPk []byte) (*btcec.PrivateKey, error) { + record, err := app.eotsManager.KeyRecord(fpPk, "") + if err != nil { + return nil, err + } + + return record.PrivKey, nil } diff --git a/finality-provider/service/app_test.go b/finality-provider/service/app_test.go index 384c8571..e1184a12 100644 --- a/finality-provider/service/app_test.go +++ b/finality-provider/service/app_test.go @@ -18,18 +18,25 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" + "github.com/babylonlabs-io/finality-provider/clientcontroller" "github.com/babylonlabs-io/finality-provider/eotsmanager" eotscfg "github.com/babylonlabs-io/finality-provider/eotsmanager/config" "github.com/babylonlabs-io/finality-provider/finality-provider/config" "github.com/babylonlabs-io/finality-provider/finality-provider/proto" "github.com/babylonlabs-io/finality-provider/finality-provider/service" + fpstore "github.com/babylonlabs-io/finality-provider/finality-provider/store" + "github.com/babylonlabs-io/finality-provider/keyring" "github.com/babylonlabs-io/finality-provider/testutil" + "github.com/babylonlabs-io/finality-provider/testutil/mocks" "github.com/babylonlabs-io/finality-provider/types" + "github.com/babylonlabs-io/finality-provider/util" ) -var ( - passphrase = "testpass" - hdPath = "" +const ( + passphrase = "testpass" + hdPath = "" + eventuallyWaitTimeOut = 5 * time.Second + eventuallyPollTime = 10 * time.Millisecond ) func FuzzRegisterFinalityProvider(f *testing.F) { @@ -125,7 +132,7 @@ func FuzzRegisterFinalityProvider(f *testing.F) { require.Equal(t, txHash, res.TxHash) mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).AnyTimes() - err = app.StartHandlingFinalityProvider(fp.GetBIP340BTCPK(), passphrase) + err = app.StartFinalityProvider(fp.GetBIP340BTCPK(), passphrase) require.NoError(t, err) fpAfterReg, err := app.GetFinalityProviderInstance() @@ -299,3 +306,155 @@ func FuzzUnjailFinalityProvider(f *testing.F) { require.Equal(t, proto.FinalityProviderStatus_INACTIVE.String(), fpInfo.GetStatus()) }) } + +func FuzzStatusUpdate(f *testing.F) { + testutil.AddRandomSeedsToFuzzer(f, 10) + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + + ctl := gomock.NewController(t) + mockClientController := mocks.NewMockClientController(ctl) + fpApp, fpPk, cleanUp := newFinalityProviderManagerWithRegisteredFp(t, r, mockClientController) + defer cleanUp() + + // setup mocks + currentHeight := uint64(r.Int63n(100) + 1) + currentBlockRes := &types.BlockInfo{ + Height: currentHeight, + Hash: datagen.GenRandomByteArray(r, 32), + } + mockClientController.EXPECT().QueryBestBlock().Return(currentBlockRes, nil).AnyTimes() + mockClientController.EXPECT().Close().Return(nil).AnyTimes() + mockClientController.EXPECT().QueryLatestFinalizedBlocks(gomock.Any()).Return(nil, nil).AnyTimes() + mockClientController.EXPECT().QueryBestBlock().Return(currentBlockRes, nil).AnyTimes() + mockClientController.EXPECT().QueryActivatedHeight().Return(uint64(1), nil).AnyTimes() + mockClientController.EXPECT().QueryFinalityActivationBlockHeight().Return(uint64(0), nil).AnyTimes() + mockClientController.EXPECT().QueryFinalityProviderHighestVotedHeight(gomock.Any()).Return(uint64(0), nil).AnyTimes() + mockClientController.EXPECT().QueryBlock(gomock.Any()).Return(currentBlockRes, nil).AnyTimes() + mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).AnyTimes() + + votingPower := uint64(r.Intn(2)) + mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), currentHeight).Return(votingPower, 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() + } + } + + err := fpApp.StartFinalityProvider(fpPk, passphrase) + require.NoError(t, err) + fpIns, err := fpApp.GetFinalityProviderInstance() + require.NoError(t, err) + // stop the finality-provider as we are testing static functionalities + err = fpIns.Stop() + 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) + } + } + }) +} + +func waitForStatus(t *testing.T, fpIns *service.FinalityProviderInstance, s proto.FinalityProviderStatus) { + require.Eventually(t, + func() bool { + return fpIns.GetStatus() == s + }, eventuallyWaitTimeOut, eventuallyPollTime) +} + +func newFinalityProviderManagerWithRegisteredFp(t *testing.T, r *rand.Rand, cc clientcontroller.ClientController) (*service.FinalityProviderApp, *bbntypes.BIP340PubKey, func()) { + logger := zap.NewNop() + // create an EOTS manager + eotsHomeDir := filepath.Join(t.TempDir(), "eots-home") + eotsCfg := eotscfg.DefaultConfigWithHomePath(eotsHomeDir) + eotsdb, err := eotsCfg.DatabaseConfig.GetDBBackend() + require.NoError(t, err) + em, err := eotsmanager.NewLocalEOTSManager(eotsHomeDir, eotsCfg.KeyringBackend, eotsdb, logger) + require.NoError(t, err) + + // create finality-provider app with randomized config + fpHomeDir := filepath.Join(t.TempDir(), "fp-home") + fpCfg := config.DefaultConfigWithHome(fpHomeDir) + fpCfg.StatusUpdateInterval = 10 * time.Millisecond + input := strings.NewReader("") + kr, err := keyring.CreateKeyring( + fpCfg.BabylonConfig.KeyDirectory, + fpCfg.BabylonConfig.ChainID, + fpCfg.BabylonConfig.KeyringBackend, + input, + ) + require.NoError(t, err) + err = util.MakeDirectory(config.DataDir(fpHomeDir)) + require.NoError(t, err) + db, err := fpCfg.DatabaseConfig.GetDBBackend() + require.NoError(t, err) + fpStore, err := fpstore.NewFinalityProviderStore(db) + require.NoError(t, err) + app, err := service.NewFinalityProviderApp(&fpCfg, cc, em, db, logger) + require.NoError(t, err) + err = app.Start() + require.NoError(t, err) + + // create registered finality-provider + keyName := datagen.GenRandomHexStr(r, 10) + chainID := datagen.GenRandomHexStr(r, 10) + kc, err := keyring.NewChainKeyringControllerWithKeyring(kr, keyName, input) + require.NoError(t, err) + btcPkBytes, err := em.CreateKey(keyName, passphrase, hdPath) + require.NoError(t, err) + btcPk, err := bbntypes.NewBIP340PubKey(btcPkBytes) + require.NoError(t, err) + keyInfo, err := kc.CreateChainKey(passphrase, hdPath, "") + require.NoError(t, err) + fpAddr := keyInfo.AccAddress + fpRecord, err := em.KeyRecord(btcPk.MustMarshal(), passphrase) + require.NoError(t, err) + pop, err := kc.CreatePop(fpAddr, fpRecord.PrivKey) + require.NoError(t, err) + + err = fpStore.CreateFinalityProvider( + fpAddr, + btcPk.MustToBTCPK(), + testutil.RandomDescription(r), + testutil.ZeroCommissionRate(), + keyName, + chainID, + pop.BtcSig, + ) + require.NoError(t, err) + err = fpStore.SetFpStatus(btcPk.MustToBTCPK(), proto.FinalityProviderStatus_REGISTERED) + require.NoError(t, err) + + cleanUp := func() { + err = app.Stop() + require.NoError(t, err) + err = eotsdb.Close() + require.NoError(t, err) + err = db.Close() + require.NoError(t, err) + err = os.RemoveAll(eotsHomeDir) + require.NoError(t, err) + err = os.RemoveAll(fpHomeDir) + require.NoError(t, err) + } + + return app, btcPk, cleanUp +} diff --git a/finality-provider/service/errors.go b/finality-provider/service/errors.go index fa6b50af..3f261ab4 100644 --- a/finality-provider/service/errors.go +++ b/finality-provider/service/errors.go @@ -1,6 +1,22 @@ package service -import "errors" +import ( + "errors" + "fmt" + + bbntypes "github.com/babylonlabs-io/babylon/types" +) + +const instanceTerminatingMsg = "terminating the finality-provider instance due to critical error" + +type CriticalError struct { + err error + fpBtcPk *bbntypes.BIP340PubKey +} + +func (ce *CriticalError) Error() string { + return fmt.Sprintf("critical err on finality-provider %s: %s", ce.fpBtcPk.MarshalHex(), ce.err.Error()) +} var ( ErrFinalityProviderShutDown = errors.New("the finality provider instance is shutting down") diff --git a/finality-provider/service/event_loops.go b/finality-provider/service/event_loops.go new file mode 100644 index 00000000..73605dfa --- /dev/null +++ b/finality-provider/service/event_loops.go @@ -0,0 +1,301 @@ +package service + +import ( + "errors" + "time" + + "go.uber.org/zap" + + "github.com/babylonlabs-io/finality-provider/finality-provider/proto" +) + +// main event loop for the finality-provider app +func (app *FinalityProviderApp) eventLoop() { + defer app.wg.Done() + + for { + select { + case req := <-app.createFinalityProviderRequestChan: + res, err := app.handleCreateFinalityProviderRequest(req) + if err != nil { + req.errResponse <- err + continue + } + + req.successResponse <- &createFinalityProviderResponse{FpInfo: res.FpInfo} + + case ev := <-app.finalityProviderRegisteredEventChan: + // change the status of the finality-provider to registered + err := app.fps.SetFpStatus(ev.btcPubKey.MustToBTCPK(), proto.FinalityProviderStatus_REGISTERED) + if err != nil { + app.logger.Fatal("failed to set finality-provider status to REGISTERED", + zap.String("pk", ev.btcPubKey.MarshalHex()), + zap.Error(err), + ) + } + app.metrics.RecordFpStatus(ev.btcPubKey.MarshalHex(), proto.FinalityProviderStatus_REGISTERED) + + // return to the caller + ev.successResponse <- &RegisterFinalityProviderResponse{ + bbnAddress: ev.bbnAddress, + btcPubKey: ev.btcPubKey, + TxHash: ev.txHash, + } + + case <-app.quit: + app.logger.Debug("exiting main event loop") + return + } + } +} + +// 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 +// 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() + + if app.config.StatusUpdateInterval == 0 { + app.logger.Info("the status update is disabled") + return + } + + statusUpdateTicker := time.NewTicker(app.config.StatusUpdateInterval) + defer statusUpdateTicker.Stop() + + for { + select { + case <-statusUpdateTicker.C: + fpi := app.fpIns + if fpi == nil { + continue + } + + latestBlock, err := app.getLatestBlockWithRetry() + if err != nil { + app.logger.Debug("failed to get the latest block", zap.Error(err)) + continue + } + oldStatus := fpi.GetStatus() + power, err := fpi.GetVotingPowerWithRetry(latestBlock.Height) + if err != nil { + app.logger.Debug( + "failed to get the voting power", + zap.String("fp_btc_pk", fpi.GetBtcPkHex()), + zap.Uint64("height", latestBlock.Height), + zap.Error(err), + ) + continue + } + // power > 0 (slashed_height must > 0), set status to ACTIVE + if power > 0 { + if oldStatus != proto.FinalityProviderStatus_ACTIVE { + fpi.MustSetStatus(proto.FinalityProviderStatus_ACTIVE) + app.logger.Debug( + "the finality-provider status is changed to ACTIVE", + zap.String("fp_btc_pk", fpi.GetBtcPkHex()), + zap.String("old_status", oldStatus.String()), + zap.Uint64("power", power), + ) + } + 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) + app.logger.Debug( + "the finality-provider status is changed to INACTIVE", + zap.String("fp_btc_pk", fpi.GetBtcPkHex()), + zap.String("old_status", oldStatus.String()), + ) + } + case <-app.quit: + return + } + } +} + +func (app *FinalityProviderApp) monitorCriticalErr() { + defer app.wg.Done() + + var criticalErr *CriticalError + + for { + select { + case criticalErr = <-app.criticalErrChan: + fpi, err := app.GetFinalityProviderInstance() + if err != nil { + app.logger.Debug("the finality-provider instance is already shutdown", + zap.String("pk", criticalErr.fpBtcPk.MarshalHex())) + continue + } + if errors.Is(criticalErr.err, ErrFinalityProviderSlashed) { + app.setFinalityProviderSlashed(fpi) + app.logger.Debug("the finality-provider has been slashed", + zap.String("pk", criticalErr.fpBtcPk.MarshalHex())) + continue + } + if errors.Is(criticalErr.err, ErrFinalityProviderJailed) { + app.setFinalityProviderJailed(fpi) + app.logger.Debug("the finality-provider has been jailed", + zap.String("pk", criticalErr.fpBtcPk.MarshalHex())) + continue + } + app.logger.Fatal(instanceTerminatingMsg, + zap.String("pk", criticalErr.fpBtcPk.MarshalHex()), zap.Error(criticalErr.err)) + case <-app.quit: + return + } + } +} + +func (app *FinalityProviderApp) registrationLoop() { + defer app.wg.Done() + for { + select { + case req := <-app.registerFinalityProviderRequestChan: + // we won't do any retries here to not block the loop for more important messages. + // Most probably it fails due so some user error so we just return the error to the user. + // TODO: need to start passing context here to be able to cancel the request in case of app quiting + popBytes, err := req.pop.Marshal() + if err != nil { + req.errResponse <- err + continue + } + + desBytes, err := req.description.Marshal() + if err != nil { + req.errResponse <- err + continue + } + res, err := app.cc.RegisterFinalityProvider( + req.btcPubKey.MustToBTCPK(), + popBytes, + req.commission, + desBytes, + ) + + if err != nil { + app.logger.Error( + "failed to register finality-provider", + zap.String("pk", req.btcPubKey.MarshalHex()), + zap.Error(err), + ) + req.errResponse <- err + continue + } + + app.logger.Info( + "successfully registered finality-provider on babylon", + zap.String("btc_pk", req.btcPubKey.MarshalHex()), + zap.String("fp_addr", req.fpAddr.String()), + zap.String("txHash", res.TxHash), + ) + + app.finalityProviderRegisteredEventChan <- &finalityProviderRegisteredEvent{ + btcPubKey: req.btcPubKey, + bbnAddress: req.fpAddr, + txHash: res.TxHash, + // pass the channel to the event so that we can send the response to the user which requested + // the registration + successResponse: req.successResponse, + } + case <-app.quit: + app.logger.Debug("exiting registration loop") + return + } + } +} + +func (app *FinalityProviderApp) metricsUpdateLoop() { + defer app.wg.Done() + + interval := app.config.Metrics.UpdateInterval + app.logger.Info("starting metrics update loop", + zap.Float64("interval seconds", interval.Seconds())) + updateTicker := time.NewTicker(interval) + + for { + select { + case <-updateTicker.C: + fps, err := app.fps.GetAllStoredFinalityProviders() + if err != nil { + app.logger.Error("failed to get finality-providers from the store", zap.Error(err)) + continue + } + app.metrics.UpdateFpMetrics(fps) + case <-app.quit: + updateTicker.Stop() + app.logger.Info("exiting metrics update loop") + return + } + } +} + +// 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/service/fp_instance.go b/finality-provider/service/fp_instance.go index f42ceab3..6b7883ca 100644 --- a/finality-provider/service/fp_instance.go +++ b/finality-provider/service/fp_instance.go @@ -130,9 +130,9 @@ func (fp *FinalityProviderInstance) Start() error { fp.poller = poller fp.quit = make(chan struct{}) - fp.wg.Add(1) + + fp.wg.Add(2) go fp.finalitySigSubmissionLoop() - fp.wg.Add(1) go fp.randomnessCommitmentLoop() return nil diff --git a/finality-provider/service/fp_manager.go b/finality-provider/service/fp_manager.go deleted file mode 100644 index 0d78f356..00000000 --- a/finality-provider/service/fp_manager.go +++ /dev/null @@ -1,396 +0,0 @@ -package service - -import ( - "errors" - "fmt" - "sync" - "time" - - "github.com/avast/retry-go/v4" - bbntypes "github.com/babylonlabs-io/babylon/types" - "go.uber.org/zap" - - "github.com/babylonlabs-io/finality-provider/clientcontroller" - "github.com/babylonlabs-io/finality-provider/eotsmanager" - fpcfg "github.com/babylonlabs-io/finality-provider/finality-provider/config" - "github.com/babylonlabs-io/finality-provider/finality-provider/proto" - "github.com/babylonlabs-io/finality-provider/finality-provider/store" - "github.com/babylonlabs-io/finality-provider/metrics" - "github.com/babylonlabs-io/finality-provider/types" -) - -const instanceTerminatingMsg = "terminating the finality-provider instance due to critical error" - -type CriticalError struct { - err error - fpBtcPk *bbntypes.BIP340PubKey -} - -func (ce *CriticalError) Error() string { - return fmt.Sprintf("critical err on finality-provider %s: %s", ce.fpBtcPk.MarshalHex(), ce.err.Error()) -} - -// FinalityProviderManager is responsible to initiate and start the given finality -// provider instance, monitor its running status -type FinalityProviderManager struct { - startOnce sync.Once - stopOnce sync.Once - - wg sync.WaitGroup - - fpIns *FinalityProviderInstance - - // needed for initiating finality-provider instances - fps *store.FinalityProviderStore - pubRandStore *store.PubRandProofStore - config *fpcfg.Config - cc clientcontroller.ClientController - em eotsmanager.EOTSManager - logger *zap.Logger - - metrics *metrics.FpMetrics - - criticalErrChan chan *CriticalError - - quit chan struct{} -} - -func NewFinalityProviderManager( - fps *store.FinalityProviderStore, - pubRandStore *store.PubRandProofStore, - config *fpcfg.Config, - cc clientcontroller.ClientController, - em eotsmanager.EOTSManager, - metrics *metrics.FpMetrics, - logger *zap.Logger, -) (*FinalityProviderManager, error) { - return &FinalityProviderManager{ - criticalErrChan: make(chan *CriticalError), - fps: fps, - pubRandStore: pubRandStore, - config: config, - cc: cc, - em: em, - metrics: metrics, - logger: logger, - quit: make(chan struct{}), - }, nil -} - -// monitorCriticalErr takes actions when it receives critical errors from a finality-provider instance -// if the finality-provider is slashed, it will be terminated and the program keeps running in case -// new finality providers join -// otherwise, the program will panic -func (fpm *FinalityProviderManager) monitorCriticalErr() { - defer fpm.wg.Done() - - var criticalErr *CriticalError - - for { - select { - case criticalErr = <-fpm.criticalErrChan: - fpi, err := fpm.GetFinalityProviderInstance() - if err != nil { - fpm.logger.Debug("the finality-provider instance is already shutdown", - zap.String("pk", criticalErr.fpBtcPk.MarshalHex())) - - continue - } - if errors.Is(criticalErr.err, ErrFinalityProviderSlashed) { - fpm.setFinalityProviderSlashed(fpi) - fpm.logger.Debug("the finality-provider has been slashed", - zap.String("pk", criticalErr.fpBtcPk.MarshalHex())) - - continue - } - if errors.Is(criticalErr.err, ErrFinalityProviderJailed) { - fpm.setFinalityProviderJailed(fpi) - fpm.logger.Debug("the finality-provider has been jailed", - zap.String("pk", criticalErr.fpBtcPk.MarshalHex())) - - continue - } - fpm.logger.Fatal(instanceTerminatingMsg, - zap.String("pk", criticalErr.fpBtcPk.MarshalHex()), zap.Error(criticalErr.err)) - case <-fpm.quit: - return - } - } -} - -// monitorStatusUpdate periodically check the status of each managed finality providers 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 -// NOTE: once error occurs, we log and continue as the status update is not critical to the entire program -func (fpm *FinalityProviderManager) monitorStatusUpdate() { - defer fpm.wg.Done() - - if fpm.config.StatusUpdateInterval == 0 { - fpm.logger.Info("the status update is disabled") - return - } - - statusUpdateTicker := time.NewTicker(fpm.config.StatusUpdateInterval) - defer statusUpdateTicker.Stop() - - for { - select { - case <-statusUpdateTicker.C: - fpi := fpm.fpIns - if fpi == nil { - continue - } - - latestBlock, err := fpm.getLatestBlockWithRetry() - if err != nil { - fpm.logger.Debug("failed to get the latest block", zap.Error(err)) - continue - } - oldStatus := fpi.GetStatus() - power, err := fpi.GetVotingPowerWithRetry(latestBlock.Height) - if err != nil { - fpm.logger.Debug( - "failed to get the voting power", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.Uint64("height", latestBlock.Height), - zap.Error(err), - ) - continue - } - // power > 0 (slashed_height must > 0), set status to ACTIVE - if power > 0 { - if oldStatus != proto.FinalityProviderStatus_ACTIVE { - fpi.MustSetStatus(proto.FinalityProviderStatus_ACTIVE) - fpm.logger.Debug( - "the finality-provider status is changed to ACTIVE", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.String("old_status", oldStatus.String()), - zap.Uint64("power", power), - ) - } - continue - } - slashed, jailed, err := fpi.GetFinalityProviderSlashedOrJailedWithRetry() - if err != nil { - fpm.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 { - fpm.setFinalityProviderSlashed(fpi) - fpm.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 { - fpm.setFinalityProviderJailed(fpi) - fpm.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) - fpm.logger.Debug( - "the finality-provider status is changed to INACTIVE", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.String("old_status", oldStatus.String()), - ) - } - case <-fpm.quit: - return - } - } -} - -func (fpm *FinalityProviderManager) setFinalityProviderSlashed(fpi *FinalityProviderInstance) { - fpi.MustSetStatus(proto.FinalityProviderStatus_SLASHED) - if err := fpm.removeFinalityProviderInstance(); err != nil { - panic(fmt.Errorf("failed to terminate a slashed finality-provider %s: %w", fpi.GetBtcPkHex(), err)) - } -} - -func (fpm *FinalityProviderManager) setFinalityProviderJailed(fpi *FinalityProviderInstance) { - fpi.MustSetStatus(proto.FinalityProviderStatus_JAILED) - if err := fpm.removeFinalityProviderInstance(); err != nil { - panic(fmt.Errorf("failed to terminate a jailed finality-provider %s: %w", fpi.GetBtcPkHex(), err)) - } -} - -func (fpm *FinalityProviderManager) StartFinalityProvider(fpPk *bbntypes.BIP340PubKey, passphrase string) error { - fpm.startOnce.Do(func() { - fpm.wg.Add(2) - go fpm.monitorCriticalErr() - go fpm.monitorStatusUpdate() - }) - - fpm.logger.Info("starting finality provider", zap.String("pk", fpPk.MarshalHex())) - - if err := fpm.startFinalityProviderInstance(fpPk, passphrase); err != nil { - return err - } - - fpm.logger.Info("finality provider is started", zap.String("pk", fpPk.MarshalHex())) - - return nil -} - -func (fpm *FinalityProviderManager) Stop() error { - var stopErr error - fpm.stopOnce.Do(func() { - close(fpm.quit) - fpm.wg.Wait() - - if fpm.fpIns == nil { - return - } - - if !fpm.fpIns.IsRunning() { - return - } - - pkHex := fpm.fpIns.GetBtcPkHex() - fpm.logger.Info("stopping finality provider", zap.String("pk", pkHex)) - - if err := fpm.fpIns.Stop(); err != nil { - stopErr = err - return - } - - fpm.logger.Info("finality provider is stopped", zap.String("pk", pkHex)) - }) - - return stopErr -} - -func (fpm *FinalityProviderManager) GetFinalityProviderInstance() (*FinalityProviderInstance, error) { - if fpm.fpIns == nil { - return nil, fmt.Errorf("finality provider does not exist") - } - - return fpm.fpIns, nil -} - -func (fpm *FinalityProviderManager) AllFinalityProviders() ([]*proto.FinalityProviderInfo, error) { - storedFps, err := fpm.fps.GetAllStoredFinalityProviders() - if err != nil { - return nil, err - } - - fpsInfo := make([]*proto.FinalityProviderInfo, 0, len(storedFps)) - for _, fp := range storedFps { - fpInfo := fp.ToFinalityProviderInfo() - - if fpm.IsFinalityProviderRunning(fp.GetBIP340BTCPK()) { - fpInfo.IsRunning = true - } - - fpsInfo = append(fpsInfo, fpInfo) - } - - return fpsInfo, nil -} - -func (fpm *FinalityProviderManager) FinalityProviderInfo(fpPk *bbntypes.BIP340PubKey) (*proto.FinalityProviderInfo, error) { - storedFp, err := fpm.fps.GetFinalityProvider(fpPk.MustToBTCPK()) - if err != nil { - return nil, err - } - - fpInfo := storedFp.ToFinalityProviderInfo() - - if fpm.IsFinalityProviderRunning(fpPk) { - fpInfo.IsRunning = true - } - - return fpInfo, nil -} - -func (fpm *FinalityProviderManager) IsFinalityProviderRunning(fpPk *bbntypes.BIP340PubKey) bool { - if fpm.fpIns == nil { - return false - } - - if fpm.fpIns.GetBtcPkHex() != fpPk.MarshalHex() { - return false - } - - return fpm.fpIns.IsRunning() -} - -func (fpm *FinalityProviderManager) removeFinalityProviderInstance() error { - fpi := fpm.fpIns - if fpi == nil { - return fmt.Errorf("the finality provider instance does not exist") - } - if fpi.IsRunning() { - if err := fpi.Stop(); err != nil { - return fmt.Errorf("failed to stop the finality provider instance %s", fpi.GetBtcPkHex()) - } - } - - fpm.fpIns = nil - - return nil -} - -// startFinalityProviderInstance creates a finality-provider instance, starts it and adds it into the finality-provider manager -func (fpm *FinalityProviderManager) startFinalityProviderInstance( - pk *bbntypes.BIP340PubKey, - passphrase string, -) error { - pkHex := pk.MarshalHex() - if fpm.fpIns == nil { - fpIns, err := NewFinalityProviderInstance( - pk, fpm.config, fpm.fps, fpm.pubRandStore, fpm.cc, fpm.em, - fpm.metrics, passphrase, fpm.criticalErrChan, fpm.logger, - ) - if err != nil { - return fmt.Errorf("failed to create finality provider instance %s: %w", pkHex, err) - } - - fpm.fpIns = fpIns - } - - return fpm.fpIns.Start() -} - -func (fpm *FinalityProviderManager) getLatestBlockWithRetry() (*types.BlockInfo, error) { - var ( - latestBlock *types.BlockInfo - err error - ) - - if err := retry.Do(func() error { - latestBlock, err = fpm.cc.QueryBestBlock() - if err != nil { - return err - } - return nil - }, RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) { - fpm.logger.Debug( - "failed to query the consumer chain for the latest block", - zap.Uint("attempt", n+1), - zap.Uint("max_attempts", RtyAttNum), - zap.Error(err), - ) - })); err != nil { - return nil, err - } - - return latestBlock, nil -} diff --git a/finality-provider/service/fp_manager_test.go b/finality-provider/service/fp_manager_test.go deleted file mode 100644 index 787586a1..00000000 --- a/finality-provider/service/fp_manager_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package service_test - -import ( - "math/rand" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/babylonlabs-io/babylon/testutil/datagen" - bbntypes "github.com/babylonlabs-io/babylon/types" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" - "go.uber.org/zap" - - "github.com/babylonlabs-io/finality-provider/clientcontroller" - "github.com/babylonlabs-io/finality-provider/eotsmanager" - eotscfg "github.com/babylonlabs-io/finality-provider/eotsmanager/config" - fpcfg "github.com/babylonlabs-io/finality-provider/finality-provider/config" - "github.com/babylonlabs-io/finality-provider/finality-provider/proto" - "github.com/babylonlabs-io/finality-provider/finality-provider/service" - fpstore "github.com/babylonlabs-io/finality-provider/finality-provider/store" - "github.com/babylonlabs-io/finality-provider/keyring" - "github.com/babylonlabs-io/finality-provider/metrics" - "github.com/babylonlabs-io/finality-provider/testutil" - "github.com/babylonlabs-io/finality-provider/testutil/mocks" - "github.com/babylonlabs-io/finality-provider/types" - "github.com/babylonlabs-io/finality-provider/util" -) - -var ( - eventuallyWaitTimeOut = 5 * time.Second - eventuallyPollTime = 10 * time.Millisecond -) - -func FuzzStatusUpdate(f *testing.F) { - testutil.AddRandomSeedsToFuzzer(f, 10) - f.Fuzz(func(t *testing.T, seed int64) { - r := rand.New(rand.NewSource(seed)) - - ctl := gomock.NewController(t) - mockClientController := mocks.NewMockClientController(ctl) - vm, fpPk, cleanUp := newFinalityProviderManagerWithRegisteredFp(t, r, mockClientController) - defer cleanUp() - - // setup mocks - currentHeight := uint64(r.Int63n(100) + 1) - currentBlockRes := &types.BlockInfo{ - Height: currentHeight, - Hash: datagen.GenRandomByteArray(r, 32), - } - mockClientController.EXPECT().QueryBestBlock().Return(currentBlockRes, nil).AnyTimes() - mockClientController.EXPECT().Close().Return(nil).AnyTimes() - mockClientController.EXPECT().QueryLatestFinalizedBlocks(gomock.Any()).Return(nil, nil).AnyTimes() - mockClientController.EXPECT().QueryBestBlock().Return(currentBlockRes, nil).AnyTimes() - mockClientController.EXPECT().QueryActivatedHeight().Return(uint64(1), nil).AnyTimes() - mockClientController.EXPECT().QueryFinalityActivationBlockHeight().Return(uint64(0), nil).AnyTimes() - mockClientController.EXPECT().QueryFinalityProviderHighestVotedHeight(gomock.Any()).Return(uint64(0), nil).AnyTimes() - mockClientController.EXPECT().QueryBlock(gomock.Any()).Return(currentBlockRes, nil).AnyTimes() - mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).AnyTimes() - - votingPower := uint64(r.Intn(2)) - mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), currentHeight).Return(votingPower, 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() - } - } - - err := vm.StartFinalityProvider(fpPk, passphrase) - require.NoError(t, err) - fpIns, err := vm.GetFinalityProviderInstance() - require.NoError(t, err) - // stop the finality-provider as we are testing static functionalities - err = fpIns.Stop() - 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) - } - } - }) -} - -func waitForStatus(t *testing.T, fpIns *service.FinalityProviderInstance, s proto.FinalityProviderStatus) { - require.Eventually(t, - func() bool { - return fpIns.GetStatus() == s - }, eventuallyWaitTimeOut, eventuallyPollTime) -} - -func newFinalityProviderManagerWithRegisteredFp(t *testing.T, r *rand.Rand, cc clientcontroller.ClientController) (*service.FinalityProviderManager, *bbntypes.BIP340PubKey, func()) { - logger := zap.NewNop() - // create an EOTS manager - eotsHomeDir := filepath.Join(t.TempDir(), "eots-home") - eotsCfg := eotscfg.DefaultConfigWithHomePath(eotsHomeDir) - eotsdb, err := eotsCfg.DatabaseConfig.GetDBBackend() - require.NoError(t, err) - em, err := eotsmanager.NewLocalEOTSManager(eotsHomeDir, eotsCfg.KeyringBackend, eotsdb, logger) - require.NoError(t, err) - - // create finality-provider app with randomized config - fpHomeDir := filepath.Join(t.TempDir(), "fp-home") - fpCfg := fpcfg.DefaultConfigWithHome(fpHomeDir) - fpCfg.StatusUpdateInterval = 10 * time.Millisecond - input := strings.NewReader("") - kr, err := keyring.CreateKeyring( - fpCfg.BabylonConfig.KeyDirectory, - fpCfg.BabylonConfig.ChainID, - fpCfg.BabylonConfig.KeyringBackend, - input, - ) - require.NoError(t, err) - err = util.MakeDirectory(fpcfg.DataDir(fpHomeDir)) - require.NoError(t, err) - db, err := fpCfg.DatabaseConfig.GetDBBackend() - require.NoError(t, err) - fpStore, err := fpstore.NewFinalityProviderStore(db) - require.NoError(t, err) - pubRandStore, err := fpstore.NewPubRandProofStore(db) - require.NoError(t, err) - - metricsCollectors := metrics.NewFpMetrics() - vm, err := service.NewFinalityProviderManager(fpStore, pubRandStore, &fpCfg, cc, em, metricsCollectors, logger) - require.NoError(t, err) - - // create registered finality-provider - keyName := datagen.GenRandomHexStr(r, 10) - chainID := datagen.GenRandomHexStr(r, 10) - kc, err := keyring.NewChainKeyringControllerWithKeyring(kr, keyName, input) - require.NoError(t, err) - btcPkBytes, err := em.CreateKey(keyName, passphrase, hdPath) - require.NoError(t, err) - btcPk, err := bbntypes.NewBIP340PubKey(btcPkBytes) - require.NoError(t, err) - keyInfo, err := kc.CreateChainKey(passphrase, hdPath, "") - require.NoError(t, err) - fpAddr := keyInfo.AccAddress - fpRecord, err := em.KeyRecord(btcPk.MustMarshal(), passphrase) - require.NoError(t, err) - pop, err := kc.CreatePop(fpAddr, fpRecord.PrivKey) - require.NoError(t, err) - - err = fpStore.CreateFinalityProvider( - fpAddr, - btcPk.MustToBTCPK(), - testutil.RandomDescription(r), - testutil.ZeroCommissionRate(), - keyName, - chainID, - pop.BtcSig, - ) - require.NoError(t, err) - err = fpStore.SetFpStatus(btcPk.MustToBTCPK(), proto.FinalityProviderStatus_REGISTERED) - require.NoError(t, err) - - cleanUp := func() { - err = vm.Stop() - require.NoError(t, err) - err = eotsdb.Close() - require.NoError(t, err) - err = db.Close() - require.NoError(t, err) - err = os.RemoveAll(eotsHomeDir) - require.NoError(t, err) - err = os.RemoveAll(fpHomeDir) - require.NoError(t, err) - } - - return vm, btcPk, cleanUp -} diff --git a/finality-provider/service/rpcserver.go b/finality-provider/service/rpcserver.go index cfae0152..a956e499 100644 --- a/finality-provider/service/rpcserver.go +++ b/finality-provider/service/rpcserver.go @@ -128,7 +128,7 @@ func (r *rpcServer) RegisterFinalityProvider(_ context.Context, req *proto.Regis } // the finality-provider instance should be started right after registration - if err := r.app.StartHandlingFinalityProvider(txRes.btcPubKey, req.Passphrase); err != nil { + if err := r.app.StartFinalityProvider(txRes.btcPubKey, req.Passphrase); err != nil { return nil, fmt.Errorf("failed to start the registered finality-provider %s: %w", txRes.bbnAddress.String(), err) } @@ -223,7 +223,7 @@ func (r *rpcServer) UnjailFinalityProvider(_ context.Context, req *proto.UnjailF } // todo: keep passphrase as empty for now - if err := r.app.StartHandlingFinalityProvider(fpPk, ""); err != nil { + if err := r.app.StartFinalityProvider(fpPk, ""); err != nil { return nil, fmt.Errorf("failed to start the finality provider instance after unjailing: %w", err) } diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 06ef609f..4ac22810 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -12,9 +12,7 @@ import ( "github.com/babylonlabs-io/babylon/testutil/datagen" "github.com/btcsuite/btcd/btcec/v2" "github.com/stretchr/testify/require" - "go.uber.org/zap" - "github.com/babylonlabs-io/finality-provider/clientcontroller" "github.com/babylonlabs-io/finality-provider/finality-provider/cmd/fpd/daemon" "github.com/babylonlabs-io/finality-provider/types" ) @@ -161,12 +159,6 @@ func TestFinalityProviderEditCmd(t *testing.T) { tm, fpIns := StartManagerWithFinalityProvider(t) defer tm.Stop(t) - cfg := tm.Fpa.GetConfig() - cfg.BabylonConfig.Key = testFpName - cc, err := clientcontroller.NewClientController(cfg.ChainType, cfg.BabylonConfig, &cfg.BTCNetParams, zap.NewNop()) - require.NoError(t, err) - tm.Fpa.UpdateClientController(cc) - cmd := daemon.CommandEditFinalityDescription() const ( @@ -200,7 +192,7 @@ func TestFinalityProviderEditCmd(t *testing.T) { cmd.SetArgs(args) // Run the command - err = cmd.Execute() + err := cmd.Execute() require.NoError(t, err) gotFp, err := tm.BBNClient.QueryFinalityProvider(fpIns.GetBtcPk()) diff --git a/itest/test_manager.go b/itest/test_manager.go index a3211d4c..513cda0d 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -36,8 +36,6 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" - "github.com/babylonlabs-io/finality-provider/clientcontroller" - fpcc "github.com/babylonlabs-io/finality-provider/clientcontroller" "github.com/babylonlabs-io/finality-provider/eotsmanager/client" eotsconfig "github.com/babylonlabs-io/finality-provider/eotsmanager/config" @@ -47,11 +45,10 @@ import ( ) var ( - eventuallyWaitTimeOut = 1 * time.Minute + eventuallyWaitTimeOut = 5 * time.Minute eventuallyPollTime = 500 * time.Millisecond btcNetworkParams = &chaincfg.SimNetParams - testFpName = "test-fp" testMoniker = "test-moniker" testChainID = "chain-test" passphrase = "testpass" @@ -193,40 +190,23 @@ func StartManagerWithFinalityProvider(t *testing.T) (*TestManager, *service.Fina tm := StartManager(t) app := tm.Fpa cfg := app.GetConfig() - oldKey := cfg.BabylonConfig.Key commission := sdkmath.LegacyZeroDec() desc := newDescription(testMoniker) - // needs to update key in config to be able to register and sign the creation of the finality provider with the - // same address. - cfg.BabylonConfig.Key = testFpName - fpBbnKeyInfo, err := service.CreateChainKey(cfg.BabylonConfig.KeyDirectory, cfg.BabylonConfig.ChainID, cfg.BabylonConfig.Key, cfg.BabylonConfig.KeyringBackend, passphrase, hdPath, "") - require.NoError(t, err) - - cc, err := clientcontroller.NewClientController(cfg.ChainType, cfg.BabylonConfig, &cfg.BTCNetParams, zap.NewNop()) - - require.NoError(t, err) - app.UpdateClientController(cc) - - // add some funds for new fp pay for fees '-' - _, _, err = tm.manager.BabylondTxBankSend(t, fpBbnKeyInfo.AccAddress.String(), "1000000ubbn", "node0") - require.NoError(t, err) - eotsKeyName := "eots-key" - require.NoError(t, err) eotsPkBz, err := tm.EOTSClient.CreateKey(eotsKeyName, passphrase, hdPath) require.NoError(t, err) eotsPk, err := bbntypes.NewBIP340PubKey(eotsPkBz) require.NoError(t, err) - res, err := app.CreateFinalityProvider(testFpName, testChainID, passphrase, hdPath, eotsPk, desc, &commission) + res, err := app.CreateFinalityProvider(cfg.BabylonConfig.Key, testChainID, passphrase, hdPath, eotsPk, desc, &commission) require.NoError(t, err) fpPk, err := bbntypes.NewBIP340PubKeyFromHex(res.FpInfo.BtcPkHex) require.NoError(t, err) _, err = app.RegisterFinalityProvider(fpPk.MarshalHex()) require.NoError(t, err) - err = app.StartHandlingFinalityProvider(fpPk, passphrase) + err = app.StartFinalityProvider(fpPk, passphrase) require.NoError(t, err) fpIns, err := app.GetFinalityProviderInstance() require.NoError(t, err) @@ -258,12 +238,6 @@ func StartManagerWithFinalityProvider(t *testing.T) (*TestManager, *service.Fina return true }, eventuallyWaitTimeOut, eventuallyPollTime) - // goes back to old key in app - cfg.BabylonConfig.Key = oldKey - cc, err = clientcontroller.NewClientController(cfg.ChainType, cfg.BabylonConfig, &cfg.BTCNetParams, zap.NewNop()) - require.NoError(t, err) - app.UpdateClientController(cc) - t.Logf("the test manager is running with a finality provider") return tm, fpIns diff --git a/testutil/datagen.go b/testutil/datagen.go index 28a7a758..c27f0cd6 100644 --- a/testutil/datagen.go +++ b/testutil/datagen.go @@ -12,6 +12,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/babylonlabs-io/finality-provider/finality-provider/store" + fpkr "github.com/babylonlabs-io/finality-provider/keyring" sdkmath "cosmossdk.io/math" "github.com/babylonlabs-io/babylon/testutil/datagen" @@ -110,7 +111,7 @@ func GenStoredFinalityProvider(r *rand.Rand, t *testing.T, app *service.Finality chainID := GenRandomHexStr(r, 4) cfg := app.GetConfig() - _, err := service.CreateChainKey(cfg.BabylonConfig.KeyDirectory, cfg.BabylonConfig.ChainID, keyName, keyring.BackendTest, passphrase, hdPath, "") + _, err := CreateChainKey(cfg.BabylonConfig.KeyDirectory, cfg.BabylonConfig.ChainID, keyName, keyring.BackendTest, passphrase, hdPath, "") require.NoError(t, err) res, err := app.CreateFinalityProvider(keyName, chainID, passphrase, hdPath, eotsPk, RandomDescription(r), ZeroCommissionRate()) @@ -124,6 +125,26 @@ func GenStoredFinalityProvider(r *rand.Rand, t *testing.T, app *service.Finality return storedFp } +func CreateChainKey(keyringDir, chainID, keyName, backend, passphrase, hdPath, mnemonic string) (*types.ChainKeyInfo, error) { + sdkCtx, err := fpkr.CreateClientCtx( + keyringDir, chainID, + ) + if err != nil { + return nil, err + } + + krController, err := fpkr.NewChainKeyringController( + sdkCtx, + keyName, + backend, + ) + if err != nil { + return nil, err + } + + return krController.CreateChainKey(passphrase, hdPath, mnemonic) +} + func GenSdkContext(r *rand.Rand, t *testing.T) client.Context { chainID := "testchain-" + GenRandomHexStr(r, 4) dir := t.TempDir() diff --git a/testutil/utils.go b/testutil/utils.go index cd138868..841e0c8f 100644 --- a/testutil/utils.go +++ b/testutil/utils.go @@ -22,7 +22,7 @@ func PrepareMockedClientController(t *testing.T, r *rand.Rand, startHeight, curr ctl := gomock.NewController(t) mockClientController := mocks.NewMockClientController(ctl) - for i := startHeight + 1; i <= currentHeight; i++ { + for i := startHeight; i <= currentHeight; i++ { resBlock := &types.BlockInfo{ Height: currentHeight, Hash: GenRandomByteArray(r, 32),