diff --git a/clientcontroller/opstackl2/consumer.go b/clientcontroller/opstackl2/consumer.go index 8e792533..018aedd9 100644 --- a/clientcontroller/opstackl2/consumer.go +++ b/clientcontroller/opstackl2/consumer.go @@ -5,6 +5,9 @@ import ( "encoding/hex" "encoding/json" "fmt" + bbnclient "github.com/babylonlabs-io/babylon/client/client" + btcstakingtypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" + sdkquerytypes "github.com/cosmos/cosmos-sdk/types/query" "math" "math/big" @@ -41,6 +44,7 @@ type OPStackL2ConsumerController struct { Cfg *fpcfg.OPStackL2Config CwClient *cwclient.Client opl2Client *ethclient.Client + bbnClient *bbnclient.Client logger *zap.Logger } @@ -66,10 +70,26 @@ func NewOPStackL2ConsumerController( return nil, fmt.Errorf("failed to create OPStack L2 client: %w", err) } + bbnConfig := opl2Cfg.ToBBNConfig() + babylonConfig := fpcfg.BBNConfigToBabylonConfig(&bbnConfig) + + if err := babylonConfig.Validate(); err != nil { + return nil, fmt.Errorf("invalid config for Babylon client: %w", err) + } + + bc, err := bbnclient.New( + &babylonConfig, + logger, + ) + if err != nil { + return nil, fmt.Errorf("failed to create Babylon client: %w", err) + } + return &OPStackL2ConsumerController{ opl2Cfg, cwClient, opl2Client, + bc, logger, }, nil } @@ -248,7 +268,38 @@ func (cc *OPStackL2ConsumerController) SubmitBatchFinalitySigs( // Now we can simply hardcode the voting power to true // TODO: see this issue https://github.com/babylonlabs-io/finality-provider/issues/390 for more details func (cc *OPStackL2ConsumerController) QueryFinalityProviderHasPower(fpPk *btcec.PublicKey, blockHeight uint64) (bool, error) { - return true, nil + fpBtcPkHex := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex() + var nextKey []byte + + btcStakingParams, err := cc.bbnClient.QueryClient.BTCStakingParams() + if err != nil { + return false, err + } + for { + resp, err := cc.bbnClient.QueryClient.FinalityProviderDelegations(fpBtcPkHex, &sdkquerytypes.PageRequest{Key: nextKey, Limit: 100}) + if err != nil { + return false, err + } + + for _, btcDels := range resp.BtcDelegatorDelegations { + for _, btcDel := range btcDels.Dels { + active, err := cc.isDelegationActive(btcStakingParams, btcDel) + if err != nil { + continue + } + if active { + return true, nil + } + } + } + + if resp.Pagination == nil || resp.Pagination.NextKey == nil { + break + } + nextKey = resp.Pagination.NextKey + } + + return false, nil } // QueryLatestFinalizedBlock returns the finalized L2 block from a RPC call @@ -259,6 +310,11 @@ func (cc *OPStackL2ConsumerController) QueryLatestFinalizedBlock() (*types.Block if err != nil { return nil, err } + + if l2Block.Number.Uint64() == 0 { + return nil, nil + } + return &types.BlockInfo{ Height: l2Block.Number.Uint64(), Hash: l2Block.Hash().Bytes(), @@ -348,6 +404,10 @@ func (cc *OPStackL2ConsumerController) QueryIsBlockFinalized(height uint64) (boo if err != nil { return false, err } + + if l2Block == nil { + return false, nil + } if height > l2Block.Height { return false, nil } @@ -486,3 +546,28 @@ func (cc *OPStackL2ConsumerController) Close() error { cc.opl2Client.Close() return cc.CwClient.Stop() } + +func (cc *OPStackL2ConsumerController) isDelegationActive( + btcStakingParams *btcstakingtypes.QueryParamsResponse, + btcDel *btcstakingtypes.BTCDelegationResponse, +) (bool, error) { + + covQuorum := btcStakingParams.GetParams().CovenantQuorum + ud := btcDel.UndelegationResponse + + if len(ud.GetDelegatorUnbondingSigHex()) > 0 { + return false, nil + } + + if uint32(len(btcDel.CovenantSigs)) < covQuorum { + return false, nil + } + if len(ud.CovenantUnbondingSigList) < int(covQuorum) { + return false, nil + } + if len(ud.CovenantSlashingSigs) < int(covQuorum) { + return false, nil + } + + return true, nil +} diff --git a/finality-provider/config/opstackl2.go b/finality-provider/config/opstackl2.go index eb7505a6..b203e102 100644 --- a/finality-provider/config/opstackl2.go +++ b/finality-provider/config/opstackl2.go @@ -7,6 +7,7 @@ import ( "time" cwcfg "github.com/babylonlabs-io/finality-provider/cosmwasmclient/config" + "github.com/cosmos/btcutil/bech32" ) @@ -78,3 +79,21 @@ func (cfg *OPStackL2Config) ToCosmwasmConfig() cwcfg.CosmwasmConfig { SubmitterAddress: "", } } + +func (cfg *OPStackL2Config) ToBBNConfig() BBNConfig { + return BBNConfig{ + Key: cfg.Key, + ChainID: cfg.ChainID, + RPCAddr: cfg.RPCAddr, + AccountPrefix: cfg.AccountPrefix, + KeyringBackend: cfg.KeyringBackend, + GasAdjustment: cfg.GasAdjustment, + GasPrices: cfg.GasPrices, + KeyDirectory: cfg.KeyDirectory, + Debug: cfg.Debug, + Timeout: cfg.Timeout, + BlockTimeout: cfg.BlockTimeout, + OutputFormat: cfg.OutputFormat, + SignModeStr: cfg.SignModeStr, + } +} diff --git a/finality-provider/service/fp_instance.go b/finality-provider/service/fp_instance.go index a7b8b711..2cabb384 100644 --- a/finality-provider/service/fp_instance.go +++ b/finality-provider/service/fp_instance.go @@ -434,7 +434,10 @@ func (fp *FinalityProviderInstance) tryFastSync(targetBlockHeight uint64) (*Fast } fp.logger.Debug("the finality-provider is entering fast sync", - zap.Uint64("start_height", startHeight), zap.Uint64("target_block_height", targetBlockHeight)) + zap.String("pk", fp.GetBtcPkHex()), + zap.Uint64("start_height", startHeight), + zap.Uint64("target_block_height", targetBlockHeight), + ) return fp.FastSync(startHeight, targetBlockHeight) } diff --git a/itest/opstackl2/README.md b/itest/opstackl2/README.md index c90deccd..f833b909 100644 --- a/itest/opstackl2/README.md +++ b/itest/opstackl2/README.md @@ -12,5 +12,5 @@ Then run the following command to start the e2e tests: $ make test-e2e-op # Filter specific test -$ make test-e2e-op Filter=TestFinalityGadget +$ make test-e2e-op FILTER=TestFinalityGadget ``` diff --git a/itest/opstackl2/op_e2e_test.go b/itest/opstackl2/op_e2e_test.go index c15daa7f..e421d5ab 100644 --- a/itest/opstackl2/op_e2e_test.go +++ b/itest/opstackl2/op_e2e_test.go @@ -15,8 +15,8 @@ import ( "github.com/stretchr/testify/require" ) -// tests the finality signature submission to the op-finality-gadget contract -func TestOpSubmitFinalitySignature(t *testing.T) { +// TestOpFpNoVotingPower tests that the FP has no voting power if it has no BTC delegation +func TestOpFpNoVotingPower(t *testing.T) { ctm := StartOpL2ConsumerManager(t, 1) defer ctm.Stop(t) @@ -27,7 +27,7 @@ func TestOpSubmitFinalitySignature(t *testing.T) { e2eutils.WaitForFpPubRandCommitted(t, fpInstance) // query the first committed pub rand - opcc := ctm.getOpCCAtIndex(1) + opcc := ctm.getOpCCAtIndex(0) committedPubRand, err := queryFirstPublicRandCommit(opcc, fpInstance.GetBtcPk()) require.NoError(t, err) committedStartHeight := committedPubRand.StartHeight @@ -35,19 +35,20 @@ func TestOpSubmitFinalitySignature(t *testing.T) { testBlocks := ctm.WaitForNBlocksAndReturn(t, committedStartHeight, 1) testBlock := testBlocks[0] - // wait for the fp sign - ctm.WaitForFpVoteReachHeight(t, fpInstance, testBlock.Height) queryBlock := &fgtypes.Block{ BlockHeight: testBlock.Height, BlockHash: hex.EncodeToString(testBlock.Hash), BlockTimestamp: 12345, // doesn't matter b/c the BTC client is mocked } - // note: QueryFinalityProviderHasPower is hardcode to return true so FPs can still submit finality sigs even if they - // don't have voting power. But the finality sigs will not be counted at tally time. + // no BTC delegation, so the FP has no voting power + hasPower, err := opcc.QueryFinalityProviderHasPower(fpInstance.GetBtcPk(), queryBlock.BlockHeight) + require.NoError(t, err) + require.Equal(t, false, hasPower) + _, err = ctm.FinalityGadget.QueryIsBlockBabylonFinalized(queryBlock) require.ErrorIs(t, err, fgtypes.ErrBtcStakingNotActivated) - t.Logf(log.Prefix("Expected no voting power")) + t.Logf(log.Prefix("Expected BTC staking not activated")) } // This test has two test cases: @@ -65,8 +66,9 @@ func TestOpMultipleFinalityProviders(t *testing.T) { {e2eutils.StakingTime, e2eutils.StakingAmount}, }) - // wait until the BTC staking is activated - l2BlockAfterActivation := ctm.waitForBTCStakingActivation(t) + // BTC delegations are activated after SetupFinalityProviders + l2BlockAfterActivation, err := ctm.getOpCCAtIndex(0).QueryLatestBlockHeight() + require.NoError(t, err) // check both FPs have committed their first public randomness // TODO: we might use go routine to do this in parallel @@ -135,8 +137,9 @@ func TestFinalityStuckAndRecover(t *testing.T) { }) fpInstance := fpList[0] - // wait until the BTC staking is activated - l2BlockAfterActivation := ctm.waitForBTCStakingActivation(t) + // BTC delegations are activated after SetupFinalityProviders + l2BlockAfterActivation, err := ctm.getOpCCAtIndex(0).QueryLatestBlockHeight() + require.NoError(t, err) // wait for the first block to be finalized since BTC staking is activated e2eutils.WaitForFpPubRandCommittedReachTargetHeight(t, fpInstance, l2BlockAfterActivation) @@ -156,16 +159,20 @@ func TestFinalityStuckAndRecover(t *testing.T) { t.Logf(log.Prefix("last voted height %d"), lastVotedHeight) // wait until the block finalized require.Eventually(t, func() bool { - latestFinalizedBlock, err := ctm.getOpCCAtIndex(1).QueryLatestFinalizedBlock() + latestFinalizedBlock, err := ctm.getOpCCAtIndex(0).QueryLatestFinalizedBlock() require.NoError(t, err) + if latestFinalizedBlock == nil { + return false + } stuckHeight := latestFinalizedBlock.Height return lastVotedHeight == stuckHeight }, e2eutils.EventuallyWaitTimeOut, e2eutils.EventuallyPollTime) // check the finality gets stuck. wait for a while to make sure it is stuck time.Sleep(5 * ctm.getL1BlockTime()) - latestFinalizedBlock, err := ctm.getOpCCAtIndex(1).QueryLatestFinalizedBlock() + latestFinalizedBlock, err := ctm.getOpCCAtIndex(0).QueryLatestFinalizedBlock() require.NoError(t, err) + require.NotNil(t, latestFinalizedBlock) stuckHeight := latestFinalizedBlock.Height require.Equal(t, lastVotedHeight, stuckHeight) t.Logf(log.Prefix("OP chain block finalized head stuck at height %d"), stuckHeight) @@ -205,8 +212,9 @@ func TestFinalityGadgetServer(t *testing.T) { e2eutils.WaitForFpPubRandCommitted(t, fpList[i]) } - // wait until the BTC staking is activated - l2BlockAfterActivation := ctm.waitForBTCStakingActivation(t) + // BTC delegations are activated after SetupFinalityProviders + l2BlockAfterActivation, err := ctm.getOpCCAtIndex(0).QueryLatestBlockHeight() + require.NoError(t, err) // both FP will sign the first block targetBlockHeight := ctm.WaitForTargetBlockPubRand(t, fpList, l2BlockAfterActivation) diff --git a/itest/opstackl2/op_test_manager.go b/itest/opstackl2/op_test_manager.go index 8e30b9ab..746bcc75 100644 --- a/itest/opstackl2/op_test_manager.go +++ b/itest/opstackl2/op_test_manager.go @@ -102,7 +102,7 @@ func StartOpL2ConsumerManager(t *testing.T, numOfConsumerFPs uint8) *OpL2Consume testDir, bh, opL2ConsumerConfig, - numOfConsumerFPs+1, + numOfConsumerFPs, logger, &shutdownInterceptor, t, @@ -765,7 +765,7 @@ func (ctm *OpL2ConsumerTestManager) SetupFinalityProviders( for i := 0; i < n; i++ { ctm.InsertBTCDelegation( t, - []*btcec.PublicKey{bbnFpPk.MustToBTCPK(), consumerFpPkList[0].MustToBTCPK()}, + []*btcec.PublicKey{bbnFpPk.MustToBTCPK(), consumerFpPkList[i].MustToBTCPK()}, stakingParams[i].stakingTime, stakingParams[i].stakingAmount, ) @@ -831,6 +831,9 @@ func (ctm *OpL2ConsumerTestManager) WaitForBlockFinalized( t.Logf(log.Prefix("failed to query latest finalized block %s"), err.Error()) return false } + if latestFinalizedBlock == nil { + return false + } finalizedBlockHeight = latestFinalizedBlock.Height return finalizedBlockHeight >= checkedHeight }, e2eutils.EventuallyWaitTimeOut, 5*ctm.getL2BlockTime()) @@ -934,29 +937,6 @@ func queryFirstOrLastPublicRandCommit( return resp, nil } -func (ctm *OpL2ConsumerTestManager) waitForBTCStakingActivation(t *testing.T) uint64 { - var l2BlockAfterActivation uint64 - require.Eventually(t, func() bool { - latestBlockHeight, err := ctm.getOpCCAtIndex(0).QueryLatestBlockHeight() - require.NoError(t, err) - latestBlock, err := ctm.getOpCCAtIndex(0).QueryEthBlock(latestBlockHeight) - require.NoError(t, err) - l2BlockAfterActivation = latestBlock.Number.Uint64() - - activatedTimestamp, err := ctm.FinalityGadget.QueryBtcStakingActivatedTimestamp() - if err != nil { - t.Logf(log.Prefix("Failed to query BTC staking activated timestamp: %v"), err) - return false - } - t.Logf(log.Prefix("Activated timestamp %d"), activatedTimestamp) - - return latestBlock.Time >= activatedTimestamp - }, 30*ctm.getL2BlockTime(), ctm.getL2BlockTime()) - - t.Logf(log.Prefix("found a L2 block after BTC staking activation: %d"), l2BlockAfterActivation) - return l2BlockAfterActivation -} - func (ctm *OpL2ConsumerTestManager) Stop(t *testing.T) { t.Log("Stopping test manager") var err error