Repository: babylonlabs-io/babylon
Pull request #944: feat(btcstaking,btcstkconsumer): Add max_multi_staked_fps validation and single FP per consumer constraint
Author: login: gusin13
Labels: name: backport-to-release/v4.x
Created: 2025-05-22T21:20:51Z | Merged: 2025-05-26T12:12:46Z

## Description
https://github.com/babylonlabs-io/babylon/issues/412
https://github.com/babylonlabs-io/babylon/issues/900

Adds `max_multi_staked_fps` field to `btcstkconsumer` module and validations in `btcstaking` module in `MsgCreateBTCDelegation`

### Changes
- (btcstkconsumer) Adds new field in `ConsumerRegister`, `MsgRegisterConsumer`, and `ConsumerRegisterResponse` protos
- (btcstaking) Validations when a new delegation is created (MsgCreateBTCDelegation) ensuring
   - atmost 1 FP per BSN
   - number_restaked_fps <= min(max_multi_staked_fps_consumerA, max_multi_staked_fps_consumerB, etc)
- Add/Update test cases in relevant files

### Next steps/related tasks
Bump babylon version and update assertions/tests in https://github.com/babylonlabs-io/babylon-rollup-integration-deployment


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 796c647e1..53f360bd6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -93,6 +93,7 @@ This PR contains a series of PRs on multi-staking support and BTC staking integr
 transfer stack
 - [#877](https://github.com/babylonlabs-io/babylon/pull/877) fix: BTC reward tracker are stored as events and updated only
 when the babylon block is being BTC rewarded.
+- [#944](https://github.com/babylonlabs-io/babylon/pull/944) Add max_multi_staked_fps validation and single FP per consumer constraint in BTC delegations.
 
 ### Bug fixes
 
diff --git a/client/docs/swagger-ui/swagger.yaml b/client/docs/swagger-ui/swagger.yaml
index d67e98827..9aabdbf92 100644
--- a/client/docs/swagger-ui/swagger.yaml
+++ b/client/docs/swagger-ui/swagger.yaml
@@ -10702,6 +10702,17 @@ paths:
                         finality contract
 
                         set only for ETH L2 integration
+                    max_multi_staked_fps:
+                      type: integer
+                      format: int64
+                      description: >-
+                        max_multi_staked_fps is the maximum number of finality
+                        providers from this consumer
+
+                        that can be part of a single BTC delegation. Must be at
+                        least 2 to allow for at least
+
+                        one Babylon FP and one consumer FP.
                   title: >-
                     ConsumerRegisterResponse contains the information for a
                     registered consumer
@@ -11043,6 +11054,17 @@ paths:
                         finality contract
 
                         set only for ETH L2 integration
+                    max_multi_staked_fps:
+                      type: integer
+                      format: int64
+                      description: >-
+                        max_multi_staked_fps is the maximum number of finality
+                        providers from this consumer
+
+                        that can be part of a single BTC delegation. Must be at
+                        least 2 to allow for at least
+
+                        one Babylon FP and one consumer FP.
                   title: >-
                     ConsumerRegisterResponse contains the information for a
                     registered consumer
@@ -23064,6 +23086,17 @@ definitions:
           contract
 
           set only for ETH L2 integration
+      max_multi_staked_fps:
+        type: integer
+        format: int64
+        description: >-
+          max_multi_staked_fps is the maximum number of finality providers from
+          this consumer
+
+          that can be part of a single BTC delegation. Must be at least 2 to
+          allow for at least
+
+          one Babylon FP and one consumer FP.
     title: >-
       ConsumerRegisterResponse contains the information for a registered
       consumer
@@ -23213,6 +23246,17 @@ definitions:
                 contract
 
                 set only for ETH L2 integration
+            max_multi_staked_fps:
+              type: integer
+              format: int64
+              description: >-
+                max_multi_staked_fps is the maximum number of finality providers
+                from this consumer
+
+                that can be part of a single BTC delegation. Must be at least 2
+                to allow for at least
+
+                one Babylon FP and one consumer FP.
           title: >-
             ConsumerRegisterResponse contains the information for a registered
             consumer
@@ -23290,6 +23334,17 @@ definitions:
                 contract
 
                 set only for ETH L2 integration
+            max_multi_staked_fps:
+              type: integer
+              format: int64
+              description: >-
+                max_multi_staked_fps is the maximum number of finality providers
+                from this consumer
+
+                that can be part of a single BTC delegation. Must be at least 2
+                to allow for at least
+
+                one Babylon FP and one consumer FP.
           title: >-
             ConsumerRegisterResponse contains the information for a registered
             consumer
diff --git a/proto/babylon/btcstkconsumer/v1/btcstkconsumer.proto b/proto/babylon/btcstkconsumer/v1/btcstkconsumer.proto
index 8ff45e03b..152812c7a 100644
--- a/proto/babylon/btcstkconsumer/v1/btcstkconsumer.proto
+++ b/proto/babylon/btcstkconsumer/v1/btcstkconsumer.proto
@@ -24,11 +24,15 @@ message ConsumerRegister {
   string consumer_name = 2;
   // consumer_description is a description for the consumer (can be empty)
   string consumer_description = 3;
+  // max_multi_staked_fps is the maximum number of finality providers from this consumer
+  // that can be part of a single BTC delegation. Must be at least 2 to allow for at least
+  // one Babylon FP and one consumer FP.
+  uint32 max_multi_staked_fps = 4;
   // consumer_metadata is necessary metadata of the consumer, and the data
   // depends on the type of integration
   oneof consumer_metadata {
-    CosmosConsumerMetadata cosmos_consumer_metadata = 4;
-    ETHL2ConsumerMetadata eth_l2_consumer_metadata = 5;
+    CosmosConsumerMetadata cosmos_consumer_metadata = 5;
+    ETHL2ConsumerMetadata eth_l2_consumer_metadata = 6;
   };
 }
 
diff --git a/proto/babylon/btcstkconsumer/v1/query.proto b/proto/babylon/btcstkconsumer/v1/query.proto
index 62d079af8..a6953b741 100644
--- a/proto/babylon/btcstkconsumer/v1/query.proto
+++ b/proto/babylon/btcstkconsumer/v1/query.proto
@@ -173,4 +173,8 @@ message ConsumerRegisterResponse {
   // eth_l2_finality_contract_address is the address of the finality contract
   // set only for ETH L2 integration
   string eth_l2_finality_contract_address = 5 [(cosmos_proto.scalar) = "cosmos.AddressString"];
+  // max_multi_staked_fps is the maximum number of finality providers from this consumer
+  // that can be part of a single BTC delegation. Must be at least 2 to allow for at least
+  // one Babylon FP and one consumer FP.
+  uint32 max_multi_staked_fps = 6;
 }
\ No newline at end of file
diff --git a/proto/babylon/btcstkconsumer/v1/tx.proto b/proto/babylon/btcstkconsumer/v1/tx.proto
index e29fbab88..fa206ce5f 100644
--- a/proto/babylon/btcstkconsumer/v1/tx.proto
+++ b/proto/babylon/btcstkconsumer/v1/tx.proto
@@ -53,11 +53,15 @@ message MsgRegisterConsumer {
   string consumer_name = 3;
   // consumer_description is a description for the consumer (can be empty)
   string consumer_description = 4;
+  // max_multi_staked_fps is the maximum number of finality providers from this consumer
+  // that can be part of a single BTC delegation. Must be at least 2 to allow for at least
+  // one Babylon FP and one consumer FP.
+  uint32 max_multi_staked_fps = 5;
   // eth_l2_finality_contract_address is the Cosmos address of the
   // finality contract. The finality contract is deployed on Babylon and
   // serves as the data availability layer for finality signatures of the ETH L2.
   // (if set, then this means this is an ETH L2 integration)
-  string eth_l2_finality_contract_address = 5 [(cosmos_proto.scalar) = "cosmos.AddressString"];
+  string eth_l2_finality_contract_address = 6 [(cosmos_proto.scalar) = "cosmos.AddressString"];
 }
 
 // MsgRegisterConsumerResponse is the response to the MsgRegisterConsumer message.
diff --git a/testutil/datagen/btcstkconsumer.go b/testutil/datagen/btcstkconsumer.go
index e76da1cfe..c59220b2c 100644
--- a/testutil/datagen/btcstkconsumer.go
+++ b/testutil/datagen/btcstkconsumer.go
@@ -12,6 +12,7 @@ func GenRandomCosmosConsumerRegister(r *rand.Rand) *bsctypes.ConsumerRegister {
 		ConsumerId:          clientID,
 		ConsumerName:        GenRandomHexStr(r, 5),
 		ConsumerDescription: "Chain description: " + GenRandomHexStr(r, 15),
+		MaxMultiStakedFps:   uint32(RandomInt(r, 10) + 2), // Random number between 2 and 11
 		ConsumerMetadata: &bsctypes.ConsumerRegister_CosmosConsumerMetadata{
 			CosmosConsumerMetadata: &bsctypes.CosmosConsumerMetadata{},
 		},
@@ -24,6 +25,7 @@ func GenRandomETHL2Register(r *rand.Rand, contractAddress string) *bsctypes.Cons
 		ConsumerId:          clientID,
 		ConsumerName:        GenRandomHexStr(r, 5),
 		ConsumerDescription: "Chain description: " + GenRandomHexStr(r, 15),
+		MaxMultiStakedFps:   uint32(RandomInt(r, 10) + 2), // Random number between 2 and 11
 		ConsumerMetadata: &bsctypes.ConsumerRegister_EthL2ConsumerMetadata{
 			EthL2ConsumerMetadata: &bsctypes.ETHL2ConsumerMetadata{
 				FinalityContractAddress: contractAddress,
diff --git a/x/btcstaking/keeper/btc_delegations.go b/x/btcstaking/keeper/btc_delegations.go
index 0ac232ed9..83f0bf4fc 100644
--- a/x/btcstaking/keeper/btc_delegations.go
+++ b/x/btcstaking/keeper/btc_delegations.go
@@ -227,11 +227,18 @@ func (k Keeper) setBTCDelegation(ctx context.Context, btcDel *types.BTCDelegatio
 
 // validateRestakedFPs ensures all finality providers are known to Babylon and at least
 // one of them is a Babylon finality provider. It also checks whether the BTC stake is
-// restaked to FPs of consumer chains
+// restaked to FPs of consumer chains. It enforces:
+// 1. Total number of FPs <= min of all consumers' max_multi_staked_fps
+// 2. At most 1 FP per consumer/BSN
 func (k Keeper) validateRestakedFPs(ctx context.Context, fpBTCPKs []bbn.BIP340PubKey) (bool, error) {
 	restakedToBabylon := false
 	restakedToConsumers := false
 
+	// Track FPs per consumer to enforce at-most-1-FP-per-consumer
+	consumerFPs := make(map[string]struct{})
+	// Track min max_multi_staked_fps across all consumers
+	minMaxMultiStakedFPs := ^uint32(0) // Initialize with MaxUint32
+
 	for i := range fpBTCPKs {
 		fpBTCPK := fpBTCPKs[i]
 
@@ -244,6 +251,21 @@ func (k Keeper) validateRestakedFPs(ctx context.Context, fpBTCPKs []bbn.BIP340Pu
 			restakedToBabylon = true
 			continue
 		} else if consumerID, err := k.BscKeeper.GetConsumerOfFinalityProvider(ctx, &fpBTCPK); err == nil {
+			// Check if we already have an FP from this consumer
+			if _, exists := consumerFPs[consumerID]; exists {
+				return false, types.ErrTooManyFPsFromSameConsumer.Wrapf("consumer %s already has an FP", consumerID)
+			}
+			consumerFPs[consumerID] = struct{}{}
+
+			// Get consumer's max_multi_staked_fps
+			maxMultiStakedFps, err := k.BscKeeper.GetConsumerRegistryMaxMultiStakedFps(ctx, consumerID)
+			if err != nil {
+				return false, err
+			}
+			if maxMultiStakedFps < minMaxMultiStakedFPs {
+				minMaxMultiStakedFPs = maxMultiStakedFps
+			}
+
 			fp, err := k.BscKeeper.GetConsumerFinalityProvider(ctx, consumerID, &fpBTCPK)
 			if err != nil {
 				return false, err
@@ -258,10 +280,17 @@ func (k Keeper) validateRestakedFPs(ctx context.Context, fpBTCPKs []bbn.BIP340Pu
 			return false, types.ErrFpNotFound.Wrapf("finality provider pk %s is not found", fpBTCPK.MarshalHex())
 		}
 	}
+
 	if !restakedToBabylon {
 		// a BTC delegation has to stake to at least 1 Babylon finality provider
 		return false, types.ErrNoBabylonFPRestaked
 	}
+
+	// Check if total number of FPs exceeds min max_multi_staked_fps
+	if minMaxMultiStakedFPs > 0 && uint32(len(fpBTCPKs)) > minMaxMultiStakedFPs {
+		return false, types.ErrTooManyFPs.Wrapf("total FPs %d exceeds min max_multi_staked_fps %d", len(fpBTCPKs), minMaxMultiStakedFPs)
+	}
+
 	return restakedToConsumers, nil
 }
 
diff --git a/x/btcstaking/keeper/msg_server.go b/x/btcstaking/keeper/msg_server.go
index d607c46a6..06b1f7e14 100644
--- a/x/btcstaking/keeper/msg_server.go
+++ b/x/btcstaking/keeper/msg_server.go
@@ -217,8 +217,9 @@ func (ms msgServer) CreateBTCDelegation(goCtx context.Context, req *types.MsgCre
 	// - are not slashed, and
 	// - their registered epochs are finalised
 	// and then check whether the BTC stake is restaked to FPs of consumers
-	// TODO: ensure the BTC delegation does not restake to too many finality providers
-	// (pending concrete design)
+	// The total number of finality providers in a delegation must be less than the minimum
+	// of all consumers' max_multi_staked_fps limits. Only one finality provider per
+	// consumer is allowed in a delegation.
 	restakedToConsumers, err := ms.validateRestakedFPs(ctx, parsedMsg.FinalityProviderKeys.PublicKeysBbnFormat)
 	if err != nil {
 		return nil, err
diff --git a/x/btcstaking/keeper/restaking_test.go b/x/btcstaking/keeper/restaking_test.go
index 29d97ba00..0ec42cd55 100644
--- a/x/btcstaking/keeper/restaking_test.go
+++ b/x/btcstaking/keeper/restaking_test.go
@@ -1,13 +1,11 @@
 package keeper_test
 
 import (
-	"errors"
 	"math/rand"
 	"testing"
 
 	testutil "github.com/babylonlabs-io/babylon/v4/testutil/btcstaking-helper"
 	"github.com/babylonlabs-io/babylon/v4/testutil/datagen"
-	bbn "github.com/babylonlabs-io/babylon/v4/types"
 	btclctypes "github.com/babylonlabs-io/babylon/v4/x/btclightclient/types"
 	"github.com/babylonlabs-io/babylon/v4/x/btcstaking/types"
 	"github.com/btcsuite/btcd/btcec/v2"
@@ -46,33 +44,44 @@ func FuzzRestaking_RestakedBTCDelegation(f *testing.F) {
 		h.Error(err)
 
 		/*
-			register a new consumer and create a new finality provider under it
-			ensure it's correctly generated
+			register multiple consumers with different max_multi_staked_fps values
+			and create finality providers under them
 		*/
-		consumerRegister := datagen.GenRandomCosmosConsumerRegister(r)
-		err = h.BTCStkConsumerKeeper.RegisterConsumer(h.Ctx, consumerRegister)
+		// Consumer 1 with max_multi_staked_fps = 2
+		consumerRegister1 := datagen.GenRandomCosmosConsumerRegister(r)
+		consumerRegister1.MaxMultiStakedFps = 2
+		err = h.BTCStkConsumerKeeper.RegisterConsumer(h.Ctx, consumerRegister1)
 		require.NoError(t, err)
-		_, consumerFPPK, consumerFP, err := h.CreateConsumerFinalityProvider(r, consumerRegister.ConsumerId)
+		_, consumerFPPK1, _, err := h.CreateConsumerFinalityProvider(r, consumerRegister1.ConsumerId)
 		h.NoError(err)
-		consumerFPBTCPK := bbn.NewBIP340PubKeyFromBTCPK(consumerFPPK)
-		consumerFP2, err := h.BTCStkConsumerKeeper.GetConsumerFinalityProvider(h.Ctx, consumerRegister.ConsumerId, consumerFPBTCPK)
+
+		// Consumer 2 with max_multi_staked_fps = 3
+		consumerRegister2 := datagen.GenRandomCosmosConsumerRegister(r)
+		consumerRegister2.MaxMultiStakedFps = 3
+		err = h.BTCStkConsumerKeeper.RegisterConsumer(h.Ctx, consumerRegister2)
+		require.NoError(t, err)
+		_, consumerFPPK2, _, err := h.CreateConsumerFinalityProvider(r, consumerRegister2.ConsumerId)
 		h.NoError(err)
-		// on finality provider creation, the commission update time is set to the
-		// current block time. The consumerFP is randomly generated with update time = 0,
-		// so we need to update it to the block time to make it equal
-		consumerFP.CommissionInfo.UpdateTime = h.Ctx.BlockTime().UTC()
-		require.Equal(t, consumerFP, consumerFP2)
+
+		// Consumer 3 with max_multi_staked_fps = 4
+		consumerRegister3 := datagen.GenRandomCosmosConsumerRegister(r)
+		consumerRegister3.MaxMultiStakedFps = 4
+		err = h.BTCStkConsumerKeeper.RegisterConsumer(h.Ctx, consumerRegister3)
+		require.NoError(t, err)
+		_, consumerFPPK3, _, err := h.CreateConsumerFinalityProvider(r, consumerRegister3.ConsumerId)
+		h.NoError(err)
+
+		stakingValue := int64(2 * 10e8)
 
 		/*
-			ensure BTC delegation request will fail if some fp PK does not exist
+			Test multiple consumers with different max_multi_staked_fps values
 		*/
-		stakingValue := int64(2 * 10e8)
-		_, randomFPPK, err := datagen.GenRandomBTCKeyPair(r)
-		h.NoError(err)
+		// Test case 1: Valid delegation with 1 Babylon FP and 1 FP from each consumer (total 4 FPs)
+		// This should fail because min(max_multi_staked_fps) = 2, but we're trying to use 4 FPs
 		_, _, _, _, _, _, err = h.CreateDelegationWithBtcBlockHeight(
 			r,
 			delSK,
-			[]*btcec.PublicKey{fpPK, randomFPPK},
+			[]*btcec.PublicKey{fpPK, consumerFPPK1, consumerFPPK2, consumerFPPK3},
 			stakingValue,
 			1000,
 			0,
@@ -83,15 +92,14 @@ func FuzzRestaking_RestakedBTCDelegation(f *testing.F) {
 			30,
 		)
 		h.Error(err)
-		require.True(t, errors.Is(err, types.ErrFpNotFound))
+		require.ErrorIs(t, err, types.ErrTooManyFPs)
 
-		/*
-			ensure BTC delegation request will fail if no PK corresponds to a Babylon fp
-		*/
-		_, _, _, _, _, _, err = h.CreateDelegationWithBtcBlockHeight(
+		// Test case 2: Valid delegation with 1 Babylon FP and 1 FP from consumer1 (total 2 FPs)
+		// This should succeed because it's within the minimum max_multi_staked_fps (2)
+		_, msgBTCDel, actualDel, _, _, _, err := h.CreateDelegationWithBtcBlockHeight(
 			r,
 			delSK,
-			[]*btcec.PublicKey{consumerFPPK},
+			[]*btcec.PublicKey{fpPK, consumerFPPK1},
 			stakingValue,
 			1000,
 			0,
@@ -101,17 +109,14 @@ func FuzzRestaking_RestakedBTCDelegation(f *testing.F) {
 			10,
 			30,
 		)
-		h.Error(err)
-		require.True(t, errors.Is(err, types.ErrNoBabylonFPRestaked), err)
-
-		/*
-			happy case -- restaking to a Babylon fp and a consumer fp
-		*/
+		h.NoError(err)
 
-		_, msgBTCDel, actualDel, _, _, _, err := h.CreateDelegationWithBtcBlockHeight(
+		// Test case 3: Valid delegation with 1 Babylon FP and 1 FP from consumer2 (total 2 FPs)
+		// This should succeed because it's within the minimum max_multi_staked_fps (2)
+		_, _, _, _, _, _, err = h.CreateDelegationWithBtcBlockHeight(
 			r,
 			delSK,
-			[]*btcec.PublicKey{fpPK, consumerFPPK},
+			[]*btcec.PublicKey{fpPK, consumerFPPK2},
 			stakingValue,
 			1000,
 			0,
@@ -123,6 +128,29 @@ func FuzzRestaking_RestakedBTCDelegation(f *testing.F) {
 		)
 		h.NoError(err)
 
+		// Test case 4: Invalid delegation with 1 Babylon FP and 2 FPs from consumer1 (total 3 FPs)
+		// This should fail because it exceeds the minimum max_multi_staked_fps (2)
+		_, consumerFPPK1_2, _, err := h.CreateConsumerFinalityProvider(r, consumerRegister1.ConsumerId)
+		h.NoError(err)
+		_, _, _, _, _, _, err = h.CreateDelegationWithBtcBlockHeight(
+			r,
+			delSK,
+			[]*btcec.PublicKey{fpPK, consumerFPPK1, consumerFPPK1_2},
+			stakingValue,
+			1000,
+			0,
+			0,
+			false,
+			false,
+			10,
+			30,
+		)
+		h.Error(err)
+		require.ErrorIs(t, err, types.ErrTooManyFPsFromSameConsumer)
+
+		/*
+			happy case -- restaking to a Babylon fp and a consumer fp
+		*/
 		// add covenant signatures to this restaked BTC delegation
 		h.CreateCovenantSigs(r, covenantSKs, msgBTCDel, actualDel, 10)
 
diff --git a/x/btcstaking/types/errors.go b/x/btcstaking/types/errors.go
index 95eb1145d..a348d6c6c 100644
--- a/x/btcstaking/types/errors.go
+++ b/x/btcstaking/types/errors.go
@@ -6,33 +6,35 @@ import (
 
 // x/btcstaking module sentinel errors
 var (
-	ErrFpNotFound                = errorsmod.Register(ModuleName, 1100, "the finality provider is not found")
-	ErrBTCDelegatorNotFound      = errorsmod.Register(ModuleName, 1101, "the BTC delegator is not found")
-	ErrBTCDelegationNotFound     = errorsmod.Register(ModuleName, 1102, "the BTC delegation is not found")
-	ErrFpRegistered              = errorsmod.Register(ModuleName, 1103, "the finality provider has already been registered")
-	ErrFpAlreadySlashed          = errorsmod.Register(ModuleName, 1104, "the finality provider has already been slashed")
-	ErrBTCHeightNotFound         = errorsmod.Register(ModuleName, 1105, "the BTC height is not found")
-	ErrReusedStakingTx           = errorsmod.Register(ModuleName, 1106, "the BTC staking tx is already used")
-	ErrInvalidCovenantPK         = errorsmod.Register(ModuleName, 1107, "the BTC staking tx specifies a wrong covenant PK")
-	ErrInvalidStakingTx          = errorsmod.Register(ModuleName, 1108, "the BTC staking tx is not valid")
-	ErrInvalidSlashingTx         = errorsmod.Register(ModuleName, 1109, "the BTC slashing tx is not valid")
-	ErrInvalidCovenantSig        = errorsmod.Register(ModuleName, 1110, "the covenant signature is not valid")
-	ErrCommissionLTMinRate       = errorsmod.Register(ModuleName, 1111, "commission cannot be less than min rate")
-	ErrCommissionGTMaxRate       = errorsmod.Register(ModuleName, 1112, "commission cannot be more than one")
-	ErrInvalidDelegationState    = errorsmod.Register(ModuleName, 1113, "Unexpected delegation state")
-	ErrInvalidUnbondingTx        = errorsmod.Register(ModuleName, 1114, "the BTC unbonding tx is not valid")
-	ErrEmptyFpList               = errorsmod.Register(ModuleName, 1115, "the finality provider list is empty")
-	ErrTooManyFpKeys             = errorsmod.Register(ModuleName, 1116, "the finality provider list contains too many public keys, it must contain exactly one")
-	ErrInvalidProofOfPossession  = errorsmod.Register(ModuleName, 1117, "the proof of possession is not valid")
-	ErrDuplicatedFp              = errorsmod.Register(ModuleName, 1118, "the staking request contains duplicated finality provider public key")
-	ErrInvalidBTCUndelegateReq   = errorsmod.Register(ModuleName, 1119, "invalid undelegation request")
-	ErrParamsNotFound            = errorsmod.Register(ModuleName, 1120, "the parameters are not found")
-	ErrFpAlreadyJailed           = errorsmod.Register(ModuleName, 1121, "the finality provider has already been jailed")
-	ErrFpNotJailed               = errorsmod.Register(ModuleName, 1122, "the finality provider is not jailed")
-	ErrDuplicatedCovenantSig     = errorsmod.Register(ModuleName, 1123, "the covenant signature is already submitted")
-	ErrStakingTxIncludedTooEarly = errorsmod.Register(ModuleName, 1124, "the staking transaction is included too early in BTC chain")
-	ErrConsumerIDNotRegistered   = errorsmod.Register(ModuleName, 1125, "Consumer is not registered")
-	ErrNoBabylonFPRestaked       = errorsmod.Register(ModuleName, 1126, "the BTC delegation request does not restake to any Babylon finality provider")
-	ErrEmptyCommissionRates      = errorsmod.Register(ModuleName, 1127, "empty commission")
-	ErrLargestBtcReorgNotFound   = errorsmod.Register(ModuleName, 1128, "there is no BTC reorg currently set")
+	ErrFpNotFound                 = errorsmod.Register(ModuleName, 1100, "the finality provider is not found")
+	ErrBTCDelegatorNotFound       = errorsmod.Register(ModuleName, 1101, "the BTC delegator is not found")
+	ErrBTCDelegationNotFound      = errorsmod.Register(ModuleName, 1102, "the BTC delegation is not found")
+	ErrFpRegistered               = errorsmod.Register(ModuleName, 1103, "the finality provider has already been registered")
+	ErrFpAlreadySlashed           = errorsmod.Register(ModuleName, 1104, "the finality provider has already been slashed")
+	ErrBTCHeightNotFound          = errorsmod.Register(ModuleName, 1105, "the BTC height is not found")
+	ErrReusedStakingTx            = errorsmod.Register(ModuleName, 1106, "the BTC staking tx is already used")
+	ErrInvalidCovenantPK          = errorsmod.Register(ModuleName, 1107, "the BTC staking tx specifies a wrong covenant PK")
+	ErrInvalidStakingTx           = errorsmod.Register(ModuleName, 1108, "the BTC staking tx is not valid")
+	ErrInvalidSlashingTx          = errorsmod.Register(ModuleName, 1109, "the BTC slashing tx is not valid")
+	ErrInvalidCovenantSig         = errorsmod.Register(ModuleName, 1110, "the covenant signature is not valid")
+	ErrCommissionLTMinRate        = errorsmod.Register(ModuleName, 1111, "commission cannot be less than min rate")
+	ErrCommissionGTMaxRate        = errorsmod.Register(ModuleName, 1112, "commission cannot be more than one")
+	ErrInvalidDelegationState     = errorsmod.Register(ModuleName, 1113, "Unexpected delegation state")
+	ErrInvalidUnbondingTx         = errorsmod.Register(ModuleName, 1114, "the BTC unbonding tx is not valid")
+	ErrEmptyFpList                = errorsmod.Register(ModuleName, 1115, "the finality provider list is empty")
+	ErrTooManyFpKeys              = errorsmod.Register(ModuleName, 1116, "the finality provider list contains too many public keys, it must contain exactly one")
+	ErrInvalidProofOfPossession   = errorsmod.Register(ModuleName, 1117, "the proof of possession is not valid")
+	ErrDuplicatedFp               = errorsmod.Register(ModuleName, 1118, "the staking request contains duplicated finality provider public key")
+	ErrInvalidBTCUndelegateReq    = errorsmod.Register(ModuleName, 1119, "invalid undelegation request")
+	ErrParamsNotFound             = errorsmod.Register(ModuleName, 1120, "the parameters are not found")
+	ErrFpAlreadyJailed            = errorsmod.Register(ModuleName, 1121, "the finality provider has already been jailed")
+	ErrFpNotJailed                = errorsmod.Register(ModuleName, 1122, "the finality provider is not jailed")
+	ErrDuplicatedCovenantSig      = errorsmod.Register(ModuleName, 1123, "the covenant signature is already submitted")
+	ErrStakingTxIncludedTooEarly  = errorsmod.Register(ModuleName, 1124, "the staking transaction is included too early in BTC chain")
+	ErrConsumerIDNotRegistered    = errorsmod.Register(ModuleName, 1125, "Consumer is not registered")
+	ErrNoBabylonFPRestaked        = errorsmod.Register(ModuleName, 1126, "the BTC delegation request does not restake to any Babylon finality provider")
+	ErrEmptyCommissionRates       = errorsmod.Register(ModuleName, 1127, "empty commission")
+	ErrLargestBtcReorgNotFound    = errorsmod.Register(ModuleName, 1128, "there is no BTC reorg currently set")
+	ErrTooManyFPs                 = errorsmod.Register(ModuleName, 1129, "too many finality providers for consumer")
+	ErrTooManyFPsFromSameConsumer = errorsmod.Register(ModuleName, 1130, "too many finality providers from same consumer")
 )
diff --git a/x/btcstaking/types/expected_keepers.go b/x/btcstaking/types/expected_keepers.go
index 93159e7d8..728fbbbf0 100644
--- a/x/btcstaking/types/expected_keepers.go
+++ b/x/btcstaking/types/expected_keepers.go
@@ -29,6 +29,7 @@ type BTCStkConsumerKeeper interface {
 	GetConsumerOfFinalityProvider(ctx context.Context, fpBTCPK *bbn.BIP340PubKey) (string, error)
 	GetConsumerFinalityProvider(ctx context.Context, consumerID string, fpBTCPK *bbn.BIP340PubKey) (*FinalityProvider, error)
 	SetConsumerFinalityProvider(ctx context.Context, fp *FinalityProvider)
+	GetConsumerRegistryMaxMultiStakedFps(ctx context.Context, consumerID string) (uint32, error)
 }
 
 type IncentiveKeeper interface {
diff --git a/x/btcstaking/types/mocked_keepers.go b/x/btcstaking/types/mocked_keepers.go
index 0243a99c5..a84c508ff 100644
--- a/x/btcstaking/types/mocked_keepers.go
+++ b/x/btcstaking/types/mocked_keepers.go
@@ -208,6 +208,21 @@ func (mr *MockBTCStkConsumerKeeperMockRecorder) GetConsumerOfFinalityProvider(ct
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsumerOfFinalityProvider", reflect.TypeOf((*MockBTCStkConsumerKeeper)(nil).GetConsumerOfFinalityProvider), ctx, fpBTCPK)
 }
 
+// GetConsumerRegistryMaxMultiStakedFps mocks base method.
+func (m *MockBTCStkConsumerKeeper) GetConsumerRegistryMaxMultiStakedFps(ctx context.Context, consumerID string) (uint32, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetConsumerRegistryMaxMultiStakedFps", ctx, consumerID)
+	ret0, _ := ret[0].(uint32)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetConsumerRegistryMaxMultiStakedFps indicates an expected call of GetConsumerRegistryMaxMultiStakedFps.
+func (mr *MockBTCStkConsumerKeeperMockRecorder) GetConsumerRegistryMaxMultiStakedFps(ctx, consumerID interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsumerRegistryMaxMultiStakedFps", reflect.TypeOf((*MockBTCStkConsumerKeeper)(nil).GetConsumerRegistryMaxMultiStakedFps), ctx, consumerID)
+}
+
 // HasConsumerFinalityProvider mocks base method.
 func (m *MockBTCStkConsumerKeeper) HasConsumerFinalityProvider(ctx context.Context, fpBTCPK *types.BIP340PubKey) bool {
 	m.ctrl.T.Helper()
diff --git a/x/btcstkconsumer/client/cli/tx.go b/x/btcstkconsumer/client/cli/tx.go
index 489de96eb..e374e0e82 100644
--- a/x/btcstkconsumer/client/cli/tx.go
+++ b/x/btcstkconsumer/client/cli/tx.go
@@ -2,6 +2,7 @@ package cli
 
 import (
 	"fmt"
+	"strconv"
 	"strings"
 
 	"github.com/cosmos/cosmos-sdk/client/flags"
@@ -32,12 +33,14 @@ func GetTxCmd() *cobra.Command {
 
 func NewRegisterConsumerCmd() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "register-consumer <consumer-id> <name> <description> [eth-l2-address]",
-		Args:  cobra.MinimumNArgs(3),
+		Use:   "register-consumer <consumer-id> <name> <description> <max-multi-staked-fps> [eth-l2-address]",
+		Args:  cobra.MinimumNArgs(4),
 		Short: "Registers a consumer",
 		Long: strings.TrimSpace(
 			`Registers a consumer with Babylon. The consumer-id must be unique and will be used to identify this consumer.
-			The name and optional description help identify the purpose of this consumer.`,
+			The name and optional description help identify the purpose of this consumer.
+			The max-multi-staked-fps specifies the maximum number of finality providers from this consumer that can be part of a single BTC delegation.
+			Must be at least 2 to allow for at least one Babylon FP and one consumer FP.`,
 		),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			clientCtx, err := client.GetClientTxContext(cmd)
@@ -47,19 +50,26 @@ func NewRegisterConsumerCmd() *cobra.Command {
 
 			consumerId := args[0]
 			if consumerId == "" {
-				return fmt.Errorf("consumer's id cannot be empty")
+				return types.ErrEmptyConsumerId
 			}
 			name := args[1]
 			if name == "" {
-				return fmt.Errorf("consumer's name cannot be empty")
+				return types.ErrEmptyConsumerName
 			}
 			description := args[2]
 			if description == "" {
-				return fmt.Errorf("consumer's description cannot be empty")
+				return types.ErrEmptyConsumerDescription
+			}
+			maxMultiStakedFps, err := strconv.ParseUint(args[3], 10, 32)
+			if err != nil {
+				return fmt.Errorf("invalid max-multi-staked-fps: %w", err)
+			}
+			if maxMultiStakedFps < 2 {
+				return types.ErrInvalidMaxMultiStakedFps
 			}
 			ethL2Address := ""
-			if len(args) > 3 {
-				ethL2Address = args[3]
+			if len(args) > 4 {
+				ethL2Address = args[4]
 			}
 
 			msg := types.MsgRegisterConsumer{
@@ -67,6 +77,7 @@ func NewRegisterConsumerCmd() *cobra.Command {
 				ConsumerId:                   consumerId,
 				ConsumerName:                 name,
 				ConsumerDescription:          description,
+				MaxMultiStakedFps:            uint32(maxMultiStakedFps),
 				EthL2FinalityContractAddress: ethL2Address,
 			}
 
diff --git a/x/btcstkconsumer/keeper/consumer_registry.go b/x/btcstkconsumer/keeper/consumer_registry.go
index 42eb3146a..4d33e30db 100644
--- a/x/btcstkconsumer/keeper/consumer_registry.go
+++ b/x/btcstkconsumer/keeper/consumer_registry.go
@@ -51,6 +51,15 @@ func (k Keeper) GetConsumerRegister(ctx context.Context, consumerID string) (*ty
 	return &consumerRegister, nil
 }
 
+// GetConsumerRegistryMaxMultiStakedFps gets the max_multi_staked_fps from the consumer registry for a given consumer ID
+func (k Keeper) GetConsumerRegistryMaxMultiStakedFps(ctx context.Context, consumerID string) (uint32, error) {
+	consumerRegister, err := k.GetConsumerRegister(ctx, consumerID)
+	if err != nil {
+		return 0, err
+	}
+	return consumerRegister.MaxMultiStakedFps, nil
+}
+
 // GetAllRegisteredConsumerIDs gets all consumer IDs that registered to Babylon
 func (k Keeper) GetAllRegisteredConsumerIDs(ctx context.Context) []string {
 	consumerIDs := []string{}
diff --git a/x/btcstkconsumer/keeper/consumer_registry_test.go b/x/btcstkconsumer/keeper/consumer_registry_test.go
index a8121ab1c..00e9c3879 100644
--- a/x/btcstkconsumer/keeper/consumer_registry_test.go
+++ b/x/btcstkconsumer/keeper/consumer_registry_test.go
@@ -20,25 +20,26 @@ func FuzzConsumerRegistry(f *testing.F) {
 		ctx := babylonApp.NewContext(false)
 
 		// generate a random consumer register
-		consumerRegister := datagen.GenRandomCosmosConsumerRegister(r)
+		validConsumer := datagen.GenRandomCosmosConsumerRegister(r)
 
 		// check that the consumer is not registered
-		isRegistered := bscKeeper.IsConsumerRegistered(ctx, consumerRegister.ConsumerId)
+		isRegistered := bscKeeper.IsConsumerRegistered(ctx, validConsumer.ConsumerId)
 		require.False(t, isRegistered)
 
 		// Check that the consumer is not registered
-		consumerRegister2, err := bscKeeper.GetConsumerRegister(ctx, consumerRegister.ConsumerId)
+		retrievedConsumer, err := bscKeeper.GetConsumerRegister(ctx, validConsumer.ConsumerId)
 		require.Error(t, err)
-		require.Nil(t, consumerRegister2)
+		require.Nil(t, retrievedConsumer)
 
 		// Register the consumer
-		err = bscKeeper.RegisterConsumer(ctx, consumerRegister)
+		err = bscKeeper.RegisterConsumer(ctx, validConsumer)
 		require.NoError(t, err)
 		// check that the consumer is registered
-		consumerRegister2, err = bscKeeper.GetConsumerRegister(ctx, consumerRegister.ConsumerId)
+		retrievedConsumer, err = bscKeeper.GetConsumerRegister(ctx, validConsumer.ConsumerId)
 		require.NoError(t, err)
-		require.Equal(t, consumerRegister.ConsumerId, consumerRegister2.ConsumerId)
-		require.Equal(t, consumerRegister.ConsumerName, consumerRegister2.ConsumerName)
-		require.Equal(t, consumerRegister.ConsumerDescription, consumerRegister2.ConsumerDescription)
+		require.Equal(t, validConsumer.ConsumerId, retrievedConsumer.ConsumerId)
+		require.Equal(t, validConsumer.ConsumerName, retrievedConsumer.ConsumerName)
+		require.Equal(t, validConsumer.ConsumerDescription, retrievedConsumer.ConsumerDescription)
+		require.Equal(t, validConsumer.MaxMultiStakedFps, retrievedConsumer.MaxMultiStakedFps)
 	})
 }
diff --git a/x/btcstkconsumer/keeper/finality_provider_registry_test.go b/x/btcstkconsumer/keeper/finality_provider_registry_test.go
index 716e5f01e..4edccb775 100644
--- a/x/btcstkconsumer/keeper/finality_provider_registry_test.go
+++ b/x/btcstkconsumer/keeper/finality_provider_registry_test.go
@@ -36,12 +36,15 @@ func FuzzFPRegistry(f *testing.F) {
 		consumerName := datagen.GenRandomHexStr(r, 5)
 		// Create a random consumer description
 		consumerDesc := "Consumer description: " + datagen.GenRandomHexStr(r, 15)
+		// Generate random max_multi_staked_fps between 1 and 10
+		maxMultiStakedFps := uint32(r.Intn(10) + 1)
 
 		// Populate ConsumerRegister object
 		consumerRegister := &types.ConsumerRegister{
 			ConsumerId:          consumerID,
 			ConsumerName:        consumerName,
 			ConsumerDescription: consumerDesc,
+			MaxMultiStakedFps:   maxMultiStakedFps,
 		}
 
 		// Register the consumer
diff --git a/x/btcstkconsumer/keeper/grpc_query_test.go b/x/btcstkconsumer/keeper/grpc_query_test.go
index 4cdd57916..328345671 100644
--- a/x/btcstkconsumer/keeper/grpc_query_test.go
+++ b/x/btcstkconsumer/keeper/grpc_query_test.go
@@ -33,13 +33,17 @@ func FuzzConsumerRegistryList(f *testing.F) {
 		// invoke the consumer registration a random number of times with random consumer IDs
 		numRegistrations := datagen.RandomInt(r, 100) + 1
 		var allConsumerIDs []string
+		consumerMaxFps := make(map[string]uint32) // Track max_multi_staked_fps for each consumer
 		for i := uint64(0); i < numRegistrations; i++ {
 			var consumerID = datagen.GenRandomHexStr(r, 30)
+			maxFps := uint32(datagen.RandomInt(r, 10) + 2)
 			allConsumerIDs = append(allConsumerIDs, consumerID)
+			consumerMaxFps[consumerID] = maxFps
 
 			err := bscKeeper.RegisterConsumer(ctx, &types.ConsumerRegister{
-				ConsumerId:   consumerID,
-				ConsumerName: datagen.GenRandomHexStr(r, 5),
+				ConsumerId:        consumerID,
+				ConsumerName:      datagen.GenRandomHexStr(r, 5),
+				MaxMultiStakedFps: maxFps,
 			})
 			require.NoError(t, err)
 		}
@@ -57,7 +61,9 @@ func FuzzConsumerRegistryList(f *testing.F) {
 
 		require.Equal(t, limit, uint64(len(actualConsumerRegisters)))
 		for i := uint64(0); i < limit; i++ {
-			require.Contains(t, allConsumerIDs, actualConsumerRegisters[i].ConsumerId)
+			consumerID := actualConsumerRegisters[i].ConsumerId
+			require.Contains(t, allConsumerIDs, consumerID)
+			require.Equal(t, consumerMaxFps[consumerID], actualConsumerRegisters[i].MaxMultiStakedFps)
 		}
 	})
 }
@@ -75,20 +81,24 @@ func FuzzConsumersRegistry(f *testing.F) {
 		var (
 			consumersRegister []consumerRegister
 			consumerIDs       []string
+			maxMultiStakedFps []uint32
 		)
 		// invoke the consumer registration a random number of times with random consumer IDs
 		numConsumers := datagen.RandomInt(r, 100) + 1
 		for i := uint64(0); i < numConsumers; i++ {
 			consumerID := datagen.GenRandomHexStr(r, 30)
+			maxFps := uint32(datagen.RandomInt(r, 10) + 2)
 
 			consumerIDs = append(consumerIDs, consumerID)
+			maxMultiStakedFps = append(maxMultiStakedFps, maxFps)
 			consumersRegister = append(consumersRegister, consumerRegister{
 				consumerID: consumerID,
 			})
 
 			err := bscKeeper.RegisterConsumer(ctx, &types.ConsumerRegister{
-				ConsumerId:   consumerID,
-				ConsumerName: datagen.GenRandomHexStr(r, 5),
+				ConsumerId:        consumerID,
+				ConsumerName:      datagen.GenRandomHexStr(r, 5),
+				MaxMultiStakedFps: maxFps,
 			})
 			require.NoError(t, err)
 		}
@@ -100,6 +110,7 @@ func FuzzConsumersRegistry(f *testing.F) {
 
 		for i, respData := range resp.ConsumerRegisters {
 			require.Equal(t, consumersRegister[i].consumerID, respData.ConsumerId)
+			require.Equal(t, maxMultiStakedFps[i], respData.MaxMultiStakedFps)
 		}
 	})
 }
diff --git a/x/btcstkconsumer/keeper/msg_server.go b/x/btcstkconsumer/keeper/msg_server.go
index 0470f4c51..e8a0f12f6 100644
--- a/x/btcstkconsumer/keeper/msg_server.go
+++ b/x/btcstkconsumer/keeper/msg_server.go
@@ -52,6 +52,7 @@ func (ms msgServer) RegisterConsumer(ctx context.Context, req *types.MsgRegister
 			req.ConsumerName,
 			req.ConsumerDescription,
 			req.EthL2FinalityContractAddress,
+			req.MaxMultiStakedFps,
 		)
 		if err := ms.Keeper.RegisterConsumer(ctx, consumerRegister); err != nil {
 			return nil, err
@@ -71,6 +72,7 @@ func (ms msgServer) RegisterConsumer(ctx context.Context, req *types.MsgRegister
 			req.ConsumerId,
 			req.ConsumerName,
 			req.ConsumerDescription,
+			req.MaxMultiStakedFps,
 		)
 		if err := ms.Keeper.RegisterConsumer(ctx, consumerRegister); err != nil {
 			return nil, err
diff --git a/x/btcstkconsumer/keeper/msg_server_test.go b/x/btcstkconsumer/keeper/msg_server_test.go
index c462aec87..26c77f237 100644
--- a/x/btcstkconsumer/keeper/msg_server_test.go
+++ b/x/btcstkconsumer/keeper/msg_server_test.go
@@ -76,6 +76,7 @@ func FuzzRegisterConsumer(f *testing.F) {
 			ConsumerId:          consumerRegister.ConsumerId,
 			ConsumerName:        consumerRegister.ConsumerName,
 			ConsumerDescription: consumerRegister.ConsumerDescription,
+			MaxMultiStakedFps:   consumerRegister.MaxMultiStakedFps,
 		})
 		require.NoError(t, err)
 		// check that the consumer is registered
@@ -83,6 +84,23 @@ func FuzzRegisterConsumer(f *testing.F) {
 		require.NoError(t, err)
 		require.Equal(t, consumerRegister.String(), consumerRegister2.String())
 
+		/*
+			Test registering consumer with invalid max_multi_staked_fps (zero)
+		*/
+		// generate a random consumer register
+		consumerRegister = datagen.GenRandomCosmosConsumerRegister(r)
+		// mock IBC light client
+		babylonApp.IBCKeeper.ClientKeeper.SetClientState(ctx, consumerRegister.ConsumerId, &ibctmtypes.ClientState{})
+		// Register the consumer with zero max_multi_staked_fps
+		_, err = msgServer.RegisterConsumer(ctx, &types.MsgRegisterConsumer{
+			ConsumerId:          consumerRegister.ConsumerId,
+			ConsumerName:        consumerRegister.ConsumerName,
+			ConsumerDescription: consumerRegister.ConsumerDescription,
+			MaxMultiStakedFps:   0,
+		})
+		require.Error(t, err)
+		require.ErrorIs(t, err, types.ErrInvalidMaxMultiStakedFps)
+
 		/*
 			Test registering ETH L2 consumer
 		*/
@@ -95,6 +113,7 @@ func FuzzRegisterConsumer(f *testing.F) {
 			ConsumerId:                   consumerRegister.ConsumerId,
 			ConsumerName:                 consumerRegister.ConsumerName,
 			ConsumerDescription:          consumerRegister.ConsumerDescription,
+			MaxMultiStakedFps:            consumerRegister.MaxMultiStakedFps,
 			EthL2FinalityContractAddress: contractAddr.String(),
 		})
 		require.NoError(t, err)
@@ -103,6 +122,7 @@ func FuzzRegisterConsumer(f *testing.F) {
 		require.NoError(t, err)
 		require.Equal(t, consumerRegister.String(), consumerRegister2.String())
 		require.Equal(t, types.ConsumerType_ETH_L2, consumerRegister2.Type())
+		require.Equal(t, consumerRegister.MaxMultiStakedFps, consumerRegister2.MaxMultiStakedFps)
 	})
 }
 
diff --git a/x/btcstkconsumer/types/btcstkconsumer.pb.go b/x/btcstkconsumer/types/btcstkconsumer.pb.go
index 297c8b7d0..a163a85dd 100644
--- a/x/btcstkconsumer/types/btcstkconsumer.pb.go
+++ b/x/btcstkconsumer/types/btcstkconsumer.pb.go
@@ -62,6 +62,10 @@ type ConsumerRegister struct {
 	ConsumerName string `protobuf:"bytes,2,opt,name=consumer_name,json=consumerName,proto3" json:"consumer_name,omitempty"`
 	// consumer_description is a description for the consumer (can be empty)
 	ConsumerDescription string `protobuf:"bytes,3,opt,name=consumer_description,json=consumerDescription,proto3" json:"consumer_description,omitempty"`
+	// max_multi_staked_fps is the maximum number of finality providers from this consumer
+	// that can be part of a single BTC delegation. Must be at least 2 to allow for at least
+	// one Babylon FP and one consumer FP.
+	MaxMultiStakedFps uint32 `protobuf:"varint,4,opt,name=max_multi_staked_fps,json=maxMultiStakedFps,proto3" json:"max_multi_staked_fps,omitempty"`
 	// consumer_metadata is necessary metadata of the consumer, and the data
 	// depends on the type of integration
 	//
@@ -111,10 +115,10 @@ type isConsumerRegister_ConsumerMetadata interface {
 }
 
 type ConsumerRegister_CosmosConsumerMetadata struct {
-	CosmosConsumerMetadata *CosmosConsumerMetadata `protobuf:"bytes,4,opt,name=cosmos_consumer_metadata,json=cosmosConsumerMetadata,proto3,oneof" json:"cosmos_consumer_metadata,omitempty"`
+	CosmosConsumerMetadata *CosmosConsumerMetadata `protobuf:"bytes,5,opt,name=cosmos_consumer_metadata,json=cosmosConsumerMetadata,proto3,oneof" json:"cosmos_consumer_metadata,omitempty"`
 }
 type ConsumerRegister_EthL2ConsumerMetadata struct {
-	EthL2ConsumerMetadata *ETHL2ConsumerMetadata `protobuf:"bytes,5,opt,name=eth_l2_consumer_metadata,json=ethL2ConsumerMetadata,proto3,oneof" json:"eth_l2_consumer_metadata,omitempty"`
+	EthL2ConsumerMetadata *ETHL2ConsumerMetadata `protobuf:"bytes,6,opt,name=eth_l2_consumer_metadata,json=ethL2ConsumerMetadata,proto3,oneof" json:"eth_l2_consumer_metadata,omitempty"`
 }
 
 func (*ConsumerRegister_CosmosConsumerMetadata) isConsumerRegister_ConsumerMetadata() {}
@@ -148,6 +152,13 @@ func (m *ConsumerRegister) GetConsumerDescription() string {
 	return ""
 }
 
+func (m *ConsumerRegister) GetMaxMultiStakedFps() uint32 {
+	if m != nil {
+		return m.MaxMultiStakedFps
+	}
+	return 0
+}
+
 func (m *ConsumerRegister) GetCosmosConsumerMetadata() *CosmosConsumerMetadata {
 	if x, ok := m.GetConsumerMetadata().(*ConsumerRegister_CosmosConsumerMetadata); ok {
 		return x.CosmosConsumerMetadata
@@ -275,34 +286,37 @@ func init() {
 }
 
 var fileDescriptor_de3ccd621fe1efd4 = []byte{
-	// 430 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x52, 0x4f, 0x6f, 0xd3, 0x30,
-	0x14, 0x4f, 0x06, 0x4c, 0xda, 0xdb, 0x90, 0x8a, 0xf7, 0x87, 0x14, 0x89, 0x30, 0x15, 0x09, 0x4d,
-	0x48, 0x4b, 0x68, 0x41, 0xe2, 0xc0, 0x89, 0x96, 0x49, 0x9d, 0xb4, 0x31, 0xa9, 0x0d, 0x17, 0x2e,
-	0x91, 0xe3, 0x98, 0xc6, 0x5a, 0x6c, 0x47, 0xb1, 0x57, 0x91, 0x6f, 0xc1, 0x87, 0xe1, 0x43, 0x70,
-	0x9c, 0x38, 0x71, 0x44, 0xed, 0xd7, 0xe0, 0x80, 0x96, 0xd8, 0x9d, 0xd4, 0x05, 0x6e, 0x7e, 0xbf,
-	0x3f, 0xfe, 0xbd, 0x67, 0x3f, 0x08, 0x12, 0x9c, 0x54, 0xb9, 0x14, 0x61, 0xa2, 0x89, 0xd2, 0x97,
-	0x44, 0x0a, 0x75, 0xc5, 0x69, 0x19, 0xce, 0xfb, 0x6b, 0x48, 0x50, 0x94, 0x52, 0x4b, 0xd4, 0x35,
-	0xfa, 0x60, 0x8d, 0x9d, 0xf7, 0x9f, 0x74, 0x89, 0x54, 0x5c, 0xaa, 0xb8, 0x16, 0x86, 0x4d, 0xd1,
-	0xb8, 0x7a, 0x7f, 0x36, 0xa0, 0x33, 0x32, 0xd2, 0x09, 0x9d, 0x31, 0xa5, 0x69, 0x89, 0x9e, 0xc1,
-	0xb6, 0xb5, 0xc7, 0x2c, 0xf5, 0xdc, 0x43, 0xf7, 0x68, 0x6b, 0x02, 0x16, 0x3a, 0x4d, 0xd1, 0x73,
-	0x78, 0xb8, 0x12, 0x08, 0xcc, 0xa9, 0xb7, 0x51, 0x4b, 0x76, 0x2c, 0xf8, 0x11, 0x73, 0x8a, 0xfa,
-	0xb0, 0xb7, 0x12, 0xa5, 0x54, 0x91, 0x92, 0x15, 0x9a, 0x49, 0xe1, 0xdd, 0xab, 0xb5, 0xbb, 0x96,
-	0xfb, 0x70, 0x4b, 0x21, 0x0e, 0x9e, 0x69, 0x75, 0xe5, 0xe4, 0x54, 0xe3, 0x14, 0x6b, 0xec, 0xdd,
-	0x3f, 0x74, 0x8f, 0xb6, 0x07, 0xfd, 0xe0, 0x9f, 0x63, 0x06, 0xa3, 0xda, 0x6a, 0xa7, 0x39, 0x37,
-	0xc6, 0xb1, 0x33, 0x39, 0x20, 0xad, 0x0c, 0xba, 0x04, 0x8f, 0xea, 0x2c, 0xce, 0x07, 0x2d, 0x71,
-	0x0f, 0xea, 0xb8, 0x57, 0xff, 0x89, 0x3b, 0x89, 0xc6, 0x67, 0x83, 0x96, 0xb4, 0x7d, 0xaa, 0xb3,
-	0xbb, 0xc4, 0x70, 0x17, 0x1e, 0xdd, 0x49, 0xe9, 0xbd, 0x85, 0x83, 0xf6, 0xae, 0xd1, 0x53, 0x00,
-	0x92, 0x61, 0x21, 0x68, 0x7e, 0xfb, 0x05, 0x5b, 0x06, 0x39, 0x4d, 0x7b, 0x1c, 0xf6, 0x5b, 0xf3,
-	0x51, 0x04, 0xdd, 0x2f, 0x4c, 0xe0, 0x9c, 0xe9, 0xea, 0x66, 0x2a, 0x5d, 0x62, 0xa2, 0x63, 0x9c,
-	0xa6, 0x25, 0x55, 0xaa, 0xb9, 0x66, 0xe8, 0xfd, 0xfc, 0x7e, 0xbc, 0x67, 0xb6, 0xe0, 0x7d, 0xc3,
-	0x4c, 0x75, 0xc9, 0xc4, 0x6c, 0xf2, 0xd8, 0x5a, 0x47, 0xc6, 0x69, 0xe8, 0x97, 0x2f, 0x60, 0xc7,
-	0x26, 0x45, 0x55, 0x41, 0x11, 0xc0, 0xe6, 0xe8, 0x62, 0x7a, 0x7e, 0x31, 0xed, 0x38, 0x37, 0xe7,
-	0x93, 0x68, 0x1c, 0x9f, 0x0d, 0x3a, 0xee, 0xf0, 0xd3, 0x8f, 0x85, 0xef, 0x5e, 0x2f, 0x7c, 0xf7,
-	0xf7, 0xc2, 0x77, 0xbf, 0x2d, 0x7d, 0xe7, 0x7a, 0xe9, 0x3b, 0xbf, 0x96, 0xbe, 0xf3, 0xf9, 0xdd,
-	0x8c, 0xe9, 0xec, 0x2a, 0x09, 0x88, 0xe4, 0xa1, 0x79, 0xd3, 0x1c, 0x27, 0xea, 0x98, 0x49, 0x5b,
-	0x86, 0xf3, 0x37, 0xe1, 0xd7, 0xf5, 0x6d, 0xd7, 0x55, 0x41, 0x55, 0xb2, 0x59, 0x2f, 0xeb, 0xeb,
-	0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0xb8, 0xae, 0x1e, 0x0b, 0x14, 0x03, 0x00, 0x00,
+	// 466 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x93, 0xc1, 0x6e, 0xd3, 0x30,
+	0x1c, 0xc6, 0x1b, 0x06, 0x95, 0xf6, 0xdf, 0x26, 0x75, 0x5e, 0x37, 0x52, 0x24, 0x42, 0x55, 0x24,
+	0x54, 0x21, 0x2d, 0xa1, 0x05, 0x89, 0x03, 0x27, 0x5a, 0x86, 0x3a, 0x69, 0x65, 0x52, 0x5a, 0x2e,
+	0x5c, 0x22, 0x27, 0xf1, 0x5a, 0xab, 0x71, 0x1c, 0xc5, 0xff, 0x56, 0xed, 0x5b, 0xf0, 0x22, 0xdc,
+	0x78, 0x08, 0x8e, 0x13, 0x27, 0x8e, 0xa8, 0x7d, 0x11, 0xd4, 0xd4, 0xe9, 0xa4, 0x2e, 0xec, 0x16,
+	0x7f, 0xbf, 0xef, 0xcb, 0x97, 0xbf, 0x63, 0x83, 0xed, 0x53, 0x7f, 0x11, 0xc9, 0xd8, 0xf1, 0x31,
+	0x50, 0x38, 0x09, 0x64, 0xac, 0xa6, 0x82, 0xa5, 0xce, 0xac, 0xb5, 0xa3, 0xd8, 0x49, 0x2a, 0x51,
+	0x92, 0x9a, 0xf6, 0xdb, 0x3b, 0x74, 0xd6, 0x7a, 0x56, 0x0b, 0xa4, 0x12, 0x52, 0x79, 0x99, 0xd1,
+	0xd9, 0x2c, 0x36, 0xa9, 0xc6, 0x8f, 0x3d, 0xa8, 0x74, 0xb5, 0xd5, 0x65, 0x23, 0xae, 0x90, 0xa5,
+	0xe4, 0x05, 0x1c, 0xe4, 0x71, 0x8f, 0x87, 0xa6, 0x51, 0x37, 0x9a, 0xfb, 0x2e, 0xe4, 0xd2, 0x65,
+	0x48, 0x5e, 0xc2, 0xd1, 0xd6, 0x10, 0x53, 0xc1, 0xcc, 0x47, 0x99, 0xe5, 0x30, 0x17, 0xbf, 0x50,
+	0xc1, 0x48, 0x0b, 0xaa, 0x5b, 0x53, 0xc8, 0x54, 0x90, 0xf2, 0x04, 0xb9, 0x8c, 0xcd, 0xbd, 0xcc,
+	0x7b, 0x92, 0xb3, 0x4f, 0x77, 0x88, 0x38, 0x50, 0x15, 0x74, 0xee, 0x89, 0x69, 0x84, 0xdc, 0x53,
+	0x48, 0x27, 0x2c, 0xf4, 0x6e, 0x12, 0x65, 0x3e, 0xae, 0x1b, 0xcd, 0x23, 0xf7, 0x58, 0xd0, 0x79,
+	0x7f, 0x8d, 0x06, 0x19, 0xf9, 0x9c, 0x28, 0x22, 0xc0, 0xd4, 0xb3, 0x6d, 0xab, 0x04, 0x43, 0x1a,
+	0x52, 0xa4, 0xe6, 0x93, 0xba, 0xd1, 0x3c, 0x68, 0xb7, 0xec, 0xff, 0xee, 0x8b, 0xdd, 0xcd, 0xa2,
+	0xf9, 0xf8, 0x7d, 0x1d, 0xec, 0x95, 0xdc, 0xb3, 0xa0, 0x90, 0x90, 0x09, 0x98, 0x0c, 0xc7, 0x5e,
+	0xd4, 0x2e, 0xa8, 0x2b, 0x67, 0x75, 0x6f, 0x1e, 0xa8, 0xbb, 0x18, 0xf6, 0xae, 0xda, 0x05, 0x6d,
+	0xa7, 0x0c, 0xc7, 0xf7, 0x41, 0xe7, 0x04, 0x8e, 0xef, 0xb5, 0x34, 0xde, 0xc3, 0x59, 0xf1, 0x57,
+	0x93, 0xe7, 0x00, 0xc1, 0x98, 0xc6, 0x31, 0x8b, 0xee, 0xfe, 0xd9, 0xbe, 0x56, 0x2e, 0xc3, 0x86,
+	0x80, 0xd3, 0xc2, 0x7e, 0x32, 0x84, 0xda, 0x0d, 0x8f, 0x69, 0xc4, 0x71, 0xb1, 0x9e, 0x0a, 0x53,
+	0x1a, 0xa0, 0x47, 0xc3, 0x30, 0x65, 0x4a, 0x6d, 0x5e, 0xd3, 0x31, 0x7f, 0xff, 0x3c, 0xaf, 0xea,
+	0x63, 0xf3, 0x71, 0x43, 0x06, 0x98, 0xf2, 0x78, 0xe4, 0x3e, 0xcd, 0xa3, 0x5d, 0x9d, 0xd4, 0xf8,
+	0xf5, 0x2b, 0x38, 0xcc, 0x9b, 0x86, 0x8b, 0x84, 0x11, 0x80, 0x72, 0xf7, 0x7a, 0xd0, 0xbf, 0x1e,
+	0x54, 0x4a, 0xeb, 0xe7, 0x8b, 0x61, 0xcf, 0xbb, 0x6a, 0x57, 0x8c, 0xce, 0xd7, 0x5f, 0x4b, 0xcb,
+	0xb8, 0x5d, 0x5a, 0xc6, 0xdf, 0xa5, 0x65, 0x7c, 0x5f, 0x59, 0xa5, 0xdb, 0x95, 0x55, 0xfa, 0xb3,
+	0xb2, 0x4a, 0xdf, 0x3e, 0x8c, 0x38, 0x8e, 0xa7, 0xbe, 0x1d, 0x48, 0xe1, 0xe8, 0x3d, 0x8d, 0xa8,
+	0xaf, 0xce, 0xb9, 0xcc, 0x97, 0xce, 0xec, 0x9d, 0x33, 0xdf, 0xbd, 0x1e, 0xb8, 0x48, 0x98, 0xf2,
+	0xcb, 0xd9, 0xe9, 0x7e, 0xfb, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x79, 0xe0, 0xc8, 0x1e, 0x45, 0x03,
+	0x00, 0x00,
 }
 
 func (m *ConsumerRegister) Marshal() (dAtA []byte, err error) {
@@ -334,6 +348,11 @@ func (m *ConsumerRegister) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 			}
 		}
 	}
+	if m.MaxMultiStakedFps != 0 {
+		i = encodeVarintBtcstkconsumer(dAtA, i, uint64(m.MaxMultiStakedFps))
+		i--
+		dAtA[i] = 0x20
+	}
 	if len(m.ConsumerDescription) > 0 {
 		i -= len(m.ConsumerDescription)
 		copy(dAtA[i:], m.ConsumerDescription)
@@ -375,7 +394,7 @@ func (m *ConsumerRegister_CosmosConsumerMetadata) MarshalToSizedBuffer(dAtA []by
 			i = encodeVarintBtcstkconsumer(dAtA, i, uint64(size))
 		}
 		i--
-		dAtA[i] = 0x22
+		dAtA[i] = 0x2a
 	}
 	return len(dAtA) - i, nil
 }
@@ -396,7 +415,7 @@ func (m *ConsumerRegister_EthL2ConsumerMetadata) MarshalToSizedBuffer(dAtA []byt
 			i = encodeVarintBtcstkconsumer(dAtA, i, uint64(size))
 		}
 		i--
-		dAtA[i] = 0x2a
+		dAtA[i] = 0x32
 	}
 	return len(dAtA) - i, nil
 }
@@ -489,6 +508,9 @@ func (m *ConsumerRegister) Size() (n int) {
 	if l > 0 {
 		n += 1 + l + sovBtcstkconsumer(uint64(l))
 	}
+	if m.MaxMultiStakedFps != 0 {
+		n += 1 + sovBtcstkconsumer(uint64(m.MaxMultiStakedFps))
+	}
 	if m.ConsumerMetadata != nil {
 		n += m.ConsumerMetadata.Size()
 	}
@@ -677,6 +699,25 @@ func (m *ConsumerRegister) Unmarshal(dAtA []byte) error {
 			m.ConsumerDescription = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
 		case 4:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field MaxMultiStakedFps", wireType)
+			}
+			m.MaxMultiStakedFps = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowBtcstkconsumer
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.MaxMultiStakedFps |= uint32(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 5:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field CosmosConsumerMetadata", wireType)
 			}
@@ -711,7 +752,7 @@ func (m *ConsumerRegister) Unmarshal(dAtA []byte) error {
 			}
 			m.ConsumerMetadata = &ConsumerRegister_CosmosConsumerMetadata{v}
 			iNdEx = postIndex
-		case 5:
+		case 6:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field EthL2ConsumerMetadata", wireType)
 			}
diff --git a/x/btcstkconsumer/types/consumer_register.go b/x/btcstkconsumer/types/consumer_register.go
index 5b5278a44..087ebc617 100644
--- a/x/btcstkconsumer/types/consumer_register.go
+++ b/x/btcstkconsumer/types/consumer_register.go
@@ -1,38 +1,39 @@
 package types
 
-import (
-	"fmt"
-)
-
 func (m *MsgRegisterConsumer) ValidateBasic() error {
 	if len(m.ConsumerId) == 0 {
-		return fmt.Errorf("ConsumerId must be non-empty")
+		return ErrEmptyConsumerId
 	}
 	if len(m.ConsumerName) == 0 {
-		return fmt.Errorf("ConsumerName must be non-empty")
+		return ErrEmptyConsumerName
 	}
 	if len(m.ConsumerDescription) == 0 {
-		return fmt.Errorf("ConsumerDescription must be non-empty")
+		return ErrEmptyConsumerDescription
+	}
+	if m.MaxMultiStakedFps < 2 {
+		return ErrInvalidMaxMultiStakedFps
 	}
 	return nil
 }
 
-func NewCosmosConsumerRegister(consumerId, consumerName, consumerDescription string) *ConsumerRegister {
+func NewCosmosConsumerRegister(consumerId, consumerName, consumerDescription string, maxMultiStakedFps uint32) *ConsumerRegister {
 	return &ConsumerRegister{
 		ConsumerId:          consumerId,
 		ConsumerName:        consumerName,
 		ConsumerDescription: consumerDescription,
+		MaxMultiStakedFps:   maxMultiStakedFps,
 		ConsumerMetadata: &ConsumerRegister_CosmosConsumerMetadata{
 			CosmosConsumerMetadata: &CosmosConsumerMetadata{},
 		},
 	}
 }
 
-func NewETHL2ConsumerRegister(consumerId, consumerName, consumerDescription string, ethL2FinalityContractAddress string) *ConsumerRegister {
+func NewETHL2ConsumerRegister(consumerId, consumerName, consumerDescription string, ethL2FinalityContractAddress string, maxMultiStakedFps uint32) *ConsumerRegister {
 	return &ConsumerRegister{
 		ConsumerId:          consumerId,
 		ConsumerName:        consumerName,
 		ConsumerDescription: consumerDescription,
+		MaxMultiStakedFps:   maxMultiStakedFps,
 		ConsumerMetadata: &ConsumerRegister_EthL2ConsumerMetadata{
 			EthL2ConsumerMetadata: &ETHL2ConsumerMetadata{
 				FinalityContractAddress: ethL2FinalityContractAddress,
@@ -53,6 +54,7 @@ func (cr *ConsumerRegister) ToResponse() *ConsumerRegisterResponse {
 		ConsumerId:          cr.ConsumerId,
 		ConsumerName:        cr.ConsumerName,
 		ConsumerDescription: cr.ConsumerDescription,
+		MaxMultiStakedFps:   cr.MaxMultiStakedFps,
 	}
 	if cr.ConsumerMetadata != nil {
 		switch md := cr.ConsumerMetadata.(type) {
@@ -65,15 +67,18 @@ func (cr *ConsumerRegister) ToResponse() *ConsumerRegisterResponse {
 	return resp
 }
 
-func (cr ConsumerRegister) Validate() error {
+func (cr *ConsumerRegister) Validate() error {
 	if len(cr.ConsumerId) == 0 {
-		return fmt.Errorf("ConsumerId must be non-empty")
+		return ErrEmptyConsumerId
 	}
 	if len(cr.ConsumerName) == 0 {
-		return fmt.Errorf("ConsumerName must be non-empty")
+		return ErrEmptyConsumerName
 	}
 	if len(cr.ConsumerDescription) == 0 {
-		return fmt.Errorf("ConsumerDescription must be non-empty")
+		return ErrEmptyConsumerDescription
+	}
+	if cr.MaxMultiStakedFps < 2 {
+		return ErrInvalidMaxMultiStakedFps
 	}
 	return nil
 }
diff --git a/x/btcstkconsumer/types/consumer_register_test.go b/x/btcstkconsumer/types/consumer_register_test.go
index 95875675a..1aa5e7344 100644
--- a/x/btcstkconsumer/types/consumer_register_test.go
+++ b/x/btcstkconsumer/types/consumer_register_test.go
@@ -4,14 +4,14 @@ import (
 	"testing"
 
 	"github.com/babylonlabs-io/babylon/v4/x/btcstkconsumer/types"
-	"github.com/test-go/testify/require"
+	"github.com/stretchr/testify/require"
 )
 
 func TestConsumerRegisterValidate(t *testing.T) {
 	testCases := []struct {
 		desc        string
 		input       types.ConsumerRegister
-		expectedErr string
+		expectedErr error
 	}{
 		{
 			desc: "valid consumer",
@@ -19,6 +19,7 @@ func TestConsumerRegisterValidate(t *testing.T) {
 				ConsumerId:          "c1",
 				ConsumerName:        "Consumer One",
 				ConsumerDescription: "A valid consumer",
+				MaxMultiStakedFps:   2,
 			},
 		},
 		{
@@ -27,8 +28,9 @@ func TestConsumerRegisterValidate(t *testing.T) {
 				ConsumerId:          "",
 				ConsumerName:        "Consumer One",
 				ConsumerDescription: "A valid consumer",
+				MaxMultiStakedFps:   2,
 			},
-			expectedErr: "ConsumerId must be non-empty",
+			expectedErr: types.ErrEmptyConsumerId,
 		},
 		{
 			desc: "missing ConsumerName",
@@ -36,8 +38,9 @@ func TestConsumerRegisterValidate(t *testing.T) {
 				ConsumerId:          "c1",
 				ConsumerName:        "",
 				ConsumerDescription: "A valid consumer",
+				MaxMultiStakedFps:   2,
 			},
-			expectedErr: "ConsumerName must be non-empty",
+			expectedErr: types.ErrEmptyConsumerName,
 		},
 		{
 			desc: "missing ConsumerDescription",
@@ -45,19 +48,49 @@ func TestConsumerRegisterValidate(t *testing.T) {
 				ConsumerId:          "c1",
 				ConsumerName:        "Consumer One",
 				ConsumerDescription: "",
+				MaxMultiStakedFps:   2,
+			},
+			expectedErr: types.ErrEmptyConsumerDescription,
+		},
+		{
+			desc: "MaxMultiStakedFps too small (0)",
+			input: types.ConsumerRegister{
+				ConsumerId:          "c1",
+				ConsumerName:        "Consumer One",
+				ConsumerDescription: "A valid consumer",
+				MaxMultiStakedFps:   0,
+			},
+			expectedErr: types.ErrInvalidMaxMultiStakedFps,
+		},
+		{
+			desc: "MaxMultiStakedFps too small (1)",
+			input: types.ConsumerRegister{
+				ConsumerId:          "c1",
+				ConsumerName:        "Consumer One",
+				ConsumerDescription: "A valid consumer",
+				MaxMultiStakedFps:   1,
+			},
+			expectedErr: types.ErrInvalidMaxMultiStakedFps,
+		},
+		{
+			desc: "valid MaxMultiStakedFps (3)",
+			input: types.ConsumerRegister{
+				ConsumerId:          "c1",
+				ConsumerName:        "Consumer One",
+				ConsumerDescription: "A valid consumer",
+				MaxMultiStakedFps:   3,
 			},
-			expectedErr: "ConsumerDescription must be non-empty",
 		},
 	}
 
 	for _, tc := range testCases {
 		t.Run(tc.desc, func(t *testing.T) {
 			err := tc.input.Validate()
-			if tc.expectedErr == "" {
+			if tc.expectedErr == nil {
 				require.NoError(t, err)
 			} else {
 				require.Error(t, err)
-				require.EqualError(t, err, tc.expectedErr)
+				require.ErrorIs(t, err, tc.expectedErr)
 			}
 		})
 	}
diff --git a/x/btcstkconsumer/types/errors.go b/x/btcstkconsumer/types/errors.go
index 752b7bf92..8aa22be1f 100644
--- a/x/btcstkconsumer/types/errors.go
+++ b/x/btcstkconsumer/types/errors.go
@@ -14,4 +14,8 @@ var (
 	ErrInvalidConsumerIDs           = sdkerrors.Register(ModuleName, 1103, "consumer ids contain duplicates or empty strings")
 	ErrInvalidCosmosConsumerRequest = sdkerrors.Register(ModuleName, 1104, "invalid registration request of Cosmos consumer")
 	ErrInvalidETHL2ConsumerRequest  = sdkerrors.Register(ModuleName, 1105, "invalid registration request of ETH L2 consumer")
+	ErrInvalidMaxMultiStakedFps     = sdkerrors.Register(ModuleName, 1106, "max multi staked fps must be at least 2 to allow for at least one Babylon FP and one consumer FP")
+	ErrEmptyConsumerId              = sdkerrors.Register(ModuleName, 1107, "consumer id must be non-empty")
+	ErrEmptyConsumerName            = sdkerrors.Register(ModuleName, 1108, "consumer name must be non-empty")
+	ErrEmptyConsumerDescription     = sdkerrors.Register(ModuleName, 1109, "consumer description must be non-empty")
 )
diff --git a/x/btcstkconsumer/types/genesis_test.go b/x/btcstkconsumer/types/genesis_test.go
index 139dd64bd..61db8eb04 100644
--- a/x/btcstkconsumer/types/genesis_test.go
+++ b/x/btcstkconsumer/types/genesis_test.go
@@ -71,6 +71,25 @@ func TestGenesisState_Validate(t *testing.T) {
 			valid:  false,
 			errMsg: "finality provider consumer is not registered",
 		},
+		{
+			desc: "invalid max_multi_staked_fps (zero)",
+			genState: &types.GenesisState{
+				Consumers: []*types.ConsumerRegister{
+					{
+						ConsumerId:          "consumer1",
+						ConsumerName:        "Consumer One",
+						ConsumerDescription: "A valid consumer",
+						MaxMultiStakedFps:   0,
+						ConsumerMetadata: &types.ConsumerRegister_CosmosConsumerMetadata{
+							CosmosConsumerMetadata: &types.CosmosConsumerMetadata{},
+						},
+					},
+				},
+				FinalityProviders: []*btcstaking.FinalityProvider{},
+			},
+			valid:  false,
+			errMsg: types.ErrInvalidMaxMultiStakedFps.Error(),
+		},
 	}
 	for _, tc := range tests {
 		t.Run(tc.desc, func(t *testing.T) {
diff --git a/x/btcstkconsumer/types/query.pb.go b/x/btcstkconsumer/types/query.pb.go
index 98d8e1796..93932bb82 100644
--- a/x/btcstkconsumer/types/query.pb.go
+++ b/x/btcstkconsumer/types/query.pb.go
@@ -751,6 +751,10 @@ type ConsumerRegisterResponse struct {
 	// eth_l2_finality_contract_address is the address of the finality contract
 	// set only for ETH L2 integration
 	EthL2FinalityContractAddress string `protobuf:"bytes,5,opt,name=eth_l2_finality_contract_address,json=ethL2FinalityContractAddress,proto3" json:"eth_l2_finality_contract_address,omitempty"`
+	// max_multi_staked_fps is the maximum number of finality providers from this consumer
+	// that can be part of a single BTC delegation. Must be at least 2 to allow for at least
+	// one Babylon FP and one consumer FP.
+	MaxMultiStakedFps uint32 `protobuf:"varint,6,opt,name=max_multi_staked_fps,json=maxMultiStakedFps,proto3" json:"max_multi_staked_fps,omitempty"`
 }
 
 func (m *ConsumerRegisterResponse) Reset()         { *m = ConsumerRegisterResponse{} }
@@ -821,6 +825,13 @@ func (m *ConsumerRegisterResponse) GetEthL2FinalityContractAddress() string {
 	return ""
 }
 
+func (m *ConsumerRegisterResponse) GetMaxMultiStakedFps() uint32 {
+	if m != nil {
+		return m.MaxMultiStakedFps
+	}
+	return 0
+}
+
 func init() {
 	proto.RegisterType((*QueryParamsRequest)(nil), "babylon.btcstkconsumer.v1.QueryParamsRequest")
 	proto.RegisterType((*QueryParamsResponse)(nil), "babylon.btcstkconsumer.v1.QueryParamsResponse")
@@ -843,84 +854,86 @@ func init() {
 }
 
 var fileDescriptor_4e6c48e60b6a8bd8 = []byte{
-	// 1227 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0xcd, 0x6f, 0x1b, 0x45,
-	0x14, 0xcf, 0xe6, 0xc3, 0xe0, 0x71, 0x23, 0xe2, 0xa9, 0x55, 0xb9, 0x26, 0x38, 0xce, 0xa6, 0x2d,
-	0x21, 0x34, 0xbb, 0x38, 0x89, 0xd4, 0x56, 0x69, 0xf9, 0x70, 0xd2, 0x36, 0x81, 0x14, 0xcc, 0x02,
-	0x17, 0x38, 0x6c, 0x67, 0xd7, 0xe3, 0xf5, 0x12, 0x7b, 0x67, 0xbb, 0x33, 0x31, 0xb1, 0xaa, 0x1c,
-	0x40, 0x1c, 0x38, 0x22, 0xf8, 0x0b, 0xb8, 0x71, 0x04, 0xa9, 0x7f, 0x44, 0x0f, 0x14, 0x45, 0xe5,
-	0x00, 0xf4, 0x10, 0xa1, 0x04, 0x89, 0x0b, 0x7f, 0x04, 0xda, 0xd9, 0x59, 0x7f, 0xac, 0x3f, 0x13,
-	0xa1, 0x5e, 0x92, 0x9d, 0x79, 0xbf, 0xf7, 0xe6, 0xfd, 0x7e, 0xf3, 0x66, 0xe6, 0x19, 0x5c, 0x36,
-	0x90, 0xd1, 0xa8, 0x12, 0x47, 0x35, 0x98, 0x49, 0xd9, 0xae, 0x49, 0x1c, 0xba, 0x57, 0xc3, 0x9e,
-	0x5a, 0xcf, 0xab, 0x0f, 0xf6, 0xb0, 0xd7, 0x50, 0x5c, 0x8f, 0x30, 0x02, 0x2f, 0x0a, 0x98, 0xd2,
-	0x09, 0x53, 0xea, 0xf9, 0x4c, 0x12, 0xd5, 0x6c, 0x87, 0xa8, 0xfc, 0x6f, 0x80, 0xce, 0xa4, 0x2c,
-	0x62, 0x11, 0xfe, 0xa9, 0xfa, 0x5f, 0x62, 0x76, 0xd6, 0x22, 0xc4, 0xaa, 0x62, 0x15, 0xb9, 0xb6,
-	0x8a, 0x1c, 0x87, 0x30, 0xc4, 0x6c, 0xe2, 0x50, 0x61, 0xbd, 0x68, 0x12, 0x5a, 0x23, 0x54, 0x0f,
-	0xdc, 0x82, 0x81, 0x30, 0x5d, 0x0a, 0x46, 0x2a, 0x65, 0x68, 0xd7, 0x76, 0x2c, 0xb5, 0x9e, 0x37,
-	0x30, 0x43, 0xf9, 0x70, 0x2c, 0x50, 0x4b, 0x02, 0x65, 0x20, 0x8a, 0x83, 0xdc, 0x9b, 0x40, 0x17,
-	0x59, 0xb6, 0xc3, 0x57, 0x13, 0xd8, 0x2b, 0x1d, 0xac, 0xc3, 0xa8, 0x6d, 0x23, 0x81, 0x9b, 0xeb,
-	0x8d, 0x73, 0x89, 0xdb, 0x33, 0x50, 0x87, 0x7c, 0x2e, 0xf2, 0x50, 0x4d, 0x50, 0x90, 0x53, 0x00,
-	0x7e, 0xe8, 0xa7, 0x54, 0xe4, 0x93, 0x1a, 0x7e, 0xb0, 0x87, 0x29, 0x93, 0x3f, 0x03, 0xe7, 0x3b,
-	0x66, 0xa9, 0x4b, 0x1c, 0x8a, 0xe1, 0x26, 0x88, 0x05, 0xce, 0x69, 0x29, 0x27, 0x2d, 0x26, 0x56,
-	0xe6, 0x95, 0xbe, 0xea, 0x2b, 0x81, 0x6b, 0x21, 0xfe, 0xf8, 0x68, 0x6e, 0xec, 0xc7, 0x7f, 0x7e,
-	0x5a, 0x92, 0x34, 0xe1, 0x2b, 0x7f, 0x0e, 0x72, 0x3c, 0xf8, 0x86, 0x40, 0x6b, 0xd8, 0xb2, 0x29,
-	0xf3, 0x1a, 0x3b, 0x36, 0x65, 0x22, 0x01, 0x78, 0x07, 0x80, 0x96, 0x36, 0x62, 0xb5, 0x2b, 0x8a,
-	0x10, 0xdf, 0x17, 0x52, 0x09, 0x8a, 0x40, 0x08, 0xa9, 0x14, 0x91, 0x85, 0x85, 0xaf, 0xd6, 0xe6,
-	0x29, 0x1f, 0x4a, 0x60, 0x7e, 0xc0, 0x62, 0x82, 0x97, 0x01, 0x60, 0x98, 0xba, 0xee, 0x71, 0x00,
-	0xf6, 0x7c, 0x8e, 0x13, 0x8b, 0x89, 0x95, 0xd5, 0x01, 0x1c, 0x3b, 0x83, 0xfa, 0xff, 0x83, 0x80,
-	0x5a, 0xd2, 0x8c, 0x58, 0x28, 0xbc, 0xdb, 0xc1, 0x68, 0x9c, 0x33, 0x7a, 0x75, 0x28, 0x23, 0x11,
-	0xaf, 0x9d, 0x52, 0x01, 0xbc, 0xd2, 0xc1, 0x88, 0x86, 0x94, 0x42, 0xed, 0xe6, 0xc1, 0xb9, 0x26,
-	0x1b, 0xbb, 0x14, 0xf0, 0x88, 0x6b, 0x89, 0x70, 0x6e, 0xbb, 0x44, 0xe5, 0xaf, 0x25, 0x90, 0xed,
-	0x17, 0xe4, 0xf9, 0x69, 0x22, 0x7f, 0x23, 0x09, 0x2e, 0x77, 0x6c, 0x07, 0x55, 0x6d, 0xd6, 0x28,
-	0x7a, 0xa4, 0x6e, 0x97, 0x78, 0x3a, 0x01, 0x97, 0x39, 0x90, 0x68, 0xe3, 0xc2, 0x0b, 0x21, 0xae,
-	0x81, 0x16, 0x95, 0x48, 0xa1, 0x8c, 0x9f, 0xb9, 0x50, 0x9e, 0x84, 0x8a, 0xf4, 0x48, 0xa5, 0xa5,
-	0x48, 0x59, 0x18, 0xfd, 0xcb, 0x20, 0xb0, 0x8e, 0xa0, 0x48, 0x34, 0x62, 0x4b, 0x91, 0x72, 0x74,
-	0xad, 0xff, 0xaf, 0x4a, 0x0c, 0x30, 0xdb, 0x93, 0xce, 0xc8, 0xc2, 0xce, 0x83, 0xe9, 0xb2, 0xab,
-	0x1b, 0xcc, 0xd4, 0xdd, 0x5d, 0xbd, 0x82, 0xf7, 0x79, 0x32, 0x71, 0x0d, 0x94, 0xdd, 0x02, 0x33,
-	0x8b, 0xbb, 0x5b, 0x78, 0x5f, 0xfe, 0xb2, 0xdf, 0xf6, 0x35, 0x25, 0xbb, 0x0f, 0x92, 0x5d, 0x92,
-	0x89, 0xd3, 0x7c, 0x26, 0xc5, 0x66, 0xa2, 0x8a, 0xc9, 0x3f, 0x4f, 0x82, 0x74, 0xdf, 0xe5, 0x6f,
-	0x83, 0x44, 0x09, 0x53, 0xd3, 0xb3, 0xdd, 0xb6, 0x6b, 0x64, 0x21, 0x94, 0x33, 0xbc, 0x51, 0x43,
-	0x2d, 0x37, 0x5b, 0x50, 0xad, 0xdd, 0x0f, 0xde, 0x03, 0xc0, 0x24, 0xb5, 0x9a, 0x4d, 0x69, 0xb8,
-	0x29, 0xf1, 0xc2, 0xf2, 0xb3, 0xa3, 0xb9, 0x97, 0x83, 0x40, 0xb4, 0xb4, 0xab, 0xd8, 0x44, 0xad,
-	0x21, 0x56, 0x51, 0x76, 0xb0, 0x85, 0xcc, 0xc6, 0x26, 0x36, 0x9f, 0x3e, 0x5a, 0x06, 0x62, 0x9d,
-	0x4d, 0x6c, 0x6a, 0x6d, 0x01, 0xe0, 0x55, 0x30, 0x89, 0x4a, 0x25, 0x2f, 0x3d, 0xc1, 0x03, 0xa5,
-	0x9f, 0x3e, 0x5a, 0x4e, 0x09, 0xe4, 0x3b, 0xa5, 0x92, 0x87, 0x29, 0xfd, 0x88, 0x79, 0xb6, 0x63,
-	0x69, 0x1c, 0x05, 0x35, 0x10, 0x0b, 0x36, 0x21, 0x3d, 0x99, 0x93, 0x16, 0xcf, 0x15, 0xd6, 0x9f,
-	0x1d, 0xcd, 0x5d, 0xb3, 0x6c, 0x56, 0xd9, 0x33, 0x14, 0x93, 0xd4, 0x54, 0xa1, 0x62, 0x15, 0x19,
-	0x74, 0xd9, 0x26, 0xe1, 0x50, 0xad, 0xaf, 0xa9, 0xac, 0xe1, 0x62, 0xaa, 0x14, 0xb6, 0x8b, 0xab,
-	0x6b, 0x6f, 0x14, 0xf7, 0x8c, 0xf7, 0x70, 0x43, 0x9b, 0x32, 0xfc, 0xbd, 0x83, 0xb7, 0xc0, 0x84,
-	0x4b, 0xdc, 0xf4, 0x14, 0xd7, 0xe3, 0xf5, 0xce, 0x8d, 0x08, 0x35, 0x51, 0x8a, 0x1e, 0x21, 0xe5,
-	0x0f, 0xca, 0x45, 0x42, 0x29, 0xe6, 0x89, 0x17, 0x3e, 0xde, 0xd0, 0x7c, 0x3f, 0xb8, 0x06, 0x2e,
-	0xd0, 0x2a, 0xa2, 0x15, 0x5c, 0xd2, 0x85, 0xab, 0x5e, 0xc1, 0xb6, 0x55, 0x61, 0xe9, 0x58, 0x4e,
-	0x5a, 0x9c, 0xd4, 0x52, 0xc2, 0x5a, 0x08, 0x8c, 0x5b, 0xdc, 0x06, 0xaf, 0x02, 0xd8, 0xf4, 0x62,
-	0x66, 0xe8, 0xf1, 0x42, 0x4e, 0x5a, 0x9c, 0xd6, 0x66, 0x42, 0x0f, 0x66, 0x0a, 0xf4, 0x05, 0x10,
-	0x13, 0x88, 0x17, 0x79, 0x4c, 0x31, 0xf2, 0x2f, 0xb7, 0x3a, 0x61, 0xb6, 0x63, 0xe9, 0x2e, 0xf9,
-	0x02, 0x7b, 0xe9, 0x38, 0xb7, 0x26, 0x82, 0xb9, 0xa2, 0x3f, 0x15, 0x2d, 0x6d, 0x10, 0x2d, 0x6d,
-	0x79, 0x1b, 0x5c, 0xea, 0x59, 0xb6, 0xad, 0xab, 0x2b, 0xbc, 0x48, 0x23, 0x47, 0x40, 0xea, 0x3a,
-	0x02, 0x5b, 0xe0, 0xf2, 0x90, 0x50, 0xa2, 0x14, 0x87, 0x9d, 0x37, 0xf9, 0x87, 0x71, 0x90, 0xee,
-	0x77, 0x77, 0x0e, 0x3f, 0xad, 0x0b, 0x60, 0xba, 0x09, 0x70, 0x50, 0x0d, 0x8b, 0xd3, 0xda, 0x7c,
-	0x08, 0xde, 0x47, 0x35, 0x0c, 0xf3, 0x20, 0xd5, 0x04, 0xb5, 0x9f, 0x0b, 0x5e, 0x88, 0xda, 0xf9,
-	0xd0, 0xd6, 0x76, 0x0e, 0xe0, 0x12, 0x48, 0x8a, 0xf6, 0xc7, 0xac, 0x20, 0xc7, 0xc1, 0x55, 0x7f,
-	0xf9, 0x49, 0x8e, 0x7f, 0x29, 0x30, 0x6c, 0x04, 0xf3, 0xdb, 0x25, 0x78, 0x1f, 0xe4, 0x30, 0xab,
-	0xe8, 0xd5, 0x15, 0xbd, 0x79, 0xe6, 0x4d, 0xe2, 0x30, 0x0f, 0x99, 0x4c, 0x47, 0x41, 0x65, 0xf3,
-	0x92, 0x1b, 0x54, 0xf3, 0xb3, 0x98, 0x55, 0x76, 0x56, 0x42, 0x35, 0x37, 0x84, 0xbb, 0xc0, 0xac,
-	0xfc, 0x1e, 0x07, 0x53, 0x5c, 0x6e, 0xf8, 0x9d, 0x04, 0x62, 0x41, 0x87, 0x01, 0x97, 0x07, 0x5c,
-	0x24, 0xdd, 0xad, 0x4d, 0x46, 0x19, 0x15, 0x1e, 0x48, 0x2f, 0xbf, 0xf6, 0xd5, 0x6f, 0x7f, 0x7f,
-	0x3f, 0xbe, 0x00, 0xe7, 0xd5, 0x61, 0x1d, 0x15, 0xfc, 0x45, 0x02, 0xa9, 0x5e, 0x7d, 0x06, 0x5c,
-	0x1f, 0xb6, 0xe6, 0x80, 0x56, 0x28, 0x73, 0xf3, 0x6c, 0xce, 0x22, 0xfd, 0x1b, 0x3c, 0xfd, 0x55,
-	0x98, 0x1f, 0x90, 0x7e, 0xe4, 0x9d, 0xf7, 0x1a, 0x7a, 0xd5, 0xcf, 0xfa, 0x57, 0x09, 0x24, 0xbb,
-	0xfa, 0x03, 0x78, 0x7d, 0xd4, 0x74, 0xa2, 0x7d, 0x49, 0xe6, 0xc6, 0x19, 0x3c, 0x05, 0x8b, 0x4d,
-	0xce, 0xe2, 0x4d, 0x78, 0x73, 0x04, 0x16, 0xb4, 0x49, 0x43, 0x7d, 0xd8, 0xde, 0x07, 0x1d, 0xc0,
-	0x7f, 0xa5, 0xee, 0xb7, 0x22, 0x5c, 0x13, 0xbe, 0x35, 0x2c, 0xbb, 0x21, 0xb7, 0x45, 0xe6, 0xed,
-	0xb3, 0x07, 0x10, 0x2c, 0xef, 0x71, 0x96, 0x77, 0xe1, 0xed, 0x01, 0x2c, 0xbb, 0x9e, 0x53, 0xbd,
-	0x69, 0x7d, 0xd8, 0x71, 0x59, 0x1d, 0xc0, 0x27, 0x12, 0x48, 0x76, 0x75, 0x33, 0xc3, 0xf7, 0xaf,
-	0x5f, 0x2f, 0x36, 0x7c, 0xff, 0xfa, 0xb6, 0x4e, 0xf2, 0x06, 0x67, 0x76, 0x0b, 0xae, 0x9f, 0x86,
-	0x19, 0xed, 0xd8, 0xbf, 0x03, 0xf8, 0xa7, 0x04, 0x66, 0xa2, 0x4b, 0xc0, 0x6b, 0xa7, 0x4d, 0x2a,
-	0x64, 0x73, 0xfd, 0xf4, 0x8e, 0x82, 0x8c, 0xc6, 0xc9, 0xec, 0xc0, 0x77, 0x4f, 0x43, 0xa6, 0x93,
-	0x4b, 0x74, 0xaf, 0x0a, 0x9f, 0x3c, 0x3e, 0xce, 0x4a, 0x87, 0xc7, 0x59, 0xe9, 0xaf, 0xe3, 0xac,
-	0xf4, 0xed, 0x49, 0x76, 0xec, 0xf0, 0x24, 0x3b, 0xf6, 0xc7, 0x49, 0x76, 0xec, 0xd3, 0xf5, 0x91,
-	0xde, 0xfa, 0xfd, 0x68, 0x0e, 0xfc, 0xf1, 0x37, 0x62, 0xfc, 0x47, 0xde, 0xea, 0x7f, 0x01, 0x00,
-	0x00, 0xff, 0xff, 0x69, 0x26, 0xa7, 0xf2, 0x4d, 0x0f, 0x00, 0x00,
+	// 1260 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5b, 0x6f, 0x1b, 0xc5,
+	0x17, 0xcf, 0xa6, 0xa9, 0xff, 0x7f, 0x8f, 0x1b, 0x51, 0x4f, 0xad, 0xca, 0x35, 0xc5, 0x71, 0xb6,
+	0x17, 0x42, 0x69, 0x76, 0x71, 0x52, 0xa9, 0xad, 0xd2, 0x72, 0x71, 0xd2, 0x4b, 0x20, 0x05, 0xb3,
+	0x85, 0x17, 0x78, 0xd8, 0xce, 0xee, 0x8e, 0xd7, 0x4b, 0xbc, 0x3b, 0xdb, 0x9d, 0xb1, 0x89, 0x55,
+	0xf5, 0x01, 0xc4, 0x03, 0x8f, 0x08, 0xbe, 0x04, 0x8f, 0x20, 0xf5, 0x43, 0xf4, 0x81, 0xa2, 0x2a,
+	0x3c, 0x00, 0x7d, 0x88, 0x50, 0x82, 0xc4, 0x0b, 0x1f, 0x02, 0xed, 0xec, 0xac, 0x2f, 0xeb, 0x6b,
+	0x22, 0xc4, 0x4b, 0xeb, 0x99, 0xf3, 0x3b, 0x67, 0xce, 0xef, 0x77, 0xce, 0xcc, 0x9e, 0x80, 0x0b,
+	0x06, 0x32, 0xda, 0x0d, 0xe2, 0xa9, 0x06, 0x33, 0x29, 0xdb, 0x36, 0x89, 0x47, 0x9b, 0x2e, 0x0e,
+	0xd4, 0x56, 0x59, 0x7d, 0xd8, 0xc4, 0x41, 0x5b, 0xf1, 0x03, 0xc2, 0x08, 0x3c, 0x23, 0x60, 0x4a,
+	0x3f, 0x4c, 0x69, 0x95, 0x0b, 0x59, 0xe4, 0x3a, 0x1e, 0x51, 0xf9, 0xbf, 0x11, 0xba, 0x90, 0xb3,
+	0x89, 0x4d, 0xf8, 0x4f, 0x35, 0xfc, 0x25, 0x76, 0xcf, 0xda, 0x84, 0xd8, 0x0d, 0xac, 0x22, 0xdf,
+	0x51, 0x91, 0xe7, 0x11, 0x86, 0x98, 0x43, 0x3c, 0x2a, 0xac, 0x67, 0x4c, 0x42, 0x5d, 0x42, 0xf5,
+	0xc8, 0x2d, 0x5a, 0x08, 0xd3, 0xf9, 0x68, 0xa5, 0x52, 0x86, 0xb6, 0x1d, 0xcf, 0x56, 0x5b, 0x65,
+	0x03, 0x33, 0x54, 0x8e, 0xd7, 0x02, 0x75, 0x49, 0xa0, 0x0c, 0x44, 0x71, 0x94, 0x7b, 0x07, 0xe8,
+	0x23, 0xdb, 0xf1, 0xf8, 0x69, 0x02, 0x7b, 0xb1, 0x8f, 0x75, 0x1c, 0xb5, 0x67, 0x25, 0x70, 0x0b,
+	0xc3, 0x71, 0x3e, 0xf1, 0x87, 0x06, 0xea, 0x93, 0xcf, 0x47, 0x01, 0x72, 0x05, 0x05, 0x39, 0x07,
+	0xe0, 0x87, 0x61, 0x4a, 0x55, 0xbe, 0xa9, 0xe1, 0x87, 0x4d, 0x4c, 0x99, 0xfc, 0x29, 0x38, 0xd5,
+	0xb7, 0x4b, 0x7d, 0xe2, 0x51, 0x0c, 0x37, 0x40, 0x2a, 0x72, 0xce, 0x4b, 0x25, 0x69, 0x29, 0xb3,
+	0xb2, 0xa8, 0x8c, 0x54, 0x5f, 0x89, 0x5c, 0x2b, 0xe9, 0xa7, 0x7b, 0x0b, 0x33, 0xdf, 0xff, 0xf5,
+	0xc3, 0x25, 0x49, 0x13, 0xbe, 0xf2, 0x67, 0xa0, 0xc4, 0x83, 0xaf, 0x0b, 0xb4, 0x86, 0x6d, 0x87,
+	0xb2, 0xa0, 0xbd, 0xe5, 0x50, 0x26, 0x12, 0x80, 0xb7, 0x01, 0xe8, 0x6a, 0x23, 0x4e, 0xbb, 0xa8,
+	0x08, 0xf1, 0x43, 0x21, 0x95, 0xa8, 0x09, 0x84, 0x90, 0x4a, 0x15, 0xd9, 0x58, 0xf8, 0x6a, 0x3d,
+	0x9e, 0xf2, 0x73, 0x09, 0x2c, 0x8e, 0x39, 0x4c, 0xf0, 0x32, 0x00, 0x8c, 0x53, 0xd7, 0x03, 0x0e,
+	0xc0, 0x41, 0xc8, 0xf1, 0xd8, 0x52, 0x66, 0x65, 0x75, 0x0c, 0xc7, 0xfe, 0xa0, 0xe1, 0xff, 0x51,
+	0x40, 0x2d, 0x6b, 0x26, 0x2c, 0x14, 0xde, 0xe9, 0x63, 0x34, 0xcb, 0x19, 0xbd, 0x3a, 0x91, 0x91,
+	0x88, 0xd7, 0x4b, 0xa9, 0x02, 0x5e, 0xe9, 0x63, 0x44, 0x63, 0x4a, 0xb1, 0x76, 0x8b, 0xe0, 0x44,
+	0x87, 0x8d, 0x63, 0x45, 0x3c, 0xd2, 0x5a, 0x26, 0xde, 0xdb, 0xb4, 0xa8, 0xfc, 0x95, 0x04, 0x8a,
+	0xa3, 0x82, 0xfc, 0x77, 0x9a, 0xc8, 0x5f, 0x4b, 0x82, 0xcb, 0x6d, 0xc7, 0x43, 0x0d, 0x87, 0xb5,
+	0xab, 0x01, 0x69, 0x39, 0x16, 0x4f, 0x27, 0xe2, 0xb2, 0x00, 0x32, 0x3d, 0x5c, 0x78, 0x23, 0xa4,
+	0x35, 0xd0, 0xa5, 0x92, 0x68, 0x94, 0xd9, 0x23, 0x37, 0xca, 0xb3, 0x58, 0x91, 0x21, 0xa9, 0x74,
+	0x15, 0xa9, 0x09, 0x63, 0xf8, 0x18, 0x44, 0xd6, 0x29, 0x14, 0x49, 0x46, 0xec, 0x2a, 0x52, 0x4b,
+	0x9e, 0xf5, 0xef, 0x75, 0x89, 0x01, 0xce, 0x0e, 0xa5, 0x33, 0xb5, 0xb0, 0x8b, 0x60, 0xbe, 0xe6,
+	0xeb, 0x06, 0x33, 0x75, 0x7f, 0x5b, 0xaf, 0xe3, 0x1d, 0x9e, 0x4c, 0x5a, 0x03, 0x35, 0xbf, 0xc2,
+	0xcc, 0xea, 0xf6, 0x5d, 0xbc, 0x23, 0x7f, 0x31, 0xaa, 0x7c, 0x1d, 0xc9, 0x1e, 0x80, 0xec, 0x80,
+	0x64, 0xe2, 0x36, 0x1f, 0x49, 0xb1, 0x93, 0x49, 0xc5, 0xe4, 0x1f, 0xe7, 0x40, 0x7e, 0xe4, 0xf1,
+	0xb7, 0x40, 0xc6, 0xc2, 0xd4, 0x0c, 0x1c, 0xbf, 0xe7, 0x19, 0x39, 0x17, 0xcb, 0x19, 0xbf, 0xa8,
+	0xb1, 0x96, 0x1b, 0x5d, 0xa8, 0xd6, 0xeb, 0x07, 0xef, 0x01, 0x60, 0x12, 0xd7, 0x75, 0x28, 0x8d,
+	0x8b, 0x92, 0xae, 0x2c, 0xbf, 0xd8, 0x5b, 0x78, 0x39, 0x0a, 0x44, 0xad, 0x6d, 0xc5, 0x21, 0xaa,
+	0x8b, 0x58, 0x5d, 0xd9, 0xc2, 0x36, 0x32, 0xdb, 0x1b, 0xd8, 0xdc, 0x7d, 0xb2, 0x0c, 0xc4, 0x39,
+	0x1b, 0xd8, 0xd4, 0x7a, 0x02, 0xc0, 0xcb, 0x60, 0x0e, 0x59, 0x56, 0x90, 0x3f, 0xc6, 0x03, 0xe5,
+	0x77, 0x9f, 0x2c, 0xe7, 0x04, 0xf2, 0x1d, 0xcb, 0x0a, 0x30, 0xa5, 0xf7, 0x59, 0xe0, 0x78, 0xb6,
+	0xc6, 0x51, 0x50, 0x03, 0xa9, 0xa8, 0x08, 0xf9, 0xb9, 0x92, 0xb4, 0x74, 0xa2, 0xb2, 0xf6, 0x62,
+	0x6f, 0xe1, 0xaa, 0xed, 0xb0, 0x7a, 0xd3, 0x50, 0x4c, 0xe2, 0xaa, 0x42, 0xc5, 0x06, 0x32, 0xe8,
+	0xb2, 0x43, 0xe2, 0xa5, 0xda, 0xba, 0xa2, 0xb2, 0xb6, 0x8f, 0xa9, 0x52, 0xd9, 0xac, 0xae, 0x5e,
+	0x79, 0xa3, 0xda, 0x34, 0xde, 0xc3, 0x6d, 0xed, 0xb8, 0x11, 0xd6, 0x0e, 0xde, 0x04, 0xc7, 0x7c,
+	0xe2, 0xe7, 0x8f, 0x73, 0x3d, 0x5e, 0xef, 0x2f, 0x44, 0xac, 0x89, 0x52, 0x0d, 0x08, 0xa9, 0x7d,
+	0x50, 0xab, 0x12, 0x4a, 0x31, 0x4f, 0xbc, 0xf2, 0xd1, 0xba, 0x16, 0xfa, 0xc1, 0x2b, 0xe0, 0x34,
+	0x6d, 0x20, 0x5a, 0xc7, 0x96, 0x2e, 0x5c, 0xf5, 0x3a, 0x76, 0xec, 0x3a, 0xcb, 0xa7, 0x4a, 0xd2,
+	0xd2, 0x9c, 0x96, 0x13, 0xd6, 0x4a, 0x64, 0xbc, 0xcb, 0x6d, 0xf0, 0x32, 0x80, 0x1d, 0x2f, 0x66,
+	0xc6, 0x1e, 0xff, 0x2b, 0x49, 0x4b, 0xf3, 0xda, 0xc9, 0xd8, 0x83, 0x99, 0x02, 0x7d, 0x1a, 0xa4,
+	0x04, 0xe2, 0xff, 0x3c, 0xa6, 0x58, 0x85, 0x8f, 0x5b, 0x8b, 0x30, 0xc7, 0xb3, 0x75, 0x9f, 0x7c,
+	0x8e, 0x83, 0x7c, 0x9a, 0x5b, 0x33, 0xd1, 0x5e, 0x35, 0xdc, 0x4a, 0xb6, 0x36, 0x48, 0xb6, 0xb6,
+	0xbc, 0x09, 0xce, 0x0f, 0x6d, 0xdb, 0xee, 0xd3, 0x15, 0x3f, 0xa4, 0x89, 0x2b, 0x20, 0x0d, 0x5c,
+	0x81, 0xbb, 0xe0, 0xc2, 0x84, 0x50, 0xa2, 0x15, 0x27, 0xdd, 0x37, 0x79, 0x77, 0x16, 0xe4, 0x47,
+	0xbd, 0x9d, 0x93, 0x6f, 0xeb, 0x39, 0x30, 0xdf, 0x01, 0x78, 0xc8, 0xc5, 0xe2, 0xb6, 0x76, 0x3e,
+	0x04, 0xef, 0x23, 0x17, 0xc3, 0x32, 0xc8, 0x75, 0x40, 0xbd, 0xf7, 0x82, 0x37, 0xa2, 0x76, 0x2a,
+	0xb6, 0xf5, 0xdc, 0x03, 0x78, 0x09, 0x64, 0xc5, 0xf8, 0x63, 0xd6, 0x91, 0xe7, 0xe1, 0x46, 0x78,
+	0xfc, 0x1c, 0xc7, 0xbf, 0x14, 0x19, 0xd6, 0xa3, 0xfd, 0x4d, 0x0b, 0x3e, 0x00, 0x25, 0xcc, 0xea,
+	0x7a, 0x63, 0x45, 0xef, 0xdc, 0x79, 0x93, 0x78, 0x2c, 0x40, 0x26, 0xd3, 0x51, 0xd4, 0xd9, 0xbc,
+	0xe5, 0xc6, 0xf5, 0xfc, 0x59, 0xcc, 0xea, 0x5b, 0x2b, 0xb1, 0x9a, 0xeb, 0xc2, 0x5d, 0x60, 0xa0,
+	0x0a, 0x72, 0x2e, 0xda, 0xd1, 0xdd, 0x66, 0x83, 0x39, 0x7a, 0xd8, 0xaa, 0xd8, 0xd2, 0x6b, 0x3e,
+	0xe5, 0x6d, 0x37, 0xaf, 0x65, 0x5d, 0xb4, 0x73, 0x2f, 0x34, 0xdd, 0xe7, 0x96, 0xdb, 0x3e, 0x5d,
+	0xf9, 0x35, 0x0d, 0x8e, 0xf3, 0xfa, 0xc0, 0x6f, 0x25, 0x90, 0x8a, 0x46, 0x12, 0xb8, 0x3c, 0xe6,
+	0xe5, 0x19, 0x9c, 0x85, 0x0a, 0xca, 0xb4, 0xf0, 0xa8, 0x56, 0xf2, 0x6b, 0x5f, 0xfe, 0xf2, 0xe7,
+	0x77, 0xb3, 0xe7, 0xe0, 0xa2, 0x3a, 0x69, 0x04, 0x83, 0x3f, 0x49, 0x20, 0x37, 0x6c, 0x30, 0x81,
+	0x6b, 0x93, 0xce, 0x1c, 0x33, 0x3b, 0x15, 0x6e, 0x1c, 0xcd, 0x59, 0xa4, 0x7f, 0x9d, 0xa7, 0xbf,
+	0x0a, 0xcb, 0x63, 0xd2, 0x4f, 0x0c, 0x06, 0x41, 0x5b, 0x6f, 0x84, 0x59, 0xff, 0x2c, 0x81, 0xec,
+	0xc0, 0x40, 0x01, 0xaf, 0x4d, 0x9b, 0x4e, 0x72, 0x90, 0x29, 0x5c, 0x3f, 0x82, 0xa7, 0x60, 0xb1,
+	0xc1, 0x59, 0xbc, 0x09, 0x6f, 0x4c, 0xc1, 0x82, 0x76, 0x68, 0xa8, 0x8f, 0x7a, 0x07, 0xa7, 0xc7,
+	0xf0, 0x6f, 0x69, 0xf0, 0xe3, 0x12, 0x9f, 0x09, 0xdf, 0x9a, 0x94, 0xdd, 0x84, 0xe7, 0xa5, 0xf0,
+	0xf6, 0xd1, 0x03, 0x08, 0x96, 0xf7, 0x38, 0xcb, 0x3b, 0xf0, 0xd6, 0x18, 0x96, 0x03, 0xdf, 0x5f,
+	0xbd, 0x63, 0x7d, 0xd4, 0xf7, 0xba, 0x3d, 0x86, 0xcf, 0x24, 0x90, 0x1d, 0x18, 0x7f, 0x26, 0xd7,
+	0x6f, 0xd4, 0xf0, 0x36, 0xb9, 0x7e, 0x23, 0x67, 0x2d, 0x79, 0x9d, 0x33, 0xbb, 0x09, 0xd7, 0x0e,
+	0xc3, 0x8c, 0xf6, 0xd5, 0xef, 0x31, 0xfc, 0x5d, 0x02, 0x27, 0x93, 0x47, 0xc0, 0xab, 0x87, 0x4d,
+	0x2a, 0x66, 0x73, 0xed, 0xf0, 0x8e, 0x82, 0x8c, 0xc6, 0xc9, 0x6c, 0xc1, 0x77, 0x0f, 0x43, 0xa6,
+	0x9f, 0x4b, 0xb2, 0x56, 0x95, 0x8f, 0x9f, 0xee, 0x17, 0xa5, 0xe7, 0xfb, 0x45, 0xe9, 0x8f, 0xfd,
+	0xa2, 0xf4, 0xcd, 0x41, 0x71, 0xe6, 0xf9, 0x41, 0x71, 0xe6, 0xb7, 0x83, 0xe2, 0xcc, 0x27, 0x6b,
+	0x53, 0x0d, 0x07, 0x3b, 0xc9, 0x1c, 0xf8, 0xb4, 0x60, 0xa4, 0xf8, 0x5f, 0x85, 0xab, 0xff, 0x04,
+	0x00, 0x00, 0xff, 0xff, 0xfc, 0xbf, 0x39, 0x30, 0x7e, 0x0f, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -1752,6 +1765,11 @@ func (m *ConsumerRegisterResponse) MarshalToSizedBuffer(dAtA []byte) (int, error
 	_ = i
 	var l int
 	_ = l
+	if m.MaxMultiStakedFps != 0 {
+		i = encodeVarintQuery(dAtA, i, uint64(m.MaxMultiStakedFps))
+		i--
+		dAtA[i] = 0x30
+	}
 	if len(m.EthL2FinalityContractAddress) > 0 {
 		i -= len(m.EthL2FinalityContractAddress)
 		copy(dAtA[i:], m.EthL2FinalityContractAddress)
@@ -2046,6 +2064,9 @@ func (m *ConsumerRegisterResponse) Size() (n int) {
 	if l > 0 {
 		n += 1 + l + sovQuery(uint64(l))
 	}
+	if m.MaxMultiStakedFps != 0 {
+		n += 1 + sovQuery(uint64(m.MaxMultiStakedFps))
+	}
 	return n
 }
 
@@ -3684,6 +3705,25 @@ func (m *ConsumerRegisterResponse) Unmarshal(dAtA []byte) error {
 			}
 			m.EthL2FinalityContractAddress = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
+		case 6:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field MaxMultiStakedFps", wireType)
+			}
+			m.MaxMultiStakedFps = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowQuery
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.MaxMultiStakedFps |= uint32(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
 		default:
 			iNdEx = preIndex
 			skippy, err := skipQuery(dAtA[iNdEx:])
diff --git a/x/btcstkconsumer/types/tx.pb.go b/x/btcstkconsumer/types/tx.pb.go
index 1b9a4ceef..54b1f05c2 100644
--- a/x/btcstkconsumer/types/tx.pb.go
+++ b/x/btcstkconsumer/types/tx.pb.go
@@ -136,11 +136,15 @@ type MsgRegisterConsumer struct {
 	ConsumerName string `protobuf:"bytes,3,opt,name=consumer_name,json=consumerName,proto3" json:"consumer_name,omitempty"`
 	// consumer_description is a description for the consumer (can be empty)
 	ConsumerDescription string `protobuf:"bytes,4,opt,name=consumer_description,json=consumerDescription,proto3" json:"consumer_description,omitempty"`
+	// max_multi_staked_fps is the maximum number of finality providers from this consumer
+	// that can be part of a single BTC delegation. Must be at least 2 to allow for at least
+	// one Babylon FP and one consumer FP.
+	MaxMultiStakedFps uint32 `protobuf:"varint,5,opt,name=max_multi_staked_fps,json=maxMultiStakedFps,proto3" json:"max_multi_staked_fps,omitempty"`
 	// eth_l2_finality_contract_address is the Cosmos address of the
 	// finality contract. The finality contract is deployed on Babylon and
 	// serves as the data availability layer for finality signatures of the ETH L2.
 	// (if set, then this means this is an ETH L2 integration)
-	EthL2FinalityContractAddress string `protobuf:"bytes,5,opt,name=eth_l2_finality_contract_address,json=ethL2FinalityContractAddress,proto3" json:"eth_l2_finality_contract_address,omitempty"`
+	EthL2FinalityContractAddress string `protobuf:"bytes,6,opt,name=eth_l2_finality_contract_address,json=ethL2FinalityContractAddress,proto3" json:"eth_l2_finality_contract_address,omitempty"`
 }
 
 func (m *MsgRegisterConsumer) Reset()         { *m = MsgRegisterConsumer{} }
@@ -204,6 +208,13 @@ func (m *MsgRegisterConsumer) GetConsumerDescription() string {
 	return ""
 }
 
+func (m *MsgRegisterConsumer) GetMaxMultiStakedFps() uint32 {
+	if m != nil {
+		return m.MaxMultiStakedFps
+	}
+	return 0
+}
+
 func (m *MsgRegisterConsumer) GetEthL2FinalityContractAddress() string {
 	if m != nil {
 		return m.EthL2FinalityContractAddress
@@ -260,41 +271,43 @@ func init() {
 }
 
 var fileDescriptor_12cd7e02078b20ee = []byte{
-	// 538 bytes of a gzipped FileDescriptorProto
+	// 572 bytes of a gzipped FileDescriptorProto
 	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x93, 0xcf, 0x6b, 0x13, 0x41,
-	0x14, 0xc7, 0xb3, 0xad, 0x0d, 0x64, 0x52, 0x51, 0xa7, 0xc1, 0x6e, 0xa2, 0x6e, 0x63, 0x04, 0x29,
-	0x91, 0xee, 0x92, 0x55, 0x7a, 0xa8, 0x20, 0x98, 0x16, 0x41, 0x30, 0x22, 0x2b, 0xbd, 0x78, 0x59,
-	0x67, 0x77, 0xc7, 0xc9, 0x60, 0x76, 0x66, 0x99, 0x99, 0x84, 0xc6, 0x93, 0x78, 0xf4, 0xe4, 0xc5,
-	0xff, 0xc1, 0x63, 0x0e, 0xfe, 0x11, 0xbd, 0x59, 0x3c, 0x79, 0x12, 0x49, 0x0e, 0xf9, 0x17, 0x3c,
-	0x4a, 0x76, 0x67, 0x1b, 0x9a, 0xfe, 0x50, 0x2f, 0x21, 0xef, 0xbd, 0xcf, 0x7b, 0x6f, 0xbe, 0xef,
-	0xbd, 0x05, 0x8d, 0x00, 0x05, 0xc3, 0x1e, 0x67, 0x4e, 0xa0, 0x42, 0xa9, 0xde, 0x86, 0x9c, 0xc9,
-	0x7e, 0x8c, 0x85, 0x33, 0x68, 0x39, 0xea, 0xc0, 0x4e, 0x04, 0x57, 0x1c, 0x56, 0x35, 0x63, 0x9f,
-	0x64, 0xec, 0x41, 0xab, 0x76, 0x0d, 0xc5, 0x94, 0x71, 0x27, 0xfd, 0xcd, 0xe8, 0xda, 0x7a, 0xc8,
-	0x65, 0xcc, 0xa5, 0x13, 0x4b, 0x32, 0xab, 0x12, 0x4b, 0xa2, 0x03, 0xd5, 0x2c, 0xe0, 0xa7, 0x96,
-	0x93, 0x19, 0x3a, 0x54, 0x21, 0x9c, 0xf0, 0xcc, 0x3f, 0xfb, 0xa7, 0xbd, 0x77, 0xcf, 0x7f, 0x5b,
-	0x82, 0x04, 0x8a, 0x75, 0x76, 0xe3, 0x9b, 0x01, 0xae, 0x74, 0x24, 0xd9, 0x4f, 0x22, 0xa4, 0xf0,
-	0x8b, 0x34, 0x02, 0xb7, 0x41, 0x09, 0xf5, 0x55, 0x97, 0x0b, 0xaa, 0x86, 0xa6, 0x51, 0x37, 0x36,
-	0x4b, 0x6d, 0xf3, 0xfb, 0xd7, 0xad, 0x8a, 0x6e, 0xfb, 0x38, 0x8a, 0x04, 0x96, 0xf2, 0xa5, 0x12,
-	0x94, 0x11, 0x6f, 0x8e, 0xc2, 0x3d, 0x50, 0xcc, 0x6a, 0x9b, 0x4b, 0x75, 0x63, 0xb3, 0xec, 0xde,
-	0xb6, 0xcf, 0x15, 0x6f, 0x67, 0xad, 0xda, 0xa5, 0xc3, 0x9f, 0x1b, 0x85, 0x2f, 0xd3, 0x51, 0xd3,
-	0xf0, 0x74, 0xee, 0xce, 0xa3, 0x0f, 0xd3, 0x51, 0x73, 0x5e, 0xf5, 0xe3, 0x74, 0xd4, 0xbc, 0x97,
-	0x8b, 0x39, 0x38, 0x43, 0xce, 0xc2, 0xeb, 0x1b, 0x55, 0xb0, 0xbe, 0xe0, 0xf2, 0xb0, 0x4c, 0x38,
-	0x93, 0xb8, 0xf1, 0x79, 0x09, 0xac, 0x75, 0x24, 0xf1, 0x30, 0xa1, 0x52, 0x61, 0xb1, 0xab, 0xab,
-	0xc0, 0xeb, 0xa0, 0x28, 0x29, 0x61, 0x58, 0x64, 0x6a, 0x3d, 0x6d, 0xc1, 0x0d, 0x50, 0xce, 0x3b,
-	0xf9, 0x34, 0x4a, 0x55, 0x95, 0x3c, 0x90, 0xbb, 0x9e, 0x46, 0xf0, 0x0e, 0xb8, 0x7c, 0x0c, 0x30,
-	0x14, 0x63, 0x73, 0x39, 0x45, 0x56, 0x73, 0xe7, 0x73, 0x14, 0x63, 0xd8, 0x02, 0x95, 0x63, 0x28,
-	0xc2, 0x32, 0x14, 0x34, 0x51, 0x94, 0x33, 0xf3, 0x52, 0xca, 0xae, 0xe5, 0xb1, 0xbd, 0x79, 0x08,
-	0xbe, 0x06, 0x75, 0xac, 0xba, 0x7e, 0xcf, 0xf5, 0xdf, 0x50, 0x86, 0x7a, 0x54, 0x0d, 0xfd, 0x90,
-	0x33, 0x25, 0x50, 0xa8, 0x7c, 0x94, 0x8d, 0xdf, 0x5c, 0xf9, 0xcb, 0x62, 0x6e, 0x62, 0xd5, 0x7d,
-	0xe6, 0x3e, 0xd1, 0xf9, 0xbb, 0x3a, 0x5d, 0x33, 0x3b, 0xe5, 0xd9, 0x94, 0xb5, 0xce, 0xc6, 0x2d,
-	0x70, 0xe3, 0x8c, 0xb1, 0xe4, 0x63, 0x73, 0x7f, 0x1b, 0x60, 0xb9, 0x23, 0x09, 0x64, 0x60, 0xf5,
-	0xc4, 0x9d, 0x34, 0x2f, 0xd8, 0xef, 0xc2, 0x0a, 0x6a, 0xee, 0xbf, 0xb3, 0x79, 0x5f, 0xf8, 0x0e,
-	0x5c, 0x3d, 0xb5, 0x2a, 0xfb, 0xe2, 0x3a, 0x8b, 0x7c, 0x6d, 0xfb, 0xff, 0xf8, 0xbc, 0x77, 0x6d,
-	0xe5, 0xfd, 0xec, 0x28, 0xdb, 0xfb, 0x87, 0x63, 0xcb, 0x38, 0x1a, 0x5b, 0xc6, 0xaf, 0xb1, 0x65,
-	0x7c, 0x9a, 0x58, 0x85, 0xa3, 0x89, 0x55, 0xf8, 0x31, 0xb1, 0x0a, 0xaf, 0x1e, 0x12, 0xaa, 0xba,
-	0xfd, 0xc0, 0x0e, 0x79, 0xec, 0xe8, 0x16, 0x3d, 0x14, 0xc8, 0x2d, 0xca, 0x73, 0xd3, 0x19, 0x3c,
-	0x38, 0x7d, 0xb0, 0x6a, 0x98, 0x60, 0x19, 0x14, 0xd3, 0x8f, 0xef, 0xfe, 0x9f, 0x00, 0x00, 0x00,
-	0xff, 0xff, 0x2b, 0x20, 0xf4, 0x55, 0x42, 0x04, 0x00, 0x00,
+	0x14, 0xc7, 0xb3, 0xad, 0x0d, 0x64, 0xda, 0xa2, 0x9d, 0x06, 0xbb, 0x8d, 0xba, 0x8d, 0x11, 0xa4,
+	0x44, 0xba, 0x4b, 0xa2, 0xf4, 0x50, 0x41, 0x30, 0x2d, 0x05, 0xc1, 0x88, 0x6c, 0xe9, 0xc5, 0xcb,
+	0x3a, 0xbb, 0x3b, 0x9d, 0x0c, 0xcd, 0xcc, 0x2c, 0x3b, 0x93, 0x90, 0x78, 0x12, 0x8f, 0x9e, 0xfc,
+	0x33, 0x3c, 0xe6, 0xe0, 0xdf, 0x20, 0xbd, 0x59, 0x3c, 0x79, 0x12, 0x49, 0x0e, 0xf9, 0x17, 0x3c,
+	0x4a, 0x76, 0x67, 0x1b, 0x9a, 0xfe, 0x50, 0x2f, 0x21, 0xef, 0x7d, 0x3f, 0xef, 0xbd, 0x79, 0x3f,
+	0x16, 0x54, 0x7c, 0xe4, 0xf7, 0xdb, 0x82, 0x3b, 0xbe, 0x0a, 0xa4, 0x3a, 0x0e, 0x04, 0x97, 0x1d,
+	0x86, 0x63, 0xa7, 0x5b, 0x73, 0x54, 0xcf, 0x8e, 0x62, 0xa1, 0x04, 0x5c, 0xd7, 0x8c, 0x7d, 0x9e,
+	0xb1, 0xbb, 0xb5, 0xd2, 0x0a, 0x62, 0x94, 0x0b, 0x27, 0xf9, 0x4d, 0xe9, 0xd2, 0x5a, 0x20, 0x24,
+	0x13, 0xd2, 0x61, 0x92, 0x4c, 0xb2, 0x30, 0x49, 0xb4, 0xb0, 0x9e, 0x0a, 0x5e, 0x62, 0x39, 0xa9,
+	0xa1, 0xa5, 0x22, 0x11, 0x44, 0xa4, 0xfe, 0xc9, 0x3f, 0xed, 0x7d, 0x78, 0xf5, 0xdb, 0x22, 0x14,
+	0x23, 0xa6, 0xa3, 0x2b, 0xdf, 0x0c, 0x70, 0xb3, 0x29, 0xc9, 0x61, 0x14, 0x22, 0x85, 0x5f, 0x27,
+	0x0a, 0xdc, 0x06, 0x05, 0xd4, 0x51, 0x2d, 0x11, 0x53, 0xd5, 0x37, 0x8d, 0xb2, 0xb1, 0x59, 0x68,
+	0x98, 0xdf, 0xbf, 0x6c, 0x15, 0x75, 0xd9, 0xe7, 0x61, 0x18, 0x63, 0x29, 0x0f, 0x54, 0x4c, 0x39,
+	0x71, 0xa7, 0x28, 0xdc, 0x03, 0xf9, 0x34, 0xb7, 0x39, 0x57, 0x36, 0x36, 0x17, 0xeb, 0xf7, 0xed,
+	0x2b, 0x9b, 0xb7, 0xd3, 0x52, 0x8d, 0xc2, 0xc9, 0xcf, 0x8d, 0xdc, 0xe7, 0xf1, 0xa0, 0x6a, 0xb8,
+	0x3a, 0x76, 0xe7, 0xd9, 0x87, 0xf1, 0xa0, 0x3a, 0xcd, 0xfa, 0x71, 0x3c, 0xa8, 0x3e, 0xca, 0x9a,
+	0xe9, 0x5d, 0xd2, 0xce, 0xcc, 0xeb, 0x2b, 0xeb, 0x60, 0x6d, 0xc6, 0xe5, 0x62, 0x19, 0x09, 0x2e,
+	0x71, 0xe5, 0xeb, 0x1c, 0x58, 0x6d, 0x4a, 0xe2, 0x62, 0x42, 0xa5, 0xc2, 0xf1, 0xae, 0xce, 0x02,
+	0x6f, 0x83, 0xbc, 0xa4, 0x84, 0xe3, 0x38, 0xed, 0xd6, 0xd5, 0x16, 0xdc, 0x00, 0x8b, 0x59, 0x25,
+	0x8f, 0x86, 0x49, 0x57, 0x05, 0x17, 0x64, 0xae, 0x17, 0x21, 0x7c, 0x00, 0x96, 0xcf, 0x00, 0x8e,
+	0x18, 0x36, 0xe7, 0x13, 0x64, 0x29, 0x73, 0xbe, 0x42, 0x0c, 0xc3, 0x1a, 0x28, 0x9e, 0x41, 0x21,
+	0x96, 0x41, 0x4c, 0x23, 0x45, 0x05, 0x37, 0x6f, 0x24, 0xec, 0x6a, 0xa6, 0xed, 0x4d, 0x25, 0xe8,
+	0x80, 0x22, 0x43, 0x3d, 0x8f, 0x75, 0xda, 0x8a, 0x7a, 0x52, 0xa1, 0x63, 0x1c, 0x7a, 0x47, 0x91,
+	0x34, 0x17, 0xca, 0xc6, 0xe6, 0xb2, 0xbb, 0xc2, 0x50, 0xaf, 0x39, 0x91, 0x0e, 0x12, 0x65, 0x3f,
+	0x92, 0xf0, 0x2d, 0x28, 0x63, 0xd5, 0xf2, 0xda, 0x75, 0xef, 0x88, 0x72, 0xd4, 0xa6, 0xaa, 0xef,
+	0x05, 0x82, 0xab, 0x18, 0x05, 0xca, 0x43, 0xe9, 0xbe, 0xcc, 0xfc, 0x5f, 0x36, 0x79, 0x17, 0xab,
+	0xd6, 0xcb, 0xfa, 0xbe, 0x8e, 0xdf, 0xd5, 0xe1, 0x9a, 0xd9, 0x59, 0x9c, 0xac, 0x45, 0x0f, 0xa6,
+	0x72, 0x0f, 0xdc, 0xb9, 0x64, 0x8e, 0xd9, 0x9c, 0xeb, 0xbf, 0x0d, 0x30, 0xdf, 0x94, 0x04, 0x72,
+	0xb0, 0x74, 0xee, 0xb0, 0xaa, 0xd7, 0x1c, 0xc4, 0xcc, 0xce, 0x4a, 0xf5, 0x7f, 0x67, 0xb3, 0xba,
+	0xf0, 0x1d, 0xb8, 0x75, 0x61, 0xb7, 0xf6, 0xf5, 0x79, 0x66, 0xf9, 0xd2, 0xf6, 0xff, 0xf1, 0x59,
+	0xed, 0xd2, 0xc2, 0xfb, 0xc9, 0x15, 0x37, 0x0e, 0x4f, 0x86, 0x96, 0x71, 0x3a, 0xb4, 0x8c, 0x5f,
+	0x43, 0xcb, 0xf8, 0x34, 0xb2, 0x72, 0xa7, 0x23, 0x2b, 0xf7, 0x63, 0x64, 0xe5, 0xde, 0x3c, 0x25,
+	0x54, 0xb5, 0x3a, 0xbe, 0x1d, 0x08, 0xe6, 0xe8, 0x12, 0x6d, 0xe4, 0xcb, 0x2d, 0x2a, 0x32, 0xd3,
+	0xe9, 0x3e, 0xb9, 0x78, 0xe1, 0xaa, 0x1f, 0x61, 0xe9, 0xe7, 0x93, 0xaf, 0xf5, 0xf1, 0x9f, 0x00,
+	0x00, 0x00, 0xff, 0xff, 0xdd, 0x26, 0x15, 0xf7, 0x73, 0x04, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -507,7 +520,12 @@ func (m *MsgRegisterConsumer) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 		copy(dAtA[i:], m.EthL2FinalityContractAddress)
 		i = encodeVarintTx(dAtA, i, uint64(len(m.EthL2FinalityContractAddress)))
 		i--
-		dAtA[i] = 0x2a
+		dAtA[i] = 0x32
+	}
+	if m.MaxMultiStakedFps != 0 {
+		i = encodeVarintTx(dAtA, i, uint64(m.MaxMultiStakedFps))
+		i--
+		dAtA[i] = 0x28
 	}
 	if len(m.ConsumerDescription) > 0 {
 		i -= len(m.ConsumerDescription)
@@ -620,6 +638,9 @@ func (m *MsgRegisterConsumer) Size() (n int) {
 	if l > 0 {
 		n += 1 + l + sovTx(uint64(l))
 	}
+	if m.MaxMultiStakedFps != 0 {
+		n += 1 + sovTx(uint64(m.MaxMultiStakedFps))
+	}
 	l = len(m.EthL2FinalityContractAddress)
 	if l > 0 {
 		n += 1 + l + sovTx(uint64(l))
@@ -965,6 +986,25 @@ func (m *MsgRegisterConsumer) Unmarshal(dAtA []byte) error {
 			m.ConsumerDescription = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
 		case 5:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field MaxMultiStakedFps", wireType)
+			}
+			m.MaxMultiStakedFps = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowTx
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.MaxMultiStakedFps |= uint32(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 6:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field EthL2FinalityContractAddress", wireType)
 			}


Reviews:
user: login: SebastianElvis
body: LGTM :+1:
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T01:05:07Z
user: login: RafilxTenfen
body: utACK, would be nice to have a replay [test](https://github.com/babylonlabs-io/babylon/blob/b9774782f38e9758c4f5aafab1e1e45dde0f3838/test/replay/btcstaking_test.go#L186) for this max multi staked fps
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T13:18:25Z
user: login: Lazar955
body: lgtm!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T13:25:01Z

Review comments:
diff_hunk: @@ -227,11 +227,18 @@ func (k Keeper) setBTCDelegation(ctx context.Context, btcDel *types.BTCDelegatio
 
 // validateRestakedFPs ensures all finality providers are known to Babylon and at least
 // one of them is a Babylon finality provider. It also checks whether the BTC stake is
-// restaked to FPs of consumer chains
+// restaked to FPs of consumer chains. It enforces:
+// 1. Total number of FPs <= min of all consumers' max_multi_staked_fps
+// 2. At most 1 FP per consumer/BSN
 func (k Keeper) validateRestakedFPs(ctx context.Context, fpBTCPKs []bbn.BIP340PubKey) (bool, error) {
 	restakedToBabylon := false
 	restakedToConsumers := false
 
+	// Track FPs per consumer to enforce at-most-1-FP-per-consumer
+	consumerFPs := make(map[string]int)
path: x/btcstaking/keeper/btc_delegations.go
user: login: SebastianElvis
body: 
```suggestion
	// Track FPs per consumer to enforce at-most-1-FP-per-consumer
	consumerFPs := make(map[string]struct{})
```

to make "at most 1 FP per consumer" explicit
created_at: 2025-05-23T01:01:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -227,11 +227,18 @@ func (k Keeper) setBTCDelegation(ctx context.Context, btcDel *types.BTCDelegatio
 
 // validateRestakedFPs ensures all finality providers are known to Babylon and at least
 // one of them is a Babylon finality provider. It also checks whether the BTC stake is
-// restaked to FPs of consumer chains
+// restaked to FPs of consumer chains. It enforces:
+// 1. Total number of FPs <= min of all consumers' max_multi_staked_fps
+// 2. At most 1 FP per consumer/BSN
 func (k Keeper) validateRestakedFPs(ctx context.Context, fpBTCPKs []bbn.BIP340PubKey) (bool, error) {
 	restakedToBabylon := false
 	restakedToConsumers := false
 
+	// Track FPs per consumer to enforce at-most-1-FP-per-consumer
+	consumerFPs := make(map[string]int)
+	// Track min max_multi_staked_fps across all consumers
+	minMaxMultiStakedFPs := uint32(0)
path: x/btcstaking/keeper/btc_delegations.go
user: login: SebastianElvis
body: Small nitpick: We can make this value `MaxUint32` such that we don't need to check `maxMultiStakedFps > 0` everytime (e.g., at L265-266 and L292). 
created_at: 2025-05-23T01:04:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: gusin13
created_at: 2025-05-24T21:07:57Z
body: > utACK, would be nice to have a replay [test](https://github.com/babylonlabs-io/babylon/blob/b9774782f38e9758c4f5aafab1e1e45dde0f3838/test/replay/btcstaking_test.go#L186) for this max multi staked fps

@RafilxTenfen @KonradStaniec I noticed only [FuzzCreatingAndActivatingDelegations](https://github.com/babylonlabs-io/babylon/blob/b9774782f38e9758c4f5aafab1e1e45dde0f3838/test/replay/btcstaking_test.go#L55-L56) includes block replay verification, while other tests in this file don't. Could you help me understand the reasoning behind this pattern?
author_association: CONTRIBUTOR
user: login: RafilxTenfen
created_at: 2025-05-26T11:55:41Z
body: > > utACK, would be nice to have a replay [test](https://github.com/babylonlabs-io/babylon/blob/b9774782f38e9758c4f5aafab1e1e45dde0f3838/test/replay/btcstaking_test.go#L186) for this max multi staked fps
> 
> @RafilxTenfen @KonradStaniec I noticed only [FuzzCreatingAndActivatingDelegations](https://github.com/babylonlabs-io/babylon/blob/b9774782f38e9758c4f5aafab1e1e45dde0f3838/test/replay/btcstaking_test.go#L55-L56) includes block replay verification, while other tests in this file don't. Could you help me understand the reasoning behind this pattern?

@gusin13 in other tests we don't want to replay and check for inconsistency apphash, but actually do a specific integration test simulating the validator building blocks and calling begin/end blocker of each module as the chain would 
author_association: CONTRIBUTOR
user: login: gusin13
created_at: 2025-05-26T12:12:25Z
body: ok thanks @RafilxTenfen, is it ok if I add this test in separate PR. 

I need to merge and backport this PR as web team is waiting for v4 devnet
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-26T12:13:08Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|release/v4.x|[<img src="https://img.shields.io/github/pulls/detail/state/babylonlabs-io/babylon/949">](https://github.com/babylonlabs-io/babylon/pull/949)|

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) and see the [Github Action logs](https://github.com/babylonlabs-io/babylon/actions/runs/15253632329) for details

<!--- Backport version: 9.5.1 -->
author_association: CONTRIBUTOR