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(adr-35): Refactor determining start height #176

Merged
merged 6 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ empty HD path to derive new key and use master private key.
* [#154](https://github.com/babylonlabs-io/finality-provider/pull/154) Use sign schnorr instead of getting private key from EOTS manager
* [#167](https://github.com/babylonlabs-io/finality-provider/pull/167) Remove last processed height
* [#168](https://github.com/babylonlabs-io/finality-provider/pull/168) Remove key creation in `create-finality-provider`
* [#176](https://github.com/babylonlabs-io/finality-provider/pull/176) Refactor
determining start height based on [ADR-35](https://github.com/babylonlabs-io/pm/blob/main/adr/adr-035-slashing-protection.md)

### v0.12.1

Expand Down
11 changes: 11 additions & 0 deletions clientcontroller/babylon.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,17 @@ func (bc *BabylonController) QueryFinalityProviderVotingPower(fpPk *btcec.Public
return res.VotingPower, nil
}

// QueryFinalityProviderHighestVotedHeight queries the highest voted height of the given finality provider
func (bc *BabylonController) QueryFinalityProviderHighestVotedHeight(fpPk *btcec.PublicKey) (uint64, error) {
fpPubKey := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk)
res, err := bc.bbnClient.QueryClient.FinalityProvider(fpPubKey.MarshalHex())
if err != nil {
return 0, fmt.Errorf("failed to query highest voted height for finality provider %s: %w", fpPubKey.MarshalHex(), err)
}

return uint64(res.FinalityProvider.HighestVotedHeight), nil
}

func (bc *BabylonController) QueryLatestFinalizedBlocks(count uint64) ([]*types.BlockInfo, error) {
return bc.queryLatestBlocks(nil, count, finalitytypes.QueriedBlockStatus_FINALIZED, true)
}
Expand Down
11 changes: 9 additions & 2 deletions clientcontroller/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type ClientController interface {
description []byte,
) (*types.TxResponse, error)

// EditFinalityProvider edits description and commission of a finality provider
EditFinalityProvider(fpPk *btcec.PublicKey, commission *math.LegacyDec, description []byte) (*btcstakingtypes.MsgEditFinalityProvider, error)

// CommitPubRandList commits a list of EOTS public randomness the consumer chain
// it returns tx hash and error
CommitPubRandList(fpPk *btcec.PublicKey, startHeight uint64, numPubRand uint64, commitment []byte, sig *schnorr.Signature) (*types.TxResponse, error)
Expand All @@ -44,14 +47,18 @@ type ClientController interface {
// UnjailFinalityProvider sends an unjail transaction to the consumer chain
UnjailFinalityProvider(fpPk *btcec.PublicKey) (*types.TxResponse, error)

/*
The following methods are queries to the consumer chain
*/

// QueryFinalityProviderVotingPower queries the voting power of the finality provider at a given height
QueryFinalityProviderVotingPower(fpPk *btcec.PublicKey, blockHeight uint64) (uint64, error)

// QueryFinalityProviderSlashedOrJailed queries if the finality provider is slashed or jailed
QueryFinalityProviderSlashedOrJailed(fpPk *btcec.PublicKey) (slashed bool, jailed bool, err error)

// EditFinalityProvider edits description and commission of a finality provider
EditFinalityProvider(fpPk *btcec.PublicKey, commission *math.LegacyDec, description []byte) (*btcstakingtypes.MsgEditFinalityProvider, error)
// QueryFinalityProviderHighestVotedHeight queries the highest voted height of the given finality provider
QueryFinalityProviderHighestVotedHeight(fpPk *btcec.PublicKey) (uint64, error)

// QueryLatestFinalizedBlocks returns the latest finalized blocks
QueryLatestFinalizedBlocks(count uint64) ([]*types.BlockInfo, error)
Expand Down
2 changes: 2 additions & 0 deletions finality-provider/service/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func FuzzSyncFinalityProviderStatus(f *testing.F) {
mockClientController.EXPECT().QueryActivatedHeight().Return(currentHeight, nil).AnyTimes()
mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), gomock.Any()).Return(uint64(2), nil).AnyTimes()
}
mockClientController.EXPECT().QueryFinalityProviderHighestVotedHeight(gomock.Any()).Return(uint64(0), nil).AnyTimes()

app, err := service.NewFinalityProviderApp(&fpCfg, mockClientController, em, fpdb, logger)
require.NoError(t, err)
Expand Down Expand Up @@ -266,6 +267,7 @@ func FuzzUnjailFinalityProvider(f *testing.F) {
mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), gomock.Any()).Return(uint64(0), nil).AnyTimes()
mockClientController.EXPECT().QueryActivatedHeight().Return(uint64(1), nil).AnyTimes()
mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, false, nil).AnyTimes()
mockClientController.EXPECT().QueryFinalityProviderHighestVotedHeight(gomock.Any()).Return(uint64(0), nil).AnyTimes()

app, err := service.NewFinalityProviderApp(&fpCfg, mockClientController, em, fpdb, logger)
require.NoError(t, err)
Expand Down
128 changes: 92 additions & 36 deletions finality-provider/service/fp_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ type FinalityProviderInstance struct {
criticalErrChan chan<- *CriticalError

isStarted *atomic.Bool
inSync *atomic.Bool
isLagging *atomic.Bool

wg sync.WaitGroup
quit chan struct{}
Expand Down Expand Up @@ -97,8 +95,6 @@ func newFinalityProviderInstanceFromStore(
cfg: cfg,
logger: logger,
isStarted: atomic.NewBool(false),
inSync: atomic.NewBool(false),
isLagging: atomic.NewBool(false),
criticalErrChan: errChan,
passphrase: passphrase,
em: em,
Expand All @@ -118,7 +114,7 @@ func (fp *FinalityProviderInstance) Start() error {

fp.logger.Info("Starting finality-provider instance", zap.String("pk", fp.GetBtcPkHex()))

startHeight, err := fp.getPollerStartingHeight()
startHeight, err := fp.DetermineStartHeight()
if err != nil {
return fmt.Errorf("failed to get the start height: %w", err)
}
Expand Down Expand Up @@ -736,43 +732,75 @@ func (fp *FinalityProviderInstance) TestSubmitFinalitySignatureAndExtractPrivKey
return res, privKey, nil
}

// getPollerStartingHeight gets the starting height of the poller with
// max(lastVotedHeight+1, lastFinalizedHeight+1, params.FinalityActivationHeight)
// this ensures that:
// (1) the fp will not vote for a height lower than params.FinalityActivationHeight
// (2) the fp will not miss for any non-finalized blocks
// (3) the fp will not process any blocks that have been already voted
// Note: if the fp starting from the last finalized height with a gap to the last
// processed height, the fp might miss some rewards due to not sending the votes
// depending on the consumer chain's reward distribution mechanism
// TODO: provide an option to start from the last processed height in case
// the consumer chain distributes rewards for late voters
func (fp *FinalityProviderInstance) getPollerStartingHeight() (uint64, error) {
// DetermineStartHeight determines trting height for block processing by:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// DetermineStartHeight determines trting height for block processing by:
// DetermineStartHeight determines starting height for block processing by:

//
// If AutoChainScanningMode is disabled:
// - Returns StaticChainScanningStartHeight from config
//
// If AutoChainScanningMode is enabled:
// - Gets finalityActivationHeight from chain
// - Gets lastFinalizedHeight from chain
// - Gets lastVotedHeight from local state
// - If fp.GetLastVotedHeight() is 0, sets lastVotedHeight = lastFinalizedHeight
// - Gets highestVotedHeight from chain
// - Sets lastVotedHeight = max(lastVotedHeight, highestVotedHeight)
// - Returns max(finalityActivationHeight, lastVotedHeight + 1)
//
// This ensures that:
// 1. The FP will not vote for heights below the finality activation height
// 2. The FP will resume from its last voting position or the chain's last finalized height
// 3. The FP will not process blocks it has already voted on
//
// Note: Starting from lastFinalizedHeight when there's a gap to the last processed height
// may result in missed rewards, depending on the consumer chain's reward distribution mechanism.
func (fp *FinalityProviderInstance) DetermineStartHeight() (uint64, error) {
// start from a height from config if AutoChainScanningMode is disabled
if !fp.cfg.PollerConfig.AutoChainScanningMode {
fp.logger.Info("using static chain scanning mode",
zap.String("pk", fp.GetBtcPkHex()),
zap.Uint64("start_height", fp.cfg.PollerConfig.StaticChainScanningStartHeight))
return fp.cfg.PollerConfig.StaticChainScanningStartHeight, nil
}

// TODO: query last voted height and update local height
finalityActivationHeight, err := fp.getFinalityActivationHeightWithRetry()
lastFinalizedHeight, err := fp.latestFinalizedHeightWithRetry()
if err != nil {
return 0, fmt.Errorf("failed to get finality activation height: %w", err)
return 0, fmt.Errorf("failed to get the last finalized height: %w", err)
}

// start from finality activation height
startHeight := finalityActivationHeight
// determine an effective lastVotedHeight
var lastVotedHeight uint64
if fp.GetLastVotedHeight() == 0 {
lastVotedHeight = lastFinalizedHeight
} else {
lastVotedHeight = fp.GetLastVotedHeight()
}

latestFinalisedBlocks, err := fp.latestFinalizedBlocksWithRetry(1)
highestVotedHeight, err := fp.highestVotedHeightWithRetry()
if err != nil {
return 0, fmt.Errorf("failed to get the last finalized block: %w", err)
return 0, fmt.Errorf("failed to get the highest voted height: %w", err)
}

// if we have finalized blocks, consider the height after the latest finalized block
if len(latestFinalisedBlocks) > 0 {
startHeight = max(startHeight, latestFinalisedBlocks[0].Height+1)
// TODO: if highestVotedHeight > lastVotedHeight, using highestVotedHeight could lead
// to issues when there are missed blocks between the gap due to bugs.
// A proper solution is to check if the fp has voted for each block within the gap
lastVotedHeight = max(lastVotedHeight, highestVotedHeight)

finalityActivationHeight, err := fp.getFinalityActivationHeightWithRetry()
if err != nil {
return 0, fmt.Errorf("failed to get finality activation height: %w", err)
}

// consider the height after the last voted height
startHeight = max(startHeight, fp.GetLastVotedHeight()+1)
// determine the final starting height
startHeight := max(finalityActivationHeight, lastVotedHeight+1)

// log how start height is determined
fp.logger.Info("determined poller starting height",
zap.String("pk", fp.GetBtcPkHex()),
zap.Uint64("start_height", startHeight),
zap.Uint64("finality_activation_height", finalityActivationHeight),
zap.Uint64("last_voted_height", fp.GetLastVotedHeight()),
zap.Uint64("last_finalized_height", lastFinalizedHeight),
zap.Uint64("highest_voted_height", highestVotedHeight))

return startHeight, nil
}
Expand Down Expand Up @@ -821,26 +849,54 @@ func (fp *FinalityProviderInstance) lastCommittedPublicRandWithRetry(count uint6
return response, nil
}

func (fp *FinalityProviderInstance) latestFinalizedBlocksWithRetry(count uint64) ([]*types.BlockInfo, error) {
var response []*types.BlockInfo
func (fp *FinalityProviderInstance) latestFinalizedHeightWithRetry() (uint64, error) {
var height uint64
if err := retry.Do(func() error {
latestFinalisedBlock, err := fp.cc.QueryLatestFinalizedBlocks(count)
blocks, err := fp.cc.QueryLatestFinalizedBlocks(1)
if err != nil {
return err
}
response = latestFinalisedBlock
if len(blocks) == 0 {
// no finalized block yet
return nil
}
height = blocks[0].Height
return nil
}, RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) {
fp.logger.Debug(
"failed to query babylon for the latest finalised blocks",
"failed to query babylon for the latest finalised height",
zap.Uint("attempt", n+1),
zap.Uint("max_attempts", RtyAttNum),
zap.Error(err),
)
})); err != nil {
return nil, err
return 0, err
}
return response, nil

return height, nil
}

func (fp *FinalityProviderInstance) highestVotedHeightWithRetry() (uint64, error) {
var height uint64
if err := retry.Do(func() error {
h, err := fp.cc.QueryFinalityProviderHighestVotedHeight(fp.GetBtcPk())
if err != nil {
return err
}
height = h
return nil
}, RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) {
fp.logger.Debug(
"failed to query babylon for the highest voted height",
zap.Uint("attempt", n+1),
zap.Uint("max_attempts", RtyAttNum),
zap.Error(err),
)
})); err != nil {
return 0, err
}

return height, nil
}

func (fp *FinalityProviderInstance) getFinalityActivationHeightWithRetry() (uint64, error) {
Expand Down
48 changes: 44 additions & 4 deletions finality-provider/service/fp_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func FuzzCommitPubRandList(f *testing.F) {
mockClientController := testutil.PrepareMockedClientController(t, r, randomStartingHeight, currentHeight, 0)
mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), gomock.Any()).
Return(uint64(0), nil).AnyTimes()
_, fpIns, cleanUp := startFinalityProviderAppWithRegisteredFp(t, r, mockClientController, randomStartingHeight)
_, fpIns, cleanUp := startFinalityProviderAppWithRegisteredFp(t, r, mockClientController, true, randomStartingHeight)
defer cleanUp()

expectedTxHash := testutil.GenRandomHexStr(r, 32)
Expand All @@ -60,7 +60,7 @@ func FuzzSubmitFinalitySigs(f *testing.F) {
startingBlock := &types.BlockInfo{Height: randomStartingHeight, Hash: testutil.GenRandomByteArray(r, 32)}
mockClientController := testutil.PrepareMockedClientController(t, r, randomStartingHeight, currentHeight, 0)
mockClientController.EXPECT().QueryLatestFinalizedBlocks(gomock.Any()).Return(nil, nil).AnyTimes()
_, fpIns, cleanUp := startFinalityProviderAppWithRegisteredFp(t, r, mockClientController, randomStartingHeight)
_, fpIns, cleanUp := startFinalityProviderAppWithRegisteredFp(t, r, mockClientController, true, randomStartingHeight)
defer cleanUp()

// commit pub rand
Expand Down Expand Up @@ -99,7 +99,47 @@ func FuzzSubmitFinalitySigs(f *testing.F) {
})
}

func startFinalityProviderAppWithRegisteredFp(t *testing.T, r *rand.Rand, cc clientcontroller.ClientController, startingHeight uint64) (*service.FinalityProviderApp, *service.FinalityProviderInstance, func()) {
func FuzzDetermineStartHeight(f *testing.F) {
testutil.AddRandomSeedsToFuzzer(f, 10)
f.Fuzz(func(t *testing.T, seed int64) {
r := rand.New(rand.NewSource(seed))

// generate random heights
finalityActivationHeight := uint64(r.Int63n(1000) + 1)
lastVotedHeight := uint64(r.Int63n(1000))
highestVotedHeight := uint64(r.Int63n(1000))
lastFinalizedHeight := uint64(r.Int63n(1000) + 1)

randomStartingHeight := uint64(r.Int63n(100) + 1)
currentHeight := randomStartingHeight + uint64(r.Int63n(10)+2)
mockClientController := testutil.PrepareMockedClientController(t, r, randomStartingHeight, currentHeight, finalityActivationHeight)

// setup mocks
mockClientController.EXPECT().
QueryFinalityProviderHighestVotedHeight(gomock.Any()).
Return(highestVotedHeight, nil).
AnyTimes()
finalizedBlocks := []*types.BlockInfo{{
Height: lastFinalizedHeight,
}}
mockClientController.EXPECT().QueryLatestFinalizedBlocks(uint64(1)).Return(finalizedBlocks, nil).AnyTimes()

_, fpIns, cleanUp := startFinalityProviderAppWithRegisteredFp(t, r, mockClientController, false, randomStartingHeight)
defer cleanUp()
fpIns.MustUpdateStateAfterFinalitySigSubmission(lastVotedHeight)

startHeight, err := fpIns.DetermineStartHeight()
require.NoError(t, err)

if lastVotedHeight == 0 {
require.Equal(t, startHeight, max(finalityActivationHeight, highestVotedHeight+1, lastFinalizedHeight+1))
} else {
require.Equal(t, startHeight, max(finalityActivationHeight, highestVotedHeight+1, lastVotedHeight+1))
}
})
}

func startFinalityProviderAppWithRegisteredFp(t *testing.T, r *rand.Rand, cc clientcontroller.ClientController, isStaticStartHeight bool, startingHeight uint64) (*service.FinalityProviderApp, *service.FinalityProviderInstance, func()) {
logger := zap.NewNop()
// create an EOTS manager
eotsHomeDir := filepath.Join(t.TempDir(), "eots-home")
Expand All @@ -113,7 +153,7 @@ func startFinalityProviderAppWithRegisteredFp(t *testing.T, r *rand.Rand, cc cli
fpHomeDir := filepath.Join(t.TempDir(), "fp-home")
fpCfg := config.DefaultConfigWithHome(fpHomeDir)
fpCfg.NumPubRand = testutil.TestPubRandNum
fpCfg.PollerConfig.AutoChainScanningMode = false
fpCfg.PollerConfig.AutoChainScanningMode = !isStaticStartHeight
fpCfg.PollerConfig.StaticChainScanningStartHeight = startingHeight
db, err := fpCfg.DatabaseConfig.GetDBBackend()
require.NoError(t, err)
Expand Down
1 change: 1 addition & 0 deletions finality-provider/service/fp_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func FuzzStatusUpdate(f *testing.F) {
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()

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
cosmossdk.io/errors v1.0.1
cosmossdk.io/math v1.4.0
github.com/avast/retry-go/v4 v4.5.1
github.com/babylonlabs-io/babylon v0.17.1
github.com/babylonlabs-io/babylon v0.9.3-0.20241128025442-457909d8c43c
github.com/btcsuite/btcd v0.24.2
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/btcsuite/btcd/btcutil v1.1.6
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1427,8 +1427,8 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX
github.com/aws/aws-sdk-go v1.44.312 h1:llrElfzeqG/YOLFFKjg1xNpZCFJ2xraIi3PqSuP+95k=
github.com/aws/aws-sdk-go v1.44.312/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/babylonlabs-io/babylon v0.17.1 h1:lyWGdR7B49qDw5pllLyTW/HAM5uQWXXPZefjFzy/Xy0=
github.com/babylonlabs-io/babylon v0.17.1/go.mod h1:sT+KG2U+M0tDMNZZ2L5CwlXX0OpagGEs56BiWXqaZFw=
github.com/babylonlabs-io/babylon v0.9.3-0.20241128025442-457909d8c43c h1:BfanV3d5TsQa/8xyj3mOlquW5ooRNMoK1Y8HDqcyAzk=
github.com/babylonlabs-io/babylon v0.9.3-0.20241128025442-457909d8c43c/go.mod h1:sT+KG2U+M0tDMNZZ2L5CwlXX0OpagGEs56BiWXqaZFw=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
Expand Down
9 changes: 4 additions & 5 deletions itest/container/config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package container

import (
"github.com/babylonlabs-io/finality-provider/testutil"
"github.com/stretchr/testify/require"
"testing"
)

Expand All @@ -20,10 +18,11 @@ const (

// NewImageConfig returns ImageConfig needed for running e2e test.
func NewImageConfig(t *testing.T) ImageConfig {
babylondVersion, err := testutil.GetBabylonVersion()
require.NoError(t, err)
// TODO: currently use specific commit, should uncomment after having a new release
// babylondVersion, err := testutil.GetBabylonVersion()
// require.NoError(t, err)
return ImageConfig{
BabylonRepository: dockerBabylondRepository,
BabylonVersion: babylondVersion,
BabylonVersion: "457909d8c43c8483655c2d3a3a01cd2190344fd4",
}
}
Loading
Loading