Skip to content

Commit

Permalink
feat: Add new external events related to finality provider status cha…
Browse files Browse the repository at this point in the history
…nge (#144)

This is a follow-up PR of #131. This PR implements events generation
related to finality providers as described in
https://babylonlabs.atlassian.net/wiki/spaces/BABYLON/pages/31195227/API+asks+for+BBN+node.
In particular, this PR

1. adds events for fp creation and editing, which are emitted within
corresponding message handlers
2. adds events for fp status update, i.e., active, inactive, jailed,
slashed, which are emitted during voting power table update per
BeginBlock
  • Loading branch information
gitferry authored Oct 9, 2024
1 parent 07e4437 commit fe232fe
Show file tree
Hide file tree
Showing 11 changed files with 1,219 additions and 182 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ messages inside `authz.MsgExec`.

### Improvements

* #[131](https://github.com/babylonlabs-io/babylon/pull/131) Add new staking
events
* [#144](https://github.com/babylonlabs-io/babylon/pull/144) Add new finality provider events
* [#131](https://github.com/babylonlabs-io/babylon/pull/131) Add new staking events
* [#113](https://github.com/babylonlabs-io/babylon/pull/113) Add multibuild binary
for upgrade handler `testnet` and `mainnet`.

Expand Down
71 changes: 69 additions & 2 deletions proto/babylon/btcstaking/v1/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,41 @@ syntax = "proto3";
package babylon.btcstaking.v1;

import "gogoproto/gogo.proto";
import "cosmos/staking/v1beta1/staking.proto";
import "babylon/btcstaking/v1/btcstaking.proto";
import "cosmos_proto/cosmos.proto";

option go_package = "github.com/babylonlabs-io/babylon/x/btcstaking/types";

// EventNewFinalityProvider is the event emitted when a finality provider is created
message EventNewFinalityProvider { FinalityProvider fp = 1; }
// EventFinalityProviderCreated is the event emitted when a finality provider is created
message EventFinalityProviderCreated {
// btc_pk is the Bitcoin secp256k1 PK of this finality provider
// the PK follows encoding in BIP-340 spec
bytes btc_pk = 1 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey" ];
// addr is the address to receive commission from delegations.
string addr = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// commission defines the commission rate of the finality provider.
string commission = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec"
];
// description defines the description terms for the finality provider.
cosmos.staking.v1beta1.Description description = 4;
}

// EventFinalityProviderEdited is the event emitted when a finality provider is edited
message EventFinalityProviderEdited {
// btc_pk is the Bitcoin secp256k1 PK of this finality provider
// the PK follows encoding in BIP-340 spec
bytes btc_pk = 1 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey" ];
// commission defines the commission rate of the finality provider.
string commission = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec"
];
// description defines the description terms for the finality provider.
cosmos.staking.v1beta1.Description description = 3;
}

// EventBTCDelegationStateUpdate is the event emitted when a BTC delegation's state is
// updated. There are the following possible state transitions:
Expand Down Expand Up @@ -65,6 +94,44 @@ message EventPowerDistUpdate {
}
}

message EventFinalityProviderStatusChange {
// btc_pk is the BTC public key of the finality provider
string btc_pk = 1;
// new_status is the new status that the finality provider
// is transitioned to
FinalityProviderStatus new_status = 2;
}

// FinalityProviderStatus is the status of a finality provider.
// A finality provider starts with status INACTIVE once registered.
// Possible status transitions are when:
// 1. it has accumulated sufficient delegations and has
// timestamped public randomness:
// INACTIVE -> ACTIVE
// 2. it is jailed due to downtime:
// ACTIVE -> JAILED
// 3. it is slashed due to double-sign:
// ACTIVE -> SLASHED
// 4. it is unjailed after a jailing period:
// JAILED -> INACTIVE/ACTIVE (depending on (1))
// 5. it does not have sufficient delegations or does not
// have timestamped public randomness:
// ACTIVE -> INACTIVE.
// Note that it is impossible for a SLASHED finality provider to
// transition to other status
enum FinalityProviderStatus {
// STATUS_INACTIVE defines a finality provider that does not have sufficient
// delegations or does not have timestamped public randomness.
STATUS_INACTIVE = 0;
// STATUS_ACTIVE defines a finality provider that have sufficient delegations
// and have timestamped public randomness.
STATUS_ACTIVE = 1;
// STATUS_JAILED defines a finality provider that is jailed due to downtime
STATUS_JAILED = 2;
// STATUS_SLASHED defines a finality provider that is slashed due to double-sign
STATUS_SLASHED = 3;
}

// EventBTCDelegationCreated is the event emitted when a BTC delegation is created
// on the Babylon chain
message EventBTCDelegationCreated {
Expand Down
4 changes: 4 additions & 0 deletions proto/babylon/btcstaking/v1/incentive.proto
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ message FinalityProviderDistInfo {
repeated BTCDelDistInfo btc_dels = 5;
// is_timestamped indicates whether the finality provider
// has timestamped public randomness committed
// if no, it should not be assigned voting power
bool is_timestamped = 6;
// is_jailed indicates whether the finality provider
// is jailed, if so, it should not be assigned voting power
bool is_jailed = 7;
// is_slashed indicates whether the finality provider
// is slashed, if so, it should not be assigned voting power
bool is_slashed = 8;
}

// BTCDelDistInfo contains the information related to reward distribution for a BTC delegation
Expand Down
2 changes: 1 addition & 1 deletion x/btcstaking/keeper/finality_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (k Keeper) AddFinalityProvider(goCtx context.Context, msg *types.MsgCreateF
k.setFinalityProvider(ctx, &fp)

// notify subscriber
return ctx.EventManager().EmitTypedEvent(&types.EventNewFinalityProvider{Fp: &fp})
return ctx.EventManager().EmitTypedEvent(types.NewEventFinalityProviderCreated(&fp))
}

// setFinalityProvider adds the given finality provider to KVStore
Expand Down
18 changes: 13 additions & 5 deletions x/btcstaking/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (ms msgServer) CreateFinalityProvider(goCtx context.Context, req *types.Msg
}

// EditFinalityProvider edits an existing finality provider
func (ms msgServer) EditFinalityProvider(ctx context.Context, req *types.MsgEditFinalityProvider) (*types.MsgEditFinalityProviderResponse, error) {
func (ms msgServer) EditFinalityProvider(goCtx context.Context, req *types.MsgEditFinalityProvider) (*types.MsgEditFinalityProviderResponse, error) {
// basic stateless checks
// NOTE: after this, description is guaranteed to be valid
if err := req.ValidateBasic(); err != nil {
Expand All @@ -88,16 +88,18 @@ func (ms msgServer) EditFinalityProvider(ctx context.Context, req *types.MsgEdit
// ensure commission rate is
// - at least the minimum commission rate in parameters, and
// - at most 1
if req.Commission.LT(ms.MinCommissionRate(ctx)) {
return nil, types.ErrCommissionLTMinRate.Wrapf("cannot set finality provider commission to less than minimum rate of %s", ms.MinCommissionRate(ctx))
if req.Commission.LT(ms.MinCommissionRate(goCtx)) {
return nil, types.ErrCommissionLTMinRate.Wrapf(
"cannot set finality provider commission to less than minimum rate of %s",
ms.MinCommissionRate(goCtx))
}
if req.Commission.GT(sdkmath.LegacyOneDec()) {
return nil, types.ErrCommissionGTMaxRate
}

// TODO: check to index the finality provider by his address instead of the BTC pk
// find the finality provider with the given BTC PK
fp, err := ms.GetFinalityProvider(ctx, req.BtcPk)
fp, err := ms.GetFinalityProvider(goCtx, req.BtcPk)
if err != nil {
return nil, err
}
Expand All @@ -115,7 +117,13 @@ func (ms msgServer) EditFinalityProvider(ctx context.Context, req *types.MsgEdit
// all good, update the finality provider and set back
fp.Description = req.Description
fp.Commission = req.Commission
ms.setFinalityProvider(ctx, fp)
ms.setFinalityProvider(goCtx, fp)

// notify subscriber
ctx := sdk.UnwrapSDKContext(goCtx)
if err := ctx.EventManager().EmitTypedEvent(types.NewEventFinalityProviderEdited(fp)); err != nil {
panic(fmt.Errorf("failed to emit EventFinalityProviderEdited event: %w", err))
}

return &types.MsgEditFinalityProviderResponse{}, nil
}
Expand Down
59 changes: 49 additions & 10 deletions x/btcstaking/keeper/power_dist_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,21 @@ func (k Keeper) UpdatePowerDist(ctx context.Context) {
// 1. the fp must have timestamped pub rand
// 2. the fp must in the top x ranked by the voting power (x is given by maxActiveFps)
// NOTE: the previous and the new dist cache cannot be nil
func (k Keeper) recordVotingPowerAndCache(ctx context.Context, prevDc, newDc *types.VotingPowerDistCache, maxActiveFps uint32) {
func (k Keeper) recordVotingPowerAndCache(goCtx context.Context, prevDc, newDc *types.VotingPowerDistCache, maxActiveFps uint32) {
if prevDc == nil || newDc == nil {
panic("the voting power distribution cache cannot be nil")
}

babylonTipHeight := uint64(sdk.UnwrapSDKContext(ctx).HeaderInfo().Height)
sdkCtx := sdk.UnwrapSDKContext(goCtx)
babylonTipHeight := uint64(sdk.UnwrapSDKContext(goCtx).HeaderInfo().Height)

// label fps with whether it has timestamped pub rand so that these fps
// will not be assigned voting power
for _, fpDistInfo := range newDc.FinalityProviders {
// TODO calling HasTimestampedPubRand potentially iterates
// all the pub rand committed by the fpDistInfo, which might slow down
// the process, need optimization
fpDistInfo.IsTimestamped = k.FinalityKeeper.HasTimestampedPubRand(ctx, fpDistInfo.BtcPk, babylonTipHeight)
fpDistInfo.IsTimestamped = k.FinalityKeeper.HasTimestampedPubRand(goCtx, fpDistInfo.BtcPk, babylonTipHeight)
}

// apply the finality provider voting power dist info to the new cache
Expand All @@ -106,21 +107,43 @@ func (k Keeper) recordVotingPowerAndCache(ctx context.Context, prevDc, newDc *ty
// set voting power table for each active finality providers at this height
for i := uint32(0); i < newDc.NumActiveFps; i++ {
fp := newDc.FinalityProviders[i]
k.SetVotingPower(ctx, fp.BtcPk.MustMarshal(), babylonTipHeight, fp.TotalVotingPower)
k.SetVotingPower(goCtx, fp.BtcPk.MustMarshal(), babylonTipHeight, fp.TotalVotingPower)
}

// find newly activated finality providers and execute the hooks by comparing
// the previous dist cache
newActivatedFinalityProviders := newDc.FindNewActiveFinalityProviders(prevDc)
for _, fp := range newActivatedFinalityProviders {
if err := k.hooks.AfterFinalityProviderActivated(ctx, fp.BtcPk); err != nil {
newActivatedFps := newDc.FindNewActiveFinalityProviders(prevDc)
for _, fp := range newActivatedFps {
if err := k.hooks.AfterFinalityProviderActivated(goCtx, fp.BtcPk); err != nil {
panic(fmt.Errorf("failed to execute after finality provider %s activated", fp.BtcPk.MarshalHex()))
}
k.Logger(sdk.UnwrapSDKContext(ctx)).Info("a new finality provider is activated", "pk", fp.BtcPk.MarshalHex())

statusChangeEvent := types.NewFinalityProviderStatusChangeEvent(fp.BtcPk, types.FinalityProviderStatus_STATUS_ACTIVE)
if err := sdkCtx.EventManager().EmitTypedEvent(statusChangeEvent); err != nil {
panic(fmt.Errorf(
"failed to emit FinalityProviderStatusChangeEvent with status %s: %w",
types.FinalityProviderStatus_STATUS_ACTIVE.String(), err))
}

k.Logger(sdkCtx).Info("a new finality provider becomes active", "pk", fp.BtcPk.MarshalHex())
}

// find finality providers that newly become inactive and emit events to
// subscribers
newInactiveFps := newDc.FindNewInactiveFinalityProviders(prevDc)
for _, fp := range newInactiveFps {
statusChangeEvent := types.NewFinalityProviderStatusChangeEvent(fp.BtcPk, types.FinalityProviderStatus_STATUS_INACTIVE)
if err := sdkCtx.EventManager().EmitTypedEvent(statusChangeEvent); err != nil {
panic(fmt.Errorf(
"failed to emit FinalityProviderStatusChangeEvent with status %s: %w",
types.FinalityProviderStatus_STATUS_INACTIVE.String(), err))
}

k.Logger(sdkCtx).Info("a new finality provider becomes inactive", "pk", fp.BtcPk.MarshalHex())
}

// set the voting power distribution cache of the current height
k.setVotingPowerDistCache(ctx, babylonTipHeight, newDc)
k.setVotingPowerDistCache(sdkCtx, babylonTipHeight, newDc)
}

func (k Keeper) recordMetrics(dc *types.VotingPowerDistCache) {
Expand Down Expand Up @@ -166,6 +189,7 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents(
/*
filter and classify all events into new/expired BTC delegations and jailed/slashed FPs
*/
sdkCtx := sdk.UnwrapSDKContext(ctx)
for _, event := range events {
switch typedEvent := event.Ev.(type) {
case *types.EventPowerDistUpdate_BtcDelStateUpdate:
Expand All @@ -183,7 +207,6 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents(
activeBTCDels[fpBTCPKHex] = append(activeBTCDels[fpBTCPKHex], btcDel)
}
} else if delEvent.NewState == types.BTCDelegationStatus_UNBONDED {
sdkCtx := sdk.UnwrapSDKContext(ctx)
// delegation expired and become unbonded emit block event about this
// information
if btcDel.IsUnbondedEarly() {
Expand Down Expand Up @@ -238,6 +261,15 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents(
// if this finality provider is slashed, continue to avoid
// assigning delegation to it
if _, ok := slashedFPs[fpBTCPKHex]; ok {
fp.IsSlashed = true

statusChangeEvent := types.NewFinalityProviderStatusChangeEvent(fp.BtcPk, types.FinalityProviderStatus_STATUS_SLASHED)
if err := sdkCtx.EventManager().EmitTypedEvent(statusChangeEvent); err != nil {
panic(fmt.Errorf(
"failed to emit FinalityProviderStatusChangeEvent with status %s: %w",
types.FinalityProviderStatus_STATUS_SLASHED.String(), err))
}

continue
}

Expand All @@ -246,6 +278,13 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents(
// but won't be assigned with voting power
if _, ok := jailedFPs[fpBTCPKHex]; ok {
fp.IsJailed = true

statusChangeEvent := types.NewFinalityProviderStatusChangeEvent(fp.BtcPk, types.FinalityProviderStatus_STATUS_JAILED)
if err := sdkCtx.EventManager().EmitTypedEvent(statusChangeEvent); err != nil {
panic(fmt.Errorf(
"failed to emit FinalityProviderStatusChangeEvent with status %s: %w",
types.FinalityProviderStatus_STATUS_JAILED.String(), err))
}
}

// set IsJailed to be false if the fp is unjailed
Expand Down
27 changes: 27 additions & 0 deletions x/btcstaking/types/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ func NewEventPowerDistUpdateWithUnjailedFP(fpBTCPK *bbn.BIP340PubKey) *EventPowe
}
}

func NewEventFinalityProviderCreated(fp *FinalityProvider) *EventFinalityProviderCreated {
return &EventFinalityProviderCreated{
BtcPk: fp.BtcPk,
Addr: fp.Addr,
Commission: fp.Commission,
Description: fp.Description,
}
}

func NewEventFinalityProviderEdited(fp *FinalityProvider) *EventFinalityProviderEdited {
return &EventFinalityProviderEdited{
BtcPk: fp.BtcPk,
Commission: fp.Commission,
Description: fp.Description,
}
}

func NewInclusionProofEvent(
stakingTxHash string,
startHeight uint64,
Expand Down Expand Up @@ -114,3 +131,13 @@ func NewExpiredDelegationEvent(
State: BTCDelegationStatus_UNBONDED,
}
}

func NewFinalityProviderStatusChangeEvent(
fpPk *bbn.BIP340PubKey,
status FinalityProviderStatus,
) *EventFinalityProviderStatusChange {
return &EventFinalityProviderStatusChange{
BtcPk: fpPk.MarshalHex(),
NewStatus: status,
}
}
Loading

0 comments on commit fe232fe

Please sign in to comment.