Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: keep sync fp status #52

Merged
merged 15 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
RafilxTenfen marked this conversation as resolved.
Show resolved Hide resolved

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) {
RafilxTenfen marked this conversation as resolved.
Show resolved Hide resolved
// 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() {
RafilxTenfen marked this conversation as resolved.
Show resolved Hide resolved
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
}
Loading