diff --git a/proto/babylon/btcstaking/v1/events.proto b/proto/babylon/btcstaking/v1/events.proto index 848a81f23..2482e4411 100644 --- a/proto/babylon/btcstaking/v1/events.proto +++ b/proto/babylon/btcstaking/v1/events.proto @@ -1,13 +1,15 @@ syntax = "proto3"; package babylon.btcstaking.v1; -import "gogoproto/gogo.proto"; import "babylon/btcstaking/v1/btcstaking.proto"; +import "gogoproto/gogo.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; } +message EventNewFinalityProvider { + FinalityProvider fp = 1; +} // EventBTCDelegationStateUpdate is the event emitted when a BTC delegation's state is // updated. There are the following possible state transitions: @@ -37,19 +39,27 @@ message EventPowerDistUpdate { // is slashed // TODO: unify with existing slashing events message EventSlashedFinalityProvider { - bytes pk = 1 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey" ]; + bytes pk = 1 [(gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey"]; + } + + // EventSlashedBTCDelegation is emitted for each BTC delegation that restakes to a slashed consumer finality provider. + // It indicates that the voting power of affected Babylon finality providers will be discounted for this delegation. + message EventSlashedBTCDelegation { + // staking_tx_hash is the hash of the staking tx. + // It uniquely identifies a BTC delegation + string staking_tx_hash = 1; } // EventJailedFinalityProvider defines an event that a finality provider // is jailed after being detected sluggish message EventJailedFinalityProvider { - bytes pk = 1 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey" ]; + bytes pk = 1 [(gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey"]; } // EventUnjailedFinalityProvider defines an event that a jailed finality provider // is unjailed after the jailing period is passed message EventUnjailedFinalityProvider { - bytes pk = 1 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey" ]; + bytes pk = 1 [(gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340PubKey"]; } // ev is the event that affects voting power distribution @@ -62,5 +72,7 @@ message EventPowerDistUpdate { EventUnjailedFinalityProvider unjailed_fp = 3; // btc_del_state_update means a BTC delegation's state is updated EventBTCDelegationStateUpdate btc_del_state_update = 4; + // slashed_btc_delegation represents the affected BTC delegation when a consumer FP is slashed + EventSlashedBTCDelegation slashed_btc_delegation = 5; } } diff --git a/test/e2e/bcd_consumer_integration/clientcontroller/babylon/babylon.go b/test/e2e/bcd_consumer_integration/clientcontroller/babylon/babylon.go index fb991bf59..db6e167c7 100644 --- a/test/e2e/bcd_consumer_integration/clientcontroller/babylon/babylon.go +++ b/test/e2e/bcd_consumer_integration/clientcontroller/babylon/babylon.go @@ -165,6 +165,15 @@ func (bc *BabylonController) QueryFinalityProviderSlashed(fpPk *btcec.PublicKey) return slashed, nil } +func (bc *BabylonController) QueryFinalityProvider(fpBtcPkHex string) (*btcstakingtypes.QueryFinalityProviderResponse, error) { + res, err := bc.bbnClient.QueryClient.FinalityProvider(fpBtcPkHex) + if err != nil { + return nil, fmt.Errorf("failed to query the finality provider %s: %v", fpBtcPkHex, err) + } + + return res, nil +} + func (bc *BabylonController) QueryNodeStatus() (*coretypes.ResultStatus, error) { return bc.bbnClient.QueryClient.GetStatus() } diff --git a/test/e2e/bcd_consumer_integration/clientcontroller/cosmwasm/cosmwasm.go b/test/e2e/bcd_consumer_integration/clientcontroller/cosmwasm/cosmwasm.go index 3b31a9058..c39e5e29d 100644 --- a/test/e2e/bcd_consumer_integration/clientcontroller/cosmwasm/cosmwasm.go +++ b/test/e2e/bcd_consumer_integration/clientcontroller/cosmwasm/cosmwasm.go @@ -12,14 +12,17 @@ import ( "context" "encoding/json" "fmt" + "math/rand" "sort" "strings" sdkErr "cosmossdk.io/errors" wasmdparams "github.com/CosmWasm/wasmd/app/params" wasmdtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "github.com/babylonlabs-io/babylon/crypto/eots" cwconfig "github.com/babylonlabs-io/babylon/test/e2e/bcd_consumer_integration/clientcontroller/config" "github.com/babylonlabs-io/babylon/test/e2e/bcd_consumer_integration/clientcontroller/types" + "github.com/babylonlabs-io/babylon/testutil/datagen" bbntypes "github.com/babylonlabs-io/babylon/types" cwcclient "github.com/babylonlabs-io/cosmwasm-client/client" "github.com/btcsuite/btcd/btcec/v2" @@ -80,7 +83,7 @@ func (wc *CosmwasmConsumerController) reliablySendMsgs(msgs []sdk.Msg, expectedE ) } -// CommitPubRandList commits a list of Schnorr public randomness via a MsgCommitPubRand to Babylon +// CommitPubRandList commits a list of Schnorr public randomness to contract deployed on Consumer Chain // it returns tx hash and error func (wc *CosmwasmConsumerController) CommitPubRandList( fpPk *btcec.PublicKey, @@ -116,28 +119,90 @@ func (wc *CosmwasmConsumerController) CommitPubRandList( return &types.TxResponse{TxHash: res.TxHash}, nil } -// SubmitFinalitySig submits the finality signature via a MsgAddVote to Babylon func (wc *CosmwasmConsumerController) SubmitFinalitySig( - fpPk *btcec.PublicKey, - block *types.BlockInfo, - pubRand *btcec.FieldVal, - proof []byte, // TODO: have a type for proof - sig *btcec.ModNScalar, + fpSK *btcec.PrivateKey, + fpBtcPk *btcec.PublicKey, + privateRand *eots.PrivateRand, + pubRand *bbntypes.SchnorrPubRand, + proof *cmtcrypto.Proof, + heightToVote int64, ) (*types.TxResponse, error) { - cmtProof := cmtcrypto.Proof{} - if err := cmtProof.Unmarshal(proof); err != nil { + block, err := wc.GetCometBlock(heightToVote) + if err != nil { return nil, err } + msgToSign := append(sdk.Uint64ToBigEndian(uint64(heightToVote)), block.Block.AppHash...) + sig, err := eots.Sign(fpSK, privateRand, msgToSign) + if err != nil { + return nil, err + } + eotsSig := bbntypes.NewSchnorrEOTSSigFromModNScalar(sig) + + submitFinalitySig := &SubmitFinalitySignature{ + FpPubkeyHex: bbntypes.NewBIP340PubKeyFromBTCPK(fpBtcPk).MarshalHex(), + Height: uint64(heightToVote), + PubRand: pubRand.MustMarshal(), + Proof: Proof{ + Total: proof.Total, + Index: proof.Index, + LeafHash: proof.LeafHash, + Aunts: proof.Aunts, + }, + BlockHash: block.Block.AppHash, + Signature: eotsSig.MustMarshal(), + } + msg := ExecMsg{ - SubmitFinalitySignature: &SubmitFinalitySignature{ - FpPubkeyHex: bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex(), - Height: block.Height, - PubRand: bbntypes.NewSchnorrPubRandFromFieldVal(pubRand).MustMarshal(), - Proof: cmtProof, - BlockHash: block.Hash, - Signature: bbntypes.NewSchnorrEOTSSigFromModNScalar(sig).MustMarshal(), + SubmitFinalitySignature: submitFinalitySig, + } + + msgBytes, err := json.Marshal(msg) + if err != nil { + return nil, err + } + + res, err := wc.ExecuteContract(msgBytes) + if err != nil { + return nil, err + } + + return &types.TxResponse{TxHash: res.TxHash, Events: fromCosmosEventsToBytes(res.Events)}, nil +} + +func (wc *CosmwasmConsumerController) SubmitInvalidFinalitySig( + r *rand.Rand, + fpSK *btcec.PrivateKey, + fpBtcPk *btcec.PublicKey, + privateRand *eots.PrivateRand, + pubRand *bbntypes.SchnorrPubRand, + proof *cmtcrypto.Proof, + heightToVote int64, +) (*types.TxResponse, error) { + invalidAppHash := datagen.GenRandomByteArray(r, 32) + invalidMsgToSign := append(sdk.Uint64ToBigEndian(uint64(heightToVote)), invalidAppHash...) + invalidSig, err := eots.Sign(fpSK, privateRand, invalidMsgToSign) + if err != nil { + return nil, err + } + invalidEotsSig := bbntypes.NewSchnorrEOTSSigFromModNScalar(invalidSig) + + submitFinalitySig := &SubmitFinalitySignature{ + FpPubkeyHex: bbntypes.NewBIP340PubKeyFromBTCPK(fpBtcPk).MarshalHex(), + Height: uint64(heightToVote), + PubRand: pubRand.MustMarshal(), + Proof: Proof{ + Total: proof.Total, + Index: proof.Index, + LeafHash: proof.LeafHash, + Aunts: proof.Aunts, }, + BlockHash: invalidAppHash, + Signature: invalidEotsSig.MustMarshal(), + } + + msg := ExecMsg{ + SubmitFinalitySignature: submitFinalitySig, } msgBytes, err := json.Marshal(msg) @@ -173,9 +238,14 @@ func (wc *CosmwasmConsumerController) SubmitBatchFinalitySigs( FpPubkeyHex: bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex(), Height: b.Height, PubRand: bbntypes.NewSchnorrPubRandFromFieldVal(pubRandList[i]).MustMarshal(), - Proof: cmtProof, - BlockHash: b.Hash, - Signature: bbntypes.NewSchnorrEOTSSigFromModNScalar(sigs[i]).MustMarshal(), + Proof: Proof{ + Total: cmtProof.Total, + Index: cmtProof.Index, + LeafHash: cmtProof.LeafHash, + Aunts: cmtProof.Aunts, + }, + BlockHash: b.Hash, + Signature: bbntypes.NewSchnorrEOTSSigFromModNScalar(sigs[i]).MustMarshal(), }, } @@ -231,6 +301,41 @@ func (wc *CosmwasmConsumerController) QueryFinalityProviderHasPower( return resp.Power > 0, nil } +func (wc *CosmwasmConsumerController) QueryFinalityProviderInfo( + fpPk *btcec.PublicKey, + opts ...uint64, +) (*ConsumerFpInfoResponse, error) { + fpBtcPkHex := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex() + + queryMsgStruct := QueryMsgFinalityProviderInfo{ + FinalityProviderInfo: FinalityProviderInfo{ + BtcPkHex: fpBtcPkHex, + }, + } + + if len(opts) > 0 { + queryMsgStruct.FinalityProviderInfo.Height = opts[0] + } + + queryMsgBytes, err := json.Marshal(queryMsgStruct) + if err != nil { + return nil, fmt.Errorf("failed to marshal query message: %v", err) + } + + dataFromContract, err := wc.QuerySmartContractState(wc.cfg.BtcStakingContractAddress, string(queryMsgBytes)) + if err != nil { + return nil, err + } + + var resp ConsumerFpInfoResponse + err = json.Unmarshal(dataFromContract.Data, &resp) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %v", err) + } + + return &resp, nil +} + func (wc *CosmwasmConsumerController) QueryFinalityProvidersByPower() (*ConsumerFpsByPowerResponse, error) { queryMsgStruct := QueryMsgFinalityProvidersByPower{ FinalityProvidersByPower: struct{}{}, @@ -424,6 +529,32 @@ func (wc *CosmwasmConsumerController) QueryFinalityProviders() (*ConsumerFpsResp return &resp, nil } +func (wc *CosmwasmConsumerController) QueryFinalityProvider(btcPkHex string) (*SingleConsumerFpResponse, error) { + queryMsgStruct := QueryMsgFinalityProvider{ + FinalityProvider: FinalityProviderQuery{ + BtcPkHex: btcPkHex, + }, + } + + queryMsgBytes, err := json.Marshal(queryMsgStruct) + if err != nil { + return nil, fmt.Errorf("failed to marshal query message: %v", err) + } + + dataFromContract, err := wc.QuerySmartContractState(wc.cfg.BtcStakingContractAddress, string(queryMsgBytes)) + if err != nil { + return nil, err + } + + var resp SingleConsumerFpResponse + err = json.Unmarshal(dataFromContract.Data, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + func (wc *CosmwasmConsumerController) QueryDelegations() (*ConsumerDelegationsResponse, error) { queryMsgStruct := QueryMsgDelegations{ Delegations: struct{}{}, @@ -613,6 +744,12 @@ func (wc *CosmwasmConsumerController) GetCometNodeStatus() (*coretypes.ResultSta return wc.cwClient.GetStatus() } +// GetCometBlock gets the tendermint block at a given height +// NOTE: this function is only meant to be used in tests. +func (wc *CosmwasmConsumerController) GetCometBlock(height int64) (*coretypes.ResultBlock, error) { + return wc.cwClient.GetBlock(height) +} + // QueryIndexedBlock queries the indexed block at a given height // NOTE: this function is only meant to be used in tests. func (wc *CosmwasmConsumerController) QueryIndexedBlock(height uint64) (*IndexedBlock, error) { diff --git a/test/e2e/bcd_consumer_integration/clientcontroller/cosmwasm/msg.go b/test/e2e/bcd_consumer_integration/clientcontroller/cosmwasm/msg.go index 48da2d873..639686da5 100644 --- a/test/e2e/bcd_consumer_integration/clientcontroller/cosmwasm/msg.go +++ b/test/e2e/bcd_consumer_integration/clientcontroller/cosmwasm/msg.go @@ -1,7 +1,5 @@ package cosmwasm -import cmtcrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" - type ConsumerFpsResponse struct { Fps []SingleConsumerFpResponse `json:"fps"` } @@ -12,10 +10,10 @@ type ConsumerFpsResponse struct { // https://github.com/babylonchain/babylon-contract/blob/v0.5.3/contracts/btc-staking/src/msg.rs // https://github.com/babylonchain/babylon-contract/blob/v0.5.3/contracts/btc-staking/schema/btc-staking.json type SingleConsumerFpResponse struct { - BtcPkHex string `json:"btc_pk_hex"` - SlashedBabylonHeight uint64 `json:"slashed_babylon_height"` - SlashedBtcHeight uint64 `json:"slashed_btc_height"` - ConsumerId string `json:"consumer_id"` + BtcPkHex string `json:"btc_pk_hex"` + SlashedHeight uint64 `json:"slashed_height"` + SlashedBtcHeight uint64 `json:"slashed_btc_height"` + ConsumerId string `json:"consumer_id"` } type ConsumerDelegationsResponse struct { @@ -142,13 +140,20 @@ type CommitPublicRandomness struct { Signature []byte `json:"signature"` } +type Proof struct { + Total int64 `json:"total"` + Index int64 `json:"index"` + LeafHash []byte `json:"leaf_hash"` + Aunts [][]byte `json:"aunts"` +} + type SubmitFinalitySignature struct { - FpPubkeyHex string `json:"fp_pubkey_hex"` - Height uint64 `json:"height"` - PubRand []byte `json:"pub_rand"` - Proof cmtcrypto.Proof `json:"proof"` // nested struct - BlockHash []byte `json:"block_hash"` - Signature []byte `json:"signature"` + FpPubkeyHex string `json:"fp_pubkey_hex"` + Height uint64 `json:"height"` + PubRand []byte `json:"pub_rand"` + Proof Proof `json:"proof"` // nested struct + BlockHash []byte `json:"block_hash"` + Signature []byte `json:"signature"` } type ExecMsg struct { @@ -159,7 +164,7 @@ type ExecMsg struct { type FinalityProviderInfo struct { BtcPkHex string `json:"btc_pk_hex"` - Height uint64 `json:"height"` + Height uint64 `json:"height,omitempty"` } type QueryMsgFinalityProviderInfo struct { @@ -202,6 +207,14 @@ type QueryMsgFinalityProviders struct { FinalityProviders struct{} `json:"finality_providers"` } +type QueryMsgFinalityProvider struct { + FinalityProvider FinalityProviderQuery `json:"finality_provider"` +} + +type FinalityProviderQuery struct { + BtcPkHex string `json:"btc_pk_hex"` +} + type QueryMsgDelegations struct { Delegations struct{} `json:"delegations"` } diff --git a/test/e2e/bcd_consumer_integration_test.go b/test/e2e/bcd_consumer_integration_test.go index 5a54383ae..8a60ff393 100644 --- a/test/e2e/bcd_consumer_integration_test.go +++ b/test/e2e/bcd_consumer_integration_test.go @@ -42,9 +42,12 @@ import ( ) var ( - MinCommissionRate = sdkmath.LegacyNewDecWithPrec(5, 2) // 5% - babylonFpBTCSK, babylonFpBTCPK, _ = datagen.GenRandomBTCKeyPair(r) - randListInfo *datagen.RandListInfo + minCommissionRate = sdkmath.LegacyNewDecWithPrec(5, 2) // 5% + babylonFpBTCSK, babylonFpBTCPK, _ = datagen.GenRandomBTCKeyPair(r) + babylonFpBTCSK2, babylonFpBTCPK2, _ = datagen.GenRandomBTCKeyPair(r) + randListInfo1 *datagen.RandListInfo + // TODO: get consumer id from ibc client-state query + consumerID = "07-tendermint-0" ) type BCDConsumerIntegrationTestSuite struct { @@ -117,9 +120,7 @@ func (s *BCDConsumerIntegrationTestSuite) Test1ChainStartup() { // 2. Checks that the consumer is automatically registered in Babylon's consumer registry // 3. Validates the consumer registration details in Babylon func (s *BCDConsumerIntegrationTestSuite) Test2AutoRegisterAndVerifyNewConsumer() { - // TODO: getting some error in ibc client-state, hardcode consumer id for now - consumerID := "07-tendermint-0" // s.getIBCClientID() - s.verifyConsumerRegistration(consumerID) + s.verifyConsumerRegistration() } // Test3CreateConsumerFinalityProvider @@ -127,13 +128,11 @@ func (s *BCDConsumerIntegrationTestSuite) Test2AutoRegisterAndVerifyNewConsumer( // 2. Babylon automatically sends IBC packets to the consumer chain to transmit this data. // 3. Verifies that the registered consumer FPs in Babylon match the data stored in the consumer chain's contract. func (s *BCDConsumerIntegrationTestSuite) Test3CreateConsumerFinalityProvider() { - consumerID := "07-tendermint-0" - // generate a random number of finality providers from 1 to 5 numConsumerFPs := datagen.RandomInt(r, 5) + 1 var consumerFps []*bstypes.FinalityProvider for i := 0; i < int(numConsumerFPs); i++ { - consumerFp := s.createVerifyConsumerFP(consumerID) + consumerFp, _, _ := s.createVerifyConsumerFP() consumerFps = append(consumerFps, consumerFp) } @@ -151,7 +150,7 @@ func (s *BCDConsumerIntegrationTestSuite) Test3CreateConsumerFinalityProvider() fpFromMap, ok := fpMap[czFp.BtcPkHex] s.True(ok) s.Equal(fpFromMap.BtcPk.MarshalHex(), czFp.BtcPkHex) - s.Equal(fpFromMap.SlashedBabylonHeight, czFp.SlashedBabylonHeight) + s.Equal(fpFromMap.SlashedBabylonHeight, czFp.SlashedHeight) s.Equal(fpFromMap.SlashedBtcHeight, czFp.SlashedBtcHeight) s.Equal(fpFromMap.ConsumerId, czFp.ConsumerId) } @@ -161,14 +160,15 @@ func (s *BCDConsumerIntegrationTestSuite) Test3CreateConsumerFinalityProvider() // 1. Creates a Babylon finality provider // 2. Creates a pending state delegation restaking to both Babylon FP and 1 consumer FP func (s *BCDConsumerIntegrationTestSuite) Test4RestakeDelegationToMultipleFPs() { - consumerID := "07-tendermint-0" - consumerFps, err := s.babylonController.QueryConsumerFinalityProviders(consumerID) s.Require().NoError(err) consumerFp := consumerFps[0] // register a babylon finality provider - babylonFp := s.createBabylonFPWithFinalizedPubRand() + babylonFp := s.createVerifyBabylonFP(babylonFpBTCSK) + // commit and finalize pub rand so Babylon FP has voting power + randList := s.commitAndFinalizePubRand(babylonFpBTCSK, babylonFpBTCPK, uint64(1)) + randListInfo1 = randList // create a delegation and restake to both Babylon and consumer finality providers // NOTE: this will create delegation in pending state as covenant sigs are not provided @@ -205,10 +205,8 @@ func (s *BCDConsumerIntegrationTestSuite) Test4RestakeDelegationToMultipleFPs() // 4. Verifies the delegation details in the consumer chain contract match Babylon // 5. Confirms the consumer FP voting power equals the total stake amount func (s *BCDConsumerIntegrationTestSuite) Test5ActivateDelegation() { - consumerId := "07-tendermint-0" - // Query consumer finality providers - consumerFps, err := s.babylonController.QueryConsumerFinalityProviders(consumerId) + consumerFps, err := s.babylonController.QueryConsumerFinalityProviders(consumerID) s.Require().NoError(err) s.Require().NotEmpty(consumerFps) consumerFp := consumerFps[0] @@ -250,29 +248,13 @@ func (s *BCDConsumerIntegrationTestSuite) Test5ActivateDelegation() { // Query and assert finality provider voting power is equal to the total stake s.Eventually(func() bool { - fpsByPower, err := s.cosmwasmController.QueryFinalityProvidersByPower() + fpInfo, err := s.cosmwasmController.QueryFinalityProviderInfo(consumerFp.BtcPk.MustToBTCPK()) if err != nil { - s.T().Logf("Error querying finality providers by power: %v", err) - return false - } - if fpsByPower == nil || len(fpsByPower.Fps) == 0 { + s.T().Logf("Error querying finality provider info: %v", err) return false } - // Create a map of BTC public keys to ConsumerFpInfoResponse - fpMap := make(map[string]cosmwasm.ConsumerFpInfoResponse) - for _, fp := range fpsByPower.Fps { - fpMap[fp.BtcPkHex] = fp - } - - // Check if the consumerFp's BTC public key exists in the map - consumerFpBtcPkHex := consumerFp.BtcPk.MarshalHex() - fpInfo, exists := fpMap[consumerFpBtcPkHex] - if !exists { - return false - } - - return fpInfo.BtcPkHex == consumerFp.BtcPk.MarshalHex() && fpInfo.Power == activeDel.TotalSat + return fpInfo != nil && fpInfo.Power == activeDel.TotalSat && fpInfo.BtcPkHex == consumerFp.BtcPk.MarshalHex() }, time.Minute, time.Second*5) } @@ -300,16 +282,15 @@ func (s *BCDConsumerIntegrationTestSuite) Test6BabylonFPCascadedSlashing() { s.NotNil(babylonFp) babylonFpBIP340PK := bbntypes.NewBIP340PubKeyFromBTCPK(babylonFpBTCPK) - - randIdx := activatedHeight.Height - 1 + randIdx := activatedHeight.Height - 1 // pub rand was committed from height 1-100 // submit finality signature txResp, err := s.babylonController.SubmitFinalitySignature( babylonFpBTCSK, babylonFpBIP340PK, - randListInfo.SRList[randIdx], - &randListInfo.PRList[randIdx], - randListInfo.ProofList[randIdx].ToProto(), + randListInfo1.SRList[randIdx], + &randListInfo1.PRList[randIdx], + randListInfo1.ProofList[randIdx].ToProto(), activatedHeight.Height) s.NoError(err) s.NotNil(txResp) @@ -339,62 +320,183 @@ func (s *BCDConsumerIntegrationTestSuite) Test6BabylonFPCascadedSlashing() { r, babylonFpBTCSK, babylonFpBIP340PK, - randListInfo.SRList[randIdx], - &randListInfo.PRList[randIdx], - randListInfo.ProofList[randIdx].ToProto(), + randListInfo1.SRList[randIdx], + &randListInfo1.PRList[randIdx], + randListInfo1.ProofList[randIdx].ToProto(), activatedHeight.Height, ) s.NoError(err) s.NotNil(txResp) - // check the finality provider is slashed on Babylon + // check the babylon finality provider is slashed + babylonFpBIP340PKHex := bbntypes.NewBIP340PubKeyFromBTCPK(babylonFpBTCPK).MarshalHex() s.Eventually(func() bool { - slashed, err := s.babylonController.QueryFinalityProviderSlashed(babylonFpBTCPK) + fp, err := s.babylonController.QueryFinalityProvider(babylonFpBIP340PKHex) if err != nil { - s.T().Logf("Error querying finality provider slashed status: %v", err) + s.T().Logf("Error querying finality provider: %v", err) return false } - return slashed + return fp != nil && + fp.FinalityProvider.SlashedBtcHeight > 0 && + fp.FinalityProvider.VotingPower == 0 }, time.Minute, time.Second*5) - consumerId := "07-tendermint-0" - // query consumer finality providers - consumerFps, err := s.babylonController.QueryConsumerFinalityProviders(consumerId) + consumerFps, err := s.babylonController.QueryConsumerFinalityProviders(consumerID) s.Require().NoError(err) s.Require().NotEmpty(consumerFps) consumerFp := consumerFps[0] // query and assert finality provider voting power is zero after slashing s.Eventually(func() bool { - fpsByPower, err := s.cosmwasmController.QueryFinalityProvidersByPower() + fpInfo, err := s.cosmwasmController.QueryFinalityProviderInfo(consumerFp.BtcPk.MustToBTCPK()) if err != nil { s.T().Logf("Error querying finality providers by power: %v", err) return false } - if fpsByPower == nil || len(fpsByPower.Fps) == 0 { + + return fpInfo != nil && fpInfo.Power == 0 && fpInfo.BtcPkHex == consumerFp.BtcPk.MarshalHex() + }, time.Minute, time.Second*5) +} + +func (s *BCDConsumerIntegrationTestSuite) Test7ConsumerFPCascadedSlashing() { + // create a new consumer finality provider + resp, czFpBTCSK, czFpBTCPK := s.createVerifyConsumerFP() + consumerFp, err := s.babylonController.QueryConsumerFinalityProvider(consumerID, resp.BtcPk.MarshalHex()) + s.NoError(err) + + // register a babylon finality provider + babylonFp := s.createVerifyBabylonFP(babylonFpBTCSK2) + + // create a new delegation and restake to both Babylon and consumer finality provider + // NOTE: this will create delegation in pending state as covenant sigs are not provided + _, stakingTxHash := s.createBabylonDelegation(babylonFp, consumerFp) + + // check delegation + delegation, err := s.babylonController.QueryBTCDelegation(stakingTxHash) + s.Require().NoError(err) + s.NotNil(delegation) + + // activate the delegation by submitting covenant sigs + s.submitCovenantSigs(consumerFp) + + // query the staking contract for delegations on the consumer chain + var dataFromContract *cosmwasm.ConsumerDelegationsResponse + s.Eventually(func() bool { + dataFromContract, err = s.cosmwasmController.QueryDelegations() + return err == nil && dataFromContract != nil && len(dataFromContract.Delegations) == 2 + }, time.Second*20, time.Second) + + // query and assert consumer finality provider's voting power is equal to the total stake + s.Eventually(func() bool { + fpInfo, err := s.cosmwasmController.QueryFinalityProviderInfo(consumerFp.BtcPk.MustToBTCPK()) + if err != nil { + s.T().Logf("Error querying finality provider info: %v", err) + return false + } + + return fpInfo != nil && fpInfo.Power == delegation.TotalSat && fpInfo.BtcPkHex == consumerFp.BtcPk.MarshalHex() + }, time.Minute, time.Second*5) + + // get the latest block height and block on the consumer chain + czNodeStatus, err := s.cosmwasmController.GetCometNodeStatus() + s.NoError(err) + s.NotNil(czNodeStatus) + czlatestBlockHeight := czNodeStatus.SyncInfo.LatestBlockHeight + czLatestBlock, err := s.cosmwasmController.QueryIndexedBlock(uint64(czlatestBlockHeight)) + s.NoError(err) + s.NotNil(czLatestBlock) + + // commit public randomness at the latest block height on the consumer chain + randListInfo, msgCommitPubRandList, err := datagen.GenRandomMsgCommitPubRandList(r, czFpBTCSK, uint64(czlatestBlockHeight), 100) + s.NoError(err) + + // submit the public randomness to the consumer chain + txResp, err := s.cosmwasmController.CommitPubRandList(czFpBTCPK, uint64(czlatestBlockHeight), 100, randListInfo.Commitment, msgCommitPubRandList.Sig.MustToBTCSig()) + s.NoError(err) + s.NotNil(txResp) + + // consumer finality provider submits finality signature + txResp, err = s.cosmwasmController.SubmitFinalitySig( + czFpBTCSK, + czFpBTCPK, + randListInfo.SRList[0], + &randListInfo.PRList[0], + randListInfo.ProofList[0].ToProto(), + czlatestBlockHeight, + ) + s.NoError(err) + s.NotNil(txResp) + + // ensure consumer finality provider's finality signature is received and stored in the smart contract + s.Eventually(func() bool { + fpSigsResponse, err := s.cosmwasmController.QueryFinalitySignature(consumerFp.BtcPk.MarshalHex(), uint64(czlatestBlockHeight)) + if err != nil { + s.T().Logf("failed to query finality signature: %s", err.Error()) + return false + } + if fpSigsResponse == nil || fpSigsResponse.Signature == nil || len(fpSigsResponse.Signature) == 0 { return false } + return true + }, time.Minute, time.Second*5) + + // consumer finality provider submits invalid finality signature + txResp, err = s.cosmwasmController.SubmitInvalidFinalitySig( + r, + czFpBTCSK, + czFpBTCPK, + randListInfo.SRList[0], + &randListInfo.PRList[0], + randListInfo.ProofList[0].ToProto(), + czlatestBlockHeight, + ) + s.NoError(err) + s.NotNil(txResp) + + // ensure consumer finality provider is slashed + s.Eventually(func() bool { + fp, err := s.cosmwasmController.QueryFinalityProvider(consumerFp.BtcPk.MarshalHex()) + return err == nil && fp != nil && fp.SlashedHeight > 0 + }, time.Minute, time.Second*5) - // create a map of BTC public keys to ConsumerFpInfoResponse - fpMap := make(map[string]cosmwasm.ConsumerFpInfoResponse) - for _, fp := range fpsByPower.Fps { - fpMap[fp.BtcPkHex] = fp + // query and assert consumer finality provider's voting power is zero after slashing + s.Eventually(func() bool { + fpInfo, err := s.cosmwasmController.QueryFinalityProviderInfo(consumerFp.BtcPk.MustToBTCPK()) + if err != nil { + s.T().Logf("Error querying finality providers by power: %v", err) + return false } - // check if the consumerFp's BTC public key exists in the map - consumerFpBtcPkHex := consumerFp.BtcPk.MarshalHex() - fpInfo, exists := fpMap[consumerFpBtcPkHex] - if !exists { + return fpInfo != nil && fpInfo.Power == 0 && fpInfo.BtcPkHex == consumerFp.BtcPk.MarshalHex() + }, time.Minute, time.Second*5) + + // check the babylon finality provider's voting power is discounted (cascaded slashing) + babylonFpBIP340PKHex := bbntypes.NewBIP340PubKeyFromBTCPK(babylonFpBTCPK2).MarshalHex() + s.Eventually(func() bool { + fp, err := s.babylonController.QueryFinalityProvider(babylonFpBIP340PKHex) + if err != nil { + s.T().Logf("Error querying finality provider: %v", err) return false } + return fp != nil && + fp.FinalityProvider.VotingPower == 0 && // as there is only 1 delegation which got slashed + fp.FinalityProvider.SlashedBtcHeight == 0 // should not be slashed + }, time.Minute, time.Second*5) - return fpInfo.BtcPkHex == consumerFp.BtcPk.MarshalHex() && fpInfo.Power == 0 + // check consumer FP record in Babylon is updated + consumerFpBIP340PKHex := consumerFp.BtcPk.MarshalHex() + s.Eventually(func() bool { + fp, err := s.babylonController.QueryFinalityProvider(consumerFpBIP340PKHex) + if err != nil { + s.T().Logf("Error querying finality provider: %v", err) + return false + } + return fp != nil && + fp.FinalityProvider.SlashedBtcHeight > 0 // should be recorded slashed }, time.Minute, time.Second*5) } -// TODO: Test7: Consumer FP cascaded slashing - // helper function: submitCovenantSigs submits the covenant signatures to activate the BTC delegation func (s *BCDConsumerIntegrationTestSuite) submitCovenantSigs(consumerFp *bsctypes.FinalityProviderResponse) { cvSK, _, _, err := getDeterministicCovenantKey() @@ -500,26 +602,19 @@ func (s *BCDConsumerIntegrationTestSuite) submitCovenantSigs(consumerFp *bsctype activeDel := activeDels.Dels[0] s.True(activeDel.HasCovenantQuorums(1)) - // eventually, the finality provider has voting power + // ensure BTC staking is activated s.Eventually(func() bool { - status, err := s.babylonController.QueryNodeStatus() - s.NoError(err) - height := uint64(status.SyncInfo.LatestBlockHeight) - - hasPower, err := s.babylonController.QueryFinalityProviderHasPower(babylonFpBTCPK, height) + activatedHeight, err := s.babylonController.QueryActivatedHeight() if err != nil { - s.T().Logf("Error querying voting power at height: %v", err) + s.T().Logf("Error querying activated height: %v", err) return false } - return hasPower - }, time.Minute, time.Second*1, "Voting power was not greater than 0 within the expected time") - - // ensure BTC staking is activated - activatedHeight, err := s.babylonController.QueryActivatedHeight() - s.NoError(err) - s.Positive(activatedHeight.Height) - - s.T().Logf("Activated height: %v", activatedHeight.Height) + if activatedHeight == nil { + s.T().Log("Activated height is nil") + return false + } + return activatedHeight.Height > 0 + }, time.Minute, time.Second*15, "BTC staking was not activated within the expected time") } // helper function: createBabylonDelegation creates a random BTC delegation restaking to Babylon and consumer finality providers @@ -642,8 +737,8 @@ func (s *BCDConsumerIntegrationTestSuite) createBabylonDelegation(babylonFp *bst return czDelBtcPk, stakingTxHash } -// helper function: createBabylonFPWithFinalizedPubRand creates a random Babylon finality provider, commits some public randomness, and finalise these public randomness -func (s *BCDConsumerIntegrationTestSuite) createBabylonFPWithFinalizedPubRand() *bstypes.FinalityProviderResponse { +// helper function: createVerifyBabylonFP creates a random Babylon finality provider and verifies it +func (s *BCDConsumerIntegrationTestSuite) createVerifyBabylonFP(babylonFpBTCSK *btcec.PrivateKey) *bstypes.FinalityProviderResponse { // NOTE: we use the node's secret key as Babylon secret key for the finality provider // babylonFpBTCSK, _, _ := datagen.GenRandomBTCKeyPair(r) sdk.SetAddrCacheEnabled(false) @@ -652,7 +747,7 @@ func (s *BCDConsumerIntegrationTestSuite) createBabylonFPWithFinalizedPubRand() s.NoError(err) babylonFp, err := datagen.GenCustomFinalityProvider(r, babylonFpBTCSK, fpBabylonAddr, "") s.NoError(err) - babylonFp.Commission = &MinCommissionRate + babylonFp.Commission = &minCommissionRate bbnFpPop, err := babylonFp.Pop.Marshal() s.NoError(err) bbnDescription, err := babylonFp.Description.Marshal() @@ -667,63 +762,65 @@ func (s *BCDConsumerIntegrationTestSuite) createBabylonFPWithFinalizedPubRand() ) s.NoError(err) - // query the existence of finality provider and assert equivalence - actualFps, err := s.babylonController.QueryFinalityProviders() - s.Require().NoError(err) - s.Len(actualFps, 1) - s.Equal(babylonFp.Description, actualFps[0].Description) - s.Equal(babylonFp.Commission, actualFps[0].Commission) - s.Equal(babylonFp.BtcPk, actualFps[0].BtcPk) - s.Equal(babylonFp.Pop, actualFps[0].Pop) - s.Equal(babylonFp.SlashedBabylonHeight, actualFps[0].SlashedBabylonHeight) - s.Equal(babylonFp.SlashedBtcHeight, actualFps[0].SlashedBtcHeight) + actualFp, err := s.babylonController.QueryFinalityProvider(babylonFp.BtcPk.MarshalHex()) + s.NoError(err) + s.Equal(babylonFp.Description, actualFp.FinalityProvider.Description) + s.Equal(babylonFp.Commission, actualFp.FinalityProvider.Commission) + s.Equal(babylonFp.BtcPk, actualFp.FinalityProvider.BtcPk) + s.Equal(babylonFp.Pop, actualFp.FinalityProvider.Pop) + s.Equal(babylonFp.SlashedBabylonHeight, actualFp.FinalityProvider.SlashedBabylonHeight) + s.Equal(babylonFp.SlashedBtcHeight, actualFp.FinalityProvider.SlashedBtcHeight) + return actualFp.FinalityProvider +} +// helper function: commitAndFinalizePubRand commits public randomness at the given start height and finalizes it +func (s *BCDConsumerIntegrationTestSuite) commitAndFinalizePubRand(babylonFpBTCSK *btcec.PrivateKey, babylonFpBTCPK *btcec.PublicKey, commitStartHeight uint64) *datagen.RandListInfo { // commit public randomness list numPubRand := uint64(100) - commitStartHeight := uint64(1) randList, msgCommitPubRandList, err := datagen.GenRandomMsgCommitPubRandList(r, babylonFpBTCSK, commitStartHeight, numPubRand) s.NoError(err) - randListInfo = randList + _, err = s.babylonController.CommitPublicRandomness(msgCommitPubRandList) s.NoError(err) - // get public randomness commit - pubRandCommitMap, err := s.babylonController.QueryLastCommittedPublicRand(babylonFp.BtcPk.MustToBTCPK(), 1) + + pubRandCommitMap, err := s.babylonController.QueryLastCommittedPublicRand(babylonFpBTCPK, commitStartHeight) s.NoError(err) s.Len(pubRandCommitMap, 1) + var firstPubRandCommit *ftypes.PubRandCommitResponse for _, commit := range pubRandCommitMap { firstPubRandCommit = commit break } + commitEpoch := firstPubRandCommit.EpochNum // finalise until the epoch of the first public randomness commit s.finalizeUntilEpoch(commitEpoch) - - return actualFps[0] + return randList } // helper function: createVerifyConsumerFP creates a random consumer finality provider on Babylon // and verifies its existence. -func (s *BCDConsumerIntegrationTestSuite) createVerifyConsumerFP(consumerId string) *bstypes.FinalityProvider { +func (s *BCDConsumerIntegrationTestSuite) createVerifyConsumerFP() (*bstypes.FinalityProvider, *btcec.PrivateKey, *btcec.PublicKey) { /* create a random consumer finality provider on Babylon */ // NOTE: we use the node's secret key as Babylon secret key for the finality provider - czFpBTCSK, _, _ := datagen.GenRandomBTCKeyPair(r) + czFpBTCSK, czFpBTCPK, _ := datagen.GenRandomBTCKeyPair(r) sdk.SetAddrCacheEnabled(false) bbnparams.SetAddressPrefixes() fpBabylonAddr, err := sdk.AccAddressFromBech32(s.babylonController.MustGetTxSigner()) s.NoError(err) - czFp, err := datagen.GenCustomFinalityProvider(r, czFpBTCSK, fpBabylonAddr, consumerId) + czFp, err := datagen.GenCustomFinalityProvider(r, czFpBTCSK, fpBabylonAddr, consumerID) s.NoError(err) - czFp.Commission = &MinCommissionRate + czFp.Commission = &minCommissionRate czFpPop, err := czFp.Pop.Marshal() s.NoError(err) czDescription, err := czFp.Description.Marshal() s.NoError(err) _, err = s.babylonController.RegisterFinalityProvider( - consumerId, + consumerID, czFp.BtcPk, czFpPop, czFp.Commission, @@ -732,7 +829,7 @@ func (s *BCDConsumerIntegrationTestSuite) createVerifyConsumerFP(consumerId stri s.NoError(err) // query the existence of finality provider and assert equivalence - actualFp, err := s.babylonController.QueryConsumerFinalityProvider(consumerId, czFp.BtcPk.MarshalHex()) + actualFp, err := s.babylonController.QueryConsumerFinalityProvider(consumerID, czFp.BtcPk.MarshalHex()) s.NoError(err) s.Equal(czFp.Description, actualFp.Description) s.Equal(czFp.Commission.String(), actualFp.Commission.String()) @@ -740,16 +837,14 @@ func (s *BCDConsumerIntegrationTestSuite) createVerifyConsumerFP(consumerId stri s.Equal(czFp.Pop, actualFp.Pop) s.Equal(czFp.SlashedBabylonHeight, actualFp.SlashedBabylonHeight) s.Equal(czFp.SlashedBtcHeight, actualFp.SlashedBtcHeight) - s.Equal(consumerId, actualFp.ConsumerId) - return czFp + s.Equal(consumerID, actualFp.ConsumerId) + return czFp, czFpBTCSK, czFpBTCPK } // helper function: initBabylonController initializes the Babylon controller with the default configuration. func (s *BCDConsumerIntegrationTestSuite) initBabylonController() error { cfg := config.DefaultBabylonConfig() - btcParams := &chaincfg.RegressionNetParams // or whichever network you're using - logger, _ := zap.NewDevelopment() // Get the current working directory @@ -777,7 +872,17 @@ func (s *BCDConsumerIntegrationTestSuite) initBabylonController() error { // helper function: initCosmwasmController initializes the Cosmwasm controller with the default configuration. func (s *BCDConsumerIntegrationTestSuite) initCosmwasmController() error { cfg := cwconfig.DefaultCosmwasmConfig() + + // Get the current working directory + currentDir, err := os.Getwd() + if err != nil { + s.T().Fatalf("Failed to get current working directory: %v", err) + } + cfg.BtcStakingContractAddress = "bbnc1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrqgn0kq0" + cfg.ChainID = "bcd-test" + cfg.KeyDirectory = filepath.Join(currentDir, "../../contrib/images/ibcsim-bcd/.testnets/bcd/bcd-test") + cfg.AccountPrefix = "bbnc" // Create a logger logger, _ := zap.NewDevelopment() @@ -852,14 +957,13 @@ func (s *BCDConsumerIntegrationTestSuite) waitForIBCConnection() { // helper function: verifyConsumerRegistration verifies the automatic registration of a consumer // and returns the consumer details. -func (s *BCDConsumerIntegrationTestSuite) verifyConsumerRegistration(consumerID string) *bsctypes.ConsumerRegister { +func (s *BCDConsumerIntegrationTestSuite) verifyConsumerRegistration() *bsctypes.ConsumerRegister { var consumerRegistryResp *bsctypes.QueryConsumersRegistryResponse s.Eventually(func() bool { var err error consumerRegistryResp, err = s.babylonController.QueryConsumerRegistry(consumerID) if err != nil { - s.T().Logf("Error querying consumer registry: %v", err) return false } return consumerRegistryResp != nil && len(consumerRegistryResp.GetConsumersRegister()) == 1 diff --git a/test/e2e/bytecode/btc_staking.wasm b/test/e2e/bytecode/btc_staking.wasm index cecb4ac59..100b5b767 100644 Binary files a/test/e2e/bytecode/btc_staking.wasm and b/test/e2e/bytecode/btc_staking.wasm differ diff --git a/x/btcstaking/keeper/btc_delegators.go b/x/btcstaking/keeper/btc_delegators.go index 7030069d0..dc95c191f 100644 --- a/x/btcstaking/keeper/btc_delegators.go +++ b/x/btcstaking/keeper/btc_delegators.go @@ -60,8 +60,31 @@ func (k Keeper) getBTCDelegatorDelegations(ctx context.Context, fpBTCPK *bbn.BIP return &types.BTCDelegatorDelegations{Dels: btcDels} } -func (k Keeper) getFPBTCDelegations(ctx context.Context, fpBTCPK *bbn.BIP340PubKey) ([]*types.BTCDelegation, error) { - store := k.btcDelegatorFpStore(ctx, fpBTCPK) +// GetFPBTCDelegations retrieves all BTC delegations for a given finality provider. +// This function works for both Babylon finality providers and consumer finality providers. +// It automatically determines and selects the appropriate KV store based on the finality provider type. +// +// Parameters: +// - ctx: The context for the operation +// - fpBTCPK: The Bitcoin public key of the finality provider +// +// Returns: +// - A slice of BTCDelegation pointers representing all delegations for the given finality provider +// - An error if the finality provider is not found or if there's an issue retrieving the delegations +func (k Keeper) GetFPBTCDelegations(ctx context.Context, fpBTCPK *bbn.BIP340PubKey) ([]*types.BTCDelegation, error) { + var store prefix.Store + // Determine which store to use based on the finality provider type + if k.HasFinalityProvider(ctx, *fpBTCPK) { + // Babylon finality provider + store = k.btcDelegatorFpStore(ctx, fpBTCPK) + } else if k.bscKeeper.HasConsumerFinalityProvider(ctx, fpBTCPK) { + // Consumer finality provider + store = k.btcConsumerDelegatorStore(ctx, fpBTCPK) + } else { + // if not found in either store, return error + return nil, types.ErrFpNotFound + } + iterator := store.Iterator(nil, nil) defer iterator.Close() diff --git a/x/btcstaking/keeper/finality_providers.go b/x/btcstaking/keeper/finality_providers.go index 526a0c7cb..5507d8766 100644 --- a/x/btcstaking/keeper/finality_providers.go +++ b/x/btcstaking/keeper/finality_providers.go @@ -118,6 +118,46 @@ func (k Keeper) SlashFinalityProvider(ctx context.Context, fpBTCPK []byte) error return nil } +// SlashConsumerFinalityProvider slashes a consumer finality provider with the given PK +func (k Keeper) SlashConsumerFinalityProvider(ctx context.Context, consumerID string, fpBTCPK *bbn.BIP340PubKey) error { + // Get consumer finality provider + fp, err := k.bscKeeper.GetConsumerFinalityProvider(ctx, consumerID, fpBTCPK) + if err != nil { + return err + } + + // Return error if already slashed + if fp.IsSlashed() { + return types.ErrFpAlreadySlashed + } + + // Set slashed height + fp.SlashedBabylonHeight = uint64(sdk.UnwrapSDKContext(ctx).HeaderInfo().Height) + btcTip := k.btclcKeeper.GetTipInfo(ctx) + if btcTip == nil { + return fmt.Errorf("failed to get current BTC tip") + } + fp.SlashedBtcHeight = btcTip.Height + k.bscKeeper.SetConsumerFinalityProvider(ctx, fp) + + // Get all delegations for this consumer finality provider + btcDels, err := k.GetFPBTCDelegations(ctx, fpBTCPK) + if err != nil { + return fmt.Errorf("failed to get BTC delegations: %w", err) + } + + // Record slashed BTC delegation events for each affected delegation + // These events will be processed in the next `BeginBlock` to update + // the power distribution of the involved Babylon FPs + for _, btcDel := range btcDels { + stakingTxHash := btcDel.MustGetStakingTxHash().String() + eventSlashedBTCDelegation := types.NewEventPowerDistUpdateWithSlashedBTCDelegation(stakingTxHash) + k.addPowerDistUpdateEvent(ctx, btcTip.Height, eventSlashedBTCDelegation) + } + + return nil +} + // PropagateFPSlashingToConsumers propagates the slashing of a finality provider (FP) to all relevant consumer chains. // It processes all delegations associated with the given FP and creates slashing events for each affected consumer chain. // @@ -137,7 +177,7 @@ func (k Keeper) SlashFinalityProvider(ctx context.Context, fpBTCPK []byte) error // - An error if any operation fails, nil otherwise. func (k Keeper) PropagateFPSlashingToConsumers(ctx context.Context, fpBTCPK *bbn.BIP340PubKey) error { // Get all delegations for this finality provider - delegations, err := k.getFPBTCDelegations(ctx, fpBTCPK) + delegations, err := k.GetFPBTCDelegations(ctx, fpBTCPK) if err != nil { return err } diff --git a/x/btcstaking/keeper/power_dist_change.go b/x/btcstaking/keeper/power_dist_change.go index 51455c812..bd0602ae6 100644 --- a/x/btcstaking/keeper/power_dist_change.go +++ b/x/btcstaking/keeper/power_dist_change.go @@ -25,6 +25,7 @@ func (k Keeper) UpdatePowerDist(ctx context.Context) { // get the power dist cache in the last height dc := k.getVotingPowerDistCache(ctx, height-1) + // get all power distribution update events during the previous tip // and the current tip lastBTCTipHeight := k.GetBTCHeightAtBabylonHeight(ctx, height-1) @@ -162,6 +163,8 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents( jailedFPs := map[string]struct{}{} // a map where key is unjailed finality providers' BTC PK unjailedFPs := map[string]struct{}{} + // a map where key is slashed BTC delegation's staking tx hash + slashedBTCDels := map[string]struct{}{} /* filter and classify all events into new/expired BTC delegations and jailed/slashed FPs @@ -197,6 +200,9 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents( case *types.EventPowerDistUpdate_UnjailedFp: // record unjailed fps unjailedFPs[typedEvent.UnjailedFp.Pk.MarshalHex()] = struct{}{} + case *types.EventPowerDistUpdate_SlashedBtcDelegation: + // Add the slashed BTC delegation to the map + slashedBTCDels[typedEvent.SlashedBtcDelegation.StakingTxHash] = struct{}{} } } @@ -241,15 +247,26 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents( // add all BTC delegations that are not unbonded to the new finality provider for j := range dc.FinalityProviders[i].BtcDels { btcDel := *dc.FinalityProviders[i].BtcDels[j] - if _, ok := unbondedBTCDels[btcDel.StakingTxHash]; !ok { - fp.AddBTCDelDistInfo(&btcDel) + // skip this delegation if it's unbonded + if _, unbonded := unbondedBTCDels[btcDel.StakingTxHash]; unbonded { + continue + } + // skip this delegation if it's slashed + if _, slashed := slashedBTCDels[btcDel.StakingTxHash]; slashed { + continue } + // add the delegation info only if it's not unbonded and not slashed (i.e it's active) + fp.AddBTCDelDistInfo(&btcDel) } // process all new BTC delegations under this finality provider if fpActiveBTCDels, ok := activeBTCDels[fpBTCPKHex]; ok { // handle new BTC delegations for this finality provider for _, d := range fpActiveBTCDels { + // skip this delegation if it's slashed + if _, slashed := slashedBTCDels[d.MustGetStakingTxHash().String()]; slashed { + continue + } fp.AddBTCDel(d) } // remove the finality provider entry in activeBTCDels map, so that @@ -291,6 +308,10 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents( // add each BTC delegation fpActiveBTCDels := activeBTCDels[fpBTCPKHex] for _, d := range fpActiveBTCDels { + // skip this delegation if it's slashed + if _, slashed := slashedBTCDels[d.MustGetStakingTxHash().String()]; slashed { + continue + } fpDistInfo.AddBTCDel(d) } diff --git a/x/btcstaking/types/events.go b/x/btcstaking/types/events.go index 1c9022466..8494c3270 100644 --- a/x/btcstaking/types/events.go +++ b/x/btcstaking/types/events.go @@ -41,3 +41,13 @@ func NewEventPowerDistUpdateWithUnjailedFP(fpBTCPK *bbn.BIP340PubKey) *EventPowe }, } } + +func NewEventPowerDistUpdateWithSlashedBTCDelegation(stakingTxHash string) *EventPowerDistUpdate { + return &EventPowerDistUpdate{ + Ev: &EventPowerDistUpdate_SlashedBtcDelegation{ + SlashedBtcDelegation: &EventPowerDistUpdate_EventSlashedBTCDelegation{ + StakingTxHash: stakingTxHash, + }, + }, + } +} diff --git a/x/btcstaking/types/events.pb.go b/x/btcstaking/types/events.pb.go index bcd198fe0..8187a1cb2 100644 --- a/x/btcstaking/types/events.pb.go +++ b/x/btcstaking/types/events.pb.go @@ -187,6 +187,7 @@ type EventPowerDistUpdate struct { // *EventPowerDistUpdate_JailedFp // *EventPowerDistUpdate_UnjailedFp // *EventPowerDistUpdate_BtcDelStateUpdate + // *EventPowerDistUpdate_SlashedBtcDelegation Ev isEventPowerDistUpdate_Ev `protobuf_oneof:"ev"` } @@ -241,11 +242,15 @@ type EventPowerDistUpdate_UnjailedFp struct { type EventPowerDistUpdate_BtcDelStateUpdate struct { BtcDelStateUpdate *EventBTCDelegationStateUpdate `protobuf:"bytes,4,opt,name=btc_del_state_update,json=btcDelStateUpdate,proto3,oneof" json:"btc_del_state_update,omitempty"` } +type EventPowerDistUpdate_SlashedBtcDelegation struct { + SlashedBtcDelegation *EventPowerDistUpdate_EventSlashedBTCDelegation `protobuf:"bytes,5,opt,name=slashed_btc_delegation,json=slashedBtcDelegation,proto3,oneof" json:"slashed_btc_delegation,omitempty"` +} -func (*EventPowerDistUpdate_SlashedFp) isEventPowerDistUpdate_Ev() {} -func (*EventPowerDistUpdate_JailedFp) isEventPowerDistUpdate_Ev() {} -func (*EventPowerDistUpdate_UnjailedFp) isEventPowerDistUpdate_Ev() {} -func (*EventPowerDistUpdate_BtcDelStateUpdate) isEventPowerDistUpdate_Ev() {} +func (*EventPowerDistUpdate_SlashedFp) isEventPowerDistUpdate_Ev() {} +func (*EventPowerDistUpdate_JailedFp) isEventPowerDistUpdate_Ev() {} +func (*EventPowerDistUpdate_UnjailedFp) isEventPowerDistUpdate_Ev() {} +func (*EventPowerDistUpdate_BtcDelStateUpdate) isEventPowerDistUpdate_Ev() {} +func (*EventPowerDistUpdate_SlashedBtcDelegation) isEventPowerDistUpdate_Ev() {} func (m *EventPowerDistUpdate) GetEv() isEventPowerDistUpdate_Ev { if m != nil { @@ -282,6 +287,13 @@ func (m *EventPowerDistUpdate) GetBtcDelStateUpdate() *EventBTCDelegationStateUp return nil } +func (m *EventPowerDistUpdate) GetSlashedBtcDelegation() *EventPowerDistUpdate_EventSlashedBTCDelegation { + if x, ok := m.GetEv().(*EventPowerDistUpdate_SlashedBtcDelegation); ok { + return x.SlashedBtcDelegation + } + return nil +} + // XXX_OneofWrappers is for the internal use of the proto package. func (*EventPowerDistUpdate) XXX_OneofWrappers() []interface{} { return []interface{}{ @@ -289,6 +301,7 @@ func (*EventPowerDistUpdate) XXX_OneofWrappers() []interface{} { (*EventPowerDistUpdate_JailedFp)(nil), (*EventPowerDistUpdate_UnjailedFp)(nil), (*EventPowerDistUpdate_BtcDelStateUpdate)(nil), + (*EventPowerDistUpdate_SlashedBtcDelegation)(nil), } } @@ -336,6 +349,58 @@ func (m *EventPowerDistUpdate_EventSlashedFinalityProvider) XXX_DiscardUnknown() var xxx_messageInfo_EventPowerDistUpdate_EventSlashedFinalityProvider proto.InternalMessageInfo +// EventSlashedBTCDelegation is emitted for each BTC delegation that restakes to a slashed consumer finality provider. +// It indicates that the voting power of affected Babylon finality providers will be discounted for this delegation. +type EventPowerDistUpdate_EventSlashedBTCDelegation struct { + // staking_tx_hash is the hash of the staking tx. + // It uniquely identifies a BTC delegation + StakingTxHash string `protobuf:"bytes,1,opt,name=staking_tx_hash,json=stakingTxHash,proto3" json:"staking_tx_hash,omitempty"` +} + +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) Reset() { + *m = EventPowerDistUpdate_EventSlashedBTCDelegation{} +} +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) String() string { + return proto.CompactTextString(m) +} +func (*EventPowerDistUpdate_EventSlashedBTCDelegation) ProtoMessage() {} +func (*EventPowerDistUpdate_EventSlashedBTCDelegation) Descriptor() ([]byte, []int) { + return fileDescriptor_74118427820fff75, []int{3, 1} +} +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_EventPowerDistUpdate_EventSlashedBTCDelegation.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) XXX_Merge(src proto.Message) { + xxx_messageInfo_EventPowerDistUpdate_EventSlashedBTCDelegation.Merge(m, src) +} +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) XXX_Size() int { + return m.Size() +} +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) XXX_DiscardUnknown() { + xxx_messageInfo_EventPowerDistUpdate_EventSlashedBTCDelegation.DiscardUnknown(m) +} + +var xxx_messageInfo_EventPowerDistUpdate_EventSlashedBTCDelegation proto.InternalMessageInfo + +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) GetStakingTxHash() string { + if m != nil { + return m.StakingTxHash + } + return "" +} + // EventJailedFinalityProvider defines an event that a finality provider // is jailed after being detected sluggish type EventPowerDistUpdate_EventJailedFinalityProvider struct { @@ -350,7 +415,7 @@ func (m *EventPowerDistUpdate_EventJailedFinalityProvider) String() string { } func (*EventPowerDistUpdate_EventJailedFinalityProvider) ProtoMessage() {} func (*EventPowerDistUpdate_EventJailedFinalityProvider) Descriptor() ([]byte, []int) { - return fileDescriptor_74118427820fff75, []int{3, 1} + return fileDescriptor_74118427820fff75, []int{3, 2} } func (m *EventPowerDistUpdate_EventJailedFinalityProvider) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -393,7 +458,7 @@ func (m *EventPowerDistUpdate_EventUnjailedFinalityProvider) String() string { } func (*EventPowerDistUpdate_EventUnjailedFinalityProvider) ProtoMessage() {} func (*EventPowerDistUpdate_EventUnjailedFinalityProvider) Descriptor() ([]byte, []int) { - return fileDescriptor_74118427820fff75, []int{3, 2} + return fileDescriptor_74118427820fff75, []int{3, 3} } func (m *EventPowerDistUpdate_EventUnjailedFinalityProvider) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -428,6 +493,7 @@ func init() { proto.RegisterType((*EventSelectiveSlashing)(nil), "babylon.btcstaking.v1.EventSelectiveSlashing") proto.RegisterType((*EventPowerDistUpdate)(nil), "babylon.btcstaking.v1.EventPowerDistUpdate") proto.RegisterType((*EventPowerDistUpdate_EventSlashedFinalityProvider)(nil), "babylon.btcstaking.v1.EventPowerDistUpdate.EventSlashedFinalityProvider") + proto.RegisterType((*EventPowerDistUpdate_EventSlashedBTCDelegation)(nil), "babylon.btcstaking.v1.EventPowerDistUpdate.EventSlashedBTCDelegation") proto.RegisterType((*EventPowerDistUpdate_EventJailedFinalityProvider)(nil), "babylon.btcstaking.v1.EventPowerDistUpdate.EventJailedFinalityProvider") proto.RegisterType((*EventPowerDistUpdate_EventUnjailedFinalityProvider)(nil), "babylon.btcstaking.v1.EventPowerDistUpdate.EventUnjailedFinalityProvider") } @@ -437,41 +503,43 @@ func init() { } var fileDescriptor_74118427820fff75 = []byte{ - // 531 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0x4d, 0x6f, 0xd3, 0x30, - 0x18, 0xc7, 0x9b, 0x30, 0x4d, 0xab, 0xc7, 0x8b, 0x88, 0x0a, 0xaa, 0x0a, 0x84, 0xa9, 0x87, 0x31, - 0x21, 0x91, 0xec, 0xa5, 0x12, 0x9c, 0x4b, 0xd7, 0x75, 0x80, 0xa6, 0x2a, 0xdd, 0x2e, 0x5c, 0x22, - 0x27, 0x7d, 0x9a, 0x98, 0x06, 0xdb, 0xaa, 0xdd, 0xb4, 0xbd, 0xf3, 0x01, 0xf6, 0xb1, 0x38, 0xee, - 0x88, 0x38, 0x20, 0xd4, 0x7e, 0x11, 0x54, 0xc7, 0xdb, 0xaa, 0xad, 0xa9, 0x98, 0xb4, 0x5b, 0x62, - 0x3d, 0xff, 0xdf, 0xef, 0xf1, 0x13, 0xc7, 0xa8, 0x1a, 0xe0, 0x60, 0x92, 0x30, 0xea, 0x06, 0x32, - 0x14, 0x12, 0xf7, 0x09, 0x8d, 0xdc, 0x74, 0xcf, 0x85, 0x14, 0xa8, 0x14, 0x0e, 0x1f, 0x30, 0xc9, - 0xac, 0x67, 0xba, 0xc6, 0xb9, 0xae, 0x71, 0xd2, 0xbd, 0x4a, 0x29, 0x62, 0x11, 0x53, 0x15, 0xee, - 0xfc, 0x29, 0x2b, 0xae, 0x6c, 0x2f, 0x07, 0x2e, 0x44, 0x55, 0x5d, 0xb5, 0x83, 0xca, 0x87, 0x73, - 0xc9, 0x09, 0x8c, 0x9a, 0x84, 0xe2, 0x84, 0xc8, 0x49, 0x7b, 0xc0, 0x52, 0xd2, 0x85, 0x81, 0xf5, - 0x1e, 0x99, 0x3d, 0x5e, 0x36, 0xb6, 0x8c, 0x9d, 0xcd, 0xfd, 0x37, 0xce, 0x52, 0xbb, 0x73, 0x33, - 0xe4, 0x99, 0x3d, 0x5e, 0x3d, 0x37, 0xd0, 0x2b, 0x45, 0xad, 0x9f, 0x7e, 0x6c, 0x40, 0x02, 0x11, - 0x96, 0x84, 0xd1, 0x8e, 0xc4, 0x12, 0xce, 0x78, 0x17, 0x4b, 0xb0, 0xb6, 0xd1, 0x13, 0x0d, 0xf1, - 0xe5, 0xd8, 0x8f, 0xb1, 0x88, 0x95, 0xa7, 0xe8, 0x3d, 0xd2, 0xcb, 0xa7, 0xe3, 0x16, 0x16, 0xb1, - 0x75, 0x84, 0x8a, 0x14, 0x46, 0xbe, 0x98, 0x47, 0xcb, 0xe6, 0x96, 0xb1, 0xf3, 0x78, 0xff, 0x6d, - 0x4e, 0x27, 0xb7, 0x5c, 0x43, 0xe1, 0x6d, 0x50, 0x18, 0x29, 0x6d, 0xb5, 0x87, 0x9e, 0xab, 0x8e, - 0x3a, 0x90, 0x40, 0x28, 0x49, 0x0a, 0x9d, 0x04, 0x8b, 0x98, 0xd0, 0xc8, 0xfa, 0x82, 0x36, 0x60, - 0xde, 0x3a, 0x0d, 0x41, 0xef, 0x75, 0x37, 0xc7, 0x70, 0x2b, 0x7b, 0xa8, 0x73, 0xde, 0x15, 0xa1, - 0xfa, 0x63, 0x1d, 0x95, 0x94, 0xa8, 0xcd, 0x46, 0x30, 0x68, 0x10, 0x21, 0xf5, 0x8e, 0x09, 0x42, - 0x62, 0x1e, 0x83, 0xae, 0x7f, 0x35, 0xd4, 0x56, 0x8e, 0x68, 0x19, 0x20, 0x5b, 0xec, 0x64, 0x88, - 0x9b, 0x53, 0x6f, 0x15, 0xbc, 0xa2, 0xa6, 0x37, 0xb9, 0xd5, 0x43, 0xc5, 0x6f, 0x98, 0x24, 0x99, - 0xc9, 0x54, 0xa6, 0xa3, 0x3b, 0x9b, 0x3e, 0x29, 0xc2, 0x12, 0xd1, 0x46, 0xc6, 0x6e, 0x72, 0x2b, - 0x41, 0x9b, 0x43, 0x7a, 0x6d, 0x7a, 0xa0, 0x4c, 0xc7, 0x77, 0x36, 0x9d, 0x69, 0xc6, 0x12, 0x17, - 0xba, 0xe4, 0x37, 0xb9, 0x15, 0xa1, 0x52, 0x20, 0x43, 0xbf, 0x0b, 0x49, 0x76, 0x1c, 0xfc, 0xa1, - 0x62, 0x94, 0xd7, 0x94, 0xb6, 0xb6, 0x4a, 0x9b, 0x77, 0x0c, 0x5b, 0x05, 0xef, 0x69, 0x20, 0xc3, - 0x06, 0x24, 0x0b, 0x8b, 0x95, 0x18, 0xbd, 0x5c, 0x35, 0x6b, 0xab, 0x85, 0x4c, 0xde, 0x57, 0x5f, - 0xf0, 0x61, 0xfd, 0xc3, 0xef, 0x3f, 0xaf, 0x6b, 0x11, 0x91, 0xf1, 0x30, 0x70, 0x42, 0xf6, 0xdd, - 0xd5, 0x4d, 0x24, 0x38, 0x10, 0xef, 0x08, 0xbb, 0x7c, 0x75, 0xe5, 0x84, 0x83, 0x70, 0xea, 0xc7, - 0xed, 0x83, 0xda, 0x6e, 0x7b, 0x18, 0x7c, 0x86, 0x89, 0x67, 0xf2, 0x7e, 0x25, 0x42, 0x2f, 0x56, - 0xcc, 0xfa, 0x1e, 0x45, 0x44, 0xff, 0x8f, 0x79, 0xa3, 0xbe, 0x3f, 0x55, 0x7d, 0x0d, 0x99, 0x90, - 0xd6, 0x4f, 0x7e, 0x4e, 0x6d, 0xe3, 0x62, 0x6a, 0x1b, 0x7f, 0xa7, 0xb6, 0x71, 0x3e, 0xb3, 0x0b, - 0x17, 0x33, 0xbb, 0xf0, 0x6b, 0x66, 0x17, 0xbe, 0xfe, 0x07, 0x79, 0xbc, 0x78, 0x69, 0x29, 0x4d, - 0xb0, 0xae, 0x6e, 0xab, 0x83, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x09, 0xde, 0x05, 0x65, 0x28, - 0x05, 0x00, 0x00, + // 569 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0xcd, 0x6e, 0xd3, 0x40, + 0x14, 0x85, 0x6d, 0x53, 0xa0, 0x99, 0xf2, 0x23, 0xac, 0x50, 0x85, 0x00, 0xa6, 0xca, 0xa2, 0x54, + 0x48, 0xd8, 0xfd, 0x89, 0x04, 0x6b, 0x37, 0x49, 0x5d, 0x40, 0x55, 0xe4, 0xb4, 0x1b, 0x36, 0xd6, + 0xd8, 0xb9, 0xb1, 0x87, 0x18, 0xdb, 0xca, 0x4c, 0x9c, 0x64, 0xc1, 0x3b, 0xf4, 0xb1, 0xba, 0xec, + 0x12, 0xb1, 0x40, 0x28, 0x79, 0x11, 0x94, 0xf1, 0xa4, 0x0d, 0x6d, 0x1c, 0xb5, 0xa8, 0xbb, 0xf1, + 0xd5, 0x9c, 0xf3, 0x9d, 0x99, 0xb9, 0xbe, 0xa8, 0xe2, 0x62, 0x77, 0x14, 0xc6, 0x91, 0xe1, 0x32, + 0x8f, 0x32, 0xdc, 0x25, 0x91, 0x6f, 0xa4, 0x3b, 0x06, 0xa4, 0x10, 0x31, 0xaa, 0x27, 0xbd, 0x98, + 0xc5, 0xea, 0x73, 0xb1, 0x47, 0xbf, 0xdc, 0xa3, 0xa7, 0x3b, 0xe5, 0xcd, 0xc5, 0xd2, 0xb9, 0x4d, + 0x5c, 0x5e, 0x2e, 0xfa, 0xb1, 0x1f, 0xf3, 0xa5, 0x31, 0x5d, 0x65, 0xd5, 0x4a, 0x0b, 0x95, 0xea, + 0x53, 0xc8, 0x11, 0x0c, 0x1a, 0x24, 0xc2, 0x21, 0x61, 0xa3, 0x66, 0x2f, 0x4e, 0x49, 0x1b, 0x7a, + 0xea, 0x07, 0xa4, 0x74, 0x92, 0x92, 0xbc, 0x21, 0x6f, 0xad, 0xed, 0xbe, 0xd5, 0x17, 0xd2, 0xf5, + 0xab, 0x22, 0x5b, 0xe9, 0x24, 0x95, 0x53, 0x19, 0xbd, 0xe6, 0xae, 0xe6, 0xf1, 0x7e, 0x0d, 0x42, + 0xf0, 0x31, 0x23, 0x71, 0xd4, 0x62, 0x98, 0xc1, 0x49, 0xd2, 0xc6, 0x0c, 0xd4, 0x4d, 0xf4, 0x54, + 0x98, 0x38, 0x6c, 0xe8, 0x04, 0x98, 0x06, 0x9c, 0x53, 0xb0, 0x1f, 0x8b, 0xf2, 0xf1, 0xd0, 0xc2, + 0x34, 0x50, 0x0f, 0x50, 0x21, 0x82, 0x81, 0x43, 0xa7, 0xd2, 0x92, 0xb2, 0x21, 0x6f, 0x3d, 0xd9, + 0x7d, 0x97, 0x93, 0xe4, 0x1a, 0xab, 0x4f, 0xed, 0xd5, 0x08, 0x06, 0x1c, 0x5b, 0xe9, 0xa0, 0x75, + 0x9e, 0xa8, 0x05, 0x21, 0x78, 0x8c, 0xa4, 0xd0, 0x0a, 0x31, 0x0d, 0x48, 0xe4, 0xab, 0x5f, 0xd0, + 0x2a, 0x4c, 0xa3, 0x47, 0x1e, 0x88, 0xb3, 0x6e, 0xe7, 0x10, 0xae, 0x69, 0xeb, 0x42, 0x67, 0x5f, + 0x38, 0x54, 0xce, 0x1e, 0xa2, 0x22, 0x07, 0x35, 0xe3, 0x01, 0xf4, 0x6a, 0x84, 0x32, 0x71, 0x62, + 0x82, 0x10, 0x9d, 0xca, 0xa0, 0xed, 0x5c, 0x5c, 0xaa, 0x95, 0x03, 0x5a, 0x64, 0x90, 0x15, 0x5b, + 0x99, 0xc5, 0xd5, 0x5b, 0xb7, 0x24, 0xbb, 0x20, 0xdc, 0x1b, 0x89, 0xda, 0x41, 0x85, 0x6f, 0x98, + 0x84, 0x19, 0x49, 0xe1, 0xa4, 0x83, 0x5b, 0x93, 0x3e, 0x71, 0x87, 0x05, 0xa0, 0xd5, 0xcc, 0xbb, + 0x91, 0xa8, 0x21, 0x5a, 0xeb, 0x47, 0x97, 0xa4, 0x7b, 0x9c, 0x74, 0x78, 0x6b, 0xd2, 0x89, 0xf0, + 0x58, 0xc0, 0x42, 0x33, 0xff, 0x46, 0xa2, 0xfa, 0xa8, 0xe8, 0x32, 0xcf, 0x69, 0x43, 0x98, 0xb5, + 0x83, 0xd3, 0xe7, 0x1e, 0xa5, 0x15, 0x8e, 0xad, 0x2e, 0xc3, 0xe6, 0xb5, 0xa1, 0x25, 0xd9, 0xcf, + 0x5c, 0xe6, 0xd5, 0x20, 0x9c, 0xef, 0xcd, 0x1f, 0x68, 0x7d, 0xf6, 0x52, 0x02, 0x28, 0x94, 0xa5, + 0xfb, 0x1c, 0x55, 0xff, 0xdf, 0x57, 0xfb, 0x27, 0x86, 0x25, 0xd9, 0x45, 0x81, 0x31, 0x79, 0x04, + 0x51, 0x2f, 0x07, 0xe8, 0xd5, 0xb2, 0xa7, 0x56, 0x2d, 0xa4, 0x24, 0x5d, 0xde, 0x40, 0x8f, 0xcc, + 0x8f, 0xbf, 0x7e, 0xbf, 0xa9, 0xfa, 0x84, 0x05, 0x7d, 0x57, 0xf7, 0xe2, 0xef, 0x86, 0x08, 0x16, + 0x62, 0x97, 0xbe, 0x27, 0xf1, 0xec, 0xd3, 0x60, 0xa3, 0x04, 0xa8, 0x6e, 0x1e, 0x36, 0xf7, 0xaa, + 0xdb, 0xcd, 0xbe, 0xfb, 0x19, 0x46, 0xb6, 0x92, 0x74, 0xcb, 0xfb, 0xe8, 0x45, 0x6e, 0xbc, 0x9b, + 0xfe, 0xa1, 0x65, 0x1f, 0xbd, 0x5c, 0xd2, 0x2f, 0x77, 0x98, 0x96, 0x88, 0x99, 0x92, 0xd7, 0x2e, + 0x77, 0x87, 0x32, 0x57, 0x90, 0x02, 0xa9, 0x79, 0x74, 0x36, 0xd6, 0xe4, 0xf3, 0xb1, 0x26, 0xff, + 0x19, 0x6b, 0xf2, 0xe9, 0x44, 0x93, 0xce, 0x27, 0x9a, 0xf4, 0x73, 0xa2, 0x49, 0x5f, 0x6f, 0xe0, + 0x3c, 0x9c, 0x1f, 0xc7, 0x1c, 0xe3, 0x3e, 0xe0, 0x13, 0x77, 0xef, 0x6f, 0x00, 0x00, 0x00, 0xff, + 0xff, 0x44, 0x44, 0x28, 0x4c, 0xec, 0x05, 0x00, 0x00, } func (m *EventNewFinalityProvider) Marshal() (dAtA []byte, err error) { @@ -695,6 +763,27 @@ func (m *EventPowerDistUpdate_BtcDelStateUpdate) MarshalToSizedBuffer(dAtA []byt } return len(dAtA) - i, nil } +func (m *EventPowerDistUpdate_SlashedBtcDelegation) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EventPowerDistUpdate_SlashedBtcDelegation) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + if m.SlashedBtcDelegation != nil { + { + size, err := m.SlashedBtcDelegation.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintEvents(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a + } + return len(dAtA) - i, nil +} func (m *EventPowerDistUpdate_EventSlashedFinalityProvider) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -730,6 +819,36 @@ func (m *EventPowerDistUpdate_EventSlashedFinalityProvider) MarshalToSizedBuffer return len(dAtA) - i, nil } +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.StakingTxHash) > 0 { + i -= len(m.StakingTxHash) + copy(dAtA[i:], m.StakingTxHash) + i = encodeVarintEvents(dAtA, i, uint64(len(m.StakingTxHash))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *EventPowerDistUpdate_EventJailedFinalityProvider) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -913,6 +1032,18 @@ func (m *EventPowerDistUpdate_BtcDelStateUpdate) Size() (n int) { } return n } +func (m *EventPowerDistUpdate_SlashedBtcDelegation) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.SlashedBtcDelegation != nil { + l = m.SlashedBtcDelegation.Size() + n += 1 + l + sovEvents(uint64(l)) + } + return n +} func (m *EventPowerDistUpdate_EventSlashedFinalityProvider) Size() (n int) { if m == nil { return 0 @@ -926,6 +1057,19 @@ func (m *EventPowerDistUpdate_EventSlashedFinalityProvider) Size() (n int) { return n } +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.StakingTxHash) + if l > 0 { + n += 1 + l + sovEvents(uint64(l)) + } + return n +} + func (m *EventPowerDistUpdate_EventJailedFinalityProvider) Size() (n int) { if m == nil { return 0 @@ -1400,6 +1544,41 @@ func (m *EventPowerDistUpdate) Unmarshal(dAtA []byte) error { } m.Ev = &EventPowerDistUpdate_BtcDelStateUpdate{v} iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SlashedBtcDelegation", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthEvents + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthEvents + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + v := &EventPowerDistUpdate_EventSlashedBTCDelegation{} + if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + m.Ev = &EventPowerDistUpdate_SlashedBtcDelegation{v} + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipEvents(dAtA[iNdEx:]) @@ -1506,6 +1685,88 @@ func (m *EventPowerDistUpdate_EventSlashedFinalityProvider) Unmarshal(dAtA []byt } return nil } +func (m *EventPowerDistUpdate_EventSlashedBTCDelegation) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: EventSlashedBTCDelegation: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: EventSlashedBTCDelegation: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StakingTxHash", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvents + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvents + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.StakingTxHash = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipEvents(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthEvents + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *EventPowerDistUpdate_EventJailedFinalityProvider) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/x/zoneconcierge/keeper/ibc_packet_btc_staking_consumer_event.go b/x/zoneconcierge/keeper/ibc_packet_btc_staking_consumer_event.go index 9218be816..4a763d4a5 100644 --- a/x/zoneconcierge/keeper/ibc_packet_btc_staking_consumer_event.go +++ b/x/zoneconcierge/keeper/ibc_packet_btc_staking_consumer_event.go @@ -4,7 +4,9 @@ import ( "context" "fmt" + bbn "github.com/babylonlabs-io/babylon/types" btcstkconsumertypes "github.com/babylonlabs-io/babylon/x/btcstkconsumer/types" + finalitytypes "github.com/babylonlabs-io/babylon/x/finality/types" "github.com/babylonlabs-io/babylon/x/zoneconcierge/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -72,3 +74,61 @@ func (k Keeper) HandleConsumerRegistration( return k.btcStkKeeper.RegisterConsumer(ctx, consumerRegisterData) } + +func (k Keeper) HandleConsumerSlashing( + ctx sdk.Context, + destinationPort string, + destinationChannel string, + consumerSlashing *types.ConsumerSlashingIBCPacket, +) error { + clientID, _, err := k.channelKeeper.GetChannelClientState(ctx, destinationPort, destinationChannel) + if err != nil { + return fmt.Errorf("failed to get client state: %w", err) + } + + evidence := consumerSlashing.Evidence + if evidence == nil { + return fmt.Errorf("consumer slashing evidence is nil") + } + + slashedFpBTCSK, err := evidence.ExtractBTCSK() + if err != nil { + return fmt.Errorf("failed to extract BTCSK: %w", err) + } + + slashedFpBTCPK := bbn.NewBIP340PubKeyFromBTCPK(slashedFpBTCSK.PubKey()) + evidenceFpBTCPKHex := evidence.FpBtcPk.MarshalHex() + if slashedFpBTCPK.MarshalHex() != evidenceFpBTCPKHex { + return fmt.Errorf("slashed FP BTC PK does not match with the one in the evidence") + } + + // Check if the finality provider is associated with a consumer + consumerID, err := k.btcStkKeeper.GetConsumerOfFinalityProvider(ctx, slashedFpBTCPK) + if err != nil { + return fmt.Errorf("failed to get consumer of finality provider: %w", err) + } + + // Verify that the consumer ID matches the client ID + if consumerID != clientID { + return fmt.Errorf("consumer ID (%s) does not match client ID (%s)", consumerID, clientID) + } + + // Update the consumer finality provider's slashed height and + // send power distribution update event so the affected Babylon FP's voting power can be adjusted + if err := k.bsKeeper.SlashConsumerFinalityProvider(ctx, consumerID, slashedFpBTCPK); err != nil { + return fmt.Errorf("failed to slash consumer finality provider: %w", err) + } + + // Send slashing event to other involved consumers + if err := k.bsKeeper.PropagateFPSlashingToConsumers(ctx, slashedFpBTCPK); err != nil { + return fmt.Errorf("failed to propagate slashing to consumers: %w", err) + } + + // Emit slashed finality provider event so btc slasher/vigilante can slash the finality provider + eventSlashing := finalitytypes.NewEventSlashedFinalityProvider(evidence) + if err := sdk.UnwrapSDKContext(ctx).EventManager().EmitTypedEvent(eventSlashing); err != nil { + return fmt.Errorf("failed to emit EventSlashedFinalityProvider event: %w", err) + } + + return nil +} diff --git a/x/zoneconcierge/module_ibc.go b/x/zoneconcierge/module_ibc.go index 0f39d8860..dfa0d092f 100644 --- a/x/zoneconcierge/module_ibc.go +++ b/x/zoneconcierge/module_ibc.go @@ -167,6 +167,12 @@ func (im IBCModule) OnRecvPacket( return channeltypes.NewErrorAcknowledgement(err) } return channeltypes.NewResultAcknowledgement([]byte("Consumer registered successfully")) + case *types.ZoneconciergePacketData_ConsumerSlashing: + err := im.keeper.HandleConsumerSlashing(ctx, modulePacket.DestinationPort, modulePacket.DestinationChannel, packet.ConsumerSlashing) + if err != nil { + return channeltypes.NewErrorAcknowledgement(err) + } + return channeltypes.NewResultAcknowledgement([]byte("Consumer slashing handled successfully")) // Add other packet types here if needed default: errMsg := fmt.Sprintf("unrecognized %s packet type: %T", types.ModuleName, packet) diff --git a/x/zoneconcierge/types/expected_keepers.go b/x/zoneconcierge/types/expected_keepers.go index db6be5bfe..97b2cde0e 100644 --- a/x/zoneconcierge/types/expected_keepers.go +++ b/x/zoneconcierge/types/expected_keepers.go @@ -108,8 +108,14 @@ type EpochingKeeper interface { type BTCStakingKeeper interface { GetAllBTCStakingConsumerIBCPackets(ctx context.Context) map[string]*bstypes.BTCStakingIBCPacket DeleteBTCStakingConsumerIBCPacket(ctx context.Context, consumerID string) + PropagateFPSlashingToConsumers(ctx context.Context, fpBTCPK *bbn.BIP340PubKey) error + SlashConsumerFinalityProvider(ctx context.Context, consumerID string, fpBTCPK *bbn.BIP340PubKey) error + GetFPBTCDelegations(ctx context.Context, fpBTCPK *bbn.BIP340PubKey) ([]*bstypes.BTCDelegation, error) } type BTCStkConsumerKeeper interface { RegisterConsumer(ctx context.Context, consumerRegister *btcstkconsumertypes.ConsumerRegister) error + GetConsumerOfFinalityProvider(ctx context.Context, fpBTCPK *bbn.BIP340PubKey) (string, error) + GetConsumerFinalityProvider(ctx context.Context, consumerID string, fpBTCPK *bbn.BIP340PubKey) (*bstypes.FinalityProvider, error) + SetConsumerFinalityProvider(ctx context.Context, fp *bstypes.FinalityProvider) }