diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd83069..df00a257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * [#221](https://github.com/babylonlabs-io/finality-provider/pull/221) Cleanup TODOs * [#228](https://github.com/babylonlabs-io/finality-provider/pull/228) Save key name mapping in eotsd import commands * [#227](https://github.com/babylonlabs-io/finality-provider/pull/227) Fix FP submission loop +* [#226](https://github.com/babylonlabs-io/finality-provider/pull/226) Update local fp before register ## v0.13.1 @@ -120,7 +121,7 @@ finality vote submission * [#117](https://github.com/babylonlabs-io/finality-provider/pull/117) Spec of commit public randomness -* [#130](https://github.com/babylonlabs-io/finality-provider/pull/130) Finality +* [#130](https://github.com/babylonlabs-io/finality-provider/pull/130) Finality Provider operation documentation ### Bug Fixes diff --git a/clientcontroller/interface.go b/clientcontroller/interface.go index 427b9b09..8aaf29d9 100644 --- a/clientcontroller/interface.go +++ b/clientcontroller/interface.go @@ -51,6 +51,9 @@ type ClientController interface { The following methods are queries to the consumer chain */ + // QueryFinalityProvider queries the finality provider by pk + QueryFinalityProvider(fpPk *btcec.PublicKey) (*btcstakingtypes.QueryFinalityProviderResponse, error) + // QueryFinalityProviderVotingPower queries the voting power of the finality provider at a given height QueryFinalityProviderVotingPower(fpPk *btcec.PublicKey, blockHeight uint64) (uint64, error) diff --git a/finality-provider/service/app.go b/finality-provider/service/app.go index 84f5cff6..cf49746f 100644 --- a/finality-provider/service/app.go +++ b/finality-provider/service/app.go @@ -1,6 +1,7 @@ package service import ( + "errors" "fmt" "strings" "sync" @@ -349,9 +350,35 @@ func (app *FinalityProviderApp) CreateFinalityProvider( return nil, fmt.Errorf("failed to create proof-of-possession of the finality-provider: %w", err) } - // TODO: query consumer chain to check if the fp is already registered + // Query the consumer chain to check if the fp is already registered // if true, update db with the fp info from the consumer chain // otherwise, proceed registration + resp, err := app.cc.QueryFinalityProvider(eotsPk.MustToBTCPK()) + if err != nil { + if !strings.Contains(err.Error(), "the finality provider is not found") { + return nil, fmt.Errorf("err getting finality provider: %w", err) + } + } + if resp != nil { + app.logger.Info("finality-provider already registered on the consumer chain", + zap.String("eots_pk", resp.FinalityProvider.BtcPk.MarshalHex()), + zap.String("addr", resp.FinalityProvider.Addr), + ) + + if err := app.putFpFromResponse(resp.FinalityProvider, chainID); err != nil { + return nil, err + } + + // get updated fp from db + storedFp, err := app.fps.GetFinalityProvider(eotsPk.MustToBTCPK()) + if err != nil { + return nil, err + } + + return &CreateFinalityProviderResult{ + FpInfo: storedFp.ToFinalityProviderInfo(), + }, nil + } // 3. register the finality provider on the consumer chain request := &CreateFinalityProviderRequest{ @@ -558,3 +585,66 @@ func (app *FinalityProviderApp) getFpPrivKey(fpPk []byte) (*btcec.PrivateKey, er return record.PrivKey, nil } + +// putFpFromResponse creates or updates finality-provider in the local store +func (app *FinalityProviderApp) putFpFromResponse(fp *bstypes.FinalityProviderResponse, chainID string) error { + btcPk := fp.BtcPk.MustToBTCPK() + _, err := app.fps.GetFinalityProvider(btcPk) + if err != nil { + if errors.Is(err, store.ErrFinalityProviderNotFound) { + addr, err := sdk.AccAddressFromBech32(fp.Addr) + if err != nil { + return fmt.Errorf("err converting fp addr: %w", err) + } + if err := app.fps.CreateFinalityProvider(addr, btcPk, fp.Description, fp.Commission, chainID); err != nil { + return fmt.Errorf("failed to save finality-provider: %w", err) + } + + app.logger.Info("finality-provider successfully saved the local db", + zap.String("eots_pk", fp.BtcPk.MarshalHex()), + zap.String("addr", fp.Addr), + ) + + return nil + } + + return err + } + + if err := app.fps.SetFpDescription(btcPk, fp.Description, fp.Commission); err != nil { + return err + } + + if err := app.fps.SetFpLastVotedHeight(btcPk, uint64(fp.HighestVotedHeight)); err != nil { + return err + } + + power, err := app.cc.QueryFinalityProviderVotingPower(btcPk, fp.Height) + if err != nil { + return fmt.Errorf("failed to query voting power for finality provider %s: %w", + fp.BtcPk.MarshalHex(), err) + } + + var status proto.FinalityProviderStatus + switch { + case power > 0: + status = proto.FinalityProviderStatus_ACTIVE + case fp.SlashedBtcHeight > 0: + status = proto.FinalityProviderStatus_SLASHED + case fp.Jailed: + status = proto.FinalityProviderStatus_JAILED + default: + status = proto.FinalityProviderStatus_INACTIVE + } + + if err := app.fps.SetFpStatus(btcPk, status); err != nil { + return fmt.Errorf("failed to update status for finality provider %s: %w", fp.BtcPk.MarshalHex(), err) + } + + app.logger.Info("finality-provider successfully updated the local db", + zap.String("eots_pk", fp.BtcPk.MarshalHex()), + zap.String("addr", fp.Addr), + ) + + return nil +} diff --git a/finality-provider/service/app_test.go b/finality-provider/service/app_test.go index 3990d0d7..15082174 100644 --- a/finality-provider/service/app_test.go +++ b/finality-provider/service/app_test.go @@ -3,6 +3,7 @@ package service_test import ( "errors" "fmt" + btcstakingtypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" "math/rand" "os" "path/filepath" @@ -64,6 +65,7 @@ func FuzzCreateFinalityProvider(f *testing.F) { mockClientController.EXPECT().QueryLatestFinalizedBlocks(gomock.Any()).Return(nil, nil).AnyTimes() mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), gomock.Any()).Return(uint64(0), nil).AnyTimes() + mockClientController.EXPECT().QueryFinalityProvider(gomock.Any()).Return(nil, nil).AnyTimes() // Create randomized config fpHomeDir := filepath.Join(t.TempDir(), "fp-home") @@ -283,6 +285,96 @@ func FuzzStatusUpdate(f *testing.F) { }) } +func FuzzSaveAlreadyRegisteredFinalityProvider(f *testing.F) { + testutil.AddRandomSeedsToFuzzer(f, 10) + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + + logger := zap.NewNop() + // create an EOTS manager + eotsHomeDir := filepath.Join(t.TempDir(), "eots-home") + eotsCfg := eotscfg.DefaultConfigWithHomePath(eotsHomeDir) + dbBackend, err := eotsCfg.DatabaseConfig.GetDBBackend() + require.NoError(t, err) + em, err := eotsmanager.NewLocalEOTSManager(eotsHomeDir, eotsCfg.KeyringBackend, dbBackend, logger) + require.NoError(t, err) + defer func() { + dbBackend.Close() + err = os.RemoveAll(eotsHomeDir) + require.NoError(t, err) + }() + + randomStartingHeight := uint64(r.Int63n(100) + 1) + currentHeight := randomStartingHeight + uint64(r.Int63n(10)+2) + mockClientController := testutil.PrepareMockedClientController(t, r, randomStartingHeight, currentHeight, 0) + rndFp, err := datagen.GenRandomFinalityProvider(r) + require.NoError(t, err) + + // Create randomized config + fpHomeDir := filepath.Join(t.TempDir(), "fp-home") + fpCfg := config.DefaultConfigWithHome(fpHomeDir) + fpCfg.PollerConfig.AutoChainScanningMode = false + fpCfg.PollerConfig.StaticChainScanningStartHeight = randomStartingHeight + fpdb, err := fpCfg.DatabaseConfig.GetDBBackend() + require.NoError(t, err) + + app, err := service.NewFinalityProviderApp(&fpCfg, mockClientController, em, fpdb, logger) + require.NoError(t, err) + + defer func() { + err = fpdb.Close() + require.NoError(t, err) + err = os.RemoveAll(fpHomeDir) + require.NoError(t, err) + }() + + err = app.Start() + require.NoError(t, err) + defer func() { + err = app.Stop() + require.NoError(t, err) + }() + + var eotsPk *bbntypes.BIP340PubKey + eotsKeyName := testutil.GenRandomHexStr(r, 4) + require.NoError(t, err) + eotsPkBz, err := em.CreateKey(eotsKeyName, passphrase, hdPath) + require.NoError(t, err) + eotsPk, err = bbntypes.NewBIP340PubKey(eotsPkBz) + require.NoError(t, err) + + // generate keyring + keyName := testutil.GenRandomHexStr(r, 4) + chainID := testutil.GenRandomHexStr(r, 4) + + cfg := app.GetConfig() + _, err = testutil.CreateChainKey(cfg.BabylonConfig.KeyDirectory, cfg.BabylonConfig.ChainID, keyName, sdkkeyring.BackendTest, passphrase, hdPath, "") + require.NoError(t, err) + + fpRes := &btcstakingtypes.QueryFinalityProviderResponse{FinalityProvider: &btcstakingtypes.FinalityProviderResponse{ + Description: rndFp.Description, + Commission: rndFp.Commission, + Addr: rndFp.Addr, + BtcPk: eotsPk, + Pop: rndFp.Pop, + SlashedBabylonHeight: rndFp.SlashedBabylonHeight, + SlashedBtcHeight: rndFp.SlashedBtcHeight, + Jailed: rndFp.Jailed, + HighestVotedHeight: rndFp.HighestVotedHeight, + }} + + mockClientController.EXPECT().QueryFinalityProvider(gomock.Any()).Return(fpRes, nil).AnyTimes() + + res, err := app.CreateFinalityProvider(keyName, chainID, passphrase, eotsPk, testutil.RandomDescription(r), testutil.ZeroCommissionRate()) + require.NoError(t, err) + require.Equal(t, res.FpInfo.BtcPkHex, eotsPk.MarshalHex()) + + fpInfo, err := app.GetFinalityProviderInfo(eotsPk) + require.NoError(t, err) + require.Equal(t, eotsPk.MarshalHex(), fpInfo.BtcPkHex) + }) +} + func waitForStatus(t *testing.T, fpIns *service.FinalityProviderInstance, s proto.FinalityProviderStatus) { require.Eventually(t, func() bool { diff --git a/testutil/mocks/babylon.go b/testutil/mocks/babylon.go index 4dddee18..404463a7 100644 --- a/testutil/mocks/babylon.go +++ b/testutil/mocks/babylon.go @@ -158,6 +158,21 @@ func (mr *MockClientControllerMockRecorder) QueryFinalityActivationBlockHeight() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryFinalityActivationBlockHeight", reflect.TypeOf((*MockClientController)(nil).QueryFinalityActivationBlockHeight)) } +// QueryFinalityProvider mocks base method. +func (m *MockClientController) QueryFinalityProvider(fpPk *btcec.PublicKey) (*types.QueryFinalityProviderResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryFinalityProvider", fpPk) + ret0, _ := ret[0].(*types.QueryFinalityProviderResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryFinalityProvider indicates an expected call of QueryFinalityProvider. +func (mr *MockClientControllerMockRecorder) QueryFinalityProvider(fpPk interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryFinalityProvider", reflect.TypeOf((*MockClientController)(nil).QueryFinalityProvider), fpPk) +} + // QueryFinalityProviderHighestVotedHeight mocks base method. func (m *MockClientController) QueryFinalityProviderHighestVotedHeight(fpPk *btcec.PublicKey) (uint64, error) { m.ctrl.T.Helper()