diff --git a/clientcontroller/babylon.go b/clientcontroller/babylon.go index eb2fa382..5468994a 100644 --- a/clientcontroller/babylon.go +++ b/clientcontroller/babylon.go @@ -47,10 +47,6 @@ func NewBabylonController( bbnConfig := fpcfg.BBNConfigToBabylonConfig(cfg) - if err := bbnConfig.Validate(); err != nil { - return nil, fmt.Errorf("invalid config for Babylon client: %w", err) - } - bc, err := bbnclient.New( &bbnConfig, logger, @@ -59,6 +55,12 @@ func NewBabylonController( return nil, fmt.Errorf("failed to create Babylon client: %w", err) } + // makes sure that the key in config really exists and it is a valid bech 32 addr + // to allow using mustGetTxSigner + if _, err := bc.GetAddr(); err != nil { + return nil, err + } + return &BabylonController{ bc, cfg, @@ -274,7 +276,7 @@ func (bc *BabylonController) QueryFinalityProviderVotingPower(fpPk *btcec.Public blockHeight, ) if err != nil { - return 0, fmt.Errorf("failed to query BTC delegations: %w", err) + return 0, fmt.Errorf("failed to query Finality Voting Power at Height %d: %w", blockHeight, err) } return res.VotingPower, nil diff --git a/finality-provider/cmd/fpd/daemon/start.go b/finality-provider/cmd/fpd/daemon/start.go index 9a0593cd..14b4dc24 100644 --- a/finality-provider/cmd/fpd/daemon/start.go +++ b/finality-provider/cmd/fpd/daemon/start.go @@ -111,11 +111,6 @@ func loadApp( return nil, fmt.Errorf("failed to create finality-provider app: %v", err) } - // sync finality-provider status - if err := fpApp.SyncFinalityProviderStatus(); err != nil { - return nil, fmt.Errorf("failed to sync finality-provider status: %w", err) - } - return fpApp, nil } diff --git a/finality-provider/config/config.go b/finality-provider/config/config.go index 12c17ed1..d8a8b423 100644 --- a/finality-provider/config/config.go +++ b/finality-provider/config/config.go @@ -32,6 +32,7 @@ const ( defaultRandomInterval = 30 * time.Second defaultSubmitRetryInterval = 1 * time.Second defaultFastSyncInterval = 10 * time.Second + defaultSyncFpStatusInterval = 30 * time.Second defaultFastSyncLimit = 10 defaultFastSyncGap = 3 defaultMaxSubmissionRetries = 20 @@ -69,7 +70,7 @@ type Config struct { 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"` MaxNumFinalityProviders uint32 `long:"maxnumfinalityproviders" description:"The maximum number of finality-provider instances running concurrently within the daemon"` - MinutesToWaitForConsumer uint32 `long:"minuteswaitforconsumer" description:"The number of minutes it should wait for the consumer chain to be available, before stop the app"` + SyncFpStatusInterval time.Duration `long:"syncfpstatusinterval" description:"The duration of time that it should sync FP status with the client blockchain"` BitcoinNetwork string `long:"bitcoinnetwork" description:"Bitcoin network to run on" choise:"mainnet" choice:"regtest" choice:"testnet" choice:"simnet" choice:"signet"` @@ -113,7 +114,7 @@ func DefaultConfigWithHome(homePath string) Config { RpcListener: DefaultRpcListener, MaxNumFinalityProviders: defaultMaxNumFinalityProviders, Metrics: metrics.DefaultFpConfig(), - MinutesToWaitForConsumer: 60 * 24 * 7, // one week in minutes + SyncFpStatusInterval: defaultSyncFpStatusInterval, } if err := cfg.Validate(); err != nil { diff --git a/finality-provider/service/app.go b/finality-provider/service/app.go index 8373b07c..39ea9d96 100644 --- a/finality-provider/service/app.go +++ b/finality-provider/service/app.go @@ -7,7 +7,6 @@ import ( "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" @@ -72,7 +71,6 @@ func NewFinalityProviderAppFromConfig( } logger.Info("successfully connected to a remote EOTS manager", zap.String("address", cfg.EOTSManagerAddress)) - return NewFinalityProviderApp(cfg, cc, em, db, logger) } @@ -148,6 +146,11 @@ func (app *FinalityProviderApp) GetInput() *strings.Reader { return app.input } +// Logger returns the current logger of FP app. +func (app *FinalityProviderApp) Logger() *zap.Logger { + return app.logger +} + func (app *FinalityProviderApp) ListFinalityProviderInstances() []*FinalityProviderInstance { return app.fpManager.ListFinalityProviderInstances() } @@ -237,73 +240,68 @@ func (app *FinalityProviderApp) getFpPrivKey(fpPk []byte) (*btcec.PrivateKey, er return record.PrivKey, nil } -// SyncFinalityProviderStatus syncs the status of the finality-providers -func (app *FinalityProviderApp) SyncFinalityProviderStatus() error { - var ( - latestBlock *types.BlockInfo - err error - ) - - attempts := uint(app.config.MinutesToWaitForConsumer) - err = retry.Do(func() error { - latestBlock, err = app.cc.QueryBestBlock() - if err != nil { - return err - } - return nil - }, 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", attempts), - zap.Error(err), - ) - // waits for consumer chain to become online for one week - // usefull to turn fpd on before the consumer chain node is available - // and waiting for the consumer node to become available to start. - }), retry.Attempts(attempts), retry.Delay(time.Minute), RtyErr) +// SyncFinalityProviderStatus syncs the status of the finality-providers with the chain. +func (app *FinalityProviderApp) SyncFinalityProviderStatus() (fpInstanceRunning bool, err error) { + latestBlock, err := app.cc.QueryBestBlock() if err != nil { - return err + return false, err } fps, err := app.fps.GetAllStoredFinalityProviders() if err != nil { - return err + return false, err } for _, fp := range fps { vp, err := app.cc.QueryFinalityProviderVotingPower(fp.BtcPk, latestBlock.Height) if err != nil { - // if error occured then the finality-provider is not registered in the Babylon chain yet + // if ther error is that there is nothing in the voting power table + // it should continue and consider the voting power + // as zero to start the finality provider and send public randomness + allowedErr := fmt.Sprintf("failed to query Finality Voting Power at Height %d: rpc error: code = Unknown desc = %s: unknown request", latestBlock.Height, bstypes.ErrVotingPowerTableNotUpdated.Wrapf("height: %d", latestBlock.Height).Error()) + if !strings.EqualFold(err.Error(), allowedErr) { + // if some other error occured then the finality-provider is not registered in the Babylon chain yet + continue + } + } + + if !fp.ShouldSyncStatusFromVotingPower(vp) { continue } - if vp > 0 { - // voting power > 0 then set the status to ACTIVE - err = app.fps.SetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_ACTIVE) - if err != nil { - return err - } - } else if vp == 0 { - // voting power == 0 then set status depending on previous status - switch fp.Status { - case proto.FinalityProviderStatus_CREATED: - // previous status is CREATED then set to REGISTERED - err = app.fps.SetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_REGISTERED) - if err != nil { - return err - } - case proto.FinalityProviderStatus_ACTIVE: - // previous status is ACTIVE then set to INACTIVE - err = app.fps.SetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_INACTIVE) - if err != nil { - return err - } - } + bip340PubKey := fp.GetBIP340BTCPK() + if app.fpManager.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 + } + + oldStatus := fp.Status + newStatus, err := app.fps.UpdateFpStatusFromVotingPower(vp, fp) + if err != nil { + return false, err + } + + app.logger.Info( + "Update FP status", + zap.String("fp_addr", fp.FPAddr), + zap.String("old_status", oldStatus.String()), + zap.String("new_status", newStatus.String()), + ) + fp.Status = newStatus + + if !fp.ShouldStart() { + continue } + + if err := app.fpManager.StartFinalityProvider(bip340PubKey, ""); err != nil { + return false, err + } + fpInstanceRunning = true } - return nil + return fpInstanceRunning, nil } // Start starts only the finality-provider daemon without any finality-provider instances @@ -312,7 +310,8 @@ func (app *FinalityProviderApp) Start() error { app.startOnce.Do(func() { app.logger.Info("Starting FinalityProviderApp") - app.wg.Add(3) + app.wg.Add(4) + go app.syncChainFpStatusLoop() go app.eventLoop() go app.registrationLoop() go app.metricsUpdateLoop() @@ -682,3 +681,38 @@ 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) + + 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: + syncFpStatusTicker.Stop() + app.logger.Info("exiting sync FP status loop") + return + } + } +} diff --git a/finality-provider/service/chain_poller_test.go b/finality-provider/service/chain_poller_test.go index 23d6158d..f3ddcdbf 100644 --- a/finality-provider/service/chain_poller_test.go +++ b/finality-provider/service/chain_poller_test.go @@ -72,6 +72,7 @@ func FuzzChainPoller_Start(f *testing.F) { // FuzzChainPoller_SkipHeight tests the functionality of SkipHeight func FuzzChainPoller_SkipHeight(f *testing.F) { testutil.AddRandomSeedsToFuzzer(f, 10) + f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) diff --git a/finality-provider/service/fp_manager.go b/finality-provider/service/fp_manager.go index 499daaae..f5d5f18b 100644 --- a/finality-provider/service/fp_manager.go +++ b/finality-provider/service/fp_manager.go @@ -35,6 +35,7 @@ func (ce *CriticalError) Error() string { type FinalityProviderManager struct { isStarted *atomic.Bool + // mutex to acess map of fp instances (fpis) mu sync.Mutex wg sync.WaitGroup @@ -246,13 +247,16 @@ func (fpm *FinalityProviderManager) StartAll() error { } for _, fp := range storedFps { - if fp.Status == proto.FinalityProviderStatus_CREATED || fp.Status == proto.FinalityProviderStatus_SLASHED { - fpm.logger.Info("the finality provider cannot be started with status", - zap.String("eots-pk", fp.GetBIP340BTCPK().MarshalHex()), - zap.String("status", fp.Status.String())) + fpBtcPk := fp.GetBIP340BTCPK() + if !fp.ShouldStart() { + fpm.logger.Info( + "the finality provider cannot be started with status", + zap.String("eots-pk", fpBtcPk.MarshalHex()), + zap.String("status", fp.Status.String()), + ) continue } - if err := fpm.StartFinalityProvider(fp.GetBIP340BTCPK(), ""); err != nil { + if err := fpm.StartFinalityProvider(fpBtcPk, ""); err != nil { return err } } @@ -266,7 +270,6 @@ func (fpm *FinalityProviderManager) Stop() error { } var stopErr error - for _, fpi := range fpm.fpis { if !fpi.IsRunning() { continue diff --git a/finality-provider/store/fpstore.go b/finality-provider/store/fpstore.go index 3afb4252..58c9f0dc 100644 --- a/finality-provider/store/fpstore.go +++ b/finality-provider/store/fpstore.go @@ -112,6 +112,29 @@ func (s *FinalityProviderStore) SetFpStatus(btcPk *btcec.PublicKey, status proto return s.setFinalityProviderState(btcPk, setFpStatus) } +// 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( + vp uint64, + fp *StoredFinalityProvider, +) (proto.FinalityProviderStatus, error) { + if vp > 0 { + // voting power > 0 then set the status to ACTIVE + return proto.FinalityProviderStatus_ACTIVE, s.SetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_ACTIVE) + } + + // voting power == 0 then set status depending on previous status + switch fp.Status { + case proto.FinalityProviderStatus_CREATED: + // previous status is CREATED then set to REGISTERED + return proto.FinalityProviderStatus_REGISTERED, s.SetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_REGISTERED) + case proto.FinalityProviderStatus_ACTIVE: + // previous status is ACTIVE then set to INACTIVE + return proto.FinalityProviderStatus_INACTIVE, s.SetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_INACTIVE) + } + return fp.Status, nil +} + // SetFpLastVotedHeight sets the last voted height to the stored last voted height and last processed height // only if it is larger than the stored one. This is to ensure the stored state to increase monotonically func (s *FinalityProviderStore) SetFpLastVotedHeight(btcPk *btcec.PublicKey, lastVotedHeight uint64) error { diff --git a/finality-provider/store/storedfp.go b/finality-provider/store/storedfp.go index 25b74e10..08a86402 100644 --- a/finality-provider/store/storedfp.go +++ b/finality-provider/store/storedfp.go @@ -77,3 +77,30 @@ func (sfp *StoredFinalityProvider) ToFinalityProviderInfo() *proto.FinalityProvi Status: sfp.Status.String(), } } + +// ShouldSyncStatusFromVotingPower returns true if the status should be updated +// based on the provided voting power or the current status of the finality provider. +// +// It returns true if the voting power is greater than zero, or if the status +// is either 'CREATED' or 'ACTIVE'. +func (sfp *StoredFinalityProvider) ShouldSyncStatusFromVotingPower(vp uint64) bool { + if vp > 0 { + return true + } + + return sfp.Status == proto.FinalityProviderStatus_CREATED || + sfp.Status == proto.FinalityProviderStatus_ACTIVE +} + +// ShouldStart returns true if the finality provider should start his instance +// based on the current status of the finality provider. +// +// It returns false if the status is either 'CREATED' or 'SLASHED'. +// It returs true for all the other status. +func (sfp *StoredFinalityProvider) ShouldStart() bool { + if sfp.Status == proto.FinalityProviderStatus_CREATED || sfp.Status == proto.FinalityProviderStatus_SLASHED { + return false + } + + return true +}