Skip to content

Commit

Permalink
feat(adr-032): Add ResumeFinalityProposal and handler (#242)
Browse files Browse the repository at this point in the history
This PR introduces the `ResumeFinalityProposal` and implements the
handler, which is part of
[ADR-32](babylonlabs-io/pm#95). This part is
quite independent and the algorithm of choosing finality providers to
jail can be implemented in the future
  • Loading branch information
gitferry authored Nov 20, 2024
1 parent 4b17a1e commit d204d98
Show file tree
Hide file tree
Showing 14 changed files with 760 additions and 61 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ nil params response

### Improvements

* [#242](https://github.com/babylonlabs-io/babylon/pull/242) Add
ResumeFinalityProposal and handler
* [#258](https://github.com/babylonlabs-io/babylon/pull/258) fix go releaser
and trigger by github action
* [#252](https://github.com/babylonlabs-io/babylon/pull/252) Fix
Expand Down
10 changes: 5 additions & 5 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import (
"github.com/CosmWasm/wasmd/x/wasm"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
"github.com/babylonlabs-io/babylon/x/mint"
minttypes "github.com/babylonlabs-io/babylon/x/mint/types"
abci "github.com/cometbft/cometbft/abci/types"
cmtos "github.com/cometbft/cometbft/libs/os"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
Expand Down Expand Up @@ -91,13 +89,15 @@ import (
ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint"
"github.com/spf13/cast"

"github.com/babylonlabs-io/babylon/app/ante"
"github.com/babylonlabs-io/babylon/app/upgrades"
bbn "github.com/babylonlabs-io/babylon/types"
"github.com/babylonlabs-io/babylon/x/mint"
minttypes "github.com/babylonlabs-io/babylon/x/mint/types"

"github.com/babylonlabs-io/babylon/app/ante"
appkeepers "github.com/babylonlabs-io/babylon/app/keepers"
appparams "github.com/babylonlabs-io/babylon/app/params"
"github.com/babylonlabs-io/babylon/app/upgrades"
"github.com/babylonlabs-io/babylon/client/docs"
bbn "github.com/babylonlabs-io/babylon/types"
"github.com/babylonlabs-io/babylon/x/btccheckpoint"
btccheckpointtypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types"
"github.com/babylonlabs-io/babylon/x/btclightclient"
Expand Down
21 changes: 21 additions & 0 deletions proto/babylon/finality/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ service Msg {
// UnjailFinalityProvider defines a method for unjailing a jailed
// finality provider, thus it can receive voting power
rpc UnjailFinalityProvider(MsgUnjailFinalityProvider) returns (MsgUnjailFinalityProviderResponse);
// ResumeFinalityProposal handles the proposal of resuming finality.
rpc ResumeFinalityProposal(MsgResumeFinalityProposal) returns (MsgResumeFinalityProposalResponse);
}

// MsgCommitPubRandList defines a message for committing a list of public randomness for EOTS
Expand Down Expand Up @@ -103,3 +105,22 @@ message MsgUnjailFinalityProvider {

// MsgUnjailFinalityProviderResponse defines the Msg/UnjailFinalityProvider response type
message MsgUnjailFinalityProviderResponse {}

// MsgResumeFinalityProposal is a governance proposal to resume finality from halting
message MsgResumeFinalityProposal {
option (cosmos.msg.v1.signer) = "authority";

// authority is the address of the governance account.
// just FYI: cosmos.AddressString marks that this field should use type alias
// for AddressString instead of string, but the functionality is not yet implemented
// in cosmos-proto
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// fp_pks_hex is a list of finality provider public keys to jail
// the public key follows encoding in BIP-340 spec
repeated string fp_pks_hex = 2;
// halting_height is the height where the finality halting begins
uint32 halting_height = 3;
}

// MsgResumeFinalityProposalResponse is the response to the MsgResumeFinalityProposal message.
message MsgResumeFinalityProposalResponse {}
2 changes: 1 addition & 1 deletion test/e2e/upgrades/v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
"title": "any title",
"summary": "any summary",
"expedited": false
}
}
3 changes: 2 additions & 1 deletion x/btcstaking/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
"fmt"
"math"

sdk "github.com/cosmos/cosmos-sdk/types"

bbn "github.com/babylonlabs-io/babylon/types"
"github.com/babylonlabs-io/babylon/x/btcstaking/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

// InitGenesis initializes the module's state from a provided genesis state.
Expand Down
3 changes: 2 additions & 1 deletion x/btcstaking/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ type (
AllowedStakingTxHashesKeySet collections.KeySet[[]byte]

btcNet *chaincfg.Params
// the address capable of executing a MsgUpdateParams message. Typically, this
// the address capable of executing a MsgUpdateParams or
// MsgResumeFinalityProposal message. Typically, this
// should be the x/gov module account.
authority string
}
Expand Down
23 changes: 23 additions & 0 deletions x/finality/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,29 @@ message MsgUpdateParams {
}
```

### MsgResumeFinalityProposal

The `MsgResumeFinalityProposal` message is used for resuming finality in case
of finality halting. It can only be executed via a governance proposal.

```protobuf
// MsgResumeFinalityProposal is a governance proposal to resume finality from halting
message MsgResumeFinalityProposal {
option (cosmos.msg.v1.signer) = "authority";
// authority is the address of the governance account.
// just FYI: cosmos.AddressString marks that this field should use type alias
// for AddressString instead of string, but the functionality is not yet implemented
// in cosmos-proto
string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// fp_pks_hex is a list of finality provider public keys to jail
// the public key follows encoding in BIP-340 spec
repeated string fp_pks_hex = 2;
// halting_height is the height where the finality halting begins
uint32 halting_height = 3;
}
```

## BeginBlocker

Upon `EndBlocker`, the Finality module of each Babylon node will [execute the
Expand Down
91 changes: 91 additions & 0 deletions x/finality/keeper/gov.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package keeper

import (
"errors"
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"

bbntypes "github.com/babylonlabs-io/babylon/types"
bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types"
)

// HandleResumeFinalityProposal handles the resume finality proposal in the following steps:
// 1. check the validity of the proposal
// 2. jail the finality providers from the list and adjust the voting power cache from the
// halting height to the current height
// 3. tally blocks to ensure finality is resumed
func (k Keeper) HandleResumeFinalityProposal(ctx sdk.Context, fpPksHex []string, haltingHeight uint32) error {
// a valid proposal should be
// 1. the halting height along with some parameterized future heights should be indeed non-finalized
// 2. all the fps from the proposal should have missed the vote for the halting height
// TODO introduce a parameter to define the finality has been halting for at least some heights

params := k.GetParams(ctx)
currentHeight := ctx.HeaderInfo().Height
currentTime := ctx.HeaderInfo().Time

// jail the given finality providers
fpPks := make([]*bbntypes.BIP340PubKey, 0, len(fpPksHex))
for _, fpPkHex := range fpPksHex {
fpPk, err := bbntypes.NewBIP340PubKeyFromHex(fpPkHex)
if err != nil {
return fmt.Errorf("invalid finality provider public key %s: %w", fpPkHex, err)
}
fpPks = append(fpPks, fpPk)

voters := k.GetVoters(ctx, uint64(haltingHeight))
_, voted := voters[fpPkHex]
if voted {
// all the given finality providers should not have voted for the halting height
return fmt.Errorf("the finality provider %s has voted for height %d", fpPkHex, haltingHeight)
}

err = k.jailSluggishFinalityProvider(ctx, fpPk)
if err != nil && !errors.Is(err, bstypes.ErrFpAlreadyJailed) {
return fmt.Errorf("failed to jail the finality provider %s: %w", fpPkHex, err)
}

// update signing info
signInfo, err := k.FinalityProviderSigningTracker.Get(ctx, fpPk.MustMarshal())
if err != nil {
return fmt.Errorf("the signing info of finality provider %s is not created: %w", fpPkHex, err)
}
signInfo.JailedUntil = currentTime.Add(params.JailDuration)
signInfo.MissedBlocksCounter = 0
if err := k.DeleteMissedBlockBitmap(ctx, fpPk); err != nil {
return fmt.Errorf("failed to remove the missed block bit map for finality provider %s: %w", fpPkHex, err)
}
err = k.FinalityProviderSigningTracker.Set(ctx, fpPk.MustMarshal(), signInfo)
if err != nil {
return fmt.Errorf("failed to set the signing info for finality provider %s: %w", fpPkHex, err)
}

k.Logger(ctx).Info(
"finality provider is jailed in the proposal",
"height", haltingHeight,
"public_key", fpPkHex,
)
}

// set the all the given finality providers voting power to 0
for h := uint64(haltingHeight); h <= uint64(currentHeight); h++ {
distCache := k.GetVotingPowerDistCache(ctx, h)
activeFps := distCache.GetActiveFinalityProviderSet()
for _, fpToJail := range fpPks {
if fp, exists := activeFps[fpToJail.MarshalHex()]; exists {
fp.IsJailed = true
k.SetVotingPower(ctx, fpToJail.MustMarshal(), h, 0)
}
}

distCache.ApplyActiveFinalityProviders(params.MaxActiveFinalityProviders)

// set the voting power distribution cache of the current height
k.SetVotingPowerDistCache(ctx, h, distCache)
}

k.TallyBlocks(ctx)

return nil
}
110 changes: 110 additions & 0 deletions x/finality/keeper/gov_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package keeper_test

import (
"math/rand"
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"

"github.com/babylonlabs-io/babylon/testutil/datagen"
keepertest "github.com/babylonlabs-io/babylon/testutil/keeper"
bbntypes "github.com/babylonlabs-io/babylon/types"
"github.com/babylonlabs-io/babylon/x/finality/keeper"
"github.com/babylonlabs-io/babylon/x/finality/types"
)

func TestHandleResumeFinalityProposal(t *testing.T) {
r := rand.New(rand.NewSource(time.Now().Unix()))
ctrl := gomock.NewController(t)
defer ctrl.Finish()

bsKeeper := types.NewMockBTCStakingKeeper(ctrl)
iKeeper := types.NewMockIncentiveKeeper(ctrl)
cKeeper := types.NewMockCheckpointingKeeper(ctrl)
fKeeper, ctx := keepertest.FinalityKeeper(t, bsKeeper, iKeeper, cKeeper)

haltingHeight := uint64(100)
currentHeight := uint64(110)

activeFpNum := 3
activeFpPks := generateNFpPks(t, r, activeFpNum)
setupActiveFps(t, activeFpPks, haltingHeight, fKeeper, ctx)
// set voting power table for each height, only the first fp votes
votedFpPk := activeFpPks[0]
for h := haltingHeight; h <= currentHeight; h++ {
fKeeper.SetBlock(ctx, &types.IndexedBlock{
Height: h,
AppHash: datagen.GenRandomByteArray(r, 32),
Finalized: false,
})
dc := types.NewVotingPowerDistCache()
for i := 0; i < activeFpNum; i++ {
fKeeper.SetVotingPower(ctx, activeFpPks[i].MustMarshal(), h, 1)
dc.AddFinalityProviderDistInfo(&types.FinalityProviderDistInfo{
BtcPk: &activeFpPks[i],
TotalBondedSat: 1,
IsTimestamped: true,
})
}
dc.ApplyActiveFinalityProviders(uint32(activeFpNum))
votedSig, err := bbntypes.NewSchnorrEOTSSig(datagen.GenRandomByteArray(r, 32))
require.NoError(t, err)
fKeeper.SetSig(ctx, h, &votedFpPk, votedSig)
fKeeper.SetVotingPowerDistCache(ctx, h, dc)
}

// tally blocks and none of them should be finalised
iKeeper.EXPECT().RewardBTCStaking(gomock.Any(), gomock.Any(), gomock.Any()).Return().AnyTimes()
ctx = datagen.WithCtxHeight(ctx, currentHeight)
fKeeper.TallyBlocks(ctx)
for i := haltingHeight; i < currentHeight; i++ {
ib, err := fKeeper.GetBlock(ctx, i)
require.NoError(t, err)
require.False(t, ib.Finalized)
}

// create a resume finality proposal to jail the last fp
bsKeeper.EXPECT().JailFinalityProvider(ctx, gomock.Any()).Return(nil).AnyTimes()
err := fKeeper.HandleResumeFinalityProposal(ctx, publicKeysToHex(activeFpPks[1:]), uint32(haltingHeight))
require.NoError(t, err)

for i := haltingHeight; i < currentHeight; i++ {
ib, err := fKeeper.GetBlock(ctx, i)
require.NoError(t, err)
require.True(t, ib.Finalized)
}
}

func generateNFpPks(t *testing.T, r *rand.Rand, n int) []bbntypes.BIP340PubKey {
fpPks := make([]bbntypes.BIP340PubKey, 0, n)
for i := 0; i < n; i++ {
fpPk, err := datagen.GenRandomBIP340PubKey(r)
require.NoError(t, err)
fpPks = append(fpPks, *fpPk)
}

return fpPks
}

func publicKeysToHex(pks []bbntypes.BIP340PubKey) []string {
hexPks := make([]string, len(pks))
for i, pk := range pks {
hexPks[i] = pk.MarshalHex()
}
return hexPks
}

func setupActiveFps(t *testing.T, fpPks []bbntypes.BIP340PubKey, height uint64, fKeeper *keeper.Keeper, ctx sdk.Context) {
for _, fpPk := range fpPks {
signingInfo := types.NewFinalityProviderSigningInfo(
&fpPk,
int64(height),
0,
)
err := fKeeper.FinalityProviderSigningTracker.Set(ctx, fpPk, signingInfo)
require.NoError(t, err)
}
}
14 changes: 14 additions & 0 deletions x/finality/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ func (ms msgServer) UpdateParams(goCtx context.Context, req *types.MsgUpdatePara
return &types.MsgUpdateParamsResponse{}, nil
}

// ResumeFinalityProposal handles the proposal for resuming finality from halting
func (ms msgServer) ResumeFinalityProposal(goCtx context.Context, req *types.MsgResumeFinalityProposal) (*types.MsgResumeFinalityProposalResponse, error) {
if ms.authority != req.Authority {
return nil, errorsmod.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", ms.authority, req.Authority)
}

ctx := sdk.UnwrapSDKContext(goCtx)
if err := ms.HandleResumeFinalityProposal(ctx, req.FpPksHex, req.HaltingHeight); err != nil {
return nil, govtypes.ErrInvalidProposalMsg.Wrapf("failed to handle resume finality proposal: %v", err)
}

return &types.MsgResumeFinalityProposalResponse{}, nil
}

// AddFinalitySig adds a new vote to a given block
func (ms msgServer) AddFinalitySig(goCtx context.Context, req *types.MsgAddFinalitySig) (*types.MsgAddFinalitySigResponse, error) {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), types.MetricsKeyAddFinalitySig)
Expand Down
1 change: 1 addition & 0 deletions x/finality/keeper/tallying.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func tally(fpSet map[string]uint64, voterBTCPKs map[string]struct{}) bool {
votedPower += power
}
}

return votedPower*3 > totalPower*2
}

Expand Down
2 changes: 2 additions & 0 deletions x/finality/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ func RegisterCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgCommitPubRandList{}, "finality/MsgCommitPubRandList", nil)
cdc.RegisterConcrete(&MsgAddFinalitySig{}, "finality/MsgAddFinalitySig", nil)
cdc.RegisterConcrete(&MsgUpdateParams{}, "finality/MsgUpdateParams", nil)
cdc.RegisterConcrete(&MsgResumeFinalityProposal{}, "finality/MsgResumeFinalityProposal", nil)
}

func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
Expand All @@ -20,6 +21,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
&MsgCommitPubRandList{},
&MsgAddFinalitySig{},
&MsgUpdateParams{},
&MsgResumeFinalityProposal{},
)

msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
Expand Down
8 changes: 5 additions & 3 deletions x/finality/types/msg.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package types

import (
fmt "fmt"
"fmt"

"github.com/babylonlabs-io/babylon/crypto/eots"
bbn "github.com/babylonlabs-io/babylon/types"
"github.com/cometbft/cometbft/crypto/merkle"
"github.com/cometbft/cometbft/crypto/tmhash"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/babylonlabs-io/babylon/crypto/eots"
bbn "github.com/babylonlabs-io/babylon/types"
)

// ensure that these message types implement the sdk.Msg interface
var (
_ sdk.Msg = &MsgResumeFinalityProposal{}
_ sdk.Msg = &MsgUpdateParams{}
_ sdk.Msg = &MsgAddFinalitySig{}
_ sdk.Msg = &MsgCommitPubRandList{}
Expand Down
Loading

0 comments on commit d204d98

Please sign in to comment.