Skip to content

Commit

Permalink
chore: keep sync fp status (#52)
Browse files Browse the repository at this point in the history
- Removed previous `MinutesToWaitForConsumer` added in #50 since it was
not good enough to wait for the chain to be up, without starting the fpd
server to receive fpd creation
- Add new loop that verifies the FP status on chain and update it
accordingly (before it was only one time at startup)
- Add check for keyring key is valid at start of `NewBabylonController`
to avoid panic at `mustGetTxSigner`
  • Loading branch information
RafilxTenfen authored Sep 18, 2024
1 parent d06cde5 commit 7a5db87
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 72 deletions.
12 changes: 7 additions & 5 deletions clientcontroller/babylon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
5 changes: 0 additions & 5 deletions finality-provider/cmd/fpd/daemon/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
5 changes: 3 additions & 2 deletions finality-provider/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`

Expand Down Expand Up @@ -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 {
Expand Down
142 changes: 88 additions & 54 deletions finality-provider/service/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
}
}
}
1 change: 1 addition & 0 deletions finality-provider/service/chain_poller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
15 changes: 9 additions & 6 deletions finality-provider/service/fp_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
}
}
Expand All @@ -266,7 +270,6 @@ func (fpm *FinalityProviderManager) Stop() error {
}

var stopErr error

for _, fpi := range fpm.fpis {
if !fpi.IsRunning() {
continue
Expand Down
23 changes: 23 additions & 0 deletions finality-provider/store/fpstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions finality-provider/store/storedfp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 7a5db87

Please sign in to comment.