Skip to content

Commit

Permalink
Feat: impl QueryFinalityProviderHasPower in consumer chain (#44)
Browse files Browse the repository at this point in the history
# Summary
context in
[#496](babylonchain/finality-provider#496)
This pr implements the logic of querying voting power in Op. 

> instead of querying the exact voting power, the FP queries BTC
delegations page by page, and if there is >=1 active BTC delegation,
then it can vote.

# Test plan
```
make test-e2e-op
```

---------

Co-authored-by: Bourne Zhang <[email protected]>
Co-authored-by: lesterli <[email protected]>
  • Loading branch information
3 people authored Aug 29, 2024
1 parent 1f84df7 commit 6455f49
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 44 deletions.
87 changes: 86 additions & 1 deletion clientcontroller/opstackl2/consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -41,6 +44,7 @@ type OPStackL2ConsumerController struct {
Cfg *fpcfg.OPStackL2Config
CwClient *cwclient.Client
opl2Client *ethclient.Client
bbnClient *bbnclient.Client
logger *zap.Logger
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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(),
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
19 changes: 19 additions & 0 deletions finality-provider/config/opstackl2.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

cwcfg "github.com/babylonlabs-io/finality-provider/cosmwasmclient/config"

"github.com/cosmos/btcutil/bech32"
)

Expand Down Expand Up @@ -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,
}
}
5 changes: 4 additions & 1 deletion finality-provider/service/fp_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion itest/opstackl2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
40 changes: 24 additions & 16 deletions itest/opstackl2/op_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -27,27 +27,28 @@ 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
t.Logf(log.Prefix("First committed pubrandList startHeight %d"), committedStartHeight)
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:
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 5 additions & 25 deletions itest/opstackl2/op_test_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func StartOpL2ConsumerManager(t *testing.T, numOfConsumerFPs uint8) *OpL2Consume
testDir,
bh,
opL2ConsumerConfig,
numOfConsumerFPs+1,
numOfConsumerFPs,
logger,
&shutdownInterceptor,
t,
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6455f49

Please sign in to comment.