diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ea7b336d..0f360e3f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -313,6 +313,28 @@ jobs: run: | make test-e2e-cache-btc-stake-expansion + e2e-run-costaking: + needs: [e2e-docker-build-babylon] + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Download babylon artifact + uses: actions/download-artifact@v4 + with: + name: babylond-${{ github.sha }} + path: /tmp + - name: Docker load babylond + run: | + docker load < /tmp/docker-babylond.tar.gz + - name: Cache Go + uses: actions/setup-go@v5 + with: + go-version: 1.23 + - name: Run e2e TestCostakingTestSuite + run: | + make test-e2e-cache-costaking + e2e-run-validator-jailing: needs: [e2e-docker-build-babylon] runs-on: ubuntu-22.04 diff --git a/CHANGELOG.md b/CHANGELOG.md index b48656e43..30936ba36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,19 +37,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## Unreleased -- [#1878](https://github.com/babylonlabs-io/babylon/pull/1878) Remove unnecessary []byte allocations in IBC size checks +### State breaking + +- [#1867](https://github.com/babylonlabs-io/babylon/pull/1867) bump wasmd `v0.60.2` +- [#1883](https://github.com/babylonlabs-io/babylon/pull/1883) Validate staker btc pk is the same with the previous btc del when stake extension +- [#1902](https://github.com/babylonlabs-io/babylon/pull/1902) fix: sorting of valset by the full address +- [#1826](https://github.com/babylonlabs-io/babylon/pull/1826) Support M-of-N multisig btc staker ### Bug Fixes - [#1875](https://github.com/babylonlabs-io/babylon/pull/1875) chore: ensure soft-deleted FPs cannot receive new/extended BTC stake, or commit pub rand +### Improvements + +- [#1878](https://github.com/babylonlabs-io/babylon/pull/1878) Remove unnecessary []byte allocations in IBC size checks +- [#1891](https://github.com/babylonlabs-io/babylon/pull/1891) fix: golangci lint misspell and removed unused func +- [#1901](https://github.com/babylonlabs-io/babylon/pull/1901) chore: update cl + +## v4.2.2 + +### Improvements + +- [#1903](https://github.com/babylonlabs-io/babylon/pull/1903) chore: bump cometbft to `v0.38.20` + +## v4.2.1 + +### Improvements + +- [#1839](https://github.com/babylonlabs-io/babylon/pull/1839) Add query to get the voting power distribution + +## v4.2.0 + +### Bug Fixes + +- [GHSA-m6wq-66p2-c8pc](https://github.com/babylonlabs-io/babylon/security/advisories/GHSA-m6wq-66p2-c8pc) fix: nil check of block hash in vote extension +- [GHSA-4rmq-mc2c-r495](https://github.com/babylonlabs-io/babylon-ghsa-4rmq-mc2c-r495/pull/1) Fix conditional logic in `AfterBtcDelegationUnbonded` hook + ## v4.1.0 ### Improvements - [#1764](https://github.com/babylonlabs-io/babylon/pull/1764) Add mergify yaml file for automatic backporting -- [#1839](https://github.com/babylonlabs-io/babylon/pull/1839) Add query to get the voting power distribution -cache (is only available until that block is finalized) ### Bug fixes diff --git a/Makefile b/Makefile index ad02e3ded..b68028d30 100644 --- a/Makefile +++ b/Makefile @@ -249,6 +249,7 @@ test-e2e-cache: $(MAKE) test-e2e-cache-epoching-spam-prevention $(MAKE) test-e2e-cache-btc-stake-expansion $(MAKE) test-e2e-cache-validator-jailing + $(MAKE) test-e2e-cache-costaking clean-e2e: docker container rm -f $(shell docker container ls -a -q) || true @@ -299,6 +300,9 @@ test-e2e-cache-btc-stake-expansion: test-e2e-cache-validator-jailing: go test -run TestValidatorJailingTestSuite -mod=readonly -timeout=60m -v $(PACKAGES_E2E) --tags=e2e +test-e2e-cache-costaking: + go test -run TestCostakingTestSuite -mod=readonly -timeout=60m -v $(PACKAGES_E2E) --tags=e2e + test-sim-nondeterminism: @echo "Running non-determinism test..." @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md index 4c93621dd..d42158bff 100644 --- a/RELEASE_PROCESS.md +++ b/RELEASE_PROCESS.md @@ -72,16 +72,26 @@ branch (see [CONTRIBUTING.md](./CONTRIBUTING.md#pull-requests)). ### Creating a new release branch -- create a new release branch, e.g., `release/v2.x` - ```bash - git checkout main - git pull - git checkout -b release/v2.x - ``` -- push the release branch upstream - ```bash - git push - ``` +**For major releases** (e.g., `release/v3.x`, `release/v4.x`): +Create the release branch from `main`: + +```bash +git checkout main +git pull +git checkout -b release/v4.x +git push +``` + +**For minor releases** (e.g., `release/v4.2.x` when `release/v4.1.x` exists): +Create the release branch from the previous minor release branch: + +```bash +git checkout release/v4.1.x +git pull +git checkout -b release/v4.2.x +git push +``` + ### Cutting a new release Before cutting a release (e.g., `v2.0.0-rc.0`), the diff --git a/app/include_upgrade_mainnet.go b/app/include_upgrade_mainnet.go index e3aca1f40..bb3644a30 100644 --- a/app/include_upgrade_mainnet.go +++ b/app/include_upgrade_mainnet.go @@ -10,6 +10,8 @@ import ( v23 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v2_3" v4 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4" v41 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4_1" + v42 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4_2" + v5 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v5" ) var WhitelistedChannelsID = map[string]struct{}{ @@ -25,6 +27,8 @@ var WhitelistedChannelsID = map[string]struct{}{ // init is used to include v2.2 upgrade for mainnet data func init() { Upgrades = []upgrades.Upgrade{ + v5.Upgrade, + v42.Upgrade, v41.Upgrade, v4.Upgrade, v23.Upgrade, // same as v3rc3 testnet diff --git a/app/include_upgrade_testnet.go b/app/include_upgrade_testnet.go index ad46d3c28..cde61a181 100644 --- a/app/include_upgrade_testnet.go +++ b/app/include_upgrade_testnet.go @@ -14,13 +14,17 @@ import ( v2rc4 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v2rc4/testnet" v4 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4" v41 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4_1" + v42 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4_2" v4rc3 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4rc3/testnet" + v5 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v5" ) // init is used to include v1 upgrade testnet data // it is also used for e2e testing func init() { Upgrades = []upgrades.Upgrade{ + v5.Upgrade, + v42.Upgrade, v41.Upgrade, v4rc3.Upgrade, v4.Upgrade, diff --git a/app/upgrades/v4_2/upgrade.go b/app/upgrades/v4_2/upgrade.go new file mode 100644 index 000000000..04c060d2a --- /dev/null +++ b/app/upgrades/v4_2/upgrade.go @@ -0,0 +1,323 @@ +package v4_2 + +import ( + "context" + "errors" + + "cosmossdk.io/collections" + corestoretypes "cosmossdk.io/core/store" + "cosmossdk.io/math" + store "cosmossdk.io/store/types" + upgradetypes "cosmossdk.io/x/upgrade/types" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/runtime" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/types/query" + + "github.com/babylonlabs-io/babylon/v4/app/keepers" + "github.com/babylonlabs-io/babylon/v4/app/upgrades" + bbn "github.com/babylonlabs-io/babylon/v4/types" + btcstkkeeper "github.com/babylonlabs-io/babylon/v4/x/btcstaking/keeper" + btcstktypes "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" + costkkeeper "github.com/babylonlabs-io/babylon/v4/x/costaking/keeper" + costktypes "github.com/babylonlabs-io/babylon/v4/x/costaking/types" + fkeeper "github.com/babylonlabs-io/babylon/v4/x/finality/keeper" + ftypes "github.com/babylonlabs-io/babylon/v4/x/finality/types" +) + +const UpgradeName = "v4.2" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: store.StoreUpgrades{ + Added: []string{}, + Deleted: []string{}, + }, +} + +func CreateUpgradeHandler(mm *module.Manager, configurator module.Configurator, keepers *keepers.AppKeepers) upgradetypes.UpgradeHandler { + return func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + // Run migrations before applying any other state changes. + migrations, err := mm.RunMigrations(ctx, configurator, fromVM) + if err != nil { + return nil, err + } + + costkStoreKey := keepers.GetKey(costktypes.StoreKey) + if costkStoreKey == nil { + return nil, errors.New("invalid costaking types store key") + } + coStkStoreService := runtime.NewKVStoreService(costkStoreKey) + + // Reset co-staker rewards tracker + if err := ResetCoStakerRwdsTracker( + ctx, + keepers.EncCfg.Codec, + coStkStoreService, + keepers.BTCStakingKeeper, + keepers.CostakingKeeper, + keepers.FinalityKeeper, + ); err != nil { + return nil, err + } + + return migrations, nil + } +} + +// ResetCoStakerRwdsTracker resets the costaker rewards tracker +// It resets tracked ActiveSats and ActiveBaby for all BTC stakers, BABY stakers, and combined stakers +func ResetCoStakerRwdsTracker( + ctx context.Context, + cdc codec.BinaryCodec, + costkStoreService corestoretypes.KVStoreService, + btcStkKeeper btcstkkeeper.Keeper, + coStkKeeper costkkeeper.Keeper, + fKeeper fkeeper.Keeper, +) error { + sb := collections.NewSchemaBuilder(costkStoreService) + rwdTrackers := collections.NewMap( + sb, + costktypes.CostakerRewardsTrackerKeyPrefix, + "costaker_rewards_tracker", + collections.BytesKey, + codec.CollValue[costktypes.CostakerRewardsTracker](cdc), + ) + + // Zero out tracked amounts in existing rewards trackers + accsWithActiveSats, err := zeroOutCoStakerRwdsActiveSats(ctx, rwdTrackers) + if err != nil { + return err + } + + params := coStkKeeper.GetParams(ctx) + endedPeriod, err := coStkKeeper.IncrementRewardsPeriod(ctx) + if err != nil { + return err + } + + // Save co-staker rwd tracker for all BTC stakers + if err := updateBTCStakersRwdTracker(ctx, endedPeriod, rwdTrackers, accsWithActiveSats, btcStkKeeper, fKeeper, coStkKeeper, params); err != nil { + return err + } + + totalScore, err := getTotalScore(ctx, rwdTrackers) + if err != nil { + return err + } + + currentRwd, err := coStkKeeper.GetCurrentRewards(ctx) + if err != nil { + return err + } + + currentRwd.TotalScore = totalScore + if err := currentRwd.Validate(); err != nil { + return err + } + + return coStkKeeper.SetCurrentRewards(ctx, *currentRwd) +} + +type ActiveSatsTracked struct { + PreviousActiveSats math.Int + CurrentActiveSats math.Int +} + +// updateBTCStakersRwdTracker retrieves all active BTC stakers with pagination and updates their costaker rewards trackers (ActiveSatoshis only) +func updateBTCStakersRwdTracker( + ctx context.Context, + period uint64, + rwdTrackers collections.Map[[]byte, costktypes.CostakerRewardsTracker], + accsWithActiveSats map[string]ActiveSatsTracked, + btcStkKeeper btcstkkeeper.Keeper, + fKeeper fkeeper.Keeper, + coStkKeeper costkkeeper.Keeper, + params costktypes.Params, +) error { + // To count as btc staker for the co-staking rewards + // need to be delegating to a FP within the current active set + // This runs on preblocker (before BeginBlock), so the active set to consider should be from previous height + sdkCtx := sdk.UnwrapSDKContext(ctx) + height := uint64(sdkCtx.HeaderInfo().Height) + vp := fKeeper.GetVotingPowerDistCache(ctx, height-1) + if vp == nil { + vp = ftypes.NewVotingPowerDistCache() + } + activeFps := vp.GetActiveFinalityProviderSet() + + var nextKey []byte + + for { + req := &btcstktypes.QueryBTCDelegationsRequest{ + Status: btcstktypes.BTCDelegationStatus_ACTIVE, + Pagination: &query.PageRequest{ + Key: nextKey, + }, + } + + btcDelRes, err := btcStkKeeper.BTCDelegations(ctx, req) + if err != nil { + return err + } + + for _, del := range btcDelRes.BtcDelegations { + // check if delegating to an active FP + if !delegatingToActiveFP(del.FpBtcPkList, activeFps) { + continue + } + // add all current active sats to the memory cache for later checking which have a diff + delSat := math.NewIntFromUint64(del.TotalSat) + data, found := accsWithActiveSats[del.StakerAddr] + if found { + data.CurrentActiveSats = data.CurrentActiveSats.Add(delSat) + accsWithActiveSats[del.StakerAddr] = data + } else { + accsWithActiveSats[del.StakerAddr] = ActiveSatsTracked{ + PreviousActiveSats: math.ZeroInt(), + CurrentActiveSats: delSat, + } + } + } + + if btcDelRes.Pagination == nil || len(btcDelRes.Pagination.NextKey) == 0 { + break + } + nextKey = btcDelRes.Pagination.NextKey + } + + // now update the costaker rewards trackers (all of them because we zeroed them out before) + for accAddrStr, satsTracked := range accsWithActiveSats { + diff := satsTracked.CurrentActiveSats.Sub(satsTracked.PreviousActiveSats) + needsCorrection := !diff.IsZero() + if err := updateCostakerActiveSatsRewardsTracker(ctx, coStkKeeper, period, rwdTrackers, sdk.MustAccAddressFromBech32(accAddrStr), satsTracked.CurrentActiveSats, params, needsCorrection); err != nil { + return err + } + } + + return nil +} + +// updateCostakerActiveSatsRewardsTracker creates or updates a costaker rewards tracker +func updateCostakerActiveSatsRewardsTracker( + ctx context.Context, + coStkKeeper costkkeeper.Keeper, + endedPeriod uint64, + rwdTrackers collections.Map[[]byte, costktypes.CostakerRewardsTracker], + stakerAddr sdk.AccAddress, + btcAmount math.Int, + params costktypes.Params, + needsCorrection bool, +) error { + addrKey := []byte(stakerAddr) + + // Try to get existing tracker + rt, err := rwdTrackers.Get(ctx, addrKey) + if err != nil && !errors.Is(err, collections.ErrNotFound) { + return err + } + + if errors.Is(err, collections.ErrNotFound) { + // this should not happen as we're updating existing trackers only + return nil + } + // Update existing tracker (need to set the ActiveSatoshis because these were zeroed out before) + // Update the StartPeriodCumulativeReward only if the ActiveSatoshis is changing + rt.ActiveSatoshis = rt.ActiveSatoshis.Add(btcAmount) + if needsCorrection { + rt.StartPeriodCumulativeReward = endedPeriod + if err := coStkKeeper.CalculateCostakerRewardsAndSendToGauge(ctx, stakerAddr, endedPeriod); err != nil { + return err + } + } + + // Update score + rt.UpdateScore(params.ScoreRatioBtcByBaby) + + // Save tracker + if err := rwdTrackers.Set(ctx, addrKey, rt); err != nil { + return err + } + + return nil +} + +// zeroOutCoStakerRwdsActiveSats zeros out ActiveSatoshis in all costaker rewards trackers +func zeroOutCoStakerRwdsActiveSats( + ctx context.Context, + rwdTrackers collections.Map[[]byte, costktypes.CostakerRewardsTracker], +) (map[string]ActiveSatsTracked, error) { + accsWithActiveSats := make(map[string]ActiveSatsTracked) + iter, err := rwdTrackers.Iterate(ctx, nil) + if err != nil { + return accsWithActiveSats, err + } + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + costakerAddr, err := iter.Key() + if err != nil { + return accsWithActiveSats, err + } + + tracker, err := iter.Value() + if err != nil { + return accsWithActiveSats, err + } + if tracker.ActiveSatoshis.IsZero() { + continue + } + + sdkAddr := sdk.AccAddress(costakerAddr) + accsWithActiveSats[sdkAddr.String()] = ActiveSatsTracked{ + PreviousActiveSats: tracker.ActiveSatoshis, + CurrentActiveSats: math.ZeroInt(), + } + + // Zero out ActiveSatoshis + tracker.ActiveSatoshis = math.ZeroInt() + if err := rwdTrackers.Set(ctx, costakerAddr, tracker); err != nil { + return accsWithActiveSats, err + } + } + + return accsWithActiveSats, nil +} + +func getTotalScore( + ctx context.Context, + rwdTrackers collections.Map[[]byte, costktypes.CostakerRewardsTracker], +) (math.Int, error) { + totalScore := math.ZeroInt() + iter, err := rwdTrackers.Iterate(ctx, nil) + if err != nil { + return totalScore, err + } + defer iter.Close() + + for ; iter.Valid(); iter.Next() { + tracker, err := iter.Value() + if err != nil { + return totalScore, err + } + + totalScore = totalScore.Add(tracker.TotalScore) + } + + return totalScore, nil +} + +func delegatingToActiveFP(fpBtcPks []bbn.BIP340PubKey, activeFps map[string]*ftypes.FinalityProviderDistInfo) bool { + // check if delegating to an active FP + isActiveDel := false + for _, fpBtcPk := range fpBtcPks { + if _, ok := activeFps[fpBtcPk.MarshalHex()]; ok { + isActiveDel = true + break + } + } + + return isActiveDel +} diff --git a/app/upgrades/v4_2/upgrades_costaking_test.go b/app/upgrades/v4_2/upgrades_costaking_test.go new file mode 100644 index 000000000..e8a6952c0 --- /dev/null +++ b/app/upgrades/v4_2/upgrades_costaking_test.go @@ -0,0 +1,492 @@ +package v4_2_test + +import ( + "context" + "math/rand" + "testing" + "time" + + "cosmossdk.io/collections" + corestore "cosmossdk.io/core/store" + "cosmossdk.io/log" + "cosmossdk.io/math" + "cosmossdk.io/store" + storemetrics "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocoded "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/runtime" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + stkkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stktypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + v4_2 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4_2" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" + testutilkeeper "github.com/babylonlabs-io/babylon/v4/testutil/keeper" + bbn "github.com/babylonlabs-io/babylon/v4/types" + btcctypes "github.com/babylonlabs-io/babylon/v4/x/btccheckpoint/types" + btclctypes "github.com/babylonlabs-io/babylon/v4/x/btclightclient/types" + btcstkkeeper "github.com/babylonlabs-io/babylon/v4/x/btcstaking/keeper" + btcstktypes "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" + costkkeeper "github.com/babylonlabs-io/babylon/v4/x/costaking/keeper" + costktypes "github.com/babylonlabs-io/babylon/v4/x/costaking/types" + fkeeper "github.com/babylonlabs-io/babylon/v4/x/finality/keeper" + ftypes "github.com/babylonlabs-io/babylon/v4/x/finality/types" +) + +func setupTestKeepers(t *testing.T) (sdk.Context, codec.BinaryCodec, corestore.KVStoreService, *stkkeeper.Keeper, btcstkkeeper.Keeper, *costkkeeper.Keeper, *fkeeper.Keeper, *gomock.Controller) { + ctrl := gomock.NewController(t) + + // Create DB and store + db := dbm.NewMemDB() + stateStore := store.NewCommitMultiStore(db, log.NewTestLogger(t), storemetrics.NewNoOpMetrics()) + + // Setup mocked keepers + btclcKeeper := btcstktypes.NewMockBTCLightClientKeeper(ctrl) + btclcKeeper.EXPECT().GetTipInfo(gomock.Any()).Return(&btclctypes.BTCHeaderInfo{Height: 10}).AnyTimes() + + btccKeeper := btcstktypes.NewMockBtcCheckpointKeeper(ctrl) + btccKeeper.EXPECT().GetParams(gomock.Any()).Return(btcctypes.DefaultParams()).AnyTimes() + + distK := costktypes.NewMockDistributionKeeper(ctrl) + + btcStkStoreKey := storetypes.NewKVStoreKey(btcstktypes.StoreKey) + btcStkKeeper, btcCtx := testutilkeeper.BTCStakingKeeperWithStore(t, db, stateStore, btcStkStoreKey, btclcKeeper, btccKeeper, nil) + + // Setup keepers + accK := testutilkeeper.AccountKeeper(t, db, stateStore) + bankKeeper := testutilkeeper.BankKeeper(t, db, stateStore, accK) + + // Create costaking module account + costkModuleAcc := authtypes.NewEmptyModuleAccount(costktypes.ModuleName) + accK.SetModuleAccount(btcCtx, costkModuleAcc) + stkKeeper := testutilkeeper.StakingKeeper(t, db, stateStore, accK, bankKeeper) + incentiveK, _ := testutilkeeper.IncentiveKeeperWithStore(t, db, stateStore, nil, bankKeeper, accK, nil) + fKeeper, _ := testutilkeeper.FinalityKeeperWithStore(t, db, stateStore, btcStkKeeper, incentiveK, ftypes.NewMockCheckpointingKeeper(ctrl), ftypes.NewMockFinalityHooks(ctrl)) + + // Setup costaking store service and keeper + costkStoreKey := storetypes.NewKVStoreKey(costktypes.StoreKey) + costkKeeper, _ := testutilkeeper.CostakingKeeperWithStore(t, db, stateStore, costkStoreKey, bankKeeper, accK, incentiveK, stkKeeper, distK) + require.NoError(t, stateStore.LoadLatestVersion()) + costkStoreService := runtime.NewKVStoreService(costkStoreKey) + + // Setup codec + registry := codectypes.NewInterfaceRegistry() + cryptocoded.RegisterInterfaces(registry) + cdc := codec.NewProtoCodec(registry) + + btcCtx = btcCtx.WithBlockHeight(10) + + return btcCtx, cdc, costkStoreService, stkKeeper, *btcStkKeeper, costkKeeper, fKeeper, ctrl +} + +// TestResetCoStakerRwdsTracker_WithPreexistingTrackers tests that existing trackers are reset +// and recalculated correctly with pre-existing costaker rewards trackers +func TestResetCoStakerRwdsTracker_WithPreexistingTrackers(t *testing.T) { + ctx, cdc, storeService, stkKeeper, btcStkKeeper, costkKeeper, fKeeper, ctrl := setupTestKeepers(t) + defer ctrl.Finish() + + require.NoError(t, btcStkKeeper.SetParams(ctx, btcstktypes.DefaultParams())) + require.NoError(t, costkKeeper.SetParams(ctx, costktypes.DefaultParams())) + require.NoError(t, stkKeeper.SetParams(ctx, stktypes.DefaultParams())) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Create a test staker address + stakerAddr := datagen.GenRandomAccount().GetAddress() + + // Create pre-existing costaker rewards tracker with arbitrary values + preexistingTracker := costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: uint64(5), + ActiveSatoshis: math.NewInt(999999), // Wrong value + ActiveBaby: math.NewInt(888888), // Wrong value + } + createCostakerRewardsTracker(t, ctx, cdc, storeService, stakerAddr, preexistingTracker) + + currPeriod := uint64(10) + setCurrentRewardsPeriod(t, ctx, costkKeeper, currPeriod) + + // Create BTC delegation + btcDel := createTestBTCDelegation(t, r, ctx, btcStkKeeper, stakerAddr, 50000) + + // seed voting power dist cache with FP as active + setupVotingPowerDistCacheWithActiveFPs(t, r, ctx, fKeeper, btcDel.FpBtcPkList) + + // Execute reset function + err := v4_2.ResetCoStakerRwdsTracker( + ctx, cdc, storeService, btcStkKeeper, *costkKeeper, *fKeeper, + ) + require.NoError(t, err) + + // Verify tracker was reset and recalculated correctly + verifyCoStakerUpdated(t, ctx, cdc, storeService, stakerAddr, math.NewIntFromUint64(btcDel.TotalSat), preexistingTracker.ActiveBaby, currPeriod) + + // Current rewards period should have increased + currRwds, err := costkKeeper.GetCurrentRewards(ctx) + require.NoError(t, err) + require.Equal(t, currPeriod+1, currRwds.Period, "Current rewards period should be updated") +} + +// TestResetCoStakerRwdsTracker_MultiplePreexistingTrackers tests resetting multiple trackers +func TestResetCoStakerRwdsTracker_MultiplePreexistingTrackers(t *testing.T) { + ctx, cdc, storeService, stkKeeper, btcStkKeeper, costkKeeper, fKeeper, ctrl := setupTestKeepers(t) + defer ctrl.Finish() + + require.NoError(t, btcStkKeeper.SetParams(ctx, btcstktypes.DefaultParams())) + require.NoError(t, costkKeeper.SetParams(ctx, costktypes.DefaultParams())) + require.NoError(t, stkKeeper.SetParams(ctx, stktypes.DefaultParams())) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Create three stakers + staker1Addr := datagen.GenRandomAccount().GetAddress() + staker2Addr := datagen.GenRandomAccount().GetAddress() + staker3Addr := datagen.GenRandomAccount().GetAddress() + staker4Addr := datagen.GenRandomAccount().GetAddress() + + babyAmount1 := math.NewInt(15000) + babyAmount2 := math.NewInt(20000) + babyAmount3 := math.NewInt(25000) + babyAmount4 := math.NewInt(0) + + // Create pre-existing trackers with incorrect values + createCostakerRewardsTracker(t, ctx, cdc, storeService, staker1Addr, costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: uint64(10), + ActiveSatoshis: math.NewInt(111111), + ActiveBaby: babyAmount1, + }) + createCostakerRewardsTracker(t, ctx, cdc, storeService, staker2Addr, costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: uint64(15), + ActiveSatoshis: math.NewInt(333333), + ActiveBaby: babyAmount2, + }) + createCostakerRewardsTracker(t, ctx, cdc, storeService, staker3Addr, costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: uint64(20), + ActiveSatoshis: math.NewInt(555555), + ActiveBaby: babyAmount3, + }) + + startPeriod4 := uint64(25) + activeSats4 := math.NewInt(75000) + createCostakerRewardsTracker(t, ctx, cdc, storeService, staker4Addr, costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: startPeriod4, + ActiveSatoshis: activeSats4, + ActiveBaby: babyAmount4, + }) + + currPeriod := uint64(30) + setCurrentRewardsPeriod(t, ctx, costkKeeper, currPeriod) + + // Create actual delegations for each staker + btcDel1 := createTestBTCDelegation(t, r, ctx, btcStkKeeper, staker1Addr, 30000) + btcDel2 := createTestBTCDelegation(t, r, ctx, btcStkKeeper, staker2Addr, 40000) + btcDel3 := createTestBTCDelegation(t, r, ctx, btcStkKeeper, staker3Addr, 50000) + // del4 has multiple delegations + btcDel41 := createTestBTCDelegation(t, r, ctx, btcStkKeeper, staker4Addr, activeSats4.Uint64()/2) + btcDel42 := createTestBTCDelegation(t, r, ctx, btcStkKeeper, staker4Addr, activeSats4.Uint64()/2) + + // Collect all FP BTC public keys + allFpBtcPks := make([]bbn.BIP340PubKey, 0) + allFpBtcPks = append(allFpBtcPks, btcDel1.FpBtcPkList...) + allFpBtcPks = append(allFpBtcPks, btcDel2.FpBtcPkList...) + allFpBtcPks = append(allFpBtcPks, btcDel3.FpBtcPkList...) + allFpBtcPks = append(allFpBtcPks, btcDel41.FpBtcPkList...) + allFpBtcPks = append(allFpBtcPks, btcDel42.FpBtcPkList...) + + // seed voting power dist cache with all FPs as active + setupVotingPowerDistCacheWithActiveFPs(t, r, ctx, fKeeper, allFpBtcPks) + + // Execute reset function + err := v4_2.ResetCoStakerRwdsTracker( + ctx, cdc, storeService, btcStkKeeper, *costkKeeper, *fKeeper, + ) + require.NoError(t, err) + + // Verify all trackers were reset and recalculated correctly + verifyCoStakerUpdated(t, ctx, cdc, storeService, staker1Addr, math.NewIntFromUint64(btcDel1.TotalSat), babyAmount1, currPeriod) + verifyCoStakerUpdated(t, ctx, cdc, storeService, staker2Addr, math.NewIntFromUint64(btcDel2.TotalSat), babyAmount2, currPeriod) + verifyCoStakerUpdated(t, ctx, cdc, storeService, staker3Addr, math.NewIntFromUint64(btcDel3.TotalSat), babyAmount3, currPeriod) + + // Active sats before == current active sats, so start period should not increase + verifyCoStakerUpdated(t, ctx, cdc, storeService, staker4Addr, activeSats4, babyAmount4, startPeriod4) + + // Verify total count is still 4 + count := countCoStakers(t, ctx, cdc, storeService) + require.Equal(t, 4, count, "Should have exactly 4 co-stakers") + + // Current rewards period should have increased + currRwds, err := costkKeeper.GetCurrentRewards(ctx) + require.NoError(t, err) + require.Equal(t, currPeriod+1, currRwds.Period, "Current rewards period should be updated") +} + +// TestResetCoStakerRwdsTracker_TrackerNoLongerValid tests that trackers for stakers +// who no longer have delegations are zeroed out +func TestResetCoStakerRwdsTracker_TrackerNoLongerValid(t *testing.T) { + ctx, cdc, storeService, stkKeeper, btcStkKeeper, costkKeeper, fKeeper, ctrl := setupTestKeepers(t) + defer ctrl.Finish() + + require.NoError(t, btcStkKeeper.SetParams(ctx, btcstktypes.DefaultParams())) + require.NoError(t, costkKeeper.SetParams(ctx, costktypes.DefaultParams())) + require.NoError(t, stkKeeper.SetParams(ctx, stktypes.DefaultParams())) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Create two stakers + staker1Addr := datagen.GenRandomAccount().GetAddress() // Will have delegations + staker2Addr := datagen.GenRandomAccount().GetAddress() // Will NOT have delegations + + // Create pre-existing trackers for both + babyAmount1 := math.NewInt(15000) + babyAmount2 := math.NewInt(100000) + + startPeriod1 := uint64(5) + createCostakerRewardsTracker(t, ctx, cdc, storeService, staker1Addr, costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: startPeriod1, + ActiveSatoshis: math.NewInt(100000), + ActiveBaby: babyAmount1, + }) + startPeriod2 := uint64(10) + createCostakerRewardsTracker(t, ctx, cdc, storeService, staker2Addr, costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: startPeriod2, + ActiveSatoshis: math.NewInt(200000), + ActiveBaby: babyAmount2, + }) + + currPeriod := uint64(20) + setCurrentRewardsPeriod(t, ctx, costkKeeper, currPeriod) + + // Create delegations ONLY for staker1 + btcDel1 := createTestBTCDelegation(t, r, ctx, btcStkKeeper, staker1Addr, 30000) + + // Setup voting power dist cache + setupVotingPowerDistCacheWithActiveFPs(t, r, ctx, fKeeper, btcDel1.FpBtcPkList) + + // Execute reset function + err := v4_2.ResetCoStakerRwdsTracker( + ctx, cdc, storeService, btcStkKeeper, *costkKeeper, *fKeeper, + ) + require.NoError(t, err) + + // Verify staker1 has correct values + verifyCoStakerUpdated(t, ctx, cdc, storeService, staker1Addr, math.NewIntFromUint64(btcDel1.TotalSat), babyAmount1, currPeriod) + + // Verify staker2 tracker is zeroed out (no delegations) + verifyCoStakerUpdated(t, ctx, cdc, storeService, staker2Addr, math.ZeroInt(), babyAmount2, currPeriod) + + // Current rewards period should have increased + currRwds, err := costkKeeper.GetCurrentRewards(ctx) + require.NoError(t, err) + require.Equal(t, currPeriod+1, currRwds.Period, "Current rewards period should be updated") +} + +// TestResetCoStakerRwdsTracker_InactiveFPAndValidator tests resetting when FP or validator becomes inactive +func TestResetCoStakerRwdsTracker_InactiveFPAndValidator(t *testing.T) { + ctx, cdc, storeService, stkKeeper, btcStkKeeper, costkKeeper, fKeeper, ctrl := setupTestKeepers(t) + defer ctrl.Finish() + + require.NoError(t, btcStkKeeper.SetParams(ctx, btcstktypes.DefaultParams())) + require.NoError(t, costkKeeper.SetParams(ctx, costktypes.DefaultParams())) + require.NoError(t, stkKeeper.SetParams(ctx, stktypes.DefaultParams())) + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + stakerAddr1 := datagen.GenRandomAccount().GetAddress() + stakerAddr2 := datagen.GenRandomAccount().GetAddress() + + // Create pre-existing tracker with both BTC and BABY + babyAmount := math.NewInt(25000) + startPeriod := uint64(5) + createCostakerRewardsTracker(t, ctx, cdc, storeService, stakerAddr1, costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: startPeriod, + ActiveSatoshis: math.NewInt(100000), + ActiveBaby: babyAmount, + }) + + // staker 2 has costaker tracker with 0 sats and some baby + createCostakerRewardsTracker(t, ctx, cdc, storeService, stakerAddr2, costktypes.CostakerRewardsTracker{ + StartPeriodCumulativeReward: startPeriod, + ActiveSatoshis: math.ZeroInt(), + ActiveBaby: babyAmount, + }) + + // Create BTC delegation (but FP will be inactive) + createTestBTCDelegation(t, r, ctx, btcStkKeeper, stakerAddr1, 50000) + createTestBTCDelegation(t, r, ctx, btcStkKeeper, stakerAddr2, 50000) + + currPeriod := uint64(20) + setCurrentRewardsPeriod(t, ctx, costkKeeper, currPeriod) + + // Setup voting power dist cache WITHOUT the FP (making it inactive) + vp, _, err := datagen.GenRandomVotingPowerDistCache(r, 10) + require.NoError(t, err) + fKeeper.SetVotingPowerDistCache(ctx, uint64(ctx.HeaderInfo().Height)-1, vp) + + // Execute reset function + err = v4_2.ResetCoStakerRwdsTracker( + ctx, cdc, storeService, btcStkKeeper, *costkKeeper, *fKeeper, + ) + require.NoError(t, err) + + // Verify tracker is zeroed out (active sats only) + verifyCoStakerUpdated(t, ctx, cdc, storeService, stakerAddr1, math.ZeroInt(), babyAmount, currPeriod) + // For staker2, there's no change as it had 0 sats before + // so start period should remain unchanged + verifyCoStakerUpdated(t, ctx, cdc, storeService, stakerAddr2, math.ZeroInt(), babyAmount, startPeriod) + + // Current rewards period should have increased + currRwds, err := costkKeeper.GetCurrentRewards(ctx) + require.NoError(t, err) + require.Equal(t, currPeriod+1, currRwds.Period, "Current rewards period should be updated") +} + +// Helper functions + +func setupVotingPowerDistCacheWithActiveFPs( + t *testing.T, + r *rand.Rand, + ctx sdk.Context, + fKeeper *fkeeper.Keeper, + fpBtcPks []bbn.BIP340PubKey, +) { + // Generate random voting power dist cache + vp, _, err := datagen.GenRandomVotingPowerDistCache(r, 10) + require.NoError(t, err) + require.NotEmpty(t, vp.FinalityProviders) + + activeFPsNeeded := len(fpBtcPks) + + // Ensure we have enough FPs in the cache + for len(vp.FinalityProviders) < activeFPsNeeded { + fp, err := datagen.GenRandomFinalityProvider(r) + require.NoError(t, err) + fpDistInfo := ftypes.NewFinalityProviderDistInfo(fp) + fpDistInfo.TotalBondedSat = datagen.RandomInt(r, 10000) + 1000 + fpDistInfo.IsTimestamped = true + vp.AddFinalityProviderDistInfo(fpDistInfo) + } + + // Replace the first N FPs with the desired ones + for i, fpBtcPk := range fpBtcPks { + if i < len(vp.FinalityProviders) { + vp.FinalityProviders[i].BtcPk = &fpBtcPk + vp.FinalityProviders[i].IsTimestamped = true + if vp.FinalityProviders[i].TotalBondedSat == 0 { + vp.FinalityProviders[i].TotalBondedSat = datagen.RandomInt(r, 10000) + 1000 + } + } + } + + // Apply active finality providers + vp.ApplyActiveFinalityProviders(uint32(max(activeFPsNeeded, 10))) + + // Set the voting power distribution cache + fKeeper.SetVotingPowerDistCache(ctx, uint64(ctx.HeaderInfo().Height)-1, vp) +} + +func createTestBTCDelegation(t *testing.T, r *rand.Rand, ctx sdk.Context, btcStkKeeper btcstkkeeper.Keeper, stakerAddr sdk.AccAddress, stakingValue uint64) *btcstktypes.BTCDelegation { + // Generate random BTC keys + delSK, _, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + + // Generate finality provider + fp, err := datagen.GenRandomFinalityProvider(r) + require.NoError(t, err) + // Create BTC delegation + startHeight := uint32(10) + endHeight := uint32(datagen.RandomInt(r, 1000)) + startHeight + btcctypes.DefaultParams().CheckpointFinalizationTimeout + 1 + stakingTime := endHeight - startHeight + slashingRate := math.LegacyNewDecWithPrec(int64(datagen.RandomInt(r, 41)+10), 2) + slashingChangeLockTime := uint16(101) + slashingAddress, err := datagen.GenRandomBTCAddress(r, &chaincfg.RegressionNetParams) + require.NoError(t, err) + slashingPkScript, err := txscript.PayToAddrScript(slashingAddress) + require.NoError(t, err) + + covenantSKs, covenantPKs, covenantQuorum := datagen.GenCovenantCommittee(r) + del, err := datagen.GenRandomBTCDelegation( + r, + t, + &chaincfg.RegressionNetParams, + []bbn.BIP340PubKey{*fp.BtcPk}, + delSK, + covenantSKs, + covenantPKs, + covenantQuorum, + slashingPkScript, + stakingTime, startHeight, endHeight, stakingValue, + slashingRate, + slashingChangeLockTime, + ) + if err != nil { + panic(err) + } + + // Set staker address + del.StakerAddr = stakerAddr.String() + del.TotalSat = stakingValue + + require.NoError(t, btcStkKeeper.AddBTCDelegation(ctx, del)) + + return del +} + +func createCostakerRewardsTracker(t *testing.T, ctx context.Context, cdc codec.BinaryCodec, storeService corestore.KVStoreService, stakerAddr sdk.AccAddress, tracker costktypes.CostakerRewardsTracker) { + rwdTrackers := rwdTrackerCollection(storeService, cdc) + err := rwdTrackers.Set(ctx, []byte(stakerAddr), tracker) + require.NoError(t, err) +} + +func verifyCoStakerUpdated(t *testing.T, ctx sdk.Context, cdc codec.BinaryCodec, storeService corestore.KVStoreService, stakerAddr sdk.AccAddress, expectedBTCAmount, expectedBabyAmount math.Int, expectedStartPeriod uint64) { + rwdTrackers := rwdTrackerCollection(storeService, cdc) + tracker, err := rwdTrackers.Get(ctx, []byte(stakerAddr)) + + require.NoError(t, err, "Co-staker rewards tracker should exist for %s", stakerAddr.String()) + require.Equal(t, expectedStartPeriod, tracker.StartPeriodCumulativeReward, "StartPeriodCumulativeReward should be %d", expectedStartPeriod) + require.True(t, tracker.ActiveSatoshis.Equal(expectedBTCAmount), "ActiveSatoshis should match expected BTC amount: expected %s, got %s", expectedBTCAmount.String(), tracker.ActiveSatoshis.String()) + require.True(t, tracker.ActiveBaby.Equal(expectedBabyAmount), "ActiveBaby should match expected baby amount: expected %s, got %s", expectedBabyAmount.String(), tracker.ActiveBaby.String()) +} + +func countCoStakers(t *testing.T, ctx sdk.Context, cdc codec.BinaryCodec, storeService corestore.KVStoreService) int { + rwdTrackers := rwdTrackerCollection(storeService, cdc) + var count int + err := rwdTrackers.Walk(ctx, nil, func(key []byte, value costktypes.CostakerRewardsTracker) (stop bool, err error) { + count++ + return false, nil + }) + require.NoError(t, err) + return count +} + +func rwdTrackerCollection(storeService corestore.KVStoreService, cdc codec.BinaryCodec) collections.Map[[]byte, costktypes.CostakerRewardsTracker] { + sb := collections.NewSchemaBuilder(storeService) + rwdTrackers := collections.NewMap( + sb, + costktypes.CostakerRewardsTrackerKeyPrefix, + "costaker_rewards_tracker", + collections.BytesKey, + codec.CollValue[costktypes.CostakerRewardsTracker](cdc), + ) + return rwdTrackers +} + +func setCurrentRewardsPeriod(t *testing.T, ctx sdk.Context, costkKeeper *costkkeeper.Keeper, period uint64) { + _, err := costkKeeper.GetCurrentRewardsInitialized(ctx) + require.NoError(t, err) + endedPeriod := uint64(0) + for endedPeriod < uint64(period-1) { + endedPeriod, err = costkKeeper.IncrementRewardsPeriod(ctx) + require.NoError(t, err) + } + + currRwds, err := costkKeeper.GetCurrentRewards(ctx) + require.NoError(t, err) + require.Equal(t, period, currRwds.Period, "current period is %d", currRwds.Period) +} diff --git a/app/upgrades/v5/upgrades.go b/app/upgrades/v5/upgrades.go new file mode 100644 index 000000000..e7f55a0ed --- /dev/null +++ b/app/upgrades/v5/upgrades.go @@ -0,0 +1,51 @@ +package v5 + +import ( + "context" + "fmt" + + store "cosmossdk.io/store/types" + upgradetypes "cosmossdk.io/x/upgrade/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/babylonlabs-io/babylon/v4/app/keepers" + "github.com/babylonlabs-io/babylon/v4/app/upgrades" + bstypes "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" +) + +const UpgradeName = "v5" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateUpgradeHandler, + StoreUpgrades: store.StoreUpgrades{ + Added: []string{}, + Deleted: []string{}, + }, +} + +func CreateUpgradeHandler(mm *module.Manager, configurator module.Configurator, keepers *keepers.AppKeepers) upgradetypes.UpgradeHandler { + return func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + currentHeight := uint64(sdkCtx.HeaderInfo().Height) + + // run migrations (includes btcstaking v1->v2 migration for multisig support) + migrations, err := mm.RunMigrations(ctx, configurator, fromVM) + if err != nil { + return nil, fmt.Errorf("failed to run migrations: %w", err) + } + + // log successful upgrade + btcStakingPrevVersion := fromVM[bstypes.ModuleName] + btcStakingNewVersion := migrations[bstypes.ModuleName] + sdkCtx.Logger().Info("multisig BTC staker upgrade completed successfully", + "upgrade", UpgradeName, + "btcstaking_migration", fmt.Sprintf("v%d->v%d", btcStakingPrevVersion, btcStakingNewVersion), + "height", currentHeight, + "epoch_boundary", true, + ) + + return migrations, nil + } +} diff --git a/app/upgrades/v5/upgrades_test.go b/app/upgrades/v5/upgrades_test.go new file mode 100644 index 000000000..14f23ce31 --- /dev/null +++ b/app/upgrades/v5/upgrades_test.go @@ -0,0 +1,159 @@ +package v5_test + +import ( + "math" + "testing" + "time" + + "github.com/stretchr/testify/suite" + + "cosmossdk.io/core/appmodule" + "cosmossdk.io/core/header" + "cosmossdk.io/x/upgrade" + upgradetypes "cosmossdk.io/x/upgrade/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/babylonlabs-io/babylon/v4/app" + "github.com/babylonlabs-io/babylon/v4/app/upgrades" + "github.com/babylonlabs-io/babylon/v4/app/upgrades/v5" + bbn "github.com/babylonlabs-io/babylon/v4/types" +) + +const ( + DummyUpgradeHeight = 8 +) + +type UpgradeTestSuite struct { + suite.Suite + + ctx sdk.Context + app *app.BabylonApp + preModule appmodule.HasPreBlocker +} + +func TestUpgradeTestSuite(t *testing.T) { + suite.Run(t, new(UpgradeTestSuite)) +} + +func (s *UpgradeTestSuite) SetupTest() { + // add the upgrade plan + app.Upgrades = []upgrades.Upgrade{v5.Upgrade} + + // set up app + s.app = app.SetupWithBitcoinConf(s.T(), false, bbn.BtcSignet) + s.ctx = s.app.BaseApp.NewContextLegacy(false, tmproto.Header{Height: 1, ChainID: "babylon-1", Time: time.Now().UTC()}) + s.preModule = upgrade.NewAppModule(s.app.UpgradeKeeper, s.app.AccountKeeper.AddressCodec()) + + // simulate pre-upgrade state by setting btcstaking version to 1 + // and resetting multisig params to 1 + vm, err := s.app.UpgradeKeeper.GetModuleVersionMap(s.ctx) + s.NoError(err) + vm["btcstaking"] = 1 + s.app.UpgradeKeeper.SetModuleVersionMap(s.ctx, vm) + + // reset multisig params to simulate pre-upgrade state + params := s.app.BTCStakingKeeper.GetParams(s.ctx) + storedParams := s.app.BTCStakingKeeper.GetParamsWithVersion(s.ctx) + params.MaxStakerQuorum = 1 // cannot reset to zero since it fails at Validate() + params.MaxStakerNum = 1 // cannot reset to zero since it fails at Validate() + err = s.app.BTCStakingKeeper.OverwriteParamsAtVersion(s.ctx, storedParams.Version, params) + s.NoError(err) +} + +func (s *UpgradeTestSuite) TestUpgrade() { + testCases := []struct { + msg string + preUpgrade func() + upgrade func() + postUpgrade func() + }{ + { + "Test v5 upgrade with multisig params migration", + s.PreUpgrade, + s.Upgrade, + func() { + s.PostUpgrade() + + vm, err := s.app.UpgradeKeeper.GetModuleVersionMap(s.ctx) + s.NoError(err) + s.Equal(uint64(2), vm["btcstaking"], "btcstaking should be version 2 after upgrade") + + allParams := s.app.BTCStakingKeeper.GetAllParams(s.ctx) + + for _, params := range allParams { + s.Equal(uint32(1), params.MaxStakerQuorum, "MaxStakerQuorum should be 1") + s.Equal(uint32(1), params.MaxStakerNum, "MaxStakerNum should be 1") + + err = params.Validate() + s.NoError(err, "migrated params should be valid") + } + }, + }, + { + "Test v5 upgrade preserves existing params", + s.PreUpgrade, + s.Upgrade, + func() { + s.PostUpgrade() + + // Get params after upgrade + params := s.app.BTCStakingKeeper.GetParams(s.ctx) + + // Verify all existing params are still present and valid + s.Equal(len(params.CovenantPks), 5, "CovenantPks should be preserved") + s.Equal(int(params.CovenantQuorum), 3, "CovenantQuorum should be preserved") + s.Equal(int(params.MinStakingValueSat), 10000, "MinStakingValueSat should be preserved") + s.Equal(params.MaxStakingValueSat, int64(10*10e8), "MaxStakingValueSat should be preserved") + s.Equal(int(params.MinStakingTimeBlocks), 400, "MinStakingTimeBlocks should be preserved") + s.Equal(int(params.MaxStakingTimeBlocks), math.MaxUint16, "MaxStakingTimeBlocks should be preserved") + s.NotEmpty(params.SlashingPkScript, "SlashingPkScript should be preserved") + s.Equal(int(params.MinSlashingTxFeeSat), 1000, "MinSlashingTxFeeSat should be preserved") + s.False(params.SlashingRate.IsNil(), "SlashingRate should be preserved") + s.Equal(int(params.UnbondingTimeBlocks), 200, "UnbondingTimeBlocks should be preserved") + s.Equal(int(params.UnbondingFeeSat), 1000, "UnbondingFeeSat should be preserved") + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.msg, func() { + s.SetupTest() // reset for each test case + + tc.preUpgrade() + tc.upgrade() + tc.postUpgrade() + }) + } +} + +func (s *UpgradeTestSuite) PreUpgrade() { + vm, err := s.app.UpgradeKeeper.GetModuleVersionMap(s.ctx) + s.NoError(err) + btcstakingVersion, exists := vm["btcstaking"] + s.True(exists, "btcstaking module should exist") + s.Equal(uint64(1), btcstakingVersion, "btcstaking should be version 1 before upgrade") +} + +func (s *UpgradeTestSuite) Upgrade() { + // inject upgrade plan + s.ctx = s.ctx.WithBlockHeight(DummyUpgradeHeight - 1) + plan := upgradetypes.Plan{Name: v5.UpgradeName, Height: DummyUpgradeHeight} + err := s.app.UpgradeKeeper.ScheduleUpgrade(s.ctx, plan) + s.NoError(err) + + // ensure upgrade plan exists + actualPlan, err := s.app.UpgradeKeeper.GetUpgradePlan(s.ctx) + s.NoError(err) + s.Equal(plan, actualPlan) + + // execute upgrade + s.ctx = s.ctx.WithHeaderInfo(header.Info{Height: DummyUpgradeHeight, Time: s.ctx.BlockTime().Add(time.Second)}).WithBlockHeight(DummyUpgradeHeight) + s.NotPanics(func() { + _, err := s.preModule.PreBlock(s.ctx) + s.Require().NoError(err) + }) +} + +func (s *UpgradeTestSuite) PostUpgrade() {} diff --git a/btcstaking/btcstaking_test.go b/btcstaking/btcstaking_test.go index 24429d44c..b86c6f5f7 100644 --- a/btcstaking/btcstaking_test.go +++ b/btcstaking/btcstaking_test.go @@ -26,6 +26,15 @@ type TestScenario struct { StakingTime uint16 } +type MultisigTestScenario struct { + StakerKeys []*btcec.PrivateKey + FinalityProviderKeys []*btcec.PrivateKey + CovenantKeys []*btcec.PrivateKey + RequiredCovenantSigs uint32 + StakingAmount btcutil.Amount + StakingTime uint16 +} + func GenerateTestScenario( r *rand.Rand, t *testing.T, @@ -85,6 +94,81 @@ func (t *TestScenario) FinalityProviderPublicKeys() []*btcec.PublicKey { return finalityProviderPubKeys } +func GenerateMultisigTestScenario( + r *rand.Rand, + t *testing.T, + numStakerKeys uint32, + requiredStakerSigs uint32, + numFinalityProviderKeys uint32, + numCovenantKeys uint32, + requiredCovenantSigs uint32, + stakingAmount btcutil.Amount, + stakingTime uint16, +) *MultisigTestScenario { + stakerPrivKeys := make([]*btcec.PrivateKey, numStakerKeys) + for i := uint32(0); i < numStakerKeys; i++ { + stakerPrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + stakerPrivKeys[i] = stakerPrivKey + } + + finalityProviderKeys := make([]*btcec.PrivateKey, numFinalityProviderKeys) + for i := uint32(0); i < numFinalityProviderKeys; i++ { + covenantPrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + finalityProviderKeys[i] = covenantPrivKey + } + + covenantKeys := make([]*btcec.PrivateKey, numCovenantKeys) + for i := uint32(0); i < numCovenantKeys; i++ { + covenantPrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + covenantKeys[i] = covenantPrivKey + } + + return &MultisigTestScenario{ + StakerKeys: stakerPrivKeys, + FinalityProviderKeys: finalityProviderKeys, + CovenantKeys: covenantKeys, + RequiredCovenantSigs: requiredCovenantSigs, + StakingAmount: stakingAmount, + StakingTime: stakingTime, + } +} + +func (t *MultisigTestScenario) StakerPublicKeys() []*btcec.PublicKey { + stakerPubKeys := make([]*btcec.PublicKey, len(t.StakerKeys)) + + for i, stakerKey := range t.StakerKeys { + stakerPubKeys[i] = stakerKey.PubKey() + } + + return stakerPubKeys +} + +func (t *MultisigTestScenario) CovenantPublicKeys() []*btcec.PublicKey { + covenantPubKeys := make([]*btcec.PublicKey, len(t.CovenantKeys)) + + for i, covenantKey := range t.CovenantKeys { + covenantPubKeys[i] = covenantKey.PubKey() + } + + return covenantPubKeys +} + +func (t *MultisigTestScenario) FinalityProviderPublicKeys() []*btcec.PublicKey { + finalityProviderPubKeys := make([]*btcec.PublicKey, len(t.FinalityProviderKeys)) + + for i, fpKey := range t.FinalityProviderKeys { + finalityProviderPubKeys[i] = fpKey.PubKey() + } + + return finalityProviderPubKeys +} + func createSpendStakeTx(amount btcutil.Amount) *wire.MsgTx { spendStakeTx := wire.NewMsgTx(2) spendStakeTx.AddTxIn(wire.NewTxIn(&wire.OutPoint{}, nil, nil)) diff --git a/btcstaking/identifiable_staking.go b/btcstaking/identifiable_staking.go index 9b8356224..f4bc9881e 100644 --- a/btcstaking/identifiable_staking.go +++ b/btcstaking/identifiable_staking.go @@ -447,6 +447,130 @@ func ParseV0StakingTxWithoutTag( }, nil } +// ParseV0MultisigStakingTx takes a btc transaction and checks whether it is a staking transaction and if so parses it +// for easy data retrieval. +// It does all necessary checks to ensure that the transaction is valid staking transaction. +func ParseV0MultisigStakingTx( + tx *wire.MsgTx, + expectedTag []byte, + stakerKeys []*btcec.PublicKey, + stakerQuorum uint32, + covenantKeys []*btcec.PublicKey, + covenantQuorum uint32, + net *chaincfg.Params, +) (*ParsedV0StakingTx, error) { + if len(expectedTag) != TagLen { + return nil, fmt.Errorf("invalid tag length: %d, expected: %d", len(expectedTag), TagLen) + } + + v0MultisigStakingTx, err := ParseV0MultisigStakingTxWithoutTag(tx, stakerKeys, stakerQuorum, covenantKeys, covenantQuorum, net) + if err != nil { + return nil, err + } + + // at this point we know that transaction has op return output which seems to match + // the expected shape. Check the tag and version. + if !bytes.Equal(v0MultisigStakingTx.OpReturnData.Tag, expectedTag) { + return nil, fmt.Errorf("unexpected tag: %s, expected: %s", + hex.EncodeToString(v0MultisigStakingTx.OpReturnData.Tag), + hex.EncodeToString(expectedTag), + ) + } + + return v0MultisigStakingTx, nil +} + +// ParseV0MultisigStakingTxWithoutTag takes a btc transaction and checks whether it is a staking transaction and if so parses it +// for easy data retrieval. +// It does all necessary checks to ensure that the transaction is valid staking transaction. +func ParseV0MultisigStakingTxWithoutTag( + tx *wire.MsgTx, + stakerKeys []*btcec.PublicKey, + stakerQuorum uint32, + covenantKeys []*btcec.PublicKey, + covenantQuorum uint32, + net *chaincfg.Params, +) (*ParsedV0StakingTx, error) { + // 1. Basic arguments checks + if tx == nil { + return nil, fmt.Errorf("nil tx") + } + + if len(stakerKeys) == 0 { + return nil, fmt.Errorf("no staker keys specified") + } + + if int(stakerQuorum) > len(stakerKeys) { + return nil, fmt.Errorf("staker quorum is greater than the number of staker keys") + } + + if len(covenantKeys) == 0 { + return nil, fmt.Errorf("no covenant keys specified") + } + + if int(covenantQuorum) > len(covenantKeys) { + return nil, fmt.Errorf("covenant quorum is greater than the number of covenant keys") + } + + // 2. Identify whether the transaction has expected shape + if len(tx.TxOut) < 2 { + return nil, fmt.Errorf("staking tx must have at least 2 outputs") + } + + opReturnData, opReturnOutputIdx, err := tryToGetOpReturnDataFromOutputs(tx.TxOut) + + if err != nil { + return nil, fmt.Errorf("cannot parse staking transaction: %w", err) + } + + if opReturnData == nil { + return nil, fmt.Errorf("transaction does not have expected op return output") + } + + if opReturnData.Version != 0 { + return nil, fmt.Errorf("unexpected version: %d, expected: %d", opReturnData.Version, 0) + } + + // 3. Op return seems to be valid V0 op return output. Now, we need to check whether + // the staking output exists and is valid. + // Note: unlike single sig btc staker case, multisig btc staker information is not stored in + // OP_RETURN since it has limited space and we don't use OP_RETURN data anywhere after phase-1, + // so it's okay to exclude extra stakerKeys in OP_RETURN. + stakingInfo, err := BuildMultisigStakingInfo( + stakerKeys, + stakerQuorum, + []*btcec.PublicKey{opReturnData.FinalityProviderPublicKey.PubKey}, + covenantKeys, + covenantQuorum, + opReturnData.StakingTime, + // we can pass 0 here, as staking amount is not used when creating taproot address + 0, + net, + ) + + if err != nil { + return nil, fmt.Errorf("cannot build staking info: %w", err) + } + + stakingOutput, stakingOutputIdx, err := tryToGetStakingOutput(tx.TxOut, stakingInfo.StakingOutput.PkScript) + + if err != nil { + return nil, fmt.Errorf("cannot parse staking transaction: %w", err) + } + + if stakingOutput == nil { + return nil, fmt.Errorf("staking output not found in potential staking transaction") + } + + return &ParsedV0StakingTx{ + StakingOutput: stakingOutput, + StakingOutputIdx: stakingOutputIdx, + OpReturnOutput: tx.TxOut[opReturnOutputIdx], + OpReturnOutputIdx: opReturnOutputIdx, + OpReturnData: opReturnData, + }, nil +} + // IsPossibleV0StakingTx checks whether transaction may be a valid staking transaction // checks: // 1. Whether the transaction has at least 2 outputs diff --git a/btcstaking/scripts_utils.go b/btcstaking/scripts_utils.go index ad2c85937..35315a4c9 100644 --- a/btcstaking/scripts_utils.go +++ b/btcstaking/scripts_utils.go @@ -13,10 +13,12 @@ import ( // private helper to assemble multisig script // if `withVerify` is true script will end with OP_NUMEQUALVERIFY otherwise with OP_NUMEQUAL // SCRIPT: OP_CHEKCSIG OP_CHECKSIGADD OP_CHECKSIGADD ... OP_CHECKSIGADD OP_NUMEQUALVERIFY (or OP_NUMEQUAL) +// withLockTime SCRIPT: <...existing script...> OP_CHECKSEQUENCEVERIFY func assembleMultiSigScript( pubkeys []*btcec.PublicKey, threshold uint32, withVerify bool, + withLockTime uint16, ) ([]byte, error) { builder := txscript.NewScriptBuilder() @@ -36,6 +38,11 @@ func assembleMultiSigScript( builder.AddOp(txscript.OP_NUMEQUAL) } + if withLockTime > 0 { + builder.AddInt64(int64(withLockTime)) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + } + return builder.Script() } @@ -72,11 +79,14 @@ func prepareKeysForMultisigScript(keys []*btcec.PublicKey) ([]*btcec.PublicKey, // successfully execute script // it validates whether threshold is not greater than number of keys // If there is only one key provided it will return single key sig script +// withLockTime is enabled when assemble multisig script with lockTime which is used for +// build multisig timelock script // Note: It is up to the caller to ensure that the keys are unique func buildMultiSigScript( keys []*btcec.PublicKey, threshold uint32, withVerify bool, + withLockTime uint16, ) ([]byte, error) { if len(keys) == 0 { return nil, fmt.Errorf("no keys provided") @@ -97,7 +107,7 @@ func buildMultiSigScript( return nil, err } - return assembleMultiSigScript(sortedKeys, threshold, withVerify) + return assembleMultiSigScript(sortedKeys, threshold, withVerify, withLockTime) } // Only holder of private key for given pubKey can spend after relative lock time diff --git a/btcstaking/staking.go b/btcstaking/staking.go index ad99065d3..178b46666 100644 --- a/btcstaking/staking.go +++ b/btcstaking/staking.go @@ -182,6 +182,64 @@ func BuildSlashingTxFromStakingTxStrict( slashingRate) } +// BuildMultisigSlashingTxFromStakingTxStrict constructs a valid slashing transaction using information from a staking transaction, +// a specified staking output index, and additional parameters such as slashing and change addresses, transaction fee, +// staking script, script version, and network. This function performs stricter validation compared to BuildSlashingTxFromStakingTx. +// +// Parameters: +// - stakingTx: The staking transaction from which the staking output is to be used for slashing. +// - stakingOutputIdx: The index of the staking output in the staking transaction. +// - stakerPks: public keys of the staker i.e., the btc holder who can spend staking output after lock time +// - stakerQuorum: threshold of the staker's signature +// - slashChangeLockTime: lock time for change output in slashing transaction +// - fee: The transaction fee to be paid. +// - slashingRate: The rate at which the staked funds will be slashed, expressed as a decimal. +// - net: The network on which transactions should take place (e.g., mainnet, testnet). +// +// Returns: +// - *wire.MsgTx: The constructed slashing transaction without script signature or witness. +// - error: An error if any validation or construction step fails. +func BuildMultisigSlashingTxFromStakingTxStrict( + stakingTx *wire.MsgTx, + stakingOutputIdx uint32, + slashingPkScript []byte, + stakerPks []*btcec.PublicKey, + stakerQuorum uint32, + slashChangeLockTime uint16, + fee int64, + slashingRate sdkmath.LegacyDec, + net *chaincfg.Params, +) (*wire.MsgTx, error) { + // Get the staking output at the specified index from the staking transaction + stakingOutput, err := getPossibleStakingOutput(stakingTx, stakingOutputIdx) + if err != nil { + return nil, err + } + + // Create an OutPoint for the staking output + stakingTxHash := stakingTx.TxHash() + stakingOutpoint := wire.NewOutPoint(&stakingTxHash, stakingOutputIdx) + + // Create taproot address committing to timelock script + si, err := BuildMultisigRelativeTimelockTaprootScript( + stakerPks, + stakerQuorum, + slashChangeLockTime, + net, + ) + + if err != nil { + return nil, err + } + + // Build slashing tx with the staking output information + return buildSlashingTxFromOutpoint( + *stakingOutpoint, + stakingOutput.Value, fee, + slashingPkScript, si.TapAddress, + slashingRate) +} + // IsTransferTx Transfer transaction is a transaction which: // - has exactly one input // - has exactly one output @@ -328,7 +386,8 @@ func validateSlashingTx( slashingPkScript []byte, slashingRate sdkmath.LegacyDec, slashingTxMinFee, stakingOutputValue int64, - stakerPk *btcec.PublicKey, + stakerPks []*btcec.PublicKey, + stakerQuorum uint32, slashingChangeLockTime uint16, net *chaincfg.Params, ) error { @@ -352,8 +411,9 @@ func validateSlashingTx( // Verify that the second output pays to the taproot address which locks funds for // slashingChangeLockTime - si, err := BuildRelativeTimelockTaprootScript( - stakerPk, + si, err := BuildMultisigRelativeTimelockTaprootScript( + stakerPks, + stakerQuorum, slashingChangeLockTime, net, ) @@ -443,6 +503,9 @@ func CheckSlashingTxMatchFundingTx( return fmt.Errorf("invalid funding output index %d, tx has %d outputs", fundingOutputIdx, len(fundingTransaction.TxOut)) } + // convert stakerPk into stakerPks to ensure backward compatibility of the function + stakerPks := []*btcec.PublicKey{stakerPk} + stakingOutput := fundingTransaction.TxOut[fundingOutputIdx] // 3. Check if slashing transaction is valid if err := validateSlashingTx( @@ -451,7 +514,75 @@ func CheckSlashingTxMatchFundingTx( slashingRate, slashingTxMinFee, stakingOutput.Value, - stakerPk, + stakerPks, + 1, + slashingChangeLockTime, + net); err != nil { + return err + } + + // 4. Check that slashing transaction input is pointing to staking transaction + stakingTxHash := fundingTransaction.TxHash() + if !slashingTx.TxIn[0].PreviousOutPoint.Hash.IsEqual(&stakingTxHash) { + return fmt.Errorf("slashing transaction must spend staking output") + } + + // 5. Check that index of the fund output matches index of the input in slashing transaction + if slashingTx.TxIn[0].PreviousOutPoint.Index != fundingOutputIdx { + return fmt.Errorf("slashing transaction input must spend staking output") + } + return nil +} + +// CheckSlashingTxMatchFundingTxMultisig validates all relevant data of slashing and funding transaction. +// - both transactions are valid from pov of BTC rules +// - slashing transaction is valid +// - slashing transaction input hash is pointing to funding transaction hash +// - slashing transaction input index is pointing to funding transaction output committing to the script +func CheckSlashingTxMatchFundingTxMultisig( + slashingTx *wire.MsgTx, + fundingTransaction *wire.MsgTx, + fundingOutputIdx uint32, + slashingTxMinFee int64, + slashingRate sdkmath.LegacyDec, + slashingPkScript []byte, + stakerPks []*btcec.PublicKey, + stakerQuorum uint32, + slashingChangeLockTime uint16, + net *chaincfg.Params, +) error { + if slashingTx == nil || fundingTransaction == nil { + return fmt.Errorf("slashing and funding transactions must not be nil") + } + + if err := blockchain.CheckTransactionSanity(btcutil.NewTx(fundingTransaction)); err != nil { + return fmt.Errorf("funding transaction does not obey BTC rules: %w", err) + } + + // Check if slashing tx min fee is valid + if slashingTxMinFee <= 0 { + return fmt.Errorf("slashing transaction min fee must be larger than 0") + } + + // Check if slashing rate is in the valid range (0,1) + if !IsSlashingRateValid(slashingRate) { + return ErrInvalidSlashingRate + } + + if int(fundingOutputIdx) >= len(fundingTransaction.TxOut) { + return fmt.Errorf("invalid funding output index %d, tx has %d outputs", fundingOutputIdx, len(fundingTransaction.TxOut)) + } + + stakingOutput := fundingTransaction.TxOut[fundingOutputIdx] + // 3. Check if slashing transaction is valid + if err := validateSlashingTx( + slashingTx, + slashingPkScript, + slashingRate, + slashingTxMinFee, + stakingOutput.Value, + stakerPks, + stakerQuorum, slashingChangeLockTime, net); err != nil { return err @@ -768,6 +899,51 @@ func VerifyTransactionSigWithOutput( return fmt.Errorf("public key must not be nil") } + pubKey2Sig := map[*btcec.PublicKey][]byte{ + pubKey: signature, + } + + return verifyTaprootScriptSpendSignature( + transaction, + 0, + map[wire.OutPoint]*wire.TxOut{ + transaction.TxIn[0].PreviousOutPoint: fundingOutput, + }, + txscript.NewBaseTapLeaf(script), + pubKey2Sig, + 1, + ) +} + +// VerifyTransactionMultiSigWithOutput verifies that: +// - provided transaction has exactly one input +// - provided signatures are valid schnorr BIP340 signatures +// - provided signatures are signing whole provided transaction (SigHashDefault) +// - pubkey to signature map should be provided to ensure verification order +// - staker quorum is the threshold of M-of-N multisig +func VerifyTransactionMultiSigWithOutput( + transaction *wire.MsgTx, + fundingOutput *wire.TxOut, + script []byte, + pubKey2Sig map[*btcec.PublicKey][]byte, + stakerQuorum uint32, +) error { + if fundingOutput == nil { + return fmt.Errorf("funding output must not be nil") + } + + if transaction == nil { + return fmt.Errorf("tx to verify not be nil") + } + + if len(transaction.TxIn) != 1 { + return fmt.Errorf("tx to sign must have exactly one input") + } + + if len(pubKey2Sig) < 1 { + return fmt.Errorf("must provide at least one signature") + } + return verifyTaprootScriptSpendSignature( transaction, 0, @@ -775,8 +951,8 @@ func VerifyTransactionSigWithOutput( transaction.TxIn[0].PreviousOutPoint: fundingOutput, }, txscript.NewBaseTapLeaf(script), - pubKey, - signature, + pubKey2Sig, + stakerQuorum, ) } @@ -811,6 +987,10 @@ func VerifyTransactionSigStkExp( return fmt.Errorf("stake spend tx must have exactly two inputs") } + pubKey2Sig := map[*btcec.PublicKey][]byte{ + pubKey: signatureOverPrevStkSpend, + } + return verifyTaprootScriptSpendSignature( stkSpendTx, 0, @@ -819,8 +999,8 @@ func VerifyTransactionSigStkExp( stkSpendTx.TxIn[1].PreviousOutPoint: fundingOutputIdx1, }, txscript.NewBaseTapLeaf(script), - pubKey, - signatureOverPrevStkSpend, + pubKey2Sig, + 1, ) } @@ -830,19 +1010,22 @@ func VerifyTransactionSigStkExp( // - The signature commits to the entire transaction with SigHashDefault. // - The TapLeaf script is what was signed. // - All prevOutputs must be supplied in full (for all inputs). +// - pubKey2Sig maps public key to signature in order to verify signature corresponding to its public key. +// - stakerQuorum is the threshold of M-of-N multisig and valid signatures must be greater or equal to stakerQuorum. +// NOTE: in this function, we assume pubKey2Sig map is correctly mapped func verifyTaprootScriptSpendSignature( tx *wire.MsgTx, inputIdx int, prevOutputs map[wire.OutPoint]*wire.TxOut, tapLeaf txscript.TapLeaf, - pubKey *btcec.PublicKey, - signature []byte, + pubKey2Sig map[*btcec.PublicKey][]byte, + stakerQuorum uint32, ) error { if tx == nil { return fmt.Errorf("tx to verify must not be nil") } - if pubKey == nil { - return fmt.Errorf("public key must not be nil") + if len(pubKey2Sig) < 1 { + return fmt.Errorf("public key must be at least one") } if inputIdx < 0 || inputIdx >= len(tx.TxIn) { return fmt.Errorf("input index %d out of bounds", inputIdx) @@ -868,13 +1051,25 @@ func verifyTaprootScriptSpendSignature( return err } - parsedSig, err := schnorr.ParseSignature(signature) - if err != nil { - return err + validSigCount := uint32(0) + for pubKey, signature := range pubKey2Sig { + if pubKey == nil { + return fmt.Errorf("public key must not be nil") + } + + parsedSig, err := schnorr.ParseSignature(signature) + if err != nil { + return err + } + + if parsedSig.Verify(sigHash, pubKey) { + validSigCount++ + } } - if !parsedSig.Verify(sigHash, pubKey) { - return fmt.Errorf("signature is not valid") + // check if there are enough valid signatures + if validSigCount < stakerQuorum { + return fmt.Errorf("not enough valid signatures: got %d, need at least %d", validSigCount, stakerQuorum) } return nil diff --git a/btcstaking/staking_test.go b/btcstaking/staking_test.go index e3f7ae123..e8b285409 100644 --- a/btcstaking/staking_test.go +++ b/btcstaking/staking_test.go @@ -252,6 +252,94 @@ func FuzzGeneratingSignatureValidation(f *testing.F) { }) } +func FuzzGeneratingMultisigSignatureValidation(f *testing.F) { + datagen.AddRandomSeedsToFuzzer(f, 10) + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + + // generate 2-5 staker keys + numStakers := r.Intn(4) + 2 // 2-5 stakers + stakerPrivKeys := make([]*btcec.PrivateKey, numStakers) + stakerPubKeys := make([]*btcec.PublicKey, numStakers) + for i := 0; i < numStakers; i++ { + pk, err := btcec.NewPrivateKey() + require.NoError(t, err) + stakerPrivKeys[i] = pk + stakerPubKeys[i] = pk.PubKey() + } + + // pick random M-of-N quorum (at least 1, at most N) + stakerQuorum := r.Intn(numStakers) + 1 + + inputHash, err := chainhash.NewHash(datagen.GenRandomByteArray(r, 32)) + require.NoError(t, err) + + tx := wire.NewMsgTx(2) + foundingOutput := wire.NewTxOut(int64(r.Intn(1000)+1000), datagen.GenRandomByteArray(r, 32)) + tx.AddTxIn( + wire.NewTxIn(wire.NewOutPoint(inputHash, uint32(r.Intn(20))), nil, nil), + ) + tx.AddTxOut( + wire.NewTxOut(int64(r.Intn(1000)+500), datagen.GenRandomByteArray(r, 32)), + ) + script := datagen.GenRandomByteArray(r, 150) + + // sign with exactly M stakers (the quorum) + pubKey2Sig := make(map[*btcec.PublicKey][]byte) + for i := 0; i < stakerQuorum; i++ { + sig, err := btcstaking.SignTxWithOneScriptSpendInputFromScript( + tx, + foundingOutput, + stakerPrivKeys[i], + script, + ) + require.NoError(t, err) + pubKey2Sig[stakerPubKeys[i]] = sig.Serialize() + } + + // verify multisig + err = btcstaking.VerifyTransactionMultiSigWithOutput( + tx, + foundingOutput, + script, + pubKey2Sig, + uint32(stakerQuorum), + ) + + require.NoError(t, err) + + // test with wrong signature should fail + wrongPk, err := btcec.NewPrivateKey() + require.NoError(t, err) + wrongSig, err := btcstaking.SignTxWithOneScriptSpendInputFromScript( + tx, + foundingOutput, + wrongPk, + script, + ) + require.NoError(t, err) + + // replace one valid signature with wrong one + if stakerQuorum > 0 { + pubKey2SigWrong := make(map[*btcec.PublicKey][]byte) + for k, v := range pubKey2Sig { + pubKey2SigWrong[k] = v + } + pubKey2SigWrong[stakerPubKeys[0]] = wrongSig.Serialize() + + err = btcstaking.VerifyTransactionMultiSigWithOutput( + tx, + foundingOutput, + script, + pubKey2SigWrong, + uint32(stakerQuorum), + ) + require.Error(t, err) + require.Contains(t, err.Error(), "signature") + } + }) +} + func TestSlashingTxWithOverflowMustNotAccepted(t *testing.T) { r := rand.New(rand.NewSource(time.Now().Unix())) // we do not care for inputs in staking tx diff --git a/btcstaking/types.go b/btcstaking/types.go index cf0e86971..63d129f30 100644 --- a/btcstaking/types.go +++ b/btcstaking/types.go @@ -271,13 +271,21 @@ func keyToString(key *btcec.PublicKey) string { } func checkForDuplicateKeys( - stakerKey *btcec.PublicKey, + stakerKeys []*btcec.PublicKey, fpKeys []*btcec.PublicKey, covenantKeys []*btcec.PublicKey, ) error { keyMap := make(map[string]struct{}) - keyMap[keyToString(stakerKey)] = struct{}{} + for _, key := range stakerKeys { + keyStr := keyToString(key) + + if _, ok := keyMap[keyStr]; ok { + return fmt.Errorf("key: %s: %w", keyStr, ErrDuplicatedKeyInScript) + } + + keyMap[keyStr] = struct{}{} + } for _, key := range fpKeys { keyStr := keyToString(key) @@ -303,24 +311,45 @@ func checkForDuplicateKeys( } func newBabylonScriptPaths( - stakerKey *btcec.PublicKey, + stakerKeys []*btcec.PublicKey, + stakerQuorum uint32, fpKeys []*btcec.PublicKey, covenantKeys []*btcec.PublicKey, covenantQuorum uint32, lockTime uint16, ) (*babylonScriptPaths, error) { - if stakerKey == nil { - return nil, fmt.Errorf("staker key is nil") + if len(stakerKeys) == 0 { + return nil, fmt.Errorf("no staker key is provided") } - if err := checkForDuplicateKeys(stakerKey, fpKeys, covenantKeys); err != nil { + if stakerQuorum < 1 { + return nil, fmt.Errorf("staker quorum must be greater than 0") + } + + if err := checkForDuplicateKeys(stakerKeys, fpKeys, covenantKeys); err != nil { return nil, fmt.Errorf("error building scripts: %w", err) } - timeLockPathScript, err := buildTimeLockScript(stakerKey, lockTime) + var ( + timeLockPathScript []byte + stakerSigScript []byte + err error + ) - if err != nil { - return nil, err + if len(stakerKeys) == 1 { + if timeLockPathScript, err = buildTimeLockScript(stakerKeys[0], lockTime); err != nil { + return nil, err + } + if stakerSigScript, err = buildSingleKeySigScript(stakerKeys[0], true); err != nil { + return nil, err + } + } else { + if timeLockPathScript, err = buildMultiSigScript(stakerKeys, stakerQuorum, true, lockTime); err != nil { + return nil, err + } + if stakerSigScript, err = buildMultiSigScript(stakerKeys, stakerQuorum, true, 0); err != nil { + return nil, err + } } covenantMultisigScript, err := buildMultiSigScript( @@ -330,24 +359,20 @@ func newBabylonScriptPaths( // last value on the stack. If we do not leave at least one element on the stack // script will always error false, + 0, ) if err != nil { return nil, err } - stakerSigScript, err := buildSingleKeySigScript(stakerKey, true) - - if err != nil { - return nil, err - } - fpMultisigScript, err := buildMultiSigScript( fpKeys, // we always require only one finality provider to sign 1, // we need to run verify to clear the stack, as finality provider multisig is in the middle of the script true, + 0, ) if err != nil { @@ -389,8 +414,78 @@ func BuildStakingInfo( ) (*StakingInfo, error) { unspendableKeyPathKey := unspendableKeyPathInternalPubKey() + // convert stakerKey to stakerKeys with one element + stakerKeys := []*btcec.PublicKey{stakerKey} + + babylonScripts, err := newBabylonScriptPaths( + stakerKeys, + 1, + fpKeys, + covenantKeys, + covenantQuorum, + stakingTime, + ) + + if err != nil { + return nil, fmt.Errorf("%s: %w", errBuildingStakingInfo, err) + } + + var unbondingPaths [][]byte + unbondingPaths = append(unbondingPaths, babylonScripts.timeLockPathScript) + unbondingPaths = append(unbondingPaths, babylonScripts.unbondingPathScript) + unbondingPaths = append(unbondingPaths, babylonScripts.slashingPathScript) + + timeLockLeafHash := txscript.NewBaseTapLeaf(babylonScripts.timeLockPathScript).TapHash() + unbondingPathLeafHash := txscript.NewBaseTapLeaf(babylonScripts.unbondingPathScript).TapHash() + slashingLeafHash := txscript.NewBaseTapLeaf(babylonScripts.slashingPathScript).TapHash() + + sh, err := newTaprootScriptHolder( + &unspendableKeyPathKey, + unbondingPaths, + ) + + if err != nil { + return nil, fmt.Errorf("%s: %w", errBuildingStakingInfo, err) + } + + taprootPkScript, err := sh.taprootPkScript(net) + + if err != nil { + return nil, fmt.Errorf("%s: %w", errBuildingStakingInfo, err) + } + + stakingOutput := wire.NewTxOut(int64(stakingAmount), taprootPkScript) + + return &StakingInfo{ + StakingOutput: stakingOutput, + scriptHolder: sh, + timeLockPathLeafHash: timeLockLeafHash, + unbondingPathLeafHash: unbondingPathLeafHash, + slashingPathLeafHash: slashingLeafHash, + }, nil +} + +// BuildMultisigStakingInfo builds all Babylon specific BTC scripts that must +// be committed to in the staking output. +// Returned `StakingInfo` object exposes methods to build spend info for each +// of the script spending paths which later must be included in the witness. +// It is up to the caller to verify whether parameters provided to this function +// obey parameters expected by Babylon chain. +func BuildMultisigStakingInfo( + stakerKeys []*btcec.PublicKey, + stakerQuorum uint32, + fpKeys []*btcec.PublicKey, + covenantKeys []*btcec.PublicKey, + covenantQuorum uint32, + stakingTime uint16, + stakingAmount btcutil.Amount, + net *chaincfg.Params, +) (*StakingInfo, error) { + unspendableKeyPathKey := unspendableKeyPathInternalPubKey() + babylonScripts, err := newBabylonScriptPaths( - stakerKey, + stakerKeys, + stakerQuorum, fpKeys, covenantKeys, covenantQuorum, @@ -475,8 +570,75 @@ func BuildUnbondingInfo( ) (*UnbondingInfo, error) { unspendableKeyPathKey := unspendableKeyPathInternalPubKey() + // convert stakerKey to stakerKeys with one element + stakerKeys := []*btcec.PublicKey{stakerKey} + babylonScripts, err := newBabylonScriptPaths( - stakerKey, + stakerKeys, + 1, + fpKeys, + covenantKeys, + covenantQuorum, + unbondingTime, + ) + + if err != nil { + return nil, fmt.Errorf("%s: %w", errBuildingUnbondingInfo, err) + } + + var unbondingPaths [][]byte + unbondingPaths = append(unbondingPaths, babylonScripts.timeLockPathScript) + unbondingPaths = append(unbondingPaths, babylonScripts.slashingPathScript) + + timeLockLeafHash := txscript.NewBaseTapLeaf(babylonScripts.timeLockPathScript).TapHash() + slashingLeafHash := txscript.NewBaseTapLeaf(babylonScripts.slashingPathScript).TapHash() + + sh, err := newTaprootScriptHolder( + &unspendableKeyPathKey, + unbondingPaths, + ) + + if err != nil { + return nil, fmt.Errorf("%s: %w", errBuildingUnbondingInfo, err) + } + + taprootPkScript, err := sh.taprootPkScript(net) + + if err != nil { + return nil, fmt.Errorf("%s: %w", errBuildingUnbondingInfo, err) + } + + unbondingOutput := wire.NewTxOut(int64(unbondingAmount), taprootPkScript) + + return &UnbondingInfo{ + UnbondingOutput: unbondingOutput, + scriptHolder: sh, + timeLockPathLeafHash: timeLockLeafHash, + slashingPathLeafHash: slashingLeafHash, + }, nil +} + +// BuildMultisigUnbondingInfo builds all Babylon specific BTC scripts that must +// be committed to in the unbonding output. +// Returned `UnbondingInfo` object exposes methods to build spend info for each +// of the script spending paths which later must be included in the witness. +// It is up to the caller to verify whether parameters provided to this function +// obey parameters expected by Babylon chain. +func BuildMultisigUnbondingInfo( + stakerKeys []*btcec.PublicKey, + stakerQuorum uint32, + fpKeys []*btcec.PublicKey, + covenantKeys []*btcec.PublicKey, + covenantQuorum uint32, + unbondingTime uint16, + unbondingAmount btcutil.Amount, + net *chaincfg.Params, +) (*UnbondingInfo, error) { + unspendableKeyPathKey := unspendableKeyPathInternalPubKey() + + babylonScripts, err := newBabylonScriptPaths( + stakerKeys, + stakerQuorum, fpKeys, covenantKeys, covenantQuorum, @@ -610,6 +772,70 @@ func BuildRelativeTimelockTaprootScript( }, nil } +func BuildMultisigRelativeTimelockTaprootScript( + pks []*btcec.PublicKey, + quorum uint32, + lockTime uint16, + net *chaincfg.Params, +) (*RelativeTimeLockTapScriptInfo, error) { + unspendableKeyPathKey := unspendableKeyPathInternalPubKey() + + var ( + script []byte + err error + ) + + if len(pks) == 1 && quorum == 1 { + script, err = buildTimeLockScript(pks[0], lockTime) + } else { + script, err = buildMultiSigScript(pks, quorum, true, lockTime) + } + + if err != nil { + return nil, err + } + + sh, err := newTaprootScriptHolder( + &unspendableKeyPathKey, + [][]byte{script}, + ) + + if err != nil { + return nil, err + } + + // there is only one script path in tree, so we can use index 0 + proof := sh.scriptTree.LeafMerkleProofs[0] + + spendInfo := &SpendInfo{ + ControlBlock: proof.ToControlBlock(&unspendableKeyPathKey), + RevealedLeaf: proof.TapLeaf, + } + + taprootAddress, err := DeriveTaprootAddress( + sh.scriptTree, + &unspendableKeyPathKey, + net, + ) + + if err != nil { + return nil, err + } + + taprootPkScript, err := txscript.PayToAddrScript(taprootAddress) + + if err != nil { + return nil, err + } + + return &RelativeTimeLockTapScriptInfo{ + SpendInfo: spendInfo, + LockTime: lockTime, + TapAddress: taprootAddress, + PkScript: taprootPkScript, + }, nil +} + // ParseBlkHeightAndPubKeyFromStoreKey expects to receive a key with // BigEndianUint64(blkHeight) || BIP340PubKey(fpBTCPK) // TODO: this function should not be in the btcstaking library diff --git a/btcstaking/witness_utils.go b/btcstaking/witness_utils.go index 8e45f2f0a..e9690cd15 100644 --- a/btcstaking/witness_utils.go +++ b/btcstaking/witness_utils.go @@ -20,7 +20,9 @@ func (si *SpendInfo) CreateTimeLockPathWitness(delegatorSig *schnorr.Signature) // CreateUnbondingPathWitness helper function to create a witness to spend // transaction through the unbonding path. // It is up to the caller to ensure that the amount of covenantSigs matches the -// expected quorum of covenenant members and the transaction has unbonding path. +// expected quorum of covenant members and the transaction has unbonding path. +// NOTE: M-of-N multisig with OP_CHECKSIGADD requires exact amount of signatures equal to the number +// of total size of the multisig party (N), even though it's nil. func (si *SpendInfo) CreateUnbondingPathWitness( covenantSigs []*schnorr.Signature, delegatorSig *schnorr.Signature, @@ -53,11 +55,58 @@ func (si *SpendInfo) CreateUnbondingPathWitness( return CreateWitness(si, witnessStack) } +// CreateMultisigUnbondingPathWitness helper function to create a witness to spend +// transaction through the unbonding path. +// It is up to the caller to ensure that the amount of covenantSigs matches the +// expected quorum of covenant members and the transaction has unbonding path. +// NOTE: M-of-N multisig with OP_CHECKSIGADD requires exact amount of signatures equal to the number +// of total size of the multisig party (N), even though it's nil. +func (si *SpendInfo) CreateMultisigUnbondingPathWitness( + covenantSigs []*schnorr.Signature, + delegatorSigs []*schnorr.Signature, +) (wire.TxWitness, error) { + if si == nil { + panic("cannot build witness without spend info") + } + + var witnessStack [][]byte + + // add covenant signatures to witness stack + // NOTE: only a quorum number of covenant signatures needs to be non-nil + if len(covenantSigs) == 0 { + return nil, fmt.Errorf("covenant signatures should not be empty") + } + for _, covSig := range covenantSigs { + if covSig == nil { + witnessStack = append(witnessStack, []byte{}) + } else { + witnessStack = append(witnessStack, covSig.Serialize()) + } + } + + // add delegator signatures to witness stack + // NOTE: only a quorum number of delegator signatures needs to be non-nil + if len(delegatorSigs) == 0 { + return nil, fmt.Errorf("delegator signatures should not be empty") + } + for _, delegatorSig := range delegatorSigs { + if delegatorSig == nil { + witnessStack = append(witnessStack, []byte{}) + } else { + witnessStack = append(witnessStack, delegatorSig.Serialize()) + } + } + + return CreateWitness(si, witnessStack) +} + // CreateSlashingPathWitness helper function to create a witness to spend // transaction through the slashing path. // It is up to the caller to ensure that the amount of covenantSigs matches the -// expected quorum of covenenant members, the finality provider sigs respect the finality providers +// expected quorum of covenant members, the finality provider sigs respect the finality providers // that the delegation belongs to, and the transaction has slashing path. +// NOTE: M-of-N multisig with OP_CHECKSIGADD requires exact amount of signatures equal to the number +// of total size of the multisig party (N), even though it's nil. func (si *SpendInfo) CreateSlashingPathWitness( covenantSigs []*schnorr.Signature, fpSigs []*schnorr.Signature, @@ -104,7 +153,67 @@ func (si *SpendInfo) CreateSlashingPathWitness( return CreateWitness(si, witnessStack) } -// createWitness creates witness for spending the tx corresponding to +// CreateMultisigSlashingPathWitness helper function to create a witness to spend +// transaction through the slashing path. +// It is up to the caller to ensure that the amount of covenantSigs matches the +// expected quorum of covenant members, the finality provider sigs respect the finality providers +// that the delegation belongs to, and the transaction has slashing path. +// NOTE: M-of-N multisig with OP_CHECKSIGADD requires exact amount of signatures equal to the number +// of total size of the multisig party (N), even though it's nil. +func (si *SpendInfo) CreateMultisigSlashingPathWitness( + covenantSigs []*schnorr.Signature, + fpSigs []*schnorr.Signature, + delegatorSigs []*schnorr.Signature, +) (wire.TxWitness, error) { + if si == nil { + panic("cannot build witness without spend info") + } + + var witnessStack [][]byte + + // add covenant signatures to witness stack + // NOTE: only a quorum number of covenant signatures needs to be non-nil + if len(covenantSigs) == 0 { + return nil, fmt.Errorf("covenant signatures should not be empty") + } + for _, covSig := range covenantSigs { + if covSig == nil { + witnessStack = append(witnessStack, []byte{}) + } else { + witnessStack = append(witnessStack, covSig.Serialize()) + } + } + + // add finality provider signatures to witness stack + // NOTE: only 1 of the finality provider signatures needs to be non-nil + if len(fpSigs) == 0 { + return nil, fmt.Errorf("finality provider signatures should not be empty") + } + for _, fpSig := range fpSigs { + if fpSig == nil { + witnessStack = append(witnessStack, []byte{}) + } else { + witnessStack = append(witnessStack, fpSig.Serialize()) + } + } + + // add delegator signatures to witness stack + // NOTE: only a quorum number of delegator signatures needs to be non-nil + if len(delegatorSigs) == 0 { + return nil, fmt.Errorf("delegator signatures should not be empty") + } + for _, delegatorSig := range delegatorSigs { + if delegatorSig == nil { + witnessStack = append(witnessStack, []byte{}) + } else { + witnessStack = append(witnessStack, delegatorSig.Serialize()) + } + } + + return CreateWitness(si, witnessStack) +} + +// CreateWitness creates witness for spending the tx corresponding to // the given spend info with the given stack of signatures // The returned witness stack follows the structure below: // - first come signatures diff --git a/cmd/babylond/cmd/flags.go b/cmd/babylond/cmd/flags.go index 6b45403ef..236f9750d 100644 --- a/cmd/babylond/cmd/flags.go +++ b/cmd/babylond/cmd/flags.go @@ -55,6 +55,8 @@ const ( flagJailDuration = "jail-duration" flagNoBlsPassword = "no-bls-password" flagBlsPasswordFile = "bls-password-file" + flagMaxStakerQuorum = "max-staker-quorum" + flagMaxStakerNum = "max-staker-num" ) type GenesisCLIArgs struct { @@ -93,6 +95,8 @@ type GenesisCLIArgs struct { FinalitySigTimeout int64 JailDuration time.Duration FinalityActivationBlockHeight uint64 + MaxStakerQuorum uint32 + MaxStakerNum uint32 } func addGenesisFlags(cmd *cobra.Command) { @@ -124,6 +128,8 @@ func addGenesisFlags(cmd *cobra.Command) { cmd.Flags().Uint32(flagMaxActiveFinalityProviders, 100, "Bitcoin staking maximum active finality providers") cmd.Flags().Uint16(flagUnbondingTime, 21, "Required timelock on unbonding transaction in btc blocks. Must be larger than btc-finalization-timeout") cmd.Flags().Int64(flagUnbondingFeeSat, 1000, "Required fee for unbonding transaction in satoshis") + cmd.Flags().Uint32(flagMaxStakerQuorum, 2, "Bitcoin staking max staker quorum") + cmd.Flags().Uint32(flagMaxStakerNum, 3, "Bitcoin staking max staker num") // inflation args cmd.Flags().Float64(flagInflationRateChange, 0.13, "Inflation rate change") cmd.Flags().Float64(flagInflationMax, 0.2, "Maximum inflation") @@ -179,6 +185,8 @@ func parseGenesisFlags(cmd *cobra.Command) (*GenesisCLIArgs, error) { finalitySigTimeout, _ := cmd.Flags().GetInt64(flagFinalitySigTimeout) jailDurationStr, _ := cmd.Flags().GetString(flagJailDuration) finalityActivationBlockHeight, _ := cmd.Flags().GetUint64(flagActivationHeight) + maxStakerQuorum, _ := cmd.Flags().GetUint32(flagMaxStakerQuorum) + maxStakerNum, _ := cmd.Flags().GetUint32(flagMaxStakerNum) if chainID == "" { chainID = "chain-" + tmrand.NewRand().Str(6) @@ -237,5 +245,7 @@ func parseGenesisFlags(cmd *cobra.Command) (*GenesisCLIArgs, error) { FinalitySigTimeout: finalitySigTimeout, JailDuration: jailDuration, FinalityActivationBlockHeight: finalityActivationBlockHeight, + MaxStakerQuorum: maxStakerQuorum, + MaxStakerNum: maxStakerNum, }, nil } diff --git a/cmd/babylond/cmd/genesis.go b/cmd/babylond/cmd/genesis.go index 6989fe5b1..b0b08aca0 100644 --- a/cmd/babylond/cmd/genesis.go +++ b/cmd/babylond/cmd/genesis.go @@ -108,6 +108,8 @@ Example: genesisCliArgs.FinalitySigTimeout, genesisCliArgs.JailDuration, genesisCliArgs.FinalityActivationBlockHeight, + genesisCliArgs.MaxStakerQuorum, + genesisCliArgs.MaxStakerNum, ) case "mainnet": panic("Mainnet params not implemented.") @@ -297,6 +299,8 @@ func TestnetGenesisParams( finalitySigTimeout int64, jailDuration time.Duration, finalityActivationBlockHeight uint64, + maxStakerQuorum uint32, + maxStakerNum uint32, ) GenesisParams { genParams := GenesisParams{} @@ -392,6 +396,8 @@ func TestnetGenesisParams( genParams.BtcstakingParams.SlashingRate = slashingRate genParams.BtcstakingParams.UnbondingTimeBlocks = uint32(unbondingTime) genParams.BtcstakingParams.UnbondingFeeSat = unbondingFeeSat + genParams.BtcstakingParams.MaxStakerQuorum = maxStakerQuorum + genParams.BtcstakingParams.MaxStakerNum = maxStakerNum if err := genParams.BtcstakingParams.Validate(); err != nil { panic(err) } diff --git a/cmd/babylond/cmd/testnet.go b/cmd/babylond/cmd/testnet.go index 4c87df215..649908339 100644 --- a/cmd/babylond/cmd/testnet.go +++ b/cmd/babylond/cmd/testnet.go @@ -128,6 +128,8 @@ Example: genesisCliArgs.FinalitySigTimeout, genesisCliArgs.JailDuration, genesisCliArgs.FinalityActivationBlockHeight, + genesisCliArgs.MaxStakerQuorum, + genesisCliArgs.MaxStakerNum, ) return InitTestnet( diff --git a/docs/architecture.md b/docs/architecture.md index 2d5f7db96..42222f8f8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -167,9 +167,8 @@ has been developed to enable these functionalities: - [BTC Staker Daemon](https://github.com/babylonlabs-io/btc-staker): Daemon program connecting to a Bitcoin wallet and Babylon. -- [BTC Staker Dashboard](https://github.com/babylonlabs-io/btc-staking-dashboard): - Web application connecting to a Bitcoin wallet extension and the Babylon API. - It should only be used for testing purposes. +- [Staking API Service](https://github.com/babylonlabs-io/staking-api-service): + Backend of Babylon staking DApp connecting with Babylon gRPC and providing API service. - Wallet Integrations (TBD) ### [Finality Provider](https://github.com/babylonlabs-io/finality-provider) @@ -210,7 +209,7 @@ this function. Most notably: - [Cosmos Relayer](https://github.com/cosmos/relayer): A fully functional relayer written in Go. -- [Babylon Relayer](https://github.com/babylonlabs-io/babylon-relayer/): +- [Babylon Relayer](https://github.com/babylonlabs-io/babylon/blob/main/docs/ibc-relayer.md): A wrapper of the Cosmos Relayer that can maintain a one-way IBC connection. It is recommended to be used when the Consumer Zone does not deploy the Babylon smart contract. diff --git a/docs/bitcoin-multisig-stake.md b/docs/bitcoin-multisig-stake.md new file mode 100644 index 000000000..74905f366 --- /dev/null +++ b/docs/bitcoin-multisig-stake.md @@ -0,0 +1,374 @@ +# Bitcoin Multisig Stake + +## Table of Contents + +1. [Introduction](#1-introduction) + 1. [Why Multisig](#11-why-multisig) + 2. [Terminology](#12-terminology) +2. [Architecture Overview](#2-architecture-overview) + 1. [Taproot Script Layout](#21-taproot-script-layout) + 2. [On-chain Metadata](#22-on-chain-metadata) +3. [Parameters and Limits](#3-parameters-and-limits) +4. [Preparing a Multisig Delegation](#4-preparing-a-multisig-delegation) + 1. [Collect Delegator Material](#41-collect-delegator-material) + 2. [Construct `AdditionalStakerInfo`](#42-construct-additionalstakerinfo) + 3. [Sample JSON Payload](#43-sample-json-payload) +5. [Registering a Multisig BTC Delegation](#5-registering-a-multisig-btc-delegation) + 1. [End-to-End Flow](#51-end-to-end-flow) + 2. [Babylon Messages](#52-babylon-messages) +6. [Managing Multisig Delegations](#6-managing-multisig-delegations) + 1. [Stake Extension](#61-stake-extension) + 2. [On-demand Unbonding and Slashing](#62-on-demand-unbonding-and-slashing) +7. [Observability and Troubleshooting](#7-observability-and-troubleshooting) + 1. [Queries and Events](#71-queries-and-events) + 2. [Common Pitfalls](#72-common-pitfalls) + +--- + +This document explains how multisignature (multisig) Bitcoin delegations are +constructed, registered, and operated on Babylon. It complements the +[Bitcoin stake registration](./register-bitcoin-stake.md) and +[stake extension](./bitcoin-stake-extension.md) guides by focusing on the data +and validation rules that are unique to M-of-N BTC stakers. +**Single-signature delegators can safely ignore this document if `multisig_info` +remains unset in your messages, the keeper follows the regular single-sig flow.** + +## 1. Introduction + +### 1.1. Why Multisig + +Many institutional or custodial stakers require shared control over their +staking keys. Babylon supports Taproot-based M-of-N multisig delegations so +that organizations can: + +- split signing authority across multiple operators +- enforce recovery policies without sacrificing liveness guarantees +- keep the same security story as self-custodial single-signature stakers + +Multisig delegations reuse the same staking, unbonding, and slashing flows as +single-sig stakers. The difference lies in how the Bitcoin scripts and Babylon +messages commit to multiple staker keys and signatures. + +### 1.2. Terminology + +- **Primary staker**: the `btc_pk` field in Babylon messages. This key produces + the proof-of-possession (PoP) and anchors delegation ownership on-chain. +- **Additional stakers**: the remaining `N-1` keys that participate in the + multisig quorum. These keys are carried in `AdditionalStakerInfo`. +- **Staker quorum (`M`)**: the number of staker signatures required to satisfy + the Bitcoin script (e.g., 2 for a 2-of-3 multisig). +- **Staker count (`N`)**: the total number of staker keys participating in the + multisig (primary key + `staker_btc_pk_list`). +- **AdditionalStakerInfo**: the protobuf object that carries additional keys + and their slashing signatures. See + [`proto/babylon/btcstaking/v1/btcstaking.proto`](../proto/babylon/btcstaking/v1/btcstaking.proto). + +## 2. Architecture Overview + +### 2.1. Taproot Script Layout + +Multisig delegations use the same Taproot tree as single-signature stakers: + +``` + Taproot output + | + ┌───────────┴────────────┐ + │ │ + Time-lock path Cooperative paths + | + ┌────────────┴────────────┐ + │ │ + Unbonding path Slashing path +``` + +The difference is the content of each script leaf: + +- **Time-lock path**: replaces the single `OP_CHECKSIG` with an + `OP_CHECKSIGADD` sequence that enforces the `M-of-N` relative timelock spend. +- **Unbonding path**: requires `M` delegator signatures plus the covenant quorum. +- **Slashing path**: requires `M` delegator signatures, one finality provider + signature, and the covenant quorum. + +For a concrete `2-of-3` staker multisig, `1-of-1` finality provider, and +`3-of-5` covenant configuration the Taproot leaves look like: + +```text +// Time-lock path (staker-only, relative timelock enforced) + OP_CHECKSIG + OP_CHECKSIGADD + OP_CHECKSIGADD +2 OP_NUMEQUALVERIFY + OP_CHECKSEQUENCEVERIFY + +// Unbonding path (stakers + covenant, cooperative exit) + OP_CHECKSIG + OP_CHECKSIGADD + OP_CHECKSIGADD +2 OP_NUMEQUALVERIFY + OP_CHECKSIG + OP_CHECKSIGADD + OP_CHECKSIGADD + OP_CHECKSIGADD + OP_CHECKSIGADD +3 OP_NUMEQUAL + +// Slashing path (stakers + finality provider + covenant) + OP_CHECKSIG + OP_CHECKSIGADD + OP_CHECKSIGADD +2 OP_NUMEQUALVERIFY + OP_CHECKSIGVERIFY + OP_CHECKSIG + OP_CHECKSIGADD + OP_CHECKSIGADD + OP_CHECKSIGADD + OP_CHECKSIGADD +3 OP_NUMEQUAL +``` + +The Bitcoin scripts are generated through +`btcstaking.BuildMultisigStakingInfo/BuildMultisigUnbondingInfo` which sort the +keys lexicographically and build a Taproot tree with three leaves. Because the +scripts use `OP_CHECKSIGADD`, the witness must contain one stack element per +staker key (empty entries stand for “no signature”). This detail is important +when constructing witnesses for spending staking or unbonding outputs. + +### 2.2. On-chain Metadata + +The Babylon chain stores the multisig configuration inside +`BTCDelegation.multisig_info`. The data includes: + +- `staker_btc_pk_list`: the `N-1` additional keys (primary key excluded) +- `staker_quorum`: the quorum `M` +- `delegator_slashing_sigs`: signatures for the staking slashing transaction +- `delegator_unbonding_slashing_sigs`: signatures for the unbonding slashing + transaction + +This metadata allows the chain to verify: + +- the Bitcoin scripts indeed commit to the declared key set +- Babylon holds at least `M` valid signatures for both slashing transactions +- no duplicate keys exist across staker, covenant, and finality provider sets + +Once stored, the metadata is exposed through the +`babylon.btcstaking.v1.QueryBTCDelegationRequest` response so wallet software and +monitoring systems can fetch the current quorum, keys, and signatures. + +## 3. Parameters and Limits + +Multisig delegations are bounded by two module parameters defined in +[`proto/babylon/btcstaking/v1/params.proto`](../proto/babylon/btcstaking/v1/params.proto): + +- `max_staker_num`: maximum allowed `N` +- `max_staker_quorum`: maximum allowed `M` + +Additional constraints enforced by the keeper (`x/btcstaking/types/validate_parsed_message.go`): + +- `staker_quorum ≥ 1` and `N ≥ 2` – multisig mode requires at least two keys +- `staker_quorum ≤ N` +- the combination `M-of-N` must be within the configured parameter bounds +- the additional staker list **must not** contain the primary staker key and + **must not** contain duplicates +- Babylon must receive ≥ `M` signatures for both staking and unbonding slashing + transactions before accepting the delegation + +These limits are governance-controlled and can be inspected via +`babylon.btcstaking.v1.QueryParamsRequest`. + +## 4. Preparing a Multisig Delegation + +### 4.1. Collect Delegator Material + +Besides the standard data described in +[Bitcoin Stake Registration](./register-bitcoin-stake.md#3-bitcoin-stake-registration), +a multisig delegation requires the following preparatory steps: + +> **NOTE**: `multisig_info` is optional. If you submit `MsgCreateBTCDelegation` +> or `MsgBtcStakeExpand` without this field (i.e., it is `nil`), the keeper treats +> the delegation as a single-signature BTC stake and runs the standard validation +> path. Populate `multisig_info` only when you actually need an M-of-N scheme. + +1. **Generate keys**: produce the primary staker key (owner) plus the `N-1` + additional staker keys. Each key must be a BIP-340 Schnorr key. +2. **Proof of possession**: only the primary key provides the PoP (`pop` + field). Additional keys do not require a PoP. +3. **Sign slashing transactions**: every staker key must sign both the staking + slashing transaction and the unbonding slashing transaction. These + signatures are embedded in `AdditionalStakerInfo`. +4. **Agree on quorum**: decide the `M-of-N` threshold that will protect the + funds on Bitcoin. The Babylon keeper verifies that this quorum matches the + provided signatures and parameter limits. + +### 4.2. Construct `AdditionalStakerInfo` + +`AdditionalStakerInfo` mirrors the protobuf definition: + +- `staker_btc_pk_list`: array containing every additional staker public key. + The primary key **must not** be included here because it is already provided + in `btc_pk`. +- `staker_quorum`: the requested `M`. +- `delegator_slashing_sigs`: list of `SignatureInfo { pk, sig }` records for + the staking slashing transaction. Provide one element per additional staker + key that signed. Babylon ensures at least `M` signatures exist overall + (primary signature is collected separately). +- `delegator_unbonding_slashing_sigs`: same structure but for the unbonding + slashing transaction. + +All signatures must be encoded as BIP-340 Schnorr signatures. The `pk` fields +must match the associated signatures; otherwise `ErrInvalidMultisigInfo` is +raised when the message is parsed. + +### 4.3. Sample JSON Payload + +Below is an illustrative JSON fragment that can be embedded in a `MsgCreateBTCDelegation` +or `MsgBtcStakeExpand` payload. This payload should be used with `--multisig-info-json` +flag when sending multisig btc delegation or stake extension in CLI. + +`babylond tx btcstaking create-btc-delegation [args] --multisig-info-json [path/to/multisig.json]` + +multisig.json: + +```json +{ + "staker_btc_pk_list": [ + "228ab7c4...f58", // staker #2 + "2fa1b042...9cd" // staker #3 + ], + "staker_quorum": 2, + "delegator_slashing_sigs": [ + { + "pk": "228ab7c4...f58", + "sig": "a1b3...9ce" + }, + { + "pk": "2fa1b042...9cd", + "sig": "bb44...1db" + } + ], + "delegator_unbonding_slashing_sigs": [ + { + "pk": "228ab7c4...f58", + "sig": "c01d...55a" + }, + { + "pk": "2fa1b042...9cd", + "sig": "de91...730" + } + ] +} +``` + +> **Note**: the primary staker key is implicit. Babylon will insert its +> signatures into the canonical key–signature maps before verifying the +> Bitcoin transactions. + +## 5. Registering a Multisig BTC Delegation + +### 5.1. End-to-End Flow + +The high-level flow follows the same steps as single-sig delegations +(see [Section 2](./register-bitcoin-stake.md#2-bitcoin-stake-registration-methods)): + +1. Build staking/unbonding Bitcoin transactions that include multisig scripts. +2. Gather slashing signatures from all staker keys. +3. Submit a `MsgCreateBTCDelegation` containing the Bitcoin transactions, + PoP, finality provider list, and the `multisig_info` payload. +4. Wait for the covenant quorum to attest (`PENDING → VERIFIED`). +5. Broadcast the staking transaction on Bitcoin (pre-staking flow) or provide + an inclusion proof (post-staking flow). +6. Submit/relay `MsgAddBTCDelegationInclusionProof` once the transaction is + `k`-deep so the delegation becomes `ACTIVE`. + +The key differences are the additional multisig metadata, signature checks, and +Witness requirements enforced by the Babylon keeper. + +### 5.2. Babylon Messages + +- **`MsgCreateBTCDelegation`**: accepts an optional `multisig_info` field. + When provided, the keeper validates quorum bounds, duplicates, and verifies + that the supplied signatures match the slashing transactions. The rest of + the message matches the single-sig flow. +- **`MsgBtcStakeExpand`**: also exposes a `multisig_info` field so that existing + multisig delegations can extend their staking amount or timelock. The new + staking transaction must spend the previous staking output (input index 0) + and include the same finality provider set. Babylon rebuilds the multisig + scripts using the provided keys to verify the covenant signatures before + they are refunded. +- **`MsgAddCovenantSigs`**: the fields stay the same, but each adaptor + signature now corresponds to the multisig Taproot leaves. Covenant members + must ensure the adaptor signature list they submit spans the *same* number of + staker entries Babylon stored (the keeper expands the witness builder with + empty slots for missing cosigners). In other words, no extra payload is + required, yet the verifier checks the adaptor signatures against the + multisig spending path rather than the single-sig one. +- **`MsgBTCUndelegate`**: the message itself is unchanged, but the included + `stake_spending_tx` witness needs one Schnorr signature placeholder per + multisig staker. Wallets should serialize the witness exactly as + `btcstaking.VerifySpendStakeTxStakerSig` expects covenant signatures (and + finality provider signatures for slashing spends) come first, followed by the + `N` staker signatures (empty byte slices for non-signers), and finally the + script plus control block. The keeper reconstructs the expected key order from + `multisig_info` and rejects the undelegation if any signature is missing or + out of order. + +## 6. Managing Multisig Delegations + +### 6.1. Stake Extension + +Stake extension for multisig delegations follows the same rules described in +[Bitcoin Stake Extension](./bitcoin-stake-extension.md): + +- The previous staking transaction must be `ACTIVE`. +- The new transaction must have exactly two inputs (old staking output and the + funding UTXO) and at least one output recreating the staking Taproot script. +- The staking amount must be ≥ the previous amount. +- The finality provider list cannot change. +- The **staker key set and quorum cannot be changed.** +- Covenant overlap requirements apply exactly as in the single-sig flow. + +### 6.2. On-demand Unbonding and Slashing + +Multisig stakers can unbond early by signing the registered unbonding +transaction (or by broadcasting a stake-spending transaction). The keeper +(`MsgBTCUndelegate`) enforces: + +- the spending transaction actually consumes the staking output +- the witness includes signatures from `M` staker keys, reconstructed from + `btc_pk` + `multisig_info` +- the funding transactions referenced in the witness are valid + +During slashing events, off-chain components such as Vigilante read the stored +`delegator_slashing_sigs` / `delegator_unbonding_slashing_sigs` via the Babylon +API and reorder them with `GetOrderedDelegatorSignatures`, which sorts slots in +reverse lexicographical order of the staker public keys. The resulting vector +(with `nil` entries for missing signatures) is then combined with covenant and +finality provider signatures to build the witness. Because OP_CHECKSIGADD +requires **exactly** one witness element per key, external wallets must follow +the same ordering when crafting on-chain transactions (fill unused slots with +empty byte slices). + +## 7. Observability and Troubleshooting + +### 7.1. Queries and Events + +- `babylond q btcstaking btc-delegation --output json` + returns `multisig_info` together with the delegation status, inclusion info, + and stake expansion data. +- `babylond q btcstaking params` shows `max_staker_num` and + `max_staker_quorum`, which help diagnose rejection errors. +- `EventBTCDelegationCreated` now emits a `multisig_staker_btc_pk_hexs` + attribute listing the additional staker keys. This is useful for explorers + and audit tooling. The event fires both for initial registrations and stake + extensions. + +### 7.2. Common Pitfalls + +- **Primary key duplication**: ensure the `btc_pk` provided in the message does + not appear inside `staker_btc_pk_list`. Babylon rejects duplicated keys with + `ErrDuplicatedStakerKey`. +- **Insufficient slashing signatures**: at least `M` signatures are required + for both slashing transactions. If a cosigner refuses to sign, the + delegation cannot be registered. +- **Witness ordering**: when spending multisig staking or unbonding outputs on + Bitcoin, the witness must contain exactly `N` entries (empty entries for + non-signers). This matches the order returned by the Taproot script builder. diff --git a/docs/bitcoin-stake-extension.md b/docs/bitcoin-stake-extension.md index 91aa16de9..5849ca7b1 100644 --- a/docs/bitcoin-stake-extension.md +++ b/docs/bitcoin-stake-extension.md @@ -63,7 +63,7 @@ staking again. that is now getting extended through the Stake Extension protocol. - **Extended Staking Transaction**: The new staking transaction extending the original staking transaction by using its staking output as an input - and following the rules of the Stake Extension protocol. + and following the rules of the Stake Extension protocol. > **Note**: An extended staking transaction can serve as the original > staking transaction for a subsequent stake extension operation. @@ -172,7 +172,7 @@ staking transaction and its state on Babylon Genesis. > the original staking output alone wouldn’t suffice, > as Bitcoin transaction fees must be deducted, > and those fees can't be covered by the staking output itself - > as the new staking output should at least have the same BTC as it. + > as the new staking output should at least have the same BTC as it. - **BTC Staking Params**: The staking extension should use the current Babylon Genesis staking parameters. Documentation on how to select the appropriate parameters can be found in the [staking registration document](./register-bitcoin-stake.md#32-babylon-genesis-chain-btc-staking-parameters). @@ -186,6 +186,8 @@ staking transaction and its state on Babylon Genesis. > the stake extension staking transaction to use different staking parameters. - **Submitter Address**: The Babylon Genesis address used to submit the stake extension transaction to Babylon Genesis should be the same as the owner of the original staking transaction. +- **Staker Bitcoin Public Keys**: The Bitcoin public keys used in the extended staking transaction staking output should + be the same as the original staking output. > **⚠️ Critical**: All stake extension transactions that do not follow > the aforementioned rules will be rejected. @@ -259,7 +261,7 @@ These transactions include: slashing during the unbonding process in case of double-signing > **Important**: -> The extension transaction must follow a strict two-input structure validated +> The extension transaction must follow a strict two-input structure validated > by the Babylon Genesis chain: > - **Input 0**: Original staking transaction output > * Must reference the exact UTXO from the original staking transaction @@ -280,15 +282,15 @@ These transactions include: **Transaction Construction:** You can create these transactions using: - [The Golang BTC staking library](../btcstaking) with extension utilities -- [The TypeScript BTC staking library](https://github.com/babylonlabs-io/btc-staking-ts) +- [The TypeScript BTC staking library](https://github.com/babylonlabs-io/btc-staking-ts) - Your own implementation following the [Bitcoin staking script specification](./staking-script.md) -> **⚡ Note**: All transactions must use the current Babylon staking parameters +> **⚡ Note**: All transactions must use the current Babylon staking parameters > retrieved from the Bitcoin light client tip height at extension submission time. -> **⚠️ Critical Warning**: The extension transaction must spend exactly 2 inputs -> in the specified order. The btcstaking module's validation will reject -> non-conforming transactions. Additionally, all transaction fee calculations +> **⚠️ Critical Warning**: The extension transaction must spend exactly 2 inputs +> in the specified order. The btcstaking module's validation will reject +> non-conforming transactions. Additionally, all transaction fee calculations > must account for the minimum fees specified in the staking parameters. ### 3.4. The `MsgBtcStakeExpand` Babylon Message @@ -300,16 +302,16 @@ is used to submit the Stake Extension request. // MsgBtcStakeExpand is the message for extending existing BTC stakes message MsgBtcStakeExpand { option (cosmos.msg.v1.signer) = "staker_addr"; - + // Standard delegation fields (same as MsgCreateBTCDelegation) string staker_addr = 1; ProofOfPossessionBTC pop = 2; bytes btc_pk = 3; repeated bytes fp_btc_pk_list = 4; // Must be identical to previous FPs - uint32 staking_time = 5; // New/extended timelock period + uint32 staking_time = 5; // New/extended timelock period int64 staking_value = 6; // Total new amount (≥ previous) bytes staking_tx = 7; // Extension transaction - + // Slashing transactions for extended stake bytes slashing_tx = 8; bytes delegator_slashing_sig = 9; @@ -318,11 +320,11 @@ message MsgBtcStakeExpand { int64 unbonding_value = 12; bytes unbonding_slashing_tx = 13; bytes delegator_unbonding_slashing_sig = 14; - + // Extension-specific fields string previous_staking_tx_hash = 15; // Hash of original delegation bytes funding_tx = 16; // Transaction with funding UTXO - + // Note: staking_tx_inclusion_proof omitted (pre-staking flow) } ``` @@ -332,10 +334,10 @@ message MsgBtcStakeExpand { * `staker_addr`: A Bech32-encoded Babylon address (`bbn...`) representing the staker's Babylon account where staking rewards will be accumulated. - *This must be the same address that signed the original delegation and + *This must be the same address that signed the original delegation and must also sign the extension registration transaction*. The Babylon signer address must be the same from the old stake (staker_addr). - + Example: `"bbn1abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"` * `pop` (Proof of Possession): @@ -353,7 +355,7 @@ message MsgBtcStakeExpand { * **BIP-340**: The hash of the staker address bytes should be signed. * **BIP-322**: Bytes of the bech32 encoded address should be signed. * **ECDSA**: Bytes of the bech32 encoded address should be signed. - + Example (BIP-340): ```json { @@ -367,10 +369,10 @@ message MsgBtcStakeExpand { in BIP-340 format (Schnorr signatures). It is a compact, 32-byte value derived from the staker's private key. This public key must be exactly the same as the one used in the original delegation being extended, - as it corresponds to the staker public key used to construct the + as it corresponds to the staker public key used to construct the [staking script](./staking-script.md) used in both the original and extension BTC Staking transactions. - + Example: `"7b3a9c8e5f1d2a4c6b8d9e0f1a2c3e4f5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e"` * `fp_btc_pk_list`: @@ -381,8 +383,8 @@ message MsgBtcStakeExpand { finality providers from the original delegation** - you cannot add or remove finality providers. - - Example: + + Example: ```json [ "7b3a9c8e5f1d2a4c6b8d9e0f1a2c3e4f5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e", @@ -390,7 +392,7 @@ message MsgBtcStakeExpand { "a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8" ] ``` - + > **⚠️ Critical Requirement**: The extension finality provider list must > be identical to the finality providers from the original delegation. > The Babylon Genesis chain will reject extensions that attempt to add or remove @@ -398,11 +400,11 @@ message MsgBtcStakeExpand { > finality provider list before constructing the extension. * `staking_time`: - The duration of staking in Bitcoin blocks for the extended delegation. - This can be the same as or longer than the original delegation's staking time. - This is the same as the timelock used when constructing the + The duration of staking in Bitcoin blocks for the extended delegation. + This is the same as the timelock used when constructing the [staking script](./staking-script.md) and must comply with the current - Babylon staking parameters. + Babylon staking parameters by being higher than or equal to + `min_staking_time_blocks` and lower than or equal to `max_staking_time_blocks` > **Important Note**: Every stake extension transaction starts > with a fresh timelock, meaning that the time the original > staking transaction spent being active will not be deducted from @@ -476,24 +478,24 @@ message MsgBtcStakeExpand { > funding transaction. Ensure all extension fields are consistent with the > original delegation and current staking parameters. -> **⚡ Note**: The extension message follows the pre-staking registration pattern. -> The inclusion proof is submitted later after Bitcoin confirmation via +> **⚡ Note**: The extension message follows the pre-staking registration pattern. +> The inclusion proof is submitted later after Bitcoin confirmation via > `MsgBTCUndelegate`. ### 3.5. Constructing the `MsgBtcStakeExpand` There are multiple ways to construct and broadcast the `MsgBtcStakeExpand` message to the Babylon network: -* **Command Line Interface (CLI)**: +* **Command Line Interface (CLI)**: Use the `babylond tx btcstaking btc-stake-extend` command. -* **TypeScript Implementation**: +* **TypeScript Implementation**: Generate the message using TypeScript following the [TypeScript staking library](https://github.com/babylonlabs-io/btc-staking-ts). -* **Golang Implementation**: +* **Golang Implementation**: Construct the message using Golang based on this [type reference](../x/btcstaking/types/tx.pb.go) and broadcast to the Babylon network. -* **External References**: +* **External References**: For detailed instructions on broadcasting transactions, refer to the external [Cosmos SDK documentation](https://docs.cosmos.network/main/learn/advanced/transactions#broadcasting-the-transaction). @@ -541,10 +543,10 @@ path to create the fully signed stake extension transaction: timelock path - ❌ Missing covenant signatures in witness - transaction will be invalid -> **⚡ Note**: Once you submit `MsgBtcStakeExpand` to Babylon, it creates a -> pending delegation. Covenants then read the pending delegation and provide -> the missing signatures needed to spend the original stake. After receiving -> covenant signatures, the BTC staker adds their signature and broadcasts +> **⚡ Note**: Once you submit `MsgBtcStakeExpand` to Babylon, it creates a +> pending delegation. Covenants then read the pending delegation and provide +> the missing signatures needed to spend the original stake. After receiving +> covenant signatures, the BTC staker adds their signature and broadcasts > the extension transaction to Bitcoin. ## 4. Managing your Bitcoin Stake Extension @@ -560,7 +562,7 @@ remains `ACTIVE`. # Query the new extended delegation babylond query btcstaking btc-delegation [extension-delegation-hash] -# Query the original delegation +# Query the original delegation babylond query btcstaking btc-delegation [previous-staking-tx-hash] ``` diff --git a/docs/ibc-relayer.md b/docs/ibc-relayer.md index 9d5288dc4..eb10101f9 100644 --- a/docs/ibc-relayer.md +++ b/docs/ibc-relayer.md @@ -71,7 +71,7 @@ babylond query btccheckpoint params ``` For RPC and LCD endpoints for different networks, refer to -the [Babylon Networks Repository](https://github.com/babylonlabs-io/networks/tree/main/bbn-test-5). +the [Babylon Networks Repository](https://github.com/babylonlabs-io/networks/tree/main/bbn-test-6). ## Relayer Configuration @@ -131,4 +131,4 @@ For detailed steps on how to submit an IBC client recovery proposal, refer to the [IBC Governance Proposals Guide](https://ibc.cosmos.network/main/ibc/proposals.html#steps). For more information about submitting governance proposals on Babylon, including parameters and requirements, see -the [Babylon Governance Guide](https://docs.babylonlabs.io/guides/governance/). +the [Babylon Governance Guide](https://docs.babylonlabs.io/guides/governance/). diff --git a/docs/transaction-impl-spec.md b/docs/transaction-impl-spec.md index 2a2e88580..d22525bfa 100644 --- a/docs/transaction-impl-spec.md +++ b/docs/transaction-impl-spec.md @@ -26,7 +26,7 @@ account before propagating a transaction to Bitcoin. For the rest of the document, we will refer to those parameters as `global_parameters`. More details about parameters can be found in the -[latest testnet docs](https://github.com/babylonlabs-io/networks/tree/main/bbn-test-5/). +[latest testnet docs](https://github.com/babylonlabs-io/networks/tree/main/bbn-test-6/). ## Taproot outputs diff --git a/go.mod b/go.mod index b92b40b92..0df1309c4 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ go 1.23.8 module github.com/babylonlabs-io/babylon/v4 require ( - github.com/CosmWasm/wasmd v0.55.1 + github.com/CosmWasm/wasmd v0.60.2 github.com/btcsuite/btcd v0.24.2 - github.com/cometbft/cometbft v0.38.19 + github.com/cometbft/cometbft v0.38.20 github.com/cometbft/cometbft-db v0.15.0 github.com/cosmos/cosmos-sdk v0.53.4 github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 v10.3.0 @@ -296,8 +296,12 @@ replace ( // use cosmos fork of keyring github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 + // Fix protoc-gen-validate moved to envoyproxy + github.com/lyft/protoc-gen-validate => github.com/envoyproxy/protoc-gen-validate v1.1.0 github.com/strangelove-ventures/tokenfactory => github.com/babylonlabs-io/tokenfactory v0.50.6-wasmvm2 // Downgraded to stable version see: https://github.com/cosmos/cosmos-sdk/pull/14952 github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + // Fix websocket version conflict + nhooyr.io/websocket => nhooyr.io/websocket v1.8.17 ) diff --git a/go.sum b/go.sum index 4c8c33ef1..626467310 100644 --- a/go.sum +++ b/go.sum @@ -661,8 +661,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CosmWasm/wasmd v0.55.1 h1:Rv6UaN+b9fRfOl7C1G2OPSegJ3G6s6/F+hOIqjxq/nY= -github.com/CosmWasm/wasmd v0.55.1/go.mod h1:EfUH0kyExr+dlcQQSWcW5phrpw/JMGBitvAwG9J5FZk= +github.com/CosmWasm/wasmd v0.60.2 h1:CnkH81lV8RYcGgH+VfKLpYsAw8voAfHxvN0lB1X/Z3U= +github.com/CosmWasm/wasmd v0.60.2/go.mod h1:FsbJzVnt7a4OJv/gYSwjU06e+rKlLRqA+L2QsfZwU2Q= github.com/CosmWasm/wasmvm/v2 v2.2.4 h1:V3UwXJMA8TNOuQETppDQkaXAevF7gOWLYpKvrThPv7o= github.com/CosmWasm/wasmvm/v2 v2.2.4/go.mod h1:Aj/rB2KMRM8nAdbWxkO23rnQYb5KsoPuH9ZizSi0sVg= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -840,8 +840,8 @@ github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/cometbft/cometbft v0.38.19 h1:vNdtCkvhuwUlrcLPAyigV7lQpmmo+tAq8CsB8gZjEYw= -github.com/cometbft/cometbft v0.38.19/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= +github.com/cometbft/cometbft v0.38.20 h1:i9v9rvh3Z4CZvGSWrByAOpiqNq5WLkat3r/tE/B49RU= +github.com/cometbft/cometbft v0.38.20/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= github.com/cometbft/cometbft-db v0.15.0 h1:VLtsRt8udD4jHCyjvrsTBpgz83qne5hnL245AcPJVRk= github.com/cometbft/cometbft-db v0.15.0/go.mod h1:EBrFs1GDRiTqrWXYi4v90Awf/gcdD5ExzdPbg4X8+mk= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= @@ -966,6 +966,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/ethereum/go-ethereum v1.15.11 h1:JK73WKeu0WC0O1eyX+mdQAVHUV+UR1a9VB/domDngBU= @@ -994,8 +995,6 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= @@ -1030,19 +1029,12 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -1197,7 +1189,6 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -1324,7 +1315,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= @@ -1349,7 +1339,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= @@ -1359,7 +1348,7 @@ github.com/linxGnu/grocksdb v1.9.8/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LF github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= @@ -1605,6 +1594,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= @@ -1662,8 +1652,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -1779,9 +1767,11 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= @@ -1899,6 +1889,7 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1922,6 +1913,7 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= @@ -2105,6 +2097,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= @@ -2127,6 +2120,7 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= @@ -2508,6 +2502,7 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -2585,7 +2580,6 @@ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/proto/babylon/btcstaking/v1/btcstaking.proto b/proto/babylon/btcstaking/v1/btcstaking.proto index 3a6e216e1..1240b8a4b 100644 --- a/proto/babylon/btcstaking/v1/btcstaking.proto +++ b/proto/babylon/btcstaking/v1/btcstaking.proto @@ -163,6 +163,9 @@ message BTCDelegation { // stk_exp is contains the relevant information about the previous staking that // originated this stake. If nil it is NOT a stake expansion. StakeExpansion stk_exp = 18; + // multisig_info is used when the given btc delegation is M-of-N multisig. + // If nil, it is not a M-of-N multisig. + AdditionalStakerInfo multisig_info = 19; } // StakeExpansion stores information necessary to construct the expanded BTC staking @@ -329,3 +332,25 @@ message LargestBtcReOrg { // RollbackTo is the BTC block header which we rollback to babylon.btclightclient.v1.BTCHeaderInfo rollback_to = 3; } + +// AdditionalStakerInfo is used when enabling multisig for btc staker +// NOTE: this structure doesn't contain original btc staker's signature, i.e., length of +// delegator_slashing_sigs and delegator_unbonding_slashing_sigs is M-1, and the length of +// staker_btc_pk_list is N-1 +message AdditionalStakerInfo { + // staker_btc_pk_list is the list of pubkeys of the btc staker that is using M-of-N multisig + // length of staker_btc_pk_list is N-1 + repeated bytes staker_btc_pk_list = 1 + [ (gogoproto.customtype) = + "github.com/babylonlabs-io/babylon/v4/types.BIP340PubKey"]; + // staker_quorum is threshold of M-of-N multisig, which value itself represent M + uint32 staker_quorum = 2; + // delegator_slashing_sigs is the (btc_pk, signature) pair on the slashing tx + // by delegators (i.e., SK corresponding to btc_pk). + // It will be a part of the witness for the staking tx output. + repeated SignatureInfo delegator_slashing_sigs = 3; + // delegator_unbonding_slashing_sigs is the (btc_pk, signature) pair on the slashing tx + // by delegators (i.e., SK corresponding to btc_pk). + // It will be a part of the witness for the unbonding tx output. + repeated SignatureInfo delegator_unbonding_slashing_sigs = 4; +} \ No newline at end of file diff --git a/proto/babylon/btcstaking/v1/events.proto b/proto/babylon/btcstaking/v1/events.proto index e63f8aca1..46287e4ba 100644 --- a/proto/babylon/btcstaking/v1/events.proto +++ b/proto/babylon/btcstaking/v1/events.proto @@ -168,6 +168,10 @@ message EventBTCDelegationCreated { // previous_staking_tx_hash_hex is the hex encoded of the hash of the staking tx // that was used as input to the stake expansion, if empty it is NOT a stake expansion. string previous_staking_tx_hash_hex = 11; + // multisig_staker_btc_pk_hexs is the hex str of Bitcoin secp256k1 PK of the multisig staker that + // create this BTC delegation. the PK follows encoding in BIP-340 spec. + // if empty, it is NOT a M-of-N multisig btc delegation. + repeated string multisig_staker_btc_pk_hexs = 12; } // EventCovenantSignatureReceived is the event emitted when a covenant committee diff --git a/proto/babylon/btcstaking/v1/params.proto b/proto/babylon/btcstaking/v1/params.proto index daee7df25..ad86e98b1 100644 --- a/proto/babylon/btcstaking/v1/params.proto +++ b/proto/babylon/btcstaking/v1/params.proto @@ -68,6 +68,10 @@ message Params { // btc_activation_height is the btc height from which parameters are activated // (inclusive) uint32 btc_activation_height = 15; + // max_staker_quorum is the max M from M-of-N multisig + uint32 max_staker_quorum = 16; + // max_staker_num is the max N from M-of-N multisig + uint32 max_staker_num = 17; } // HeightVersionPair pairs a btc height with a version of the parameters diff --git a/proto/babylon/btcstaking/v1/query.proto b/proto/babylon/btcstaking/v1/query.proto index 32242dda2..7d88e7fb4 100644 --- a/proto/babylon/btcstaking/v1/query.proto +++ b/proto/babylon/btcstaking/v1/query.proto @@ -236,6 +236,31 @@ message BTCDelegationResponse { uint32 params_version = 17; // stk_exp contains the stake expansion information, if nil it is NOT a stake expansion. StakeExpansionResponse stk_exp = 18; + // multisig_info is used when the given btc delegation is M-of-N multisig. + // If nil, it is not a M-of-N multisig. + AdditionalStakerInfoResponse multisig_info = 19; +} + +// AdditionalStakerInfoResponse provides multisig info for the given btc staker +// NOTE: this structure doesn't contain original btc staker's signature, i.e., length of +// delegator_slashing_sigs and delegator_unbonding_slashing_sigs is M-1, and the length of +// staker_btc_pk_list is N-1 +message AdditionalStakerInfoResponse { + // staker_btc_pk_list is the list of pubkeys of the btc staker that is using M-of-N multisig + // length of staker_btc_pk_list is N-1 + repeated bytes staker_btc_pk_list = 1 + [ (gogoproto.customtype) = + "github.com/babylonlabs-io/babylon/v4/types.BIP340PubKey"]; + // staker_quorum is threshold of M-of-N multisig, which value itself represent M + uint32 staker_quorum = 2; + // delegator_slashing_sigs is the (btc_pk, signature) pair on the slashing tx + // by delegators (i.e., SK corresponding to btc_pk). + // It will be a part of the witness for the staking tx output. + repeated SignatureInfo delegator_slashing_sigs = 3; + // delegator_unbonding_slashing_sigs is the (btc_pk, signature) pair on the slashing tx + // by delegators (i.e., SK corresponding to btc_pk). + // It will be a part of the witness for the unbonding tx output. + repeated SignatureInfo delegator_unbonding_slashing_sigs = 4; } // StakeExpansionResponse stores information necessary to construct the expanded BTC staking diff --git a/proto/babylon/btcstaking/v1/tx.proto b/proto/babylon/btcstaking/v1/tx.proto index 0971722e0..b8f5e1337 100644 --- a/proto/babylon/btcstaking/v1/tx.proto +++ b/proto/babylon/btcstaking/v1/tx.proto @@ -185,6 +185,9 @@ message MsgCreateBTCDelegation { bytes delegator_unbonding_slashing_sig = 15 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/v4/types.BIP340Signature" ]; + // multisig_info is used when the given btc delegation is M-of-N multisig. + // If nil, it is not a M-of-N multisig. + AdditionalStakerInfo multisig_info = 16; } // MsgCreateBTCDelegationResponse is the response for MsgCreateBTCDelegation message MsgCreateBTCDelegationResponse {} @@ -253,6 +256,9 @@ message MsgBtcStakeExpand { // to at least pay the fees for it. It can also be used to increase the total amount // of satoshi staked. This will be parsed into a *wire.MsgTx bytes funding_tx = 16; + // multisig_info is used when the given btc delegation is M-of-N multisig. + // If nil, it is not a M-of-N multisig. + AdditionalStakerInfo multisig_info = 17; } // MsgBtcStakeExpandResponse is the response for MsgBtcStakeExpand diff --git a/test/e2e/costaking_e2e_test.go b/test/e2e/costaking_e2e_test.go new file mode 100644 index 000000000..b42afcc89 --- /dev/null +++ b/test/e2e/costaking_e2e_test.go @@ -0,0 +1,285 @@ +package e2e + +import ( + "math" + "math/rand" + "strings" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + + "github.com/babylonlabs-io/babylon/v4/test/e2e/configurer" + "github.com/babylonlabs-io/babylon/v4/test/e2e/configurer/chain" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" + bbn "github.com/babylonlabs-io/babylon/v4/types" + bstypes "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" +) + +type CostakingTestSuite struct { + suite.Suite + + r *rand.Rand + net *chaincfg.Params + + covenantSKs []*btcec.PrivateKey + covenantQuorum uint32 + + configurer configurer.Configurer +} + +func (s *CostakingTestSuite) SetupSuite() { + s.T().Log("setting up costaking e2e test suite...") + var err error + + s.r = rand.New(rand.NewSource(time.Now().Unix())) + s.net = &chaincfg.SimNetParams + s.covenantSKs, _, s.covenantQuorum = bstypes.DefaultCovenantCommittee() + + s.configurer, err = configurer.NewBabylonConfigurer(s.T(), true) + s.NoError(err) + err = s.configurer.ConfigureChains() + s.NoError(err) + err = s.configurer.RunSetup() + s.NoError(err) +} + +func (s *CostakingTestSuite) TearDownSuite() { + if s.configurer != nil { + if err := s.configurer.ClearResources(); err != nil { + s.T().Logf("error clearing resources: %v", err) + } + } +} + +func (s *CostakingTestSuite) TestFinalityProviderExit() { + chainA := s.configurer.GetChainConfig(0) + chainA.WaitUntilHeight(1) + + delegatorNode, err := chainA.GetNodeAtIndex(2) + s.NoError(err) + + validators, err := delegatorNode.QueryValidators() + s.NoError(err) + s.Require().NotEmpty(validators) + targetValidator := validators[0] + + babyDelegation := "20000000ubbn" + txHash := delegatorNode.Delegate(delegatorNode.WalletName, targetValidator.OperatorAddress, babyDelegation, "--gas=500000") + chainA.WaitForNumHeights(2) + res, _ := delegatorNode.QueryTx(txHash) + s.Equal(res.Code, uint32(0), res.RawLog) + + _, err = delegatorNode.WaitForNextEpoch() + s.NoError(err) + + fpSk, _, _ := datagen.GenRandomBTCKeyPair(s.r) + fp := chain.CreateFpFromNodeAddr(s.T(), s.r, fpSk, delegatorNode) + + numPubRand := uint64(1000) + commitStartHeight := uint64(1) + _, msgCommitPubRandList, err := datagen.GenRandomMsgCommitPubRandList(s.r, fpSk, commitStartHeight, numPubRand) + s.NoError(err) + delegatorNode.CommitPubRandListFromNode( + msgCommitPubRandList.FpBtcPk, + msgCommitPubRandList.StartHeight, + msgCommitPubRandList.NumPubRand, + msgCommitPubRandList.Commitment, + msgCommitPubRandList.Sig, + ) + + var commitEpoch uint64 + s.Require().Eventually(func() bool { + pubRandCommitMap := delegatorNode.QueryListPubRandCommit(msgCommitPubRandList.FpBtcPk) + if len(pubRandCommitMap) == 0 { + return false + } + for _, commit := range pubRandCommitMap { + commitEpoch = commit.EpochNum + break + } + return true + }, time.Minute, time.Second, "finality provider should have public randomness committed") + s.T().Logf("fp pub rand commitment stored for epoch %d", commitEpoch) + + lastFinalizedEpoch := delegatorNode.WaitUntilCurrentEpochIsSealedAndFinalized(1) + s.Require().GreaterOrEqual(lastFinalizedEpoch, commitEpoch, "finalized epoch must include fp pub rand commit") + + _, err = delegatorNode.WaitForNextEpoch() + s.NoError(err) + + btcDelegatorSK, _, _ := datagen.GenRandomBTCKeyPair(s.r) + delegatorAddr := sdk.MustAccAddressFromBech32(delegatorNode.PublicAddress) + pop, err := datagen.NewPoPBTC(delegatorAddr, btcDelegatorSK) + s.NoError(err) + + params := delegatorNode.QueryBTCStakingParams() + stakingTimeBlocks := uint16(math.MaxUint16) + stakingValue := int64(2 * 10e8) + + testStakingInfo, stakingTx, stakingInclusionProof, testUnbondingInfo, delegatorSig := delegatorNode.BTCStakingUnbondSlashInfo( + s.r, + s.T(), + s.net, + params, + fp, + btcDelegatorSK, + stakingTimeBlocks, + stakingValue, + ) + + delUnbondingSlashingSig, err := testUnbondingInfo.GenDelSlashingTxSig(btcDelegatorSK) + s.NoError(err) + + delegatorNode.CreateBTCDelegation( + bbn.NewBIP340PubKeyFromBTCPK(btcDelegatorSK.PubKey()), + pop, + stakingTx, + stakingInclusionProof, + fp.BtcPk, + stakingTimeBlocks, + btcutil.Amount(stakingValue), + testStakingInfo.SlashingTx, + delegatorSig, + testUnbondingInfo.UnbondingTx, + testUnbondingInfo.SlashingTx, + uint16(params.UnbondingTimeBlocks), + btcutil.Amount(testUnbondingInfo.UnbondingInfo.UnbondingOutput.Value), + delUnbondingSlashingSig, + delegatorNode.WalletName, + false, + ) + + delegatorNode.WaitForNextBlock() + + pendingSet := delegatorNode.QueryFinalityProviderDelegations(fp.BtcPk.MarshalHex()) + s.Require().Len(pendingSet, 1) + pendingResp := pendingSet[0] + s.Require().Len(pendingResp.Dels, 1) + pendingDel, err := chain.ParseRespBTCDelToBTCDel(pendingResp.Dels[0]) + s.Require().NoError(err) + + delegatorNode.SendCovenantSigsAsValAndCheck(s.r, s.T(), s.net, s.covenantSKs, pendingDel) + _, err = delegatorNode.WaitForNextEpoch() + s.NoError(err) + + activeSet := delegatorNode.QueryFinalityProviderDelegations(fp.BtcPk.MarshalHex()) + s.Require().Len(activeSet, 1) + activeDelegations, err := chain.ParseRespsBTCDelToBTCDel(activeSet[0]) + s.Require().NoError(err) + s.Require().Len(activeDelegations.Dels, 1) + activeDel := activeDelegations.Dels[0] + s.Require().True(activeDel.HasCovenantQuorums(s.covenantQuorum, 0)) + + stakingMsgTx, err := bbn.NewBTCTxFromBytes(activeDel.StakingTx) + s.NoError(err) + stakingTxHash := stakingMsgTx.TxHash() + + expectedSats := sdkmath.NewIntFromUint64(activeDel.TotalSat) + s.T().Logf("tracker target before unbonding: staking_tx=%s expected_sats=%s", stakingTxHash.String(), expectedSats.String()) + var trackerReady bool + s.Require().Eventually(func() bool { + tracker, err := delegatorNode.QueryCostakerRewardsTracker(delegatorNode.PublicAddress) + if err != nil { + return false + } + trackerReady = tracker.ActiveSatoshis.Equal(expectedSats) + if !trackerReady { + s.T().Logf( + "tracker mismatch before unbonding: staking_tx=%s tracker_sats=%s expected_sats=%s", + stakingTxHash.String(), + tracker.ActiveSatoshis.String(), + expectedSats.String(), + ) + } + return trackerReady + }, time.Minute, time.Second, "costaker tracker should reflect active sats before unbonding") + s.Require().True(trackerReady) + s.T().Logf("tracker synced before unbonding: staking_tx=%s tracker_sats=%s", stakingTxHash.String(), expectedSats.String()) + + currentBtcTipResp, err := delegatorNode.QueryTip() + s.NoError(err) + currentBtcTip, err := chain.ParseBTCHeaderInfoResponseToInfo(currentBtcTipResp) + s.NoError(err) + + unbondingTx := activeDel.MustGetUnbondingTx() + _, unbondingTxMsg := datagen.AddWitnessToUnbondingTx( + s.T(), + stakingMsgTx.TxOut[activeDel.StakingOutputIdx], + btcDelegatorSK, + s.covenantSKs, + s.covenantQuorum, + []*btcec.PublicKey{fp.BtcPk.MustToBTCPK()}, + uint16(activeDel.GetStakingTime()), + int64(activeDel.TotalSat), + unbondingTx, + s.net, + ) + + blockWithUnbondingTx := datagen.CreateBlockWithTransaction(s.r, currentBtcTip.Header.ToBlockHeader(), unbondingTxMsg) + delegatorNode.InsertHeader(&blockWithUnbondingTx.HeaderBytes) + unbondingInclusionProof := bstypes.NewInclusionProofFromSpvProof(blockWithUnbondingTx.SpvProof) + + delegatorNode.SubmitRefundableTxWithAssertion(func() { + delegatorNode.BTCUndelegate( + &stakingTxHash, + unbondingTxMsg, + unbondingInclusionProof, + []*wire.MsgTx{stakingMsgTx}, + ) + delegatorNode.WaitForNextBlock() + }, true) + + s.Require().Eventually(func() bool { + unbonded := delegatorNode.QueryUnbondedDelegations() + for _, resp := range unbonded { + del, err := chain.ParseRespBTCDelToBTCDel(resp) + if err != nil { + continue + } + hash := del.MustGetStakingTxHash() + if hash.IsEqual(&stakingTxHash) { + return true + } + } + return false + }, 2*time.Minute, time.Second, "BTC delegation should enter UNBONDED state") + + fpHex := fp.BtcPk.MarshalHex() + s.Require().Eventually(func() bool { + currentHeight, err := delegatorNode.QueryCurrentHeight() + if err != nil { + s.T().Logf("error querying height: %v", err) + return false + } + activeFps := delegatorNode.QueryActiveFinalityProvidersAtHeight(uint64(currentHeight)) + for _, activeFP := range activeFps { + if strings.EqualFold(activeFP.BtcPkHex.MarshalHex(), fpHex) { + return false + } + } + return true + }, 2*time.Minute, 2*time.Second, "finality provider should leave active set once delegation unbonds") + + _, err = delegatorNode.WaitForNextEpoch() + s.NoError(err) + + trackerAfter, err := delegatorNode.QueryCostakerRewardsTracker(delegatorNode.PublicAddress) + s.NoError(err) + s.T().Logf( + "tracker after finality provider exit: staking_tx=%s tracker_sats=%s", + stakingTxHash.String(), + trackerAfter.ActiveSatoshis.String(), + ) + + s.Require().True( + trackerAfter.ActiveSatoshis.IsZero(), + "expected costaker to lose all sats after FP removal, but tracker still holds %s", + trackerAfter.ActiveSatoshis.String(), + ) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 89da86b44..5f586defc 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -63,3 +63,8 @@ func TestBTCStakeExpansionTestSuite(t *testing.T) { func TestValidatorJailingTestSuite(t *testing.T) { suite.Run(t, new(ValidatorJailingTestSuite)) } + +// TestCostakingTestSuite tests costaking phantom sats handling end-to-end +func TestCostakingTestSuite(t *testing.T) { + suite.Run(t, new(CostakingTestSuite)) +} diff --git a/test/e2ev2/multisig_test.go b/test/e2ev2/multisig_test.go new file mode 100644 index 000000000..a48ee5bf8 --- /dev/null +++ b/test/e2ev2/multisig_test.go @@ -0,0 +1,954 @@ +package e2e2 + +import ( + "fmt" + "github.com/babylonlabs-io/babylon/v4/btcstaking" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + + "github.com/babylonlabs-io/babylon/v4/test/e2ev2/tmanager" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" + bbn "github.com/babylonlabs-io/babylon/v4/types" + bstypes "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" +) + +const ( + BTCDelegationPending = "PENDING" + BTCDelegationVerified = "VERIFIED" + BTCDelegationActive = "ACTIVE" +) + +func TestMultisigBtcDel(t *testing.T) { + bbn2, fpSK, r := startChainAndCreateFp(t) + bbn2.DefaultWallet().VerifySentTx = true + + testCases := []struct { + title string + stakerQuorum uint32 + stakerCount uint32 + sigsCount uint32 + expErr string + }{ + { + title: "2-of-3 multisig delegation, 2 valid signatures", + stakerQuorum: 2, + stakerCount: 3, + sigsCount: 2, + expErr: "", + }, + { + title: "2-of-3 multisig delegation, 3 valid signatures", + stakerQuorum: 2, + stakerCount: 3, + sigsCount: 3, + expErr: "", + }, + { + title: "3-of-5 multisig delegation, 3 valid signatures - max 2-of-3 multisig params", + stakerQuorum: 3, + stakerCount: 5, + sigsCount: 3, + expErr: "invalid multisig info", + }, + { + title: "2-of-3 multisig delegation, 1 valid signatures", + stakerQuorum: 2, + stakerCount: 3, + sigsCount: 1, + expErr: "invalid multisig info", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + // override VerifySentTx if it expects error + if tc.expErr != "" { + bbn2.DefaultWallet().VerifySentTx = false + } + + // multisig delegation from bbn2 to fp (bbn1) + stkSKs, _, err := datagen.GenRandomBTCKeyPairs(r, int(tc.stakerCount)) + require.NoError(t, err) + + msg, stakingInfo := buildMultisigDelegationMsgWithSigCount( + t, r, bbn2, + bbn2.DefaultWallet(), + stkSKs, + tc.stakerQuorum, + fpSK.PubKey(), + int64(2*10e8), + 1000, + tc.sigsCount, + ) + + txHash := bbn2.CreateBTCDelegation(bbn2.DefaultWallet().KeyName, msg) + bbn2.WaitForNextBlock() + + // if it expects error, don't query btc delegation and stop here + if tc.expErr != "" { + bbn2.RequireTxErrorContain(txHash, tc.expErr) + return + } + + // query and verify delegation + del := bbn2.QueryBTCDelegation(stakingInfo.StakingTx.TxHash().String()) + require.NotNil(t, del) + require.Equal(t, BTCDelegationPending, del.StatusDesc) + require.NotNil(t, del.MultisigInfo) + require.Equal(t, tc.stakerQuorum, del.MultisigInfo.StakerQuorum) + require.Len(t, del.MultisigInfo.StakerBtcPkList, int(tc.stakerCount-1)) + require.Len(t, del.MultisigInfo.DelegatorSlashingSigs, int(tc.sigsCount-1)) + }) + } +} + +// TestSingleSigBtcDel tests original single-signature BTC delegation (no multisig info) +// this is a regression test to ensure multisig changes don't break single-sig functionality +func TestSingleSigBtcDel(t *testing.T) { + bbn2, fpSK, r := startChainAndCreateFp(t) + bbn2.DefaultWallet().VerifySentTx = true + + // single-sig delegation from bbn2 to fp (bbn1) + stakerSK, _, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + + msg, stakingInfoBuilt := BuildSingleSigDelegationMsg( + t, r, bbn2, + bbn2.DefaultWallet(), + stakerSK, + fpSK.PubKey(), + int64(2*10e8), + 1000, + ) + + bbn2.CreateBTCDelegation(bbn2.DefaultWallet().KeyName, msg) + bbn2.WaitForNextBlock() + + pendingDelResp := bbn2.QueryBTCDelegation(stakingInfoBuilt.StakingTx.TxHash().String()) + require.NotNil(t, pendingDelResp) + require.Equal(t, BTCDelegationPending, pendingDelResp.StatusDesc) + + /* + generate and insert new covenant signatures, in order to verify the BTC delegation + */ + stakingMsgTx, stakingTxHash, bsParams := verifyBTCDelegation(t, bbn2, pendingDelResp) + + /* + generate and add inclusion proof, in order to activate the BTC delegation + */ + activeBtcDel := waitBtcBlockForKDeepWithInclusionProof(t, r, bbn2, stakingMsgTx, stakingTxHash) + + require.Len(t, activeBtcDel.CovenantSigs, int(bsParams.CovenantQuorum)) + require.True(t, activeBtcDel.HasCovenantQuorums(bsParams.CovenantQuorum, 0)) + require.Nil(t, activeBtcDel.MultisigInfo, "Single-sig delegation should not have MultisigInfo") + require.NotNil(t, activeBtcDel.DelegatorSig, "Single-sig delegation should have delegator signature") +} + +// TestMultisigBtcDelWithDuplicates tests that duplicate staker keys and signatures +func TestMultisigBtcDelWithDuplicates(t *testing.T) { + bbn2, fpSK, r := startChainAndCreateFp(t) + + testCases := []struct { + title string + dupSetup func(*bstypes.MsgCreateBTCDelegation) *bstypes.MsgCreateBTCDelegation + expErr string + }{ + { + title: "duplicated staker pk in StakerBtcPkList", + dupSetup: func(msg *bstypes.MsgCreateBTCDelegation) *bstypes.MsgCreateBTCDelegation { + msg.MultisigInfo.StakerBtcPkList[0] = msg.MultisigInfo.StakerBtcPkList[1] + return msg + }, + expErr: "multisig staker key is duplicated", + }, + { + title: "duplicated between BtcPk and StakerBtcPkList", + dupSetup: func(msg *bstypes.MsgCreateBTCDelegation) *bstypes.MsgCreateBTCDelegation { + msg.MultisigInfo.StakerBtcPkList[0] = *msg.BtcPk + return msg + }, + expErr: "staker pk list contains the main staker pk", + }, + { + title: "duplicated slashing sigs in the multisig info", + dupSetup: func(msg *bstypes.MsgCreateBTCDelegation) *bstypes.MsgCreateBTCDelegation { + msg.MultisigInfo.DelegatorSlashingSigs[0].Sig = msg.DelegatorSlashingSig + return msg + }, + expErr: "invalid delegator signature", + }, + } + + for _, tc := range testCases { + t.Run(tc.title, func(t *testing.T) { + // multisig delegation from bbn2 to fp (bbn1) + stkSKs, _, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + + msg, _ := buildMultisigDelegationMsgWithSigCount( + t, r, bbn2, + bbn2.DefaultWallet(), + stkSKs, + 2, + fpSK.PubKey(), + int64(2*10e8), + 1000, + 2, + ) + + // setup duplicated staker keys + msg = tc.dupSetup(msg) + + // for duplicate checks, the error might happen before block inclusion (during ValidateBasic) + wallet2 := bbn2.DefaultWallet() + signedTx := wallet2.SignMsg(msg) + txHash, err := bbn2.SubmitTx(signedTx) + if err != nil { + // transaction rejected before block inclusion + require.Error(t, err) + require.Contains(t, err.Error(), tc.expErr) + // reset sequence since it fails to submit tx + bbn2.DefaultWallet().DecSeq() + } else { + // transaction included in block but failed during execution + bbn2.WaitForNextBlock() + bbn2.RequireTxErrorContain(txHash, tc.expErr) + } + }) + } +} + +func TestMultisigBtcDelWithZeroQuorum(t *testing.T) { + bbn2, fpSK, r := startChainAndCreateFp(t) + + // multisig delegation from bbn2 to fp (bbn1) + stkSKs, _, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + + msg, _ := buildMultisigDelegationMsgWithSigCount( + t, r, bbn2, + bbn2.DefaultWallet(), + stkSKs, + 2, + fpSK.PubKey(), + int64(2*10e8), + 1000, + 2, + ) + msg.MultisigInfo.StakerQuorum = 0 + + txHash := bbn2.CreateBTCDelegation(bbn2.DefaultWallet().KeyName, msg) + bbn2.WaitForNextBlock() + + bbn2.RequireTxErrorContain(txHash, "number of staker btc pk list and staker quorum must be greater than 0") +} + +func TestMultisigBtcStkExpansionFlow(t *testing.T) { + // step 0. create a fp + bbn2, fpSK, r := startChainAndCreateFp(t) + bbn2.DefaultWallet().VerifySentTx = true + + // step 1. multisig delegation from bbn2 to fp (bbn1), and activate btc delegation + stkSKs, _, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + + msg, stakingInfoBuilt := buildMultisigDelegationMsgWithSigCount( + t, r, bbn2, + bbn2.DefaultWallet(), + stkSKs, + 2, + fpSK.PubKey(), + int64(2*10e8), + 1000, + 2, + ) + + // creating multisig btc delegation + bbn2.CreateBTCDelegation(bbn2.DefaultWallet().KeyName, msg) + bbn2.WaitForNextBlock() + + pendingDelResp := bbn2.QueryBTCDelegation(stakingInfoBuilt.StakingTx.TxHash().String()) + require.NotNil(t, pendingDelResp) + require.Equal(t, BTCDelegationPending, pendingDelResp.StatusDesc) + + // verify multisig btc delegation by sending covenant signatures + stakingMsgTx, stakingTxHash, bsParams := verifyBTCDelegation(t, bbn2, pendingDelResp) + + // wait for btc block k-deep and add btc delegation inclusion proof + prevActiveDel := waitBtcBlockForKDeepWithInclusionProof(t, r, bbn2, stakingMsgTx, stakingTxHash) + require.NotNil(t, prevActiveDel) + + // step 2. multisig btc stake expansion of prev multisig btc delegation + stkExpStakingInfo, fundingTx := bbn2.BtcStakeExpand( + bbn2.DefaultWallet().KeyName, + prevActiveDel, + r, + stkSKs, + 2, + fpSK.PubKey(), + nil, + ) + bbn2.WaitForNextBlock() + + stkExpBtcDelResp := bbn2.QueryBTCDelegation(stkExpStakingInfo.StakingTx.TxHash().String()) + require.NotNil(t, stkExpBtcDelResp) + require.Equal(t, BTCDelegationPending, stkExpBtcDelResp.StatusDesc) + require.Equal(t, stkExpBtcDelResp.StakerAddr, bbn2.DefaultWallet().Address.String()) + require.NotNil(t, stkExpBtcDelResp.StkExp) + + // step 3. submit covenant signatures to verify BTC expansion delegation + verifyBTCDelegation(t, bbn2, stkExpBtcDelResp) + + // step 4. submit MsgBTCUndelegate for the origin BTC delegation to activate + // the BTC expansion delegation + // spendingTx of the previous BTC delegation + // staking output is the staking tx of the BTC stake expansion delegation + spendingTx := stkExpStakingInfo.StakingTx + + // NOTE: covSKs should be changed when modifying covenant pk on chain start + covSKs, _, _ := bstypes.DefaultCovenantCommittee() + net := &chaincfg.SimNetParams + + _, stkExpMsgTx := datagen.AddMultisigWitnessToStakeExpTx( + t, + stakingInfoBuilt.StakingTx.TxOut[prevActiveDel.StakingOutputIdx], + fundingTx.TxOut[0], + stkSKs, + 2, + covSKs, + bsParams.CovenantQuorum, + []*btcec.PublicKey{fpSK.PubKey()}, + uint16(prevActiveDel.GetStakingTime()), + int64(prevActiveDel.TotalSat), + spendingTx, + net, + ) + + // wait for stake expansion transaction to be k-deep and then send MsgBTCUndelegate + // to activate stake expansion + waitBtcBlockForKDeepWithBTCUndelegate( + t, r, + bbn2, + stkExpMsgTx, + stakingInfoBuilt.StakingTx, + fundingTx, + stakingTxHash, + stkExpMsgTx.TxHash().String(), + ) + + var unbondedDelsResp []*bstypes.BTCDelegationResponse + require.Eventually(t, func() bool { + unbondedDelsResp = bbn2.QueryBTCDelegations(bstypes.BTCDelegationStatus_UNBONDED) + return len(unbondedDelsResp) > 0 + }, time.Minute, time.Second*2) + + unbondDel, err := tmanager.ParseRespBTCDelToBTCDel(unbondedDelsResp[0]) + require.NoError(t, err) + require.Equal(t, stakingTxHash, unbondDel.MustGetStakingTxHash().String()) +} + +func TestBtcStakeExpandWithDifferentBtcPks(t *testing.T) { + testCases := []struct { + title string + prevBtcSkList []*btcec.PrivateKey + prevStakerQuorum uint32 + newBtcSkList []*btcec.PrivateKey + newStakerQuorum uint32 + expErr error + }{ + { + title: "different btc pks with same length", + prevBtcSkList: func() []*btcec.PrivateKey { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + stkSks, _, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + return stkSks + }(), + prevStakerQuorum: 2, + newBtcSkList: func() []*btcec.PrivateKey { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + stkSks, _, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + return stkSks + }(), + newStakerQuorum: 2, + expErr: fmt.Errorf("does not match previous primary staker pk"), + }, + { + title: "different btc pks with different length — multisig -> single sig expansion", + prevBtcSkList: func() []*btcec.PrivateKey { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + stkSks, _, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + return stkSks + }(), + prevStakerQuorum: 2, + newBtcSkList: func() []*btcec.PrivateKey { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + stkSks, _, err := datagen.GenRandomBTCKeyPairs(r, 1) + require.NoError(t, err) + return stkSks + }(), + newStakerQuorum: 1, + expErr: fmt.Errorf("does not match previous primary staker pk"), + }, + { + title: "different btc pks with different length — single sig -> multisig expansion", + prevBtcSkList: func() []*btcec.PrivateKey { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + stkSks, _, err := datagen.GenRandomBTCKeyPairs(r, 1) + require.NoError(t, err) + return stkSks + }(), + prevStakerQuorum: 1, + newBtcSkList: func() []*btcec.PrivateKey { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + stkSks, _, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + return stkSks + }(), + newStakerQuorum: 2, + expErr: fmt.Errorf("does not match previous primary staker pk"), + }, + } + + // step 0. create a fp + bbn2, fpSK, r := startChainAndCreateFp(t) + bbn2.DefaultWallet().VerifySentTx = true + + for _, tc := range testCases { + // step 1. create btc delegation from bbn2 to fp (bbn1), and activate btc delegation + var ( + msg *bstypes.MsgCreateBTCDelegation + stakingInfoBuilt *datagen.TestStakingSlashingInfo + ) + + if len(tc.prevBtcSkList) == 1 { + msg, stakingInfoBuilt = BuildSingleSigDelegationMsg( + t, r, bbn2, + bbn2.DefaultWallet(), + tc.prevBtcSkList[0], + fpSK.PubKey(), + int64(2*10e8), + 1000, + ) + } else { + msg, stakingInfoBuilt = buildMultisigDelegationMsgWithSigCount( + t, r, bbn2, + bbn2.DefaultWallet(), + tc.prevBtcSkList, + tc.prevStakerQuorum, + fpSK.PubKey(), + int64(2*10e8), + 1000, + 2, + ) + } + + // creating multisig btc delegation + bbn2.CreateBTCDelegation(bbn2.DefaultWallet().KeyName, msg) + bbn2.WaitForNextBlock() + + pendingDelResp := bbn2.QueryBTCDelegation(stakingInfoBuilt.StakingTx.TxHash().String()) + require.NotNil(t, pendingDelResp) + require.Equal(t, BTCDelegationPending, pendingDelResp.StatusDesc) + + // verify multisig btc delegation by sending covenant signatures + stakingMsgTx, stakingTxHash, _ := verifyBTCDelegation(t, bbn2, pendingDelResp) + + // wait for btc block k-deep and add btc delegation inclusion proof + prevActiveDel := waitBtcBlockForKDeepWithInclusionProof(t, r, bbn2, stakingMsgTx, stakingTxHash) + require.NotNil(t, prevActiveDel) + + // step 2. btc stake expansion of prev btc delegation with expected error + bbn2.BtcStakeExpand( + bbn2.DefaultWallet().KeyName, + prevActiveDel, + r, + tc.newBtcSkList, + tc.newStakerQuorum, + fpSK.PubKey(), + tc.expErr, + ) + bbn2.WaitForNextBlock() + } +} + +func startChainAndCreateFp(t *testing.T) (bbn2 *tmanager.Node, fpSK *btcec.PrivateKey, r *rand.Rand) { + t.Parallel() + tm := tmanager.NewTestManager(t) + cfg := tmanager.NewChainConfig(tm.TempDir, tmanager.CHAIN_ID_BABYLON) + cfg.NodeCount = 2 + cfg.StartingBtcStakingParams = &tmanager.StartingBtcStakingParams{ + MaxStakerNum: 3, + MaxStakerQuorum: 2, + } + tm.Chains[tmanager.CHAIN_ID_BABYLON] = tmanager.NewChain(tm, cfg) + tm.Start() + + tm.ChainsWaitUntilHeight(3) + + bbns := tm.ChainNodes() + bbn1 := bbns[0] + bbn2 = bbns[1] + bbn1.DefaultWallet().VerifySentTx = true + bbn2.DefaultWallet().VerifySentTx = false + + // create bbn1 as a fp + r = rand.New(rand.NewSource(time.Now().Unix())) + fpSK, _, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + fp, err := datagen.GenCustomFinalityProvider(r, fpSK, bbn1.DefaultWallet().Address) + require.NoError(t, err) + bbn1.CreateFinalityProvider(bbn1.DefaultWallet().KeyName, fp) + bbn1.WaitForNextBlock() + + fpResp := bbn1.QueryFinalityProvider(fp.BtcPk.MarshalHex()) + require.NotNil(t, fpResp) + + return bbn2, fpSK, r +} + +// buildMultisigDelegationWithSigCount construct multisig btc delegation msg +// - sigsCount is the number of signatures +// +//nolint:unparam +func buildMultisigDelegationMsgWithSigCount( + t *testing.T, + r *rand.Rand, + node *tmanager.Node, + wallet *tmanager.WalletSender, + stakerSKs []*btcec.PrivateKey, + stakerQuorum uint32, + fpPK *btcec.PublicKey, + stakingValue int64, + stakingTime uint16, + sigsCount uint32, +) (*bstypes.MsgCreateBTCDelegation, *datagen.TestStakingSlashingInfo) { + params := node.QueryBtcStakingParams() + net := &chaincfg.SimNetParams + + covPKs, err := bbn.NewBTCPKsFromBIP340PKs(params.CovenantPks) + require.NoError(t, err) + + // generate staking + slashing info + stakingInfo := datagen.GenMultisigBTCStakingSlashingInfo( + r, t, net, + stakerSKs, + stakerQuorum, + []*btcec.PublicKey{fpPK}, + covPKs, + params.CovenantQuorum, + stakingTime, + stakingValue, + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + // generate unbonding info + unbondingValue := stakingValue - params.UnbondingFeeSat + stkTxHash := stakingInfo.StakingTx.TxHash() + + unbondingInfo := datagen.GenMultisigBTCUnbondingSlashingInfo( + r, t, net, + stakerSKs, + stakerQuorum, + []*btcec.PublicKey{fpPK}, + covPKs, + params.CovenantQuorum, + &wire.OutPoint{Hash: stkTxHash, Index: 0}, + uint16(params.UnbondingTimeBlocks), + unbondingValue, + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + // sign slashing tx with primary staker (first one) + slashingSpendInfo, err := stakingInfo.StakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + delegatorSig, err := stakingInfo.SlashingTx.Sign( + stakingInfo.StakingTx, 0, + slashingSpendInfo.GetPkScriptPath(), + stakerSKs[0], + ) + require.NoError(t, err) + + // generate extra staker signatures (for remaining stakers) + var extraSlashingSigs []*bstypes.SignatureInfo + stakerSKList := stakerSKs[1:sigsCount] + for _, sk := range stakerSKList { + sig, err := stakingInfo.SlashingTx.Sign( + stakingInfo.StakingTx, 0, + slashingSpendInfo.GetPkScriptPath(), + sk, + ) + require.NoError(t, err) + + extraSlashingSigs = append(extraSlashingSigs, &bstypes.SignatureInfo{ + Pk: bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()), + Sig: sig, + }) + } + + // sign unbonding slashing tx with primary staker + delUnbondingSig, err := unbondingInfo.GenDelSlashingTxSig(stakerSKs[0]) + require.NoError(t, err) + + // generate extra unbonding signatures + var extraUnbondingSigs []*bstypes.SignatureInfo + for _, sk := range stakerSKList { + sig, err := unbondingInfo.GenDelSlashingTxSig(sk) + require.NoError(t, err) + + extraUnbondingSigs = append(extraUnbondingSigs, &bstypes.SignatureInfo{ + Pk: bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()), + Sig: sig, + }) + } + + // generate PoP for primary staker + pop, err := datagen.NewPoPBTC(wallet.Address, stakerSKs[0]) + require.NoError(t, err) + + // serialize transactions + serializedStakingTx, err := bbn.SerializeBTCTx(stakingInfo.StakingTx) + require.NoError(t, err) + serializedUnbondingTx, err := bbn.SerializeBTCTx(unbondingInfo.UnbondingTx) + require.NoError(t, err) + + // build extra staker PK list (all stakers except the first one) + extraStakerPKs := make([]bbn.BIP340PubKey, len(stakerSKs)-1) + for i, sk := range stakerSKs[1:] { + extraStakerPKs[i] = *bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()) + } + + return &bstypes.MsgCreateBTCDelegation{ + StakerAddr: wallet.Address.String(), + BtcPk: bbn.NewBIP340PubKeyFromBTCPK(stakerSKs[0].PubKey()), + FpBtcPkList: []bbn.BIP340PubKey{*bbn.NewBIP340PubKeyFromBTCPK(fpPK)}, + Pop: pop, + StakingTime: uint32(stakingTime), + StakingValue: stakingValue, + StakingTx: serializedStakingTx, + SlashingTx: stakingInfo.SlashingTx, + DelegatorSlashingSig: delegatorSig, + UnbondingTx: serializedUnbondingTx, + UnbondingTime: params.UnbondingTimeBlocks, + UnbondingValue: unbondingValue, + UnbondingSlashingTx: unbondingInfo.SlashingTx, + DelegatorUnbondingSlashingSig: delUnbondingSig, + MultisigInfo: &bstypes.AdditionalStakerInfo{ + StakerBtcPkList: extraStakerPKs, + StakerQuorum: stakerQuorum, + DelegatorSlashingSigs: extraSlashingSigs, + DelegatorUnbondingSlashingSigs: extraUnbondingSigs, + }, + }, stakingInfo +} + +// BuildSingleSigDelegationMsg constructs a original single-sig BTC delegation message +func BuildSingleSigDelegationMsg( + t *testing.T, + r *rand.Rand, + node *tmanager.Node, + wallet *tmanager.WalletSender, + stakerSK *btcec.PrivateKey, + fpPK *btcec.PublicKey, + stakingValue int64, + stakingTime uint16, +) (*bstypes.MsgCreateBTCDelegation, *datagen.TestStakingSlashingInfo) { + params := node.QueryBtcStakingParams() + net := &chaincfg.SimNetParams + + covPKs, err := bbn.NewBTCPKsFromBIP340PKs(params.CovenantPks) + require.NoError(t, err) + + // generate staking + slashing info + stakingInfo := datagen.GenBTCStakingSlashingInfo( + r, t, net, + stakerSK, + []*btcec.PublicKey{fpPK}, + covPKs, + params.CovenantQuorum, + stakingTime, + stakingValue, + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + // generate unbonding info + unbondingValue := stakingValue - params.UnbondingFeeSat + stkTxHash := stakingInfo.StakingTx.TxHash() + + unbondingInfo := datagen.GenBTCUnbondingSlashingInfo( + r, t, net, + stakerSK, + []*btcec.PublicKey{fpPK}, + covPKs, + params.CovenantQuorum, + &wire.OutPoint{Hash: stkTxHash, Index: 0}, + uint16(params.UnbondingTimeBlocks), + unbondingValue, + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + // sign slashing tx + slashingSpendInfo, err := stakingInfo.StakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + delegatorSig, err := stakingInfo.SlashingTx.Sign( + stakingInfo.StakingTx, 0, + slashingSpendInfo.GetPkScriptPath(), + stakerSK, + ) + require.NoError(t, err) + + // sign unbonding slashing tx + delUnbondingSig, err := unbondingInfo.GenDelSlashingTxSig(stakerSK) + require.NoError(t, err) + + // generate PoP + pop, err := datagen.NewPoPBTC(wallet.Address, stakerSK) + require.NoError(t, err) + + // serialize transactions + serializedStakingTx, err := bbn.SerializeBTCTx(stakingInfo.StakingTx) + require.NoError(t, err) + serializedUnbondingTx, err := bbn.SerializeBTCTx(unbondingInfo.UnbondingTx) + require.NoError(t, err) + + return &bstypes.MsgCreateBTCDelegation{ + StakerAddr: wallet.Address.String(), + BtcPk: bbn.NewBIP340PubKeyFromBTCPK(stakerSK.PubKey()), + FpBtcPkList: []bbn.BIP340PubKey{*bbn.NewBIP340PubKeyFromBTCPK(fpPK)}, + Pop: pop, + StakingTime: uint32(stakingTime), + StakingValue: stakingValue, + StakingTx: serializedStakingTx, + SlashingTx: stakingInfo.SlashingTx, + DelegatorSlashingSig: delegatorSig, + UnbondingTx: serializedUnbondingTx, + UnbondingTime: params.UnbondingTimeBlocks, + UnbondingValue: unbondingValue, + UnbondingSlashingTx: unbondingInfo.SlashingTx, + DelegatorUnbondingSlashingSig: delUnbondingSig, + MultisigInfo: nil, // no multisig info for single-sig delegation + }, stakingInfo +} + +// verifyBTCDelegation generate and insert new covenant signatures, +// in order to verify the BTC delegation +func verifyBTCDelegation(t *testing.T, bbn2 *tmanager.Node, pendingDelResp *bstypes.BTCDelegationResponse) (*wire.MsgTx, string, *bstypes.Params) { + pendingDel, err := tmanager.ParseRespBTCDelToBTCDel(pendingDelResp) + require.NoError(t, err) + require.Len(t, pendingDel.CovenantSigs, 0) + stakingMsgTx, err := bbn.NewBTCTxFromBytes(pendingDel.StakingTx) + require.NoError(t, err) + + isMultisig := pendingDel.IsMultisigBtcDel() + + slashingTx := pendingDel.SlashingTx + stakingTxHash := stakingMsgTx.TxHash().String() + bsParams := bbn2.QueryBtcStakingParams() + + fpBTCPKs, err := bbn.NewBTCPKsFromBIP340PKs(pendingDel.FpBtcPkList) + require.NoError(t, err) + + btcCfg := &chaincfg.SimNetParams + + var ( + stakingInfo *btcstaking.StakingInfo + unbondingInfo *btcstaking.UnbondingInfo + ) + + if isMultisig { + stakingInfo, err = pendingDel.GetMultisigStakingInfo(bsParams, btcCfg) + require.NoError(t, err) + } else { + stakingInfo, err = pendingDel.GetStakingInfo(bsParams, btcCfg) + require.NoError(t, err) + } + + stakingSlashingPathInfo, err := stakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + // NOTE: covSKs should be changed when modifying covenant pk on chain start + covSKs, _, _ := bstypes.DefaultCovenantCommittee() + + // covenant signatures on slashing tx + covenantSlashingSigs, err := datagen.GenCovenantAdaptorSigs( + covSKs, + fpBTCPKs, + stakingMsgTx, + stakingSlashingPathInfo.GetPkScriptPath(), + slashingTx, + ) + require.NoError(t, err) + + // cov Schnorr sigs on unbonding signature + unbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() + require.NoError(t, err) + unbondingTx, err := bbn.NewBTCTxFromBytes(pendingDel.BtcUndelegation.UnbondingTx) + require.NoError(t, err) + + covUnbondingSigs, err := datagen.GenCovenantUnbondingSigs( + covSKs, + stakingMsgTx, + pendingDel.StakingOutputIdx, + unbondingPathInfo.GetPkScriptPath(), + unbondingTx, + ) + require.NoError(t, err) + + if isMultisig { + unbondingInfo, err = pendingDel.GetMultisigUnbondingInfo(bsParams, btcCfg) + require.NoError(t, err) + } else { + unbondingInfo, err = pendingDel.GetUnbondingInfo(bsParams, btcCfg) + require.NoError(t, err) + } + unbondingSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + covenantUnbondingSlashingSigs, err := datagen.GenCovenantAdaptorSigs( + covSKs, + fpBTCPKs, + unbondingTx, + unbondingSlashingPathInfo.GetPkScriptPath(), + pendingDel.BtcUndelegation.SlashingTx, + ) + require.NoError(t, err) + + covStkExpSigs := make([]*bbn.BIP340Signature, 0, len(covSKs)) + if pendingDel.IsStakeExpansion() { + var prevDelStakingInfo *btcstaking.StakingInfo + + prevDelTxHash, err := chainhash.NewHash(pendingDel.StkExp.PreviousStakingTxHash) + require.NoError(t, err) + prevDelRes := bbn2.QueryBTCDelegation(prevDelTxHash.String()) + require.NotNil(t, prevDelRes) + prevParams := bbn2.QueryBtcStakingParamsByVersion(prevDelRes.ParamsVersion) + pDel, err := tmanager.ParseRespBTCDelToBTCDel(prevDelRes) + require.NoError(t, err) + + if isMultisig { + prevDelStakingInfo, err = pDel.GetMultisigStakingInfo(prevParams, btcCfg) + require.NoError(t, err) + } else { + prevDelStakingInfo, err = pendingDel.GetStakingInfo(bsParams, btcCfg) + require.NoError(t, err) + } + + covStkExpSigs, err = datagen.GenCovenantStakeExpSig(covSKs, pendingDel, prevDelStakingInfo) + require.NoError(t, err) + } + + for i := 0; i < int(bsParams.CovenantQuorum); i++ { + var stkExpSig *bbn.BIP340Signature + if pendingDel.IsStakeExpansion() { + stkExpSig = covStkExpSigs[i] + } + + bbn2.SubmitRefundableTxWithAssertion(func() { + bbn2.AddCovenantSigs( + bbn2.DefaultWallet().KeyName, + covenantSlashingSigs[i].CovPk, + stakingTxHash, + covenantSlashingSigs[i].AdaptorSigs, + bbn.NewBIP340SignatureFromBTCSig(covUnbondingSigs[i]), + covenantUnbondingSlashingSigs[i].AdaptorSigs, + stkExpSig, + ) + }, true, bbn2.DefaultWallet().KeyName) + } + + verifiedDelResp := bbn2.QueryBTCDelegation(stakingTxHash) + require.Equal(t, BTCDelegationVerified, verifiedDelResp.StatusDesc) + verifiedDel, err := tmanager.ParseRespBTCDelToBTCDel(verifiedDelResp) + require.NoError(t, err) + require.Len(t, verifiedDel.CovenantSigs, int(bsParams.CovenantQuorum)) + require.True(t, verifiedDel.HasCovenantQuorums(bsParams.CovenantQuorum, 0)) + + return stakingMsgTx, stakingTxHash, bsParams +} + +func waitBtcBlockForKDeepWithInclusionProof(t *testing.T, r *rand.Rand, bbn2 *tmanager.Node, stakingMsgTx *wire.MsgTx, stakingTxHash string) *bstypes.BTCDelegation { + // wait for btc block k-deep + currentBtcTipResp, err := bbn2.QueryTip() + require.NoError(t, err) + currentBtcTip, err := tmanager.ParseBTCHeaderInfoResponseToInfo(currentBtcTipResp) + blockWithStakingTx := datagen.CreateBlockWithTransaction(r, currentBtcTip.Header.ToBlockHeader(), stakingMsgTx) + bbn2.InsertHeader(&blockWithStakingTx.HeaderBytes) + + // make block k-deep + for i := 0; i < tmanager.BabylonBtcConfirmationPeriod; i++ { + bbn2.InsertNewEmptyBtcHeader(r) + } + inclusionProof := bstypes.NewInclusionProofFromSpvProof(blockWithStakingTx.SpvProof) + + // activate btc delegation by adding btc inclusion proof + bbn2.SubmitRefundableTxWithAssertion(func() { + bbn2.AddBTCDelegationInclusionProof(bbn2.DefaultWallet().KeyName, stakingTxHash, inclusionProof) + }, true, bbn2.DefaultWallet().KeyName) + + activeBtcDelResp := bbn2.QueryBTCDelegation(stakingTxHash) + require.Equal(t, BTCDelegationActive, activeBtcDelResp.StatusDesc) + activeBtcDel, err := tmanager.ParseRespBTCDelToBTCDel(activeBtcDelResp) + require.NoError(t, err) + + return activeBtcDel +} + +func waitBtcBlockForKDeepWithBTCUndelegate( + t *testing.T, + r *rand.Rand, + bbn2 *tmanager.Node, + stkExpMsgTx, prevDelStkMsgTx, fundingTx *wire.MsgTx, + prevStakingTxHash, stkExpTxHash string, +) { + // wait for btc block k-deep + currentBtcTipResp, err := bbn2.QueryTip() + require.NoError(t, err) + currentBtcTip, err := tmanager.ParseBTCHeaderInfoResponseToInfo(currentBtcTipResp) + blockWithStakingTx := datagen.CreateBlockWithTransaction(r, currentBtcTip.Header.ToBlockHeader(), stkExpMsgTx) + bbn2.InsertHeader(&blockWithStakingTx.HeaderBytes) + + // make block k-deep + for i := 0; i < tmanager.BabylonBtcConfirmationPeriod; i++ { + bbn2.InsertNewEmptyBtcHeader(r) + } + inclusionProof := bstypes.NewInclusionProofFromSpvProof(blockWithStakingTx.SpvProof) + + // activate btc delegation by adding btc inclusion proof + bbn2.SubmitRefundableTxWithAssertion(func() { + bbn2.BTCUndelegate( + bbn2.DefaultWallet().KeyName, + prevStakingTxHash, + stkExpMsgTx, + inclusionProof, + []*wire.MsgTx{ + prevDelStkMsgTx, + fundingTx, + }, + ) + }, true, bbn2.DefaultWallet().KeyName) + + activeBtcDelResp := bbn2.QueryBTCDelegation(stkExpTxHash) + require.Equal(t, BTCDelegationActive, activeBtcDelResp.StatusDesc) + activeBtcDel, err := tmanager.ParseRespBTCDelToBTCDel(activeBtcDelResp) + require.NoError(t, err) + require.NotNil(t, activeBtcDel.BtcUndelegation) +} diff --git a/test/e2ev2/tmanager/chain.go b/test/e2ev2/tmanager/chain.go index f9d006e89..a8d683185 100644 --- a/test/e2ev2/tmanager/chain.go +++ b/test/e2ev2/tmanager/chain.go @@ -32,16 +32,21 @@ var ( // ChainConfig defines configuration for a blockchain type ChainConfig struct { - ChainID string - Home string - ValidatorCount int - NodeCount int - BlockTime time.Duration - EpochLength int64 - VotingPeriod time.Duration - ExpeditedVotingPeriod time.Duration - BTCConfirmationDepth int - GasLimit int64 + ChainID string + Home string + ValidatorCount int + NodeCount int + BlockTime time.Duration + EpochLength int64 + VotingPeriod time.Duration + ExpeditedVotingPeriod time.Duration + BTCConfirmationDepth int + GasLimit int64 + IsUpgrade bool // true when chain is used for upgrade test + Tag string // Tag is only used for upgrade test + UpgradePropHeight int64 // height for upgrade plan + BootstrapRepository string // repository that will be used before upgrade + StartingBtcStakingParams *StartingBtcStakingParams // customizable x/btcstaking params when starting the new chain } // Chain represents a blockchain with multiple nodes @@ -148,7 +153,11 @@ func (c *Chain) InitGenesis() { c.UpdateWalletSequenceAndAccountNumbers(sanitizedAccs) // update all other modules - err = UpdateGenModulesState(appGenState, *c.InitialGenesis, c.Validators, nil, nil, balancesToAdd) + var startingBtcStakingParams *StartingBtcStakingParams + if c.Config.StartingBtcStakingParams != nil { + startingBtcStakingParams = c.Config.StartingBtcStakingParams + } + err = UpdateGenModulesState(appGenState, *c.InitialGenesis, c.Validators, nil, startingBtcStakingParams, balancesToAdd, c.Config.IsUpgrade) require.NoError(c.T(), err, "failed to update gen state for all other modules") appStateJSON, err := json.Marshal(appGenState) diff --git a/test/e2ev2/tmanager/container.go b/test/e2ev2/tmanager/container.go index a1f02beb8..ec5928b67 100644 --- a/test/e2ev2/tmanager/container.go +++ b/test/e2ev2/tmanager/container.go @@ -20,6 +20,9 @@ const ( // Images that do not have specified tag, latest will be used by default. // name of babylon image produced by running `make build-docker` BabylonContainerName = "babylonlabs-io/babylond" + // name of babylon image before the upgrade + BabylonContainerNameBeforeUpgrade = "babylonlabs/babylond" + BabylonContainerTagBeforeUpgrade = "v4.0.0-rc.1" HermesRelayerRepository = "informalsystems/hermes" HermesRelayerTag = "1.13.1" @@ -75,6 +78,20 @@ func NewContainerBbnNode(containerName string) *Container { } } +// NewContainerOldBbnNode create an older binary version of a bbn node which is used before upgrade +func NewContainerOldBbnNode(containerName, tag string) *Container { + cTag := BabylonContainerTagBeforeUpgrade + if tag != "" { + cTag = tag + } + + return &Container{ + Name: containerName, + Repository: BabylonContainerNameBeforeUpgrade, + Tag: cTag, + } +} + func NewContainerHermes(containerName string) *Container { return &Container{ Name: containerName, diff --git a/test/e2ev2/tmanager/genesis.go b/test/e2ev2/tmanager/genesis.go index 13e8262d4..777d99d2a 100644 --- a/test/e2ev2/tmanager/genesis.go +++ b/test/e2ev2/tmanager/genesis.go @@ -5,16 +5,19 @@ import ( "fmt" "time" + "github.com/stretchr/testify/require" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" staketypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/gogoproto/proto" + ratelimiter "github.com/cosmos/ibc-apps/modules/rate-limiting/v10/types" tokenfactorytypes "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/types" - "github.com/stretchr/testify/require" appparams "github.com/babylonlabs-io/babylon/v4/app/params" "github.com/babylonlabs-io/babylon/v4/test/e2e/util" @@ -25,8 +28,6 @@ import ( costktypes "github.com/babylonlabs-io/babylon/v4/x/costaking/types" finalitytypes "github.com/babylonlabs-io/babylon/v4/x/finality/types" minttypes "github.com/babylonlabs-io/babylon/v4/x/mint/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - ratelimiter "github.com/cosmos/ibc-apps/modules/rate-limiting/v10/types" ) const ( @@ -53,6 +54,8 @@ type InitGenesis struct { type StartingBtcStakingParams struct { CovenantCommittee []bbn.BIP340PubKey CovenantQuorum uint32 + MaxStakerQuorum uint32 + MaxStakerNum uint32 } func UpdateGenAccounts( @@ -96,6 +99,7 @@ func UpdateGenModulesState( btcHeaders []*btclighttypes.BTCHeaderInfo, startingBtcStakingParams *StartingBtcStakingParams, bankBalancesToAdd []banktypes.Balance, + isUpgrade bool, ) error { err := UpdateModuleGenesis(appGenState, banktypes.ModuleName, &banktypes.GenesisState{}, UpdateGenesisBank(bankBalancesToAdd)) if err != nil { @@ -152,9 +156,13 @@ func UpdateGenModulesState( return fmt.Errorf("failed to update tokenfactory genesis state: %w", err) } - err = UpdateModuleGenesis(appGenState, btcstktypes.ModuleName, &btcstktypes.GenesisState{}, UpdateGenesisBtcStaking(startingBtcStakingParams)) - if err != nil { - return fmt.Errorf("failed to update btc staking genesis state: %w", err) + // NOTE: in case of the software upgrade test, we don't want to update + // genesis state since it will introduce version incompatibility of genesis.json + if !isUpgrade { + err = UpdateModuleGenesis(appGenState, btcstktypes.ModuleName, &btcstktypes.GenesisState{}, UpdateGenesisBtcStaking(startingBtcStakingParams)) + if err != nil { + return fmt.Errorf("failed to update btc staking genesis state: %w", err) + } } return nil @@ -258,8 +266,12 @@ func UpdateGenesisFinality(finalityGenState *finalitytypes.GenesisState) { func UpdateGenesisBtcStaking(p *StartingBtcStakingParams) func(*btcstktypes.GenesisState) { return func(gen *btcstktypes.GenesisState) { if p != nil { - gen.Params[0].CovenantPks = p.CovenantCommittee - gen.Params[0].CovenantQuorum = p.CovenantQuorum + gen.Params[0].MaxStakerNum = p.MaxStakerNum + gen.Params[0].MaxStakerQuorum = p.MaxStakerQuorum + if len(p.CovenantCommittee) != 0 && p.CovenantQuorum != 0 { + gen.Params[0].CovenantPks = p.CovenantCommittee + gen.Params[0].CovenantQuorum = p.CovenantQuorum + } } } } diff --git a/test/e2ev2/tmanager/manager.go b/test/e2ev2/tmanager/manager.go index 0a4cb8668..9d6733f12 100644 --- a/test/e2ev2/tmanager/manager.go +++ b/test/e2ev2/tmanager/manager.go @@ -8,6 +8,10 @@ import ( "github.com/ory/dockertest/v3" "github.com/stretchr/testify/require" + + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + + v5 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v5" ) // TestManager manages isolated Docker networks for tests @@ -29,6 +33,14 @@ type TestManagerIbc struct { Hermes *HermesRelayer } +// TestManagerUpgrade manages software upgrade, which includes proposal upgrade and fork upgrade +type TestManagerUpgrade struct { + *TestManager + ForkHeight int64 // ForkHeight > 0 implies that this is a fork upgrade, otherwise, proposal upgrade +} + +type PreUpgradeFunc func([]*Node) + // NewTestManager creates a new network manager with isolated Docker network func NewTestManager(t *testing.T) *TestManager { pool, err := dockertest.NewPool("") @@ -90,6 +102,28 @@ func NewTmWithIbc(t *testing.T) *TestManagerIbc { } } +func NewTmWithUpgrade( + t *testing.T, + forkHeight int64, + tag string, +) *TestManagerUpgrade { + tm := NewTestManager(t) + bbnCfg := NewChainConfig(tm.TempDir, CHAIN_ID_BABYLON) + bbnCfg.IsUpgrade = true + // if tag is empty string, use default tag v4.0.0-rc.1 + if tag == "" { + tag = BabylonContainerTagBeforeUpgrade + } + bbnCfg.Tag = tag + bbnCfg.BootstrapRepository = BabylonContainerNameBeforeUpgrade + tm.Chains[CHAIN_ID_BABYLON] = NewChain(tm, bbnCfg) + + return &TestManagerUpgrade{ + TestManager: tm, + ForkHeight: forkHeight, + } +} + func (tm *TestManager) NetworkID() string { return tm.Network.Network.ID } @@ -119,6 +153,46 @@ func (tm *TestManagerIbc) Start() { tm.UpdateWalletsAccSeqNumber() } +// Start runs all the nodes and wait for block 1 +func (tm *TestManagerUpgrade) Start() { + tm.TestManager.Start() + + // wait for chains to produce at least one block + tm.ChainsWaitUntilHeight(1) +} + +// Upgrade executes preUpgradeFunc and processes upgrade +// NOTE: this function must be invoked after Start() +func (tm *TestManagerUpgrade) Upgrade(govMsg *govtypes.MsgSubmitProposal, preUpgradeFunc PreUpgradeFunc) { + var nodes []*Node + for _, chain := range tm.Chains { + nodes = append(nodes, chain.AllNodes()...) + } + preUpgradeFunc(nodes) + + // run upgrade either fork or proposal upgrade + if tm.ForkHeight > 0 { + tm.runForkUpgrade() + } else { + if err := tm.runProposalUpgrade(govMsg); err != nil { + tm.T.Fatalf("failed to run proposal upgrade: %v", err) + } + } + + // check if the upgrade was applied + for _, chain := range tm.Chains { + for _, node := range chain.AllNodes() { + height, err := node.LatestBlockNumber() + if err != nil { + tm.T.Fatalf("failed to get latest block height: %v", err) + } + tm.T.Logf("node %s: latest block height on chain %s: %d", node.Name, chain.ChainID(), height) + appliedHeight := node.QueryAppliedPlan(v5.UpgradeName) + tm.T.Logf("node %s: %s plan applied at height: %d", node.Name, v5.UpgradeName, appliedHeight) + } + } +} + // UpdateWalletsAccSeqNumber iterates over all chains, nodes and wallets // to update the acc sequence and number func (tm *TestManagerIbc) UpdateWalletsAccSeqNumber() { @@ -142,6 +216,18 @@ func (tm *TestManager) ChainsWaitUntilNextBlock() { } } +func (tm *TestManager) ChainNodes() []*Node { + var nodes []*Node + for _, chain := range tm.Chains { + nodes = append(nodes, chain.Nodes...) + } + return nodes +} + +func (tm *TestManager) ChainValidator() *ValidatorNode { + return tm.Chains[CHAIN_ID_BABYLON].Validators[0] +} + // GenerateNetworkID creates a unique network identifier for the test func GenerateNetworkID(t *testing.T) string { // Use test name + timestamp + random to ensure uniqueness diff --git a/test/e2ev2/tmanager/node.go b/test/e2ev2/tmanager/node.go index 9675faa2f..1c1611ef4 100644 --- a/test/e2ev2/tmanager/node.go +++ b/test/e2ev2/tmanager/node.go @@ -2,6 +2,7 @@ package tmanager import ( "context" + "encoding/json" "fmt" "io" "math/rand" @@ -15,10 +16,10 @@ import ( "testing" "time" - "github.com/babylonlabs-io/babylon/v4/testutil/datagen" - blc "github.com/babylonlabs-io/babylon/v4/x/btclightclient/types" - - "encoding/json" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" sdkmath "cosmossdk.io/math" appsigner "github.com/babylonlabs-io/babylon/v4/app/signer" @@ -37,16 +38,14 @@ import ( "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" - "github.com/spf13/viper" - "github.com/stretchr/testify/require" bbnapp "github.com/babylonlabs-io/babylon/v4/app" appparams "github.com/babylonlabs-io/babylon/v4/app/params" "github.com/babylonlabs-io/babylon/v4/cmd/babylond/cmd" "github.com/babylonlabs-io/babylon/v4/test/e2e/util" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" bbn "github.com/babylonlabs-io/babylon/v4/types" + blc "github.com/babylonlabs-io/babylon/v4/x/btclightclient/types" checkpointingtypes "github.com/babylonlabs-io/babylon/v4/x/checkpointing/types" ) @@ -95,7 +94,7 @@ type ValidatorNode struct { func NewNode(tm *TestManager, name string, cfg *ChainConfig) *Node { n := NewNodeWithoutBls(tm, name, cfg) // even regular nodes needs bls keys - // to avoid erros in signer.LoadOrGenBlsKey + // to avoid errors in signer.LoadOrGenBlsKey _, err := GenBlsKey(n.Home) require.NoError(n.T(), err) return n @@ -106,13 +105,19 @@ func NewNodeWithoutBls(tm *TestManager, name string, cfg *ChainConfig) *Node { nPorts, err := tm.PortMgr.AllocateNodePorts() require.NoError(tm.T, err) - cointanerName := fmt.Sprintf("%s-%s-%s", cfg.ChainID, name, tm.NetworkID()[:4]) + containerName := fmt.Sprintf("%s-%s-%s", cfg.ChainID, name, tm.NetworkID()[:4]) + container := NewContainerBbnNode(containerName) + if cfg.IsUpgrade { + // build a container with the given tag before upgrade + container = NewContainerOldBbnNode(containerName, cfg.Tag) + } + n := &Node{ Tm: tm, ChainConfig: cfg, Name: name, Home: filepath.Join(cfg.Home, name), - Container: NewContainerBbnNode(cointanerName), + Container: container, Ports: nPorts, Wallets: make(map[string]*WalletSender, 0), } @@ -167,6 +172,18 @@ func (n *Node) ContainerResource() *dockertest.Resource { return n.Tm.ContainerManager.Resources[n.Container.Name] } +func (n *Node) RemoveResource() error { + resource := n.ContainerResource() + var opts docker.RemoveContainerOptions + opts.ID = resource.Container.ID + opts.Force = true + if err := n.Tm.Pool.Client.RemoveContainer(opts); err != nil { + return err + } + delete(n.Tm.ContainerManager.Resources, n.Container.Name) + return nil +} + func (n *ValidatorNode) CreateValidatorMsg(selfDelegationAmt sdk.Coin) sdk.Msg { description := stakingtypes.NewDescription(n.Name, "", "", "", "") commissionRates := stakingtypes.CommissionRates{ @@ -364,18 +381,28 @@ func (n *Node) WriteConfigAndGenesis() { config.SetRoot(n.Home) config.Moniker = n.Name + if n.ChainConfig.BootstrapRepository != "" { + n.ensureBootstrapGenesis(config) + } + appGenesis, err := AppGenesisFromConfig(n.Home) require.NoError(n.T(), err) - // Create a temp app to get the default genesis state - tempApp := bbnapp.NewTmpBabylonApp() - appState, err := json.MarshalIndent(tempApp.DefaultGenesis(), "", " ") - require.NoError(n.T(), err) + if len(appGenesis.AppState) == 0 { + tempApp := bbnapp.NewTmpBabylonApp() + appState, err := json.MarshalIndent(tempApp.DefaultGenesis(), "", " ") + require.NoError(n.T(), err) + appGenesis.AppState = appState + } appGenesis.ChainID = n.ChainConfig.ChainID - appGenesis.AppState = appState - appGenesis.Consensus = &genutiltypes.ConsensusGenesis{ - Params: cmttypes.DefaultConsensusParams(), + if appGenesis.Consensus == nil { + appGenesis.Consensus = &genutiltypes.ConsensusGenesis{ + Params: cmttypes.DefaultConsensusParams(), + } + } + if appGenesis.Consensus.Params == nil { + appGenesis.Consensus.Params = cmttypes.DefaultConsensusParams() } appGenesis.Consensus.Params.Block.MaxGas = n.ChainConfig.GasLimit appGenesis.Consensus.Params.ABCI.VoteExtensionsEnableHeight = bbnapp.DefaultVoteExtensionsEnableHeight @@ -385,6 +412,56 @@ func (n *Node) WriteConfigAndGenesis() { cmtconfig.WriteConfigFile(filepath.Join(n.ConfigDirPath(), "config.toml"), config) } +func (n *Node) ensureBootstrapGenesis(config *cmtconfig.Config) { + genesisFile := config.GenesisFile() + + _, err := os.Stat(genesisFile) + if err == nil { + return + } + require.Truef(n.T(), os.IsNotExist(err), "failed to check genesis file before bootstrap: %v", err) + + currentUser, err := user.Current() + require.NoError(n.T(), err) + + userSpec := fmt.Sprintf("%s:%s", currentUser.Uid, currentUser.Gid) + containerName := fmt.Sprintf("%s-bootstrap-%s", n.Container.Name, n.Name) + + script := fmt.Sprintf(` +set -euo pipefail +export BABYLON_HOME=%s +export BABYLON_BLS_PASSWORD=password +mkdir -p "$BABYLON_HOME/config" +rm -f "$BABYLON_HOME/config/genesis.json" +rm -f "$BABYLON_HOME/config/app.toml" +rm -f "$BABYLON_HOME/config/config.toml" +rm -rf "$BABYLON_HOME/data" +babylond init %s --chain-id %s --home $BABYLON_HOME +`, BabylonHomePathInContainer, n.Name, n.ChainConfig.ChainID) + + runOpts := &dockertest.RunOptions{ + Name: containerName, + Repository: n.ChainConfig.BootstrapRepository, + Tag: n.ChainConfig.Tag, + NetworkID: n.Tm.NetworkID(), + User: userSpec, + Entrypoint: []string{"sh", "-c", script}, + Mounts: []string{ + fmt.Sprintf("%s/:%s", n.Home, BabylonHomePathInContainer), + }, + } + + resource, err := n.Tm.ContainerManager.Pool.RunWithOptions(runOpts, NoRestart) + require.NoError(n.T(), err, "failed to run bootstrap container") + + exitCode, err := n.Tm.ContainerManager.Pool.Client.WaitContainer(resource.Container.ID) + require.NoError(n.T(), err, "failed waiting for bootstrap container") + require.Equal(n.T(), 0, exitCode, "bootstrap container exited with non-zero code") + + err = resource.Close() + require.NoError(n.T(), err, "failed to clean up bootstrap container") +} + func (n *Node) InitConfigWithPeers(persistentPeers []string) { cmtCfgPath := filepath.Join(n.ConfigDirPath(), "config.toml") @@ -453,7 +530,7 @@ func (n *Node) RunNodeResource() *dockertest.Resource { if !n.Container.ImageExistsLocally() { // builds it locally if it doesn't have // needs to be in the path where the makefile is located '-' - err := RunMakeCommand(filepath.Join(pwd, "../../"), "build-docker-e2e") + err := RunMakeCommand(filepath.Join(pwd, "../../contrib/images"), "babylond-e2e") require.NoError(n.T(), err) } @@ -679,6 +756,13 @@ func (n *Node) RequireTxSuccess(txHash string) { require.Equal(n.T(), uint32(0), txResp.TxResponse.Code, "Transaction %s failed with code %d: %s", txHash, txResp.TxResponse.Code, txResp.TxResponse.RawLog) } +// RequireTxErrorContain queries a transaction by hash and requires it to have code other than 0 (fail) +func (n *Node) RequireTxErrorContain(txHash string, err string) { + txResp := n.QueryTxByHash(txHash) + require.NotEqual(n.T(), uint32(0), txResp.TxResponse.Code, "Transaction %s response code shouldn't be 0", txHash) + require.Contains(n.T(), txResp.TxResponse.RawLog, err, "Transaction %s doesn't contain expected error %s", txHash, err) +} + // UpdateWalletsAccSeqNumber updates all wallets in a node by querying the chain func (n *Node) UpdateWalletsAccSeqNumber() { addrs := make([]string, 0) @@ -819,6 +903,15 @@ func (n *Node) InsertNewEmptyBtcHeader(r *rand.Rand) *blc.BTCHeaderInfo { return child } +// InsertHeader inserts a BTC header to the chain +func (n *Node) InsertHeader(h *bbn.BTCHeaderBytes) { + tip, err := n.QueryTip() + require.NoError(n.T(), err) + n.T().Logf("Retrieved current tip of btc headerchain. Height: %d", tip.Height) + n.SendHeaderHex(h.MarshalHex()) + n.WaitUntilBtcHeight(tip.Height + 1) +} + // SendHeaderHex sends a BTC header in hex format to the node func (n *Node) SendHeaderHex(headerHex string) { wallet := n.Wallet("node-key") @@ -834,3 +927,28 @@ func (n *Node) SendHeaderHex(headerHex string) { _, tx := wallet.SubmitMsgs(msg) require.NotNil(n.T(), tx, "RegisterConsumerChain transaction should not be nil") } + +// SubmitRefundableTxWithAssertion submits a refundable transaction, +// and asserts that the tx fee is refunded +func (n *Node) SubmitRefundableTxWithAssertion( + f func(), + shouldBeRefunded bool, + walletName string, +) { + wallet := n.Wallet(walletName) + require.NotNil(n.T(), wallet, "Wallet %s should not be nil", walletName) + + // balance before submitting the refundable tx + submitterBalanceBefore := n.QueryAllBalances(wallet.Address.String()) + + // submit refundable tx + f() + + // ensure the tx fee is refunded and the balance is not changed + submitterBalanceAfter := n.QueryAllBalances(wallet.Address.String()) + if shouldBeRefunded { + require.Equal(n.T(), submitterBalanceBefore, submitterBalanceAfter) + } else { + require.False(n.T(), submitterBalanceBefore.Equal(submitterBalanceAfter)) + } +} diff --git a/test/e2ev2/tmanager/node_queries.go b/test/e2ev2/tmanager/node_queries.go index c506832cf..f2450ebeb 100644 --- a/test/e2ev2/tmanager/node_queries.go +++ b/test/e2ev2/tmanager/node_queries.go @@ -2,22 +2,29 @@ package tmanager import ( "context" + "encoding/hex" "fmt" "net" "net/url" - "github.com/babylonlabs-io/babylon/v4/test/e2e/util" - bbn "github.com/babylonlabs-io/babylon/v4/types" - btclighttypes "github.com/babylonlabs-io/babylon/v4/x/btclightclient/types" - ictvtypes "github.com/babylonlabs-io/babylon/v4/x/incentive/types" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + upgradetypes "cosmossdk.io/x/upgrade/types" sdk "github.com/cosmos/cosmos-sdk/types" sdktx "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + + "github.com/babylonlabs-io/babylon/v4/test/e2e/util" + bbn "github.com/babylonlabs-io/babylon/v4/types" + btclighttypes "github.com/babylonlabs-io/babylon/v4/x/btclightclient/types" + btcstktypes "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" + ictvtypes "github.com/babylonlabs-io/babylon/v4/x/incentive/types" ) // ParseBTCHeaderInfoResponseToInfo converts BTCHeaderInfoResponse to BTCHeaderInfo @@ -76,6 +83,27 @@ func (n *Node) IncentiveQuery(f func(ictvtypes.QueryClient)) { }) } +func (n *Node) BtcStkQuery(f func(btcstktypes.QueryClient)) { + n.GrpcConn(func(conn *grpc.ClientConn) { + btcStakingClient := btcstktypes.NewQueryClient(conn) + f(btcStakingClient) + }) +} + +func (n *Node) GovQuery(f func(govtypes.QueryClient)) { + n.GrpcConn(func(conn *grpc.ClientConn) { + govClient := govtypes.NewQueryClient(conn) + f(govClient) + }) +} + +func (n *Node) UpgradeQuery(f func(upgradetypes.QueryClient)) { + n.GrpcConn(func(conn *grpc.ClientConn) { + upgradeClient := upgradetypes.NewQueryClient(conn) + f(upgradeClient) + }) +} + func (n *Node) LatestBlockNumber() (uint64, error) { status, err := n.RpcClient.Status(context.Background()) if err != nil { @@ -206,6 +234,130 @@ func (n *Node) QueryIctvRewardGauges(addrs []string, holderType ictvtypes.Stakeh return rewards } +func (n *Node) QueryBtcStakingParams() *btcstktypes.Params { + var ( + resp *btcstktypes.QueryParamsResponse + err error + ) + + n.BtcStkQuery(func(btcStkClient btcstktypes.QueryClient) { + resp, err = btcStkClient.Params(context.Background(), &btcstktypes.QueryParamsRequest{}) + require.NoError(n.T(), err) + }) + + return &resp.Params +} + +func (n *Node) QueryBtcStakingParamsByVersion(version uint32) *btcstktypes.Params { + var ( + resp *btcstktypes.QueryParamsByVersionResponse + err error + ) + + n.BtcStkQuery(func(btcStkClient btcstktypes.QueryClient) { + resp, err = btcStkClient.ParamsByVersion(context.Background(), &btcstktypes.QueryParamsByVersionRequest{ + Version: version, + }) + require.NoError(n.T(), err) + }) + + return &resp.Params +} + +func (n *Node) QueryBTCDelegation(stakingTxHash string) *btcstktypes.BTCDelegationResponse { + var ( + resp *btcstktypes.QueryBTCDelegationResponse + err error + ) + + n.BtcStkQuery(func(btcStkClient btcstktypes.QueryClient) { + resp, err = btcStkClient.BTCDelegation(context.Background(), &btcstktypes.QueryBTCDelegationRequest{ + StakingTxHashHex: stakingTxHash, + }) + require.NoError(n.T(), err) + }) + + return resp.BtcDelegation +} + +func (n *Node) QueryBTCDelegations(status btcstktypes.BTCDelegationStatus) []*btcstktypes.BTCDelegationResponse { + var ( + resp *btcstktypes.QueryBTCDelegationsResponse + err error + ) + + n.BtcStkQuery(func(btcStkClient btcstktypes.QueryClient) { + resp, err = btcStkClient.BTCDelegations(context.Background(), &btcstktypes.QueryBTCDelegationsRequest{ + Status: status, + }) + require.NoError(n.T(), err) + }) + + return resp.BtcDelegations +} + +func (n *Node) QueryFinalityProvider(fpBtcPkHex string) *btcstktypes.FinalityProviderResponse { + var ( + resp *btcstktypes.QueryFinalityProviderResponse + err error + ) + + n.BtcStkQuery(func(btcStkClient btcstktypes.QueryClient) { + resp, err = btcStkClient.FinalityProvider(context.Background(), &btcstktypes.QueryFinalityProviderRequest{ + FpBtcPkHex: fpBtcPkHex, + }) + require.NoError(n.T(), err) + }) + + return resp.FinalityProvider +} + +func (n *Node) QueryProposals() *govtypes.QueryProposalsResponse { + var ( + resp *govtypes.QueryProposalsResponse + err error + ) + + n.GovQuery(func(govClient govtypes.QueryClient) { + resp, err = govClient.Proposals(context.Background(), &govtypes.QueryProposalsRequest{}) + require.NoError(n.T(), err) + }) + + return resp +} + +func (n *Node) QueryTallyResult(propID uint64) *govtypes.TallyResult { + var ( + resp *govtypes.QueryTallyResultResponse + err error + ) + + n.GovQuery(func(govClient govtypes.QueryClient) { + resp, err = govClient.TallyResult(context.Background(), &govtypes.QueryTallyResultRequest{ + ProposalId: propID, + }) + require.NoError(n.T(), err) + }) + + return resp.Tally +} + +func (n *Node) QueryAppliedPlan(planName string) int64 { + var ( + resp *upgradetypes.QueryAppliedPlanResponse + err error + ) + + n.UpgradeQuery(func(upgradeClient upgradetypes.QueryClient) { + resp, err = upgradeClient.AppliedPlan(context.Background(), &upgradetypes.QueryAppliedPlanRequest{ + Name: planName, + }) + require.NoError(n.T(), err) + }) + + return resp.Height +} + // QueryLatestEpochHeaderCLI retrieves the latest epoch header for the specified consumer ID using CLI func (n *Node) QueryLatestEpochHeaderCLI(consumerID string) string { cmd := []string{"babylond", "query", "zc", "latest-epoch-header", consumerID, "--output=json", "--node", n.GetRpcEndpoint()} @@ -234,3 +386,105 @@ func (n *Node) QueryGetSealedEpochProofCLI(epochNum uint64) string { func (n *Node) GetRpcEndpoint() string { return "tcp://" + net.JoinHostPort(n.Container.Name, fmt.Sprintf("%d", n.Ports.RPC)) } + +// ParseRespBTCDelToBTCDel parses an BTC delegation response to BTC Delegation +func ParseRespBTCDelToBTCDel(resp *btcstktypes.BTCDelegationResponse) (btcDel *btcstktypes.BTCDelegation, err error) { + stakingTx, err := hex.DecodeString(resp.StakingTxHex) + if err != nil { + return nil, err + } + + delSig, err := bbn.NewBIP340SignatureFromHex(resp.DelegatorSlashSigHex) + if err != nil { + return nil, err + } + + slashingTx, err := btcstktypes.NewBTCSlashingTxFromHex(resp.SlashingTxHex) + if err != nil { + return nil, err + } + + btcDel = &btcstktypes.BTCDelegation{ + StakerAddr: resp.StakerAddr, + BtcPk: resp.BtcPk, + FpBtcPkList: resp.FpBtcPkList, + StartHeight: resp.StartHeight, + StakingTime: resp.StakingTime, + EndHeight: resp.EndHeight, + TotalSat: resp.TotalSat, + StakingTx: stakingTx, + DelegatorSig: delSig, + StakingOutputIdx: resp.StakingOutputIdx, + CovenantSigs: resp.CovenantSigs, + UnbondingTime: resp.UnbondingTime, + SlashingTx: slashingTx, + } + + if resp.UndelegationResponse != nil { + ud := resp.UndelegationResponse + unbondTx, err := hex.DecodeString(ud.UnbondingTxHex) + if err != nil { + return nil, err + } + + slashTx, err := btcstktypes.NewBTCSlashingTxFromHex(ud.SlashingTxHex) + if err != nil { + return nil, err + } + + delSlashingSig, err := bbn.NewBIP340SignatureFromHex(ud.DelegatorSlashingSigHex) + if err != nil { + return nil, err + } + + btcDel.BtcUndelegation = &btcstktypes.BTCUndelegation{ + UnbondingTx: unbondTx, + CovenantUnbondingSigList: ud.CovenantUnbondingSigList, + CovenantSlashingSigs: ud.CovenantSlashingSigs, + SlashingTx: slashTx, + DelegatorSlashingSig: delSlashingSig, + } + + if ud.DelegatorUnbondingInfoResponse != nil { + var spendStakeTx []byte = make([]byte, 0) + if ud.DelegatorUnbondingInfoResponse.SpendStakeTxHex != "" { + spendStakeTx, err = hex.DecodeString(ud.DelegatorUnbondingInfoResponse.SpendStakeTxHex) + if err != nil { + return nil, err + } + } + + btcDel.BtcUndelegation.DelegatorUnbondingInfo = &btcstktypes.DelegatorUnbondingInfo{ + SpendStakeTx: spendStakeTx, + } + } + } + + if resp.StkExp != nil { + prevTxHash, err := chainhash.NewHashFromStr(resp.StkExp.PreviousStakingTxHashHex) + if err != nil { + return nil, err + } + + otherFundOutput, err := hex.DecodeString(resp.StkExp.OtherFundingTxOutHex) + if err != nil { + return nil, err + } + btcDel.StkExp = &btcstktypes.StakeExpansion{ + PreviousStakingTxHash: prevTxHash.CloneBytes(), + OtherFundingTxOut: otherFundOutput, + PreviousStkCovenantSigs: resp.StkExp.PreviousStkCovenantSigs, + } + } + + if resp.MultisigInfo != nil { + btcDel.MultisigInfo = &btcstktypes.AdditionalStakerInfo{ + StakerBtcPkList: resp.MultisigInfo.StakerBtcPkList, + StakerQuorum: resp.MultisigInfo.StakerQuorum, + DelegatorSlashingSigs: resp.MultisigInfo.DelegatorSlashingSigs, + DelegatorUnbondingSlashingSigs: resp.MultisigInfo.DelegatorUnbondingSlashingSigs, + } + } + + return btcDel, nil +} diff --git a/test/e2ev2/tmanager/node_txs.go b/test/e2ev2/tmanager/node_txs.go index e88b2ffc3..446d99fb2 100644 --- a/test/e2ev2/tmanager/node_txs.go +++ b/test/e2ev2/tmanager/node_txs.go @@ -1,16 +1,25 @@ package tmanager import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + "math/rand" + "testing" "time" + "github.com/stretchr/testify/require" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" tokenfactorytypes "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/types" - "github.com/stretchr/testify/require" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" + bbn "github.com/babylonlabs-io/babylon/v4/types" bstypes "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" ) @@ -71,6 +80,9 @@ func (n *Node) MintDenom(walletName, amount, denom string) { n.T().Logf("Minted %s %s to %s", amount, denom, wallet.Address.String()) } +/* + x/btcstaking txs +*/ // CreateFinalityProvider creates a finality provider on the given chain/consumer using the specified wallet func (n *Node) CreateFinalityProvider(walletName string, fp *bstypes.FinalityProvider) { wallet := n.Wallet(walletName) @@ -95,3 +107,444 @@ func (n *Node) CreateFinalityProvider(walletName string, fp *bstypes.FinalityPro require.NotNil(n.T(), tx, "CreateFinalityProvider transaction should not be nil") n.T().Logf("Created finality provider: %s", fp.BtcPk.MarshalHex()) } + +// CreateBTCDelegation submits a BTC delegation transaction with a specified wallet +func (n *Node) CreateBTCDelegation(walletName string, msg *bstypes.MsgCreateBTCDelegation) string { + wallet := n.Wallet(walletName) + require.NotNil(n.T(), wallet, "Wallet %s not found", walletName) + + txHash, tx := wallet.SubmitMsgs(msg) + require.NotNil(n.T(), tx, "CreateBTCDelegation transaction should not be nil") + n.T().Logf("BTC delegation created, tx hash: %s", txHash) + return txHash +} + +// AddCovenantSigs submits covenant signatures of the covenant committee with a specified wallet +func (n *Node) AddCovenantSigs( + walletName string, + covPK *bbn.BIP340PubKey, + stakingTxHash string, + slashingSigs [][]byte, + unbondingSig *bbn.BIP340Signature, + unbondingSlashingSigs [][]byte, + stakeExpTxSig *bbn.BIP340Signature, +) { + wallet := n.Wallet(walletName) + require.NotNil(n.T(), wallet, "Wallet %s not found", walletName) + + msg := &bstypes.MsgAddCovenantSigs{ + Signer: wallet.Address.String(), + Pk: covPK, + StakingTxHash: stakingTxHash, + SlashingTxSigs: slashingSigs, + UnbondingTxSig: unbondingSig, + SlashingUnbondingTxSigs: unbondingSlashingSigs, + StakeExpansionTxSig: stakeExpTxSig, + } + _, tx := wallet.SubmitMsgs(msg) + require.NotNil(n.T(), tx, "AddCovenantSigs transaction should not be nil") + n.T().Logf("Covenant signatures added") +} + +// AddBTCDelegationInclusionProof adds btc delegation inclusion proof with a specified wallet +func (n *Node) AddBTCDelegationInclusionProof( + walletName string, + stakingTxHash string, + inclusionProof *bstypes.InclusionProof, +) { + wallet := n.Wallet(walletName) + require.NotNil(n.T(), wallet, "Wallet %s not found", walletName) + + msg := &bstypes.MsgAddBTCDelegationInclusionProof{ + Signer: wallet.Address.String(), + StakingTxHash: stakingTxHash, + StakingTxInclusionProof: inclusionProof, + } + _, tx := wallet.SubmitMsgs(msg) + require.NotNil(n.T(), tx, "AddBTCDelegationInclusionProof transaction should not be nil") + n.T().Logf("BTC delegation inclusion proof added") +} + +func (n *Node) BtcStakeExpand( + walletName string, + prevDel *bstypes.BTCDelegation, + r *rand.Rand, + stakerSKs []*btcec.PrivateKey, + stakerQuorum uint32, + fpPK *btcec.PublicKey, + expErr error, +) (*datagen.TestStakingSlashingInfo, *wire.MsgTx) { + wallet := n.Wallet(walletName) + require.NotNil(n.T(), wallet, "Wallet %s not found", walletName) + + var ( + msg *bstypes.MsgBtcStakeExpand + testStakingInfo *datagen.TestStakingSlashingInfo + fundingTx *wire.MsgTx + ) + + if len(stakerSKs) == 1 { + msg, testStakingInfo, fundingTx = n.createBtcStakeExpandMessage( + n.T(), r, + wallet, + stakerSKs[0], + fpPK, + int64(2*10e8), + 1000, + prevDel, + ) + } else { + msg, testStakingInfo, fundingTx = n.createMultisigBtcStakeExpandMessage( + n.T(), r, + wallet, + stakerSKs, + stakerQuorum, + fpPK, + int64(2*10e8), + 1000, + prevDel, + ) + } + + _, tx := wallet.SubmitMsgsWithErrContain(expErr, msg) + require.NotNil(n.T(), tx, "BtcStakeExpand transaction should not be nil") + n.T().Logf("BtcStakeExpand transaction submitted") + + return testStakingInfo, fundingTx +} + +func (n *Node) BTCUndelegate( + walletName string, + stakingTxHash string, + spendStakeTx *wire.MsgTx, + spendStakeInclusionProof *bstypes.InclusionProof, + fundingTxs []*wire.MsgTx, +) string { + wallet := n.Wallet(walletName) + require.NotNil(n.T(), wallet, "Wallet %s not found", walletName) + + spendStakeTxBz, err := bbn.SerializeBTCTx(spendStakeTx) + require.NoError(n.T(), err) + fundingTxsBz := make([][]byte, 0, len(fundingTxs)) + for _, tx := range fundingTxs { + fundingTxBz, err := bbn.SerializeBTCTx(tx) + require.NoError(n.T(), err) + fundingTxsBz = append(fundingTxsBz, fundingTxBz) + } + + txHash, tx := wallet.SubmitMsgs(&bstypes.MsgBTCUndelegate{ + Signer: wallet.Address.String(), + StakingTxHash: stakingTxHash, + StakeSpendingTx: spendStakeTxBz, + StakeSpendingTxInclusionProof: spendStakeInclusionProof, + FundingTransactions: fundingTxsBz, + }) + require.NotNil(n.T(), tx, "BTCUndelegate transaction should not be nil") + + return txHash +} + +/* + x/gov txs +*/ +// SubmitProposal submits a governance proposal with a specified wallet +func (n *Node) SubmitProposal(walletName string, govMsg *govtypes.MsgSubmitProposal) { + wallet := n.Wallet(walletName) + require.NotNil(n.T(), wallet, "Wallet %s not found", walletName) + + _, tx := wallet.SubmitMsgs(govMsg) + require.NotNil(n.T(), tx, "SubmitProposal transaction should not be nil") + n.T().Logf("Governance proposal submitted") +} + +func (n *Node) Vote(walletName string, proposalID uint64, voteOption govtypes.VoteOption) { + wallet := n.Wallet(walletName) + require.NotNil(n.T(), wallet, "Wallet %s not found", walletName) + + govMsg := &govtypes.MsgVote{ + ProposalId: proposalID, + Voter: wallet.Address.String(), + Option: voteOption, + Metadata: "", + } + _, tx := wallet.SubmitMsgs(govMsg) + require.NotNil(n.T(), tx, "Vote transaction should not be nil") + n.T().Logf("Governance vote submitted") +} + +/* + helper functions +*/ + +// createBtcStakeExpandMessage create a btc stake expansion message and return +// MsgBtcStakeExpand, staking info, and funding tx +func (n *Node) createBtcStakeExpandMessage( + t *testing.T, + r *rand.Rand, + wallet *WalletSender, + stakerSK *btcec.PrivateKey, + fpPK *btcec.PublicKey, + stakingValue int64, + stakingTime uint16, + prevDel *bstypes.BTCDelegation, +) (*bstypes.MsgBtcStakeExpand, *datagen.TestStakingSlashingInfo, *wire.MsgTx) { + params := n.QueryBtcStakingParams() + net := &chaincfg.SimNetParams + + covPKs, err := bbn.NewBTCPKsFromBIP340PKs(params.CovenantPks) + require.NoError(t, err) + + // create funding transaction + fundingTx := datagen.GenRandomTxWithOutputValue(r, 10000000) + + // convert previousStakingTxHash to OutPoint + prevDelTxHash := prevDel.MustGetStakingTxHash() + prevStakingOutPoint := wire.NewOutPoint(&prevDelTxHash, datagen.StakingOutIdx) + + // convert fundingTxHash to OutPoint + fundingTxHash := fundingTx.TxHash() + fundingOutPoint := wire.NewOutPoint(&fundingTxHash, 0) + outPoints := []*wire.OutPoint{prevStakingOutPoint, fundingOutPoint} + + // Generate staking slashing info using multiple inputs + stakingSlashingInfo := datagen.GenBTCStakingSlashingInfoWithInputs( + r, + t, + net, + outPoints, + stakerSK, + []*btcec.PublicKey{fpPK}, + covPKs, + params.CovenantQuorum, + stakingTime, + stakingValue, + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + slashingPathSpendInfo, err := stakingSlashingInfo.StakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + // sign the slashing tx with the staker + delegatorSig, err := stakingSlashingInfo.SlashingTx.Sign( + stakingSlashingInfo.StakingTx, 0, + slashingPathSpendInfo.GetPkScriptPath(), + stakerSK, + ) + require.NoError(t, err) + + stkTxHash := stakingSlashingInfo.StakingTx.TxHash() + unbondingValue := uint64(stakingValue) - uint64(params.UnbondingFeeSat) + + // Generate unbonding slashing info + unbondingSlashingInfo := datagen.GenBTCUnbondingSlashingInfo( + r, + t, + net, + stakerSK, + []*btcec.PublicKey{fpPK}, + covPKs, + params.CovenantQuorum, + wire.NewOutPoint(&stkTxHash, datagen.StakingOutIdx), + uint16(params.UnbondingTimeBlocks), + int64(unbondingValue), + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + // sign unbonding slashing tx with staker + delUnbondingSig, err := unbondingSlashingInfo.GenDelSlashingTxSig(stakerSK) + require.NoError(t, err) + + // generate PoP for primary staker + pop, err := datagen.NewPoPBTC(wallet.Address, stakerSK) + require.NoError(t, err) + + // serialize transactions + serializedStakingTx, err := bbn.SerializeBTCTx(stakingSlashingInfo.StakingTx) + require.NoError(t, err) + serializedUnbondingTx, err := bbn.SerializeBTCTx(unbondingSlashingInfo.UnbondingTx) + require.NoError(t, err) + fundingTxBz, err := bbn.SerializeBTCTx(fundingTx) + require.NoError(t, err) + + return &bstypes.MsgBtcStakeExpand{ + StakerAddr: prevDel.StakerAddr, + Pop: pop, + BtcPk: bbn.NewBIP340PubKeyFromBTCPK(stakerSK.PubKey()), + FpBtcPkList: []bbn.BIP340PubKey{*bbn.NewBIP340PubKeyFromBTCPK(fpPK)}, + StakingTime: uint32(stakingTime), + StakingValue: stakingValue, + StakingTx: serializedStakingTx, + SlashingTx: stakingSlashingInfo.SlashingTx, + DelegatorSlashingSig: delegatorSig, + UnbondingValue: int64(unbondingValue), + UnbondingTime: params.UnbondingTimeBlocks, + UnbondingTx: serializedUnbondingTx, + UnbondingSlashingTx: unbondingSlashingInfo.SlashingTx, + DelegatorUnbondingSlashingSig: delUnbondingSig, + PreviousStakingTxHash: prevDelTxHash.String(), + FundingTx: fundingTxBz, + }, stakingSlashingInfo, fundingTx +} + +// createMultisigBtcStakeExpandMessage create a multisig btc stake expansion message and return +// MsgBtcStakeExpand, staking info, and funding tx +func (n *Node) createMultisigBtcStakeExpandMessage( + t *testing.T, + r *rand.Rand, + wallet *WalletSender, + stakerSKs []*btcec.PrivateKey, + stakerQuorum uint32, + fpPK *btcec.PublicKey, + stakingValue int64, + stakingTime uint16, + prevDel *bstypes.BTCDelegation, +) (*bstypes.MsgBtcStakeExpand, *datagen.TestStakingSlashingInfo, *wire.MsgTx) { + params := n.QueryBtcStakingParams() + net := &chaincfg.SimNetParams + + covPKs, err := bbn.NewBTCPKsFromBIP340PKs(params.CovenantPks) + require.NoError(t, err) + + // create funding transaction + fundingTx := datagen.GenRandomTxWithOutputValue(r, 10000000) + + // convert previousStakingTxHash to OutPoint + prevDelTxHash := prevDel.MustGetStakingTxHash() + prevStakingOutPoint := wire.NewOutPoint(&prevDelTxHash, datagen.StakingOutIdx) + + // convert fundingTxHash to OutPoint + fundingTxHash := fundingTx.TxHash() + fundingOutPoint := wire.NewOutPoint(&fundingTxHash, 0) + outPoints := []*wire.OutPoint{prevStakingOutPoint, fundingOutPoint} + + // Generate staking slashing info using multiple inputs + stakingSlashingInfo := datagen.GenMultisigBTCStakingSlashingInfoWithInputs( + r, + t, + net, + outPoints, + stakerSKs, + stakerQuorum, + []*btcec.PublicKey{fpPK}, + covPKs, + params.CovenantQuorum, + stakingTime, + stakingValue, + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + 10000, + ) + + slashingPathSpendInfo, err := stakingSlashingInfo.StakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + // sign the slashing tx with the primary staker + delegatorSig, err := stakingSlashingInfo.SlashingTx.Sign( + stakingSlashingInfo.StakingTx, 0, + slashingPathSpendInfo.GetPkScriptPath(), + stakerSKs[0], + ) + require.NoError(t, err) + + // generate extra staker signatures (for remaining stakers) + var extraSlashingSigs []*bstypes.SignatureInfo + stakerSKList := stakerSKs[1:stakerQuorum] + for _, sk := range stakerSKList { + sig, err := stakingSlashingInfo.SlashingTx.Sign( + stakingSlashingInfo.StakingTx, 0, + slashingPathSpendInfo.GetPkScriptPath(), + sk, + ) + require.NoError(t, err) + + extraSlashingSigs = append(extraSlashingSigs, &bstypes.SignatureInfo{ + Pk: bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()), + Sig: sig, + }) + } + + stkTxHash := stakingSlashingInfo.StakingTx.TxHash() + unbondingValue := uint64(stakingValue) - uint64(params.UnbondingFeeSat) + + // Generate unbonding slashing info + unbondingSlashingInfo := datagen.GenMultisigBTCUnbondingSlashingInfo( + r, + t, + net, + stakerSKs, + stakerQuorum, + []*btcec.PublicKey{fpPK}, + covPKs, + params.CovenantQuorum, + wire.NewOutPoint(&stkTxHash, datagen.StakingOutIdx), + uint16(params.UnbondingTimeBlocks), + int64(unbondingValue), + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + // sign unbonding slashing tx with primary staker + delUnbondingSig, err := unbondingSlashingInfo.GenDelSlashingTxSig(stakerSKs[0]) + require.NoError(t, err) + + // generate extra unbonding signatures + var extraUnbondingSigs []*bstypes.SignatureInfo + for _, sk := range stakerSKList { + sig, err := unbondingSlashingInfo.GenDelSlashingTxSig(sk) + require.NoError(t, err) + + extraUnbondingSigs = append(extraUnbondingSigs, &bstypes.SignatureInfo{ + Pk: bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()), + Sig: sig, + }) + } + + // generate PoP for primary staker + pop, err := datagen.NewPoPBTC(wallet.Address, stakerSKs[0]) + require.NoError(t, err) + + // serialize transactions + serializedStakingTx, err := bbn.SerializeBTCTx(stakingSlashingInfo.StakingTx) + require.NoError(t, err) + serializedUnbondingTx, err := bbn.SerializeBTCTx(unbondingSlashingInfo.UnbondingTx) + require.NoError(t, err) + fundingTxBz, err := bbn.SerializeBTCTx(fundingTx) + require.NoError(t, err) + + // build extra staker PK list (all stakers except the first one) + extraStakerPKs := make([]bbn.BIP340PubKey, len(stakerSKs)-1) + for i, sk := range stakerSKs[1:] { + extraStakerPKs[i] = *bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()) + } + + return &bstypes.MsgBtcStakeExpand{ + StakerAddr: prevDel.StakerAddr, + Pop: pop, + BtcPk: bbn.NewBIP340PubKeyFromBTCPK(stakerSKs[0].PubKey()), + FpBtcPkList: []bbn.BIP340PubKey{*bbn.NewBIP340PubKeyFromBTCPK(fpPK)}, + StakingTime: uint32(stakingTime), + StakingValue: stakingValue, + StakingTx: serializedStakingTx, + SlashingTx: stakingSlashingInfo.SlashingTx, + DelegatorSlashingSig: delegatorSig, + UnbondingValue: int64(unbondingValue), + UnbondingTime: params.UnbondingTimeBlocks, + UnbondingTx: serializedUnbondingTx, + UnbondingSlashingTx: unbondingSlashingInfo.SlashingTx, + DelegatorUnbondingSlashingSig: delUnbondingSig, + PreviousStakingTxHash: prevDelTxHash.String(), + FundingTx: fundingTxBz, + MultisigInfo: &bstypes.AdditionalStakerInfo{ + StakerBtcPkList: extraStakerPKs, + StakerQuorum: stakerQuorum, + DelegatorSlashingSigs: extraSlashingSigs, + DelegatorUnbondingSlashingSigs: extraUnbondingSigs, + }, + }, stakingSlashingInfo, fundingTx +} diff --git a/test/e2ev2/tmanager/upgrade.go b/test/e2ev2/tmanager/upgrade.go new file mode 100644 index 000000000..6c612533a --- /dev/null +++ b/test/e2ev2/tmanager/upgrade.go @@ -0,0 +1,140 @@ +package tmanager + +import ( + "fmt" + + upgradetypes "cosmossdk.io/x/upgrade/types" + "github.com/cosmos/cosmos-sdk/codec/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +var ( + ErrInvalidUpgradeMsg = fmt.Errorf("invalid upgrade message type") + ErrNoProposalSubmitted = fmt.Errorf("no proposal submitted") + ErrProposalNotPassed = fmt.Errorf("proposal is not passed") +) + +func (tm *TestManagerUpgrade) runForkUpgrade() { + tm.T.Logf("waiting to reach fork height on chain") + tm.ChainsWaitUntilHeight(uint32(tm.ForkHeight)) + tm.T.Logf("fork height reached on chain") +} + +func (tm *TestManagerUpgrade) runProposalUpgrade(govMsg *govtypes.MsgSubmitProposal) error { + // submit, deposit, and vote for upgrade proposal + for _, chain := range tm.Chains { + validator := chain.Validators[0] + currentHeight, err := validator.LatestBlockNumber() + if err != nil { + return err + } + + msgs, err := govMsg.GetMsgs() + if err != nil { + return err + } + upgradeMsg, ok := msgs[0].(*upgradetypes.MsgSoftwareUpgrade) + if !ok { + return ErrInvalidUpgradeMsg + } + + var updatedGovMsg *govtypes.MsgSubmitProposal + if upgradeMsg.Plan.Height <= int64(currentHeight) { + // update govMsg giving buffer 10 block to the current height + upgradeMsg.Plan.Height = int64(currentHeight + 20) + chain.Config.UpgradePropHeight = upgradeMsg.Plan.Height + updatedGovMsg, err = updateGovUpgradeMsg(govMsg, upgradeMsg.Plan) + if err != nil { + return err + } + govMsg = updatedGovMsg + } + chain.Config.UpgradePropHeight = upgradeMsg.Plan.Height + + // submit upgrade gov proposal and vote yes + // force increase sequence of validator + validator.Wallet.WalletSender.IncSeq() + validator.SubmitProposal(validator.Wallet.KeyName, govMsg) + validator.WaitForNextBlock() + propsResp := validator.QueryProposals() + if len(propsResp.Proposals) == 0 { + return ErrNoProposalSubmitted + } + proposalID := propsResp.Proposals[0].Id + tm.T.Logf("proposal %d submitted, current status: %d", proposalID, propsResp.Proposals[0].Status) + validator.Vote(validator.Wallet.KeyName, proposalID, govtypes.VoteOption_VOTE_OPTION_YES) + validator.WaitForNextBlock() + tallyResult := validator.QueryTallyResult(proposalID) + tm.T.Logf("tally result from validator: %v", tallyResult) + } + + // wait till all chains halt at upgrade height + for _, chain := range tm.Chains { + tm.T.Logf("waiting to reach upgrade height on chain %s", chain.ChainID()) + chain.WaitUntilBlkHeight(uint32(chain.Config.UpgradePropHeight)) + tm.T.Logf("upgrade height %d reached on chain %s", chain.Config.UpgradePropHeight, chain.ChainID()) + } + + // check proposal status + validator := tm.ChainValidator() + propsResp := validator.QueryProposals() + if propsResp.Proposals[0].Status != 3 { + return ErrProposalNotPassed + } + + // remove all containers so we can upgrade them to the new version + for _, chain := range tm.Chains { + for _, node := range chain.AllNodes() { + if err := node.RemoveResource(); err != nil { + return err + } + } + } + + // upgrade all containers + for _, chain := range tm.Chains { + if err := tm.upgradeContainers(chain, chain.Config.UpgradePropHeight); err != nil { + return err + } + } + + return nil +} + +func (tm *TestManagerUpgrade) upgradeContainers(chain *Chain, propHeight int64) error { + tm.T.Logf("starting upgrade for chain-if: %s...", chain.ChainID()) + // update all nodes current repository and current tag + for _, node := range chain.AllNodes() { + node.Container.Repository = BabylonContainerName + node.Container.Tag = "latest" + } + + // run chain + chain.Start() + + tm.T.Logf("waiting to upgrade containers on chain %s", chain.ChainID()) + chain.WaitUntilBlkHeight(uint32(propHeight + 1)) + tm.T.Logf("upgrade successful on chain %s", chain.ChainID()) + + return nil +} + +func updateGovUpgradeMsg(govMsg *govtypes.MsgSubmitProposal, plan upgradetypes.Plan) (*govtypes.MsgSubmitProposal, error) { + msgs, err := govMsg.GetMsgs() + if err != nil { + return nil, err + } + upgradeMsg, ok := msgs[0].(*upgradetypes.MsgSoftwareUpgrade) + if !ok { + return nil, ErrInvalidUpgradeMsg + } + + upgradeMsg.Plan = plan + anyMsg, err := types.NewAnyWithValue(upgradeMsg) + if err != nil { + return nil, err + } + govMsg.Messages = []*types.Any{anyMsg} + + return govMsg, nil +} diff --git a/test/e2ev2/tmanager/wallet.go b/test/e2ev2/tmanager/wallet.go index f27dbce0d..3be973dce 100644 --- a/test/e2ev2/tmanager/wallet.go +++ b/test/e2ev2/tmanager/wallet.go @@ -4,9 +4,11 @@ import ( "fmt" "testing" - "cosmossdk.io/math" - appsigner "github.com/babylonlabs-io/babylon/v4/app/signer" + "github.com/stretchr/testify/require" + "github.com/btcsuite/btcd/btcec/v2" + + "cosmossdk.io/math" cmtcfg "github.com/cometbft/cometbft/config" "github.com/cometbft/cometbft/crypto/ed25519" "github.com/cometbft/cometbft/privval" @@ -20,9 +22,9 @@ import ( sdksigning "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" "github.com/cosmos/go-bip39" - "github.com/stretchr/testify/require" appparams "github.com/babylonlabs-io/babylon/v4/app/params" + appsigner "github.com/babylonlabs-io/babylon/v4/app/signer" "github.com/babylonlabs-io/babylon/v4/test/e2e/util" ) @@ -119,6 +121,14 @@ func (ws *WalletSender) IncSeq() { ws.SequenceNumber++ } +// DecSeq decrements the sequence number +func (ws *WalletSender) DecSeq() { + if ws.SequenceNumber == 0 { + ws.T().Fatalf("sequence number is 0") + } + ws.SequenceNumber-- +} + // Addr returns the account address func (ws *WalletSender) Addr() string { return ws.Address.String() @@ -137,7 +147,7 @@ func (ws *WalletSender) SignMsg(msgs ...sdk.Msg) *sdktx.Tx { // Set fee and gas txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, math.NewInt(20000)))) - txBuilder.SetGasLimit(300000) + txBuilder.SetGasLimit(500000) pubKey := ws.PrivKey.PubKey() signerData := authsigning.SignerData{ @@ -211,12 +221,43 @@ func (ws *WalletSender) SubmitMsgs(msgs ...sdk.Msg) (txHash string, tx *sdktx.Tx signedTx := ws.SignMsg(msgs...) txHash, err := ws.Node.SubmitTx(signedTx) - require.NoError(ws.T(), err, "Failed to submit IBC transfer transaction") + require.NoError(ws.T(), err, "Failed to submit transaction") + + ws.AddTxSent(txHash) + if ws.VerifySentTx { + ws.Node.WaitForNextBlock() + ws.T().Logf("Wallet %s is set to verify tx: %s", ws.KeyName, txHash) + ws.Node.RequireTxSuccess(txHash) + } + + return txHash, signedTx +} + +// SubmitMsgsWithErrContain builds the tx with the messages and sign it. +// If the wallet is tagged to wait to verify the transaction it waits for one block +// and checks if the transaction execution was success or contain expected error. +func (ws *WalletSender) SubmitMsgsWithErrContain(expErr error, msgs ...sdk.Msg) (txHash string, tx *sdktx.Tx) { + // Sign and submit the transaction + signedTx := ws.SignMsg(msgs...) + + txHash, err := ws.Node.SubmitTx(signedTx) + if expErr != nil && err != nil { + require.Error(ws.T(), err, "Expected error not found") + require.Contains(ws.T(), err.Error(), expErr.Error(), "Expected error not found") + // revert sequence increment since it fails to submit tx, transaction rejected before block inclusion + ws.DecSeq() + return txHash, signedTx + } + require.NoError(ws.T(), err, "Failed to submit transaction") ws.AddTxSent(txHash) if ws.VerifySentTx { ws.Node.WaitForNextBlock() ws.T().Logf("Wallet %s is set to verify tx: %s", ws.KeyName, txHash) + if expErr != nil { + ws.Node.RequireTxErrorContain(txHash, expErr.Error()) + return txHash, signedTx + } ws.Node.RequireTxSuccess(txHash) } diff --git a/test/e2ev2/upgrades_v5_test.go b/test/e2ev2/upgrades_v5_test.go new file mode 100644 index 000000000..0a52d726a --- /dev/null +++ b/test/e2ev2/upgrades_v5_test.go @@ -0,0 +1,228 @@ +package e2e2 + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg" + + "cosmossdk.io/math" + upgradetypes "cosmossdk.io/x/upgrade/types" + "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + + v5 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v5" + "github.com/babylonlabs-io/babylon/v4/test/e2e/configurer/chain" + "github.com/babylonlabs-io/babylon/v4/test/e2ev2/tmanager" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" + bbn "github.com/babylonlabs-io/babylon/v4/types" + bstypes "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" +) + +func TestUpgradeV5(t *testing.T) { + t.Parallel() + tm := tmanager.NewTmWithUpgrade(t, 0, "") + validator := tm.ChainValidator() + govMsg, preUpgradeFunc, err := createGovPropAndPreUpgradeFunc(t, validator.Wallet.WalletSender) + require.NoError(t, err) + + // start chain with previous binary + tm.Start() + // execute preUpgradeFunc, submit a proposal, vote, and then process upgrade + tm.Upgrade(govMsg, preUpgradeFunc) + + // post-upgrade state verification + bsParams := validator.QueryBtcStakingParams() + require.Equal(t, uint32(1), bsParams.MaxStakerQuorum) + require.Equal(t, uint32(1), bsParams.MaxStakerNum) + btcDelsResp := validator.QueryBTCDelegations(bstypes.BTCDelegationStatus_ACTIVE) + require.Len(t, btcDelsResp, 1) +} + +func createGovPropAndPreUpgradeFunc(t *testing.T, valWallet *tmanager.WalletSender) (*govtypes.MsgSubmitProposal, tmanager.PreUpgradeFunc, error) { + // create the upgrade message + upgradeMsg := &upgradetypes.MsgSoftwareUpgrade{ + Authority: "bbn10d07y265gmmuvt4z0w9aw880jnsr700jduz5f2", + Plan: upgradetypes.Plan{ + Name: v5.UpgradeName, + Height: int64(20), + Info: "Upgrade to v5", + }, + } + + anyMsg, err := types.NewAnyWithValue(upgradeMsg) + if err != nil { + return nil, nil, err + } + + govMsg := &govtypes.MsgSubmitProposal{ + Messages: []*types.Any{anyMsg}, + InitialDeposit: []sdk.Coin{sdk.NewCoin("ubbn", math.NewInt(1000000))}, + Proposer: valWallet.Address.String(), + Metadata: "", + Title: "v5", + Summary: "v5 upgrade", + Expedited: false, + } + + // create PreUpgradeFunc for a v5 upgrade scenario. this function will be executed before upgrade. + preUpgradeFunc := func(nodes []*tmanager.Node) { + r := rand.New(rand.NewSource(time.Now().Unix())) + fpSK := setupFp(t, r, nodes[0]) + createSingleSigBtcDel(t, r, nodes[1], fpSK) + } + + // return the path that will be accessible in Docker containers + return govMsg, preUpgradeFunc, nil +} + +func setupFp(t *testing.T, r *rand.Rand, n *tmanager.Node) *btcec.PrivateKey { + fpSK, _, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + fp, err := datagen.GenCustomFinalityProvider(r, fpSK, n.DefaultWallet().Address) + require.NoError(t, err) + n.CreateFinalityProvider(n.DefaultWallet().KeyName, fp) + n.WaitForNextBlock() + + fpResp := n.QueryFinalityProvider(fp.BtcPk.MarshalHex()) + require.NotNil(t, fpResp) + + return fpSK +} + +func createSingleSigBtcDel(t *testing.T, r *rand.Rand, n *tmanager.Node, fpSK *btcec.PrivateKey) { + n.DefaultWallet().VerifySentTx = true + + // single-sig delegation from n to fp + stakerSK, _, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + + msg, stakingInfoBuilt := BuildSingleSigDelegationMsg( + t, r, n, + n.DefaultWallet(), + stakerSK, + fpSK.PubKey(), + int64(2*10e8), + 1000, + ) + + n.CreateBTCDelegation(n.DefaultWallet().KeyName, msg) + n.WaitForNextBlock() + + pendingDelResp := n.QueryBTCDelegation(stakingInfoBuilt.StakingTx.TxHash().String()) + require.NotNil(t, pendingDelResp) + require.Equal(t, "PENDING", pendingDelResp.StatusDesc) + + /* + generate and insert new covenant signatures, in order to verify the BTC delegation + */ + pendingDel, err := chain.ParseRespBTCDelToBTCDel(pendingDelResp) + require.NoError(t, err) + require.Len(t, pendingDel.CovenantSigs, 0) + stakingMsgTx, err := bbn.NewBTCTxFromBytes(pendingDel.StakingTx) + require.NoError(t, err) + + slashingTx := pendingDel.SlashingTx + stakingTxHash := stakingMsgTx.TxHash().String() + bsParams := n.QueryBtcStakingParams() + + fpBTCPKs, err := bbn.NewBTCPKsFromBIP340PKs(pendingDel.FpBtcPkList) + require.NoError(t, err) + + btcCfg := &chaincfg.SimNetParams + stakingInfo, err := pendingDel.GetStakingInfo(bsParams, btcCfg) + require.NoError(t, err) + + stakingSlashingPathInfo, err := stakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + // it should be changed when modifying covenant pk on chain start + covSKs, _, _ := bstypes.DefaultCovenantCommittee() + + // covenant signatures on slashing tx + covenantSlashingSigs, err := datagen.GenCovenantAdaptorSigs( + covSKs, + fpBTCPKs, + stakingMsgTx, + stakingSlashingPathInfo.GetPkScriptPath(), + slashingTx, + ) + require.NoError(t, err) + + // cov Schnorr sigs on unbonding signature + unbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() + require.NoError(t, err) + unbondingTx, err := bbn.NewBTCTxFromBytes(pendingDel.BtcUndelegation.UnbondingTx) + require.NoError(t, err) + + covUnbondingSigs, err := datagen.GenCovenantUnbondingSigs( + covSKs, + stakingMsgTx, + pendingDel.StakingOutputIdx, + unbondingPathInfo.GetPkScriptPath(), + unbondingTx, + ) + require.NoError(t, err) + + unbondingInfo, err := pendingDel.GetUnbondingInfo(bsParams, btcCfg) + require.NoError(t, err) + unbondingSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + covenantUnbondingSlashingSigs, err := datagen.GenCovenantAdaptorSigs( + covSKs, + fpBTCPKs, + unbondingTx, + unbondingSlashingPathInfo.GetPkScriptPath(), + pendingDel.BtcUndelegation.SlashingTx, + ) + require.NoError(t, err) + + for i := 0; i < int(bsParams.CovenantQuorum); i++ { + n.SubmitRefundableTxWithAssertion(func() { + n.AddCovenantSigs( + n.DefaultWallet().KeyName, + covenantSlashingSigs[i].CovPk, + stakingTxHash, + covenantSlashingSigs[i].AdaptorSigs, + bbn.NewBIP340SignatureFromBTCSig(covUnbondingSigs[i]), + covenantUnbondingSlashingSigs[i].AdaptorSigs, + nil, + ) + }, true, n.DefaultWallet().KeyName) + } + + verifiedDelResp := n.QueryBTCDelegation(stakingTxHash) + require.Equal(t, "VERIFIED", verifiedDelResp.StatusDesc) + verifiedDel, err := chain.ParseRespBTCDelToBTCDel(verifiedDelResp) + require.NoError(t, err) + require.Len(t, verifiedDel.CovenantSigs, int(bsParams.CovenantQuorum)) + require.True(t, verifiedDel.HasCovenantQuorums(bsParams.CovenantQuorum, 0)) + + /* + generate and add inclusion proof, in order to activate the BTC delegation + */ + // wait for btc delegation is k-deep + currentBtcTipResp, err := n.QueryTip() + require.NoError(t, err) + currentBtcTip, err := chain.ParseBTCHeaderInfoResponseToInfo(currentBtcTipResp) + blockWithStakingTx := datagen.CreateBlockWithTransaction(r, currentBtcTip.Header.ToBlockHeader(), stakingMsgTx) + n.InsertHeader(&blockWithStakingTx.HeaderBytes) + + inclusionProof := bstypes.NewInclusionProofFromSpvProof(blockWithStakingTx.SpvProof) + for i := 0; i < tmanager.BabylonBtcConfirmationPeriod; i++ { + n.InsertNewEmptyBtcHeader(r) + } + + // add btc inclusion proof + n.SubmitRefundableTxWithAssertion(func() { + n.AddBTCDelegationInclusionProof(n.DefaultWallet().KeyName, stakingTxHash, inclusionProof) + }, true, n.DefaultWallet().KeyName) + + activeBtcDelResp := n.QueryBTCDelegation(stakingTxHash) + require.Equal(t, "ACTIVE", activeBtcDelResp.StatusDesc) +} diff --git a/test/replay/costaking_test.go b/test/replay/costaking_test.go index 454f6ba8c..9c6f3e817 100644 --- a/test/replay/costaking_test.go +++ b/test/replay/costaking_test.go @@ -1537,3 +1537,329 @@ func assertActiveBabyWithinRange(t *testing.T, expected, actual sdkmath.Int, tol require.True(t, diff.LTE(maxDiff), "ActiveBaby difference exceeds tolerance: expected %s ± %d, got %s (diff: %s). %v", expected.String(), tolerance, actual.String(), diff.String(), msgAndArgs) } + +func TestCostakingFpRemovalAndBtcUnbondSameBlockClearsActiveSats(t *testing.T) { + t.Parallel() + r := rand.New(rand.NewSource(time.Now().UnixNano())) + d := NewBabylonAppDriverTmpDir(r, t) + d.GenerateNewBlockAssertExecutionSuccess() + + stkK, costkK, finalityK := d.App.StakingKeeper, d.App.CostakingKeeper, d.App.FinalityKeeper + covSender := d.CreateCovenantSender() + + // Only one active FP allowed so someone must be evicted + fParams := finalityK.GetParams(d.Ctx()) + fParams.MaxActiveFinalityProviders = 1 + err := finalityK.SetParams(d.Ctx(), fParams) + require.NoError(t, err) + + // Validator / baby staking setup + validators, err := stkK.GetAllValidators(d.Ctx()) + require.NoError(t, err) + val := validators[0] + valAddr := sdk.MustValAddressFromBech32(val.OperatorAddress) + + delegators := d.CreateNStakerAccounts(2) + del1, del2 := delegators[0], delegators[1] + + del1BabyDelegatedAmt := sdkmath.NewInt(20_000000) + del2BabyDelegatedAmt := sdkmath.NewInt(20_000000) + + d.MintNativeTo(del1.Address(), 100_000000) + d.MintNativeTo(del2.Address(), 100_000000) + + d.TxWrappedDelegate(del1.SenderInfo, valAddr.String(), del1BabyDelegatedAmt) + d.TxWrappedDelegate(del2.SenderInfo, valAddr.String(), del2BabyDelegatedAmt) + + d.GenerateNewBlockAssertExecutionSuccess() + d.ProgressTillFirstBlockTheNextEpoch() + + // Two finality providers + fps := d.CreateNFinalityProviderAccounts(2) + fp1, fp2 := fps[0], fps[1] + fp1.RegisterFinalityProvider() + fp2.RegisterFinalityProvider() + d.GenerateNewBlockAssertExecutionSuccess() + + // BTC delegations: fp1 has 2x power of fp2 + p := costkK.GetParams(d.Ctx()) + del1BtcStakedAmtFp1 := del1BabyDelegatedAmt.Quo(p.ScoreRatioBtcByBaby) // e.g. 100k sats + del2BtcStakedAmtFp2 := del1BtcStakedAmtFp1.QuoRaw(2) // e.g. 50k sats + + del1MsgCreateFp1 := del1.CreatePreApprovalDelegation( + []*bbn.BIP340PubKey{fp1.BTCPublicKey()}, + defaultStakingTime, + del1BtcStakedAmtFp1.Int64(), + ) + + del2.CreatePreApprovalDelegation( + []*bbn.BIP340PubKey{fp2.BTCPublicKey()}, + defaultStakingTime, + del2BtcStakedAmtFp2.Int64(), + ) + + d.GenerateNewBlockAssertExecutionSuccess() + + // Activate both BTC delegations + covSender.SendCovenantSignatures() + d.GenerateNewBlockAssertExecutionSuccess() + d.ActivateVerifiedDelegations(2) + + // Make both FPs eligible and then finalize + fp1.CommitRandomness() + fp2.CommitRandomness() + d.GenerateNewBlockAssertExecutionSuccess() + + currentEpoch := d.GetEpoch().EpochNumber + d.ProgressTillFirstBlockTheNextEpoch() + d.FinalizeCkptForEpoch(currentEpoch - 1) + d.FinalizeCkptForEpoch(currentEpoch) + d.GenerateNewBlockAssertExecutionSuccess() + + // At this point fp1 should be the only active FP (more power than fp2) + activeFps := d.GetActiveFpsAtCurrentHeight(t) + require.Len(t, activeFps, 1, "expected exactly 1 active FP before unbond") + require.True(t, activeFps[0].BtcPkHex.Equals(fp1.BTCPublicKey()), "fp1 should be active before unbond") + + // Precondition: del1 has some active sats in costaking + trkBefore, err := costkK.GetCostakerRewards(d.Ctx(), del1.Address()) + require.NoError(t, err) + require.Equal(t, trkBefore.ActiveSatoshis.Uint64(), del1BtcStakedAmtFp1.Uint64()) + require.Equal(t, trkBefore.ActiveBaby.Uint64(), del1BabyDelegatedAmt.Uint64()) + + // Unbond the *entire* BTC delegation to fp1. + stakingTx := &wire.MsgTx{} + err = stakingTx.Deserialize(bytes.NewReader(del1MsgCreateFp1.StakingTx)) + require.NoError(t, err) + stakingTxHash := stakingTx.TxHash() + + del1.UnbondDelegation(&stakingTxHash, stakingTx, covSender) + + // Process the blocks with the unbond + d.GenerateNewBlockAssertExecutionSuccess() + d.GenerateNewBlockAssertExecutionSuccess() + + unbonded := d.GetUnbondedBTCDelegations(t) + require.Len(t, unbonded, 1, "delegation should be unbonded") + + ub := unbonded[0] + require.Equal(t, del1.AddressString(), ub.StakerAddr, + "unbonded delegation should belong to del1") + + foundFp1 := false + for _, pk := range ub.FpBtcPkList { + if pk.Equals(fp1.BTCPublicKey()) { + foundFp1 = true + break + } + } + require.True(t, foundFp1, "unbonded delegation should target fp1") + + activeDelegations := d.GetActiveBTCDelegations(t) + for _, ad := range activeDelegations { + for _, pk := range ad.FpBtcPkList { + require.False(t, pk.Equals(fp1.BTCPublicKey()), + "expected no active BTC delegation to fp1 after unbond", + ) + } + } + + // fp1 should no longer be active + activeFps = d.GetActiveFpsAtCurrentHeight(t) + require.Len(t, activeFps, 1, "expected exactly 1 active FP before unbond") + require.True(t, activeFps[0].BtcPkHex.Equals(fp2.BTCPublicKey()), "fp1 should be inactive after unbond, and fp2 active") + + unbondedLater := d.GetUnbondedBTCDelegations(t) + require.Len(t, unbondedLater, 1, "delegation should remain unbonded after extra blocks") + require.Equal(t, del1.AddressString(), unbondedLater[0].StakerAddr) + + trkAfter, err := costkK.GetCostakerRewards(d.Ctx(), del1.Address()) + require.NoError(t, err) + + require.True(t, trkAfter.ActiveSatoshis.IsZero(), + "costaker ActiveSatoshis must be zero after unbonding last delegation to fp1") + require.Equal(t, trkBefore.ActiveBaby.Uint64(), del1BabyDelegatedAmt.Uint64()) +} + +// TestCostakingFpBecomesActiveAndBtcUnbondSameBlockKeepsActiveSatsZero tests that when: +// - Block X: FP is inactive, BTC staker has BTC stake with ActiveSatoshis = 0 +// - Block X+1: FP becomes active, and the BTC staker unbonds its BTC stake +// The BTC staker's ActiveSatoshis remains 0 +func TestCostakingFpBecomesActiveAndBtcUnbondSameBlockKeepsActiveSatsZero(t *testing.T) { + t.Parallel() + r := rand.New(rand.NewSource(time.Now().UnixNano())) + d := NewBabylonAppDriverTmpDir(r, t) + d.GenerateNewBlockAssertExecutionSuccess() + + stkK, costkK, finalityK := d.App.StakingKeeper, d.App.CostakingKeeper, d.App.FinalityKeeper + covSender := d.CreateCovenantSender() + + // Only one active FP allowed so fp2 will be inactive initially + fParams := finalityK.GetParams(d.Ctx()) + fParams.MaxActiveFinalityProviders = 1 + err := finalityK.SetParams(d.Ctx(), fParams) + require.NoError(t, err) + + // Validator / baby staking setup + validators, err := stkK.GetAllValidators(d.Ctx()) + require.NoError(t, err) + val := validators[0] + valAddr := sdk.MustValAddressFromBech32(val.OperatorAddress) + + delegators := d.CreateNStakerAccounts(3) + del1, del2, del3 := delegators[0], delegators[1], delegators[2] + + del1BabyDelegatedAmt := sdkmath.NewInt(20_000000) + del2BabyDelegatedAmt := sdkmath.NewInt(20_000000) + del3BabyDelegatedAmt := sdkmath.NewInt(20_000000) + + d.MintNativeTo(del1.Address(), 100_000000) + d.MintNativeTo(del2.Address(), 100_000000) + d.MintNativeTo(del3.Address(), 100_000000) + + d.TxWrappedDelegate(del1.SenderInfo, valAddr.String(), del1BabyDelegatedAmt) + d.TxWrappedDelegate(del2.SenderInfo, valAddr.String(), del2BabyDelegatedAmt) + d.TxWrappedDelegate(del3.SenderInfo, valAddr.String(), del3BabyDelegatedAmt) + + d.GenerateNewBlockAssertExecutionSuccess() + d.ProgressTillFirstBlockTheNextEpoch() + + // Two finality providers + fps := d.CreateNFinalityProviderAccounts(2) + fp1, fp2 := fps[0], fps[1] + fp1.RegisterFinalityProvider() + fp2.RegisterFinalityProvider() + d.GenerateNewBlockAssertExecutionSuccess() + + // BTC delegations: fp1 has 1.5x power of fp2 + // fp1 will be active (more power), fp2 will be inactive + p := costkK.GetParams(d.Ctx()) + del1BtcStakedAmtFp1 := del1BabyDelegatedAmt.Quo(p.ScoreRatioBtcByBaby) // e.g. 100k sats + del2BtcStakedAmtFp2 := del1BtcStakedAmtFp1.QuoRaw(2) // e.g. 50k sats + del3BtcStakedAmtFp2 := del2BtcStakedAmtFp2.QuoRaw(2) // e.g. 25K sats + + del1MsgCreateFp1 := del1.CreatePreApprovalDelegation( + []*bbn.BIP340PubKey{fp1.BTCPublicKey()}, + defaultStakingTime, + del1BtcStakedAmtFp1.Int64(), + ) + + del2MsgCreateFp2 := del2.CreatePreApprovalDelegation( + []*bbn.BIP340PubKey{fp2.BTCPublicKey()}, + defaultStakingTime, + del2BtcStakedAmtFp2.Int64(), + ) + + del3.CreatePreApprovalDelegation( + []*bbn.BIP340PubKey{fp2.BTCPublicKey()}, + defaultStakingTime, + del3BtcStakedAmtFp2.Int64(), + ) + + d.GenerateNewBlockAssertExecutionSuccess() + + // Activate both BTC delegations + covSender.SendCovenantSignatures() + d.GenerateNewBlockAssertExecutionSuccess() + d.ActivateVerifiedDelegations(3) + + // Make both FPs eligible and then finalize + fp1.CommitRandomness() + fp2.CommitRandomness() + d.GenerateNewBlockAssertExecutionSuccess() + + currentEpoch := d.GetEpoch().EpochNumber + d.ProgressTillFirstBlockTheNextEpoch() + d.FinalizeCkptForEpoch(currentEpoch - 1) + d.FinalizeCkptForEpoch(currentEpoch) + d.GenerateNewBlockAssertExecutionSuccess() + + // At this point fp1 should be the only active FP (more power than fp2) + // del2 and del3 have BTC stake to fp2 (inactive), so del2's and del3's ActiveSatoshis should be 0 + activeFps := d.GetActiveFpsAtCurrentHeight(t) + require.Len(t, activeFps, 1, "expected exactly 1 active FP") + require.True(t, activeFps[0].BtcPkHex.Equals(fp1.BTCPublicKey()), "fp1 should be active") + + // Precondition: del2 and del3 have zero active sats (fp2 is inactive) + trkBefore, err := costkK.GetCostakerRewards(d.Ctx(), del2.Address()) + require.NoError(t, err) + require.True(t, trkBefore.ActiveSatoshis.IsZero(), + "del2 ActiveSatoshis must be zero because fp2 is inactive") + require.Equal(t, trkBefore.ActiveBaby.Uint64(), del2BabyDelegatedAmt.Uint64()) + + // Precondition: del3 has zero active sats (fp2 is inactive) + trkBefore3, err := costkK.GetCostakerRewards(d.Ctx(), del3.Address()) + require.NoError(t, err) + require.True(t, trkBefore3.ActiveSatoshis.IsZero(), + "del3 ActiveSatoshis must be zero because fp2 is inactive") + require.Equal(t, trkBefore3.ActiveBaby.Uint64(), del3BabyDelegatedAmt.Uint64()) + + // Now unbond del1's BTC delegation to fp1, which will make fp1 inactive + // and fp2 will become active + stakingTx1 := &wire.MsgTx{} + err = stakingTx1.Deserialize(bytes.NewReader(del1MsgCreateFp1.StakingTx)) + require.NoError(t, err) + stakingTxHash1 := stakingTx1.TxHash() + + // del2 delegation is also unbonded below + stakingTx2 := &wire.MsgTx{} + err = stakingTx2.Deserialize(bytes.NewReader(del2MsgCreateFp2.StakingTx)) + require.NoError(t, err) + stakingTxHash2 := stakingTx2.TxHash() + + // Unbond del1's delegation to fp1, making fp2 become active + // At the same time, del2 unbonds from fp2 + unbonding1 := del1.PrepareUnbonding(&stakingTxHash1, stakingTx1, covSender) + unbonding2 := del2.PrepareUnbonding(&stakingTxHash2, stakingTx2, covSender) + d.BatchUnbondDelegations([]*UnbondingInfo{unbonding1, unbonding2}) + d.GenerateNewBlockAssertExecutionSuccess() + + // Verify del2's unbond happened + unbonded := d.GetUnbondedBTCDelegations(t) + require.Len(t, unbonded, 2, "both delegations should be unbonded") + + var del2Unbonded bool + for _, ub := range unbonded { + if ub.StakerAddr == del2.AddressString() { + for _, pk := range ub.FpBtcPkList { + if pk.Equals(fp2.BTCPublicKey()) { + del2Unbonded = true + break + } + } + } + } + require.True(t, del2Unbonded, "del2's delegation to fp2 should be unbonded") + + // fp2 should now be active + activeFps = d.GetActiveFpsAtCurrentHeight(t) + require.Len(t, activeFps, 1, "expected exactly 1 active FP") + require.True(t, activeFps[0].BtcPkHex.Equals(fp2.BTCPublicKey()), "fp2 should be active") + + // Key assertion: del2's ActiveSatoshis should still be 0 + // Even though fp2 became active in the same block as del2's unbond, + // del2's ActiveSatoshis should remain 0 + del2TrkAfter, err := costkK.GetCostakerRewards(d.Ctx(), del2.Address()) + require.NoError(t, err) + + require.True(t, del2TrkAfter.ActiveSatoshis.IsZero(), + "costaker ActiveSatoshis must remain zero - fp2 became active but del2 unbonded in same block") + require.Equal(t, del2TrkAfter.ActiveBaby.Uint64(), del2BabyDelegatedAmt.Uint64()) + + // del3's ActiveSatoshis should now become > 0 + // because fp2 is now active + del3TrkAfter, err := costkK.GetCostakerRewards(d.Ctx(), del3.Address()) + require.NoError(t, err) + + require.Equal(t, del3TrkAfter.ActiveSatoshis, del3BtcStakedAmtFp2, + "costaker ActiveSatoshis must be greater than zero - fp2 is now active") + require.Equal(t, del3TrkAfter.ActiveBaby.Uint64(), del3BabyDelegatedAmt.Uint64()) + + // del1 active sats should be zero as it unbonded its only delegation + del1TrkAfter, err := costkK.GetCostakerRewards(d.Ctx(), del1.Address()) + require.NoError(t, err) + + require.True(t, del1TrkAfter.ActiveSatoshis.IsZero(), + "costaker ActiveSatoshis must be zero after unbonding last delegation to fp1") + require.Equal(t, del1TrkAfter.ActiveBaby.Uint64(), del1BabyDelegatedAmt.Uint64()) +} diff --git a/test/replay/driver.go b/test/replay/driver.go index 9ac76fd51..f26786140 100644 --- a/test/replay/driver.go +++ b/test/replay/driver.go @@ -777,6 +777,66 @@ func (d *BabylonAppDriver) IncludeTxsInBTCAndConfirm( return block, bbnBlock } +// BatchUnbondDelegations processes multiple unbondings in a single Babylon block. +// All unbonding BTC txs are included in the same BTC block, and all MsgBTCUndelegate +// messages are submitted before generating the Babylon block. +func (d *BabylonAppDriver) BatchUnbondDelegations(unbondings []*UnbondingInfo) { + if len(unbondings) == 0 { + return + } + + // Collect all unbonding txs + var unbondingTxs []*wire.MsgTx + for _, ub := range unbondings { + unbondingTxs = append(unbondingTxs, ub.UnbondingTx) + } + + // Include all unbonding txs in a single BTC block and confirm + btcCheckpointParams := d.GetBTCCkptParams(d.t) + tip, _ := d.GetBTCLCTip() + + block := datagen.GenRandomBtcdBlockWithTransactions(d.r, unbondingTxs, tip) + headers := BlocksWithProofsToHeaderBytes([]*datagen.BlockWithProofs{block}) + + confirmationBlocks := datagen.GenNEmptyBlocks( + d.r, + uint64(btcCheckpointParams.BtcConfirmationDepth), + &block.Block.Header, + ) + confirmationHeaders := BlocksWithProofsToHeaderBytes(confirmationBlocks) + headers = append(headers, confirmationHeaders...) + + // Insert BTC headers (this creates a Babylon block) + d.SendTxWithMsgsFromDriverAccount(d.t, &btclighttypes.MsgInsertHeaders{ + Signer: d.GetDriverAccountAddress().String(), + Headers: headers, + }) + + // Now send all MsgBTCUndelegate messages (one per staker) + // Each proof index is: 0 = coinbase, 1 = first unbonding tx, 2 = second, etc. + for i, ub := range unbondings { + stakingTxBz, err := bbn.SerializeBTCTx(ub.StakingTx) + require.NoError(d.t, err) + + unbondingTxBytes, err := bbn.SerializeBTCTx(ub.UnbondingTx) + require.NoError(d.t, err) + + // Proof index is i+1 because index 0 is the coinbase tx + msg := &bstypes.MsgBTCUndelegate{ + Signer: ub.Staker.AddressString(), + StakingTxHash: ub.StakingTxHash.String(), + StakeSpendingTx: unbondingTxBytes, + StakeSpendingTxInclusionProof: bstypes.NewInclusionProofFromSpvProof(block.Proofs[i+1]), + FundingTransactions: [][]byte{stakingTxBz}, + } + + ub.Staker.SendMessage(msg) + } + + // Generate the Babylon block that includes all unbonding messages + d.GenerateNewBlockAssertExecutionSuccess() +} + func (d *BabylonAppDriver) IncludeTxsInBTC(txs []*wire.MsgTx) *datagen.BlockWithProofs { tip, _ := d.GetBTCLCTip() diff --git a/test/replay/staker_sender.go b/test/replay/staker_sender.go index 9e4724d70..194a5f521 100644 --- a/test/replay/staker_sender.go +++ b/test/replay/staker_sender.go @@ -29,6 +29,14 @@ type Staker struct { BTCPrivateKey *btcec.PrivateKey } +// UnbondingInfo holds the data needed to submit an unbonding transaction +type UnbondingInfo struct { + Staker *Staker + StakingTx *wire.MsgTx + StakingTxHash *chainhash.Hash + UnbondingTx *wire.MsgTx +} + func (s *Staker) BTCPublicKey() *bbn.BIP340PubKey { pk := bbn.NewBIP340PubKeyFromBTCPK(s.BTCPrivateKey.PubKey()) return pk @@ -457,3 +465,52 @@ func (s *Staker) UnbondDelegation( s.SendMessage(msg) } + +// PrepareUnbonding prepares unbonding data without including it in BTC or sending the message. +// Returns the UnbondingInfo which can be used with BatchUnbondDelegations. +func (s *Staker) PrepareUnbonding( + stakingTxHash *chainhash.Hash, + stakingTx *wire.MsgTx, + covSender *CovenantSender, +) *UnbondingInfo { + params := s.d.GetBTCStakingParams(s.t) + + delegation := s.d.GetBTCDelegation(s.t, stakingTxHash.String()) + require.NotNil(s.t, delegation, "delegation should exist") + infos := parseInfos(s.t, delegation, params) + + unbondingPathSpendInfo, err := infos.StakingSlashingInfo.StakingInfo.UnbondingPathSpendInfo() + require.NoError(s.t, err) + + stakingOutput := stakingTx.TxOut[delegation.StakingOutputIdx] + + covenantSKs := covSender.CovenantPrivateKeys() + covenantSigs := datagen.GenerateSignatures( + s.t, + covenantSKs, + infos.UnbondingSlashingInfo.UnbondingTx, + stakingOutput, + unbondingPathSpendInfo.RevealedLeaf, + ) + + stakerSig, err := btcstaking.SignTxWithOneScriptSpendInputFromTapLeaf( + infos.UnbondingSlashingInfo.UnbondingTx, + stakingOutput, + s.BTCPrivateKey, + unbondingPathSpendInfo.RevealedLeaf, + ) + require.NoError(s.t, err) + + witness, err := unbondingPathSpendInfo.CreateUnbondingPathWitness(covenantSigs, stakerSig) + require.NoError(s.t, err) + + unbondingTxMsg := infos.UnbondingSlashingInfo.UnbondingTx + unbondingTxMsg.TxIn[0].Witness = witness + + return &UnbondingInfo{ + Staker: s, + StakingTx: stakingTx, + StakingTxHash: stakingTxHash, + UnbondingTx: unbondingTxMsg, + } +} diff --git a/testutil/btcstaking-helper/keeper.go b/testutil/btcstaking-helper/keeper.go index 0b521b861..892be36cf 100644 --- a/testutil/btcstaking-helper/keeper.go +++ b/testutil/btcstaking-helper/keeper.go @@ -1,6 +1,7 @@ package testutil import ( + "github.com/babylonlabs-io/babylon/v4/btcstaking" "math/rand" "testing" "time" @@ -334,6 +335,8 @@ func (h *Helper) GenAndApplyCustomParams( UnbondingFeeSat: 1000, AllowListExpirationHeight: allowListExpirationHeight, BtcActivationHeight: 1, + MaxStakerQuorum: 2, + MaxStakerNum: 3, }) h.NoError(err) return covenantSKs, covenantPKs @@ -569,6 +572,227 @@ func (h *Helper) CreateDelegationWithBtcBlockHeight( }, nil } +func (h *Helper) CreateMultisigDelegationWithBtcBlockHeight( + r *rand.Rand, + delSKs []*btcec.PrivateKey, + delQuorum uint32, + fpPK *btcec.PublicKey, + stakingValue int64, + stakingTime uint16, + unbondingValue int64, + unbondingTime uint16, + usePreApproval bool, + addToAllowList bool, + stakingTransactionInclusionHeight uint32, + lightClientTipHeight uint32, +) (string, *types.MsgCreateBTCDelegation, *types.BTCDelegation, *btclctypes.BTCHeaderInfo, *types.InclusionProof, *UnbondingTxInfo, error) { + stakingTimeBlocks := stakingTime + bsParams := h.BTCStakingKeeper.GetParams(h.Ctx) + covPKs, err := bbn.NewBTCPKsFromBIP340PKs(bsParams.CovenantPks) + h.NoError(err) + + // if not set, use default values for unbonding value and time + defaultUnbondingValue := stakingValue - 1000 + if unbondingValue == 0 { + unbondingValue = defaultUnbondingValue + } + defaultUnbondingTime := bsParams.UnbondingTimeBlocks + if unbondingTime == 0 { + unbondingTime = uint16(defaultUnbondingTime) + } + + testStakingInfo := datagen.GenMultisigBTCStakingSlashingInfo( + r, + h.t, + h.Net, + delSKs, + delQuorum, + []*btcec.PublicKey{fpPK}, + covPKs, + bsParams.CovenantQuorum, + stakingTimeBlocks, + stakingValue, + bsParams.SlashingPkScript, + bsParams.SlashingRate, + unbondingTime, + ) + h.NoError(err) + stakingTxHash := testStakingInfo.StakingTx.TxHash().String() + + // random signer + staker := sdk.MustAccAddressFromBech32(datagen.GenRandomAccount().Address) + + // PoP + pop, err := datagen.NewPoPBTC(staker, delSKs[0]) + h.NoError(err) + // generate staking tx info + prevBlock, _ := datagen.GenRandomBtcdBlock(r, 0, nil) + btcHeaderWithProof := datagen.CreateBlockWithTransaction(r, &prevBlock.Header, testStakingInfo.StakingTx) + btcHeader := btcHeaderWithProof.HeaderBytes + btcHeaderInfo := &btclctypes.BTCHeaderInfo{Header: &btcHeader, Height: stakingTransactionInclusionHeight} + serializedStakingTx, err := bbn.SerializeBTCTx(testStakingInfo.StakingTx) + h.NoError(err) + + txInclusionProof := types.NewInclusionProof(&btcctypes.TransactionKey{Index: 1, Hash: btcHeader.Hash()}, btcHeaderWithProof.SpvProof.MerkleNodes) + + slashingSpendInfo, err := testStakingInfo.StakingInfo.SlashingPathSpendInfo() + h.NoError(err) + + // generate proper delegator sig + delegatorSig, err := testStakingInfo.SlashingTx.Sign( + testStakingInfo.StakingTx, + 0, + slashingSpendInfo.GetPkScriptPath(), + delSKs[0], + ) + h.NoError(err) + + stakerPk := delSKs[0].PubKey() + stPk := bbn.NewBIP340PubKeyFromBTCPK(stakerPk) + + // generate extra delegator sigs + var delegatorSI []*types.SignatureInfo + for _, delSK := range delSKs[1:] { + delegatorSig, err := testStakingInfo.SlashingTx.Sign( + testStakingInfo.StakingTx, + 0, + slashingSpendInfo.GetPkScriptPath(), + delSK, + ) + h.NoError(err) + si := &types.SignatureInfo{ + Pk: bbn.NewBIP340PubKeyFromBTCPK(delSK.PubKey()), + Sig: delegatorSig, + } + delegatorSI = append(delegatorSI, si) + } + + /* + logics related to on-demand unbonding + */ + stkTxHash := testStakingInfo.StakingTx.TxHash() + stkOutputIdx := uint32(0) + + testUnbondingInfo := datagen.GenMultisigBTCUnbondingSlashingInfo( + r, + h.t, + h.Net, + delSKs, + delQuorum, + []*btcec.PublicKey{fpPK}, + covPKs, + bsParams.CovenantQuorum, + wire.NewOutPoint(&stkTxHash, stkOutputIdx), + unbondingTime, + unbondingValue, + bsParams.SlashingPkScript, + bsParams.SlashingRate, + unbondingTime, + ) + h.NoError(err) + + delSlashingTxSig, err := testUnbondingInfo.GenDelSlashingTxSig(delSKs[0]) + h.NoError(err) + + // generate extra delegator unbonding tx sigs + var delUnbondingSI []*types.SignatureInfo + for _, delSK := range delSKs[1:] { + delSlashingTxSig, err := testUnbondingInfo.GenDelSlashingTxSig(delSK) + h.NoError(err) + si := &types.SignatureInfo{ + Pk: bbn.NewBIP340PubKeyFromBTCPK(delSK.PubKey()), + Sig: delSlashingTxSig, + } + delUnbondingSI = append(delUnbondingSI, si) + } + + serializedUnbondingTx, err := bbn.SerializeBTCTx(testUnbondingInfo.UnbondingTx) + h.NoError(err) + + prevBlockForUnbonding, _ := datagen.GenRandomBtcdBlock(r, 0, nil) + btcUnbondingHeaderWithProof := datagen.CreateBlockWithTransaction(r, &prevBlockForUnbonding.Header, testUnbondingInfo.UnbondingTx) + btcUnbondingHeader := btcUnbondingHeaderWithProof.HeaderBytes + btcUnbondingHeaderInfo := &btclctypes.BTCHeaderInfo{Header: &btcUnbondingHeader, Height: 11} + unbondingTxInclusionProof := types.NewInclusionProof( + &btcctypes.TransactionKey{Index: 1, Hash: btcUnbondingHeader.Hash()}, + btcUnbondingHeaderWithProof.SpvProof.MerkleNodes, + ) + h.BTCLightClientKeeper.EXPECT().GetHeaderByHash(gomock.Eq(h.Ctx), gomock.Eq(btcUnbondingHeader.Hash())).Return(btcUnbondingHeaderInfo, nil).AnyTimes() + + // construct extra staker info for multisig btc delegation + stBtcPkList := make([]bbn.BIP340PubKey, len(delSKs)-1) + for i, delSK := range delSKs[1:] { + stBtcPkList[i] = *bbn.NewBIP340PubKeyFromBTCPK(delSK.PubKey()) + } + + multisigInfo := &types.AdditionalStakerInfo{ + StakerBtcPkList: stBtcPkList, + StakerQuorum: delQuorum, + DelegatorSlashingSigs: delegatorSI, + DelegatorUnbondingSlashingSigs: delUnbondingSI, + } + + // all good, construct and send MsgCreateBTCDelegation message + fpBTCPK := bbn.NewBIP340PubKeyFromBTCPK(fpPK) + msgCreateBTCDel := &types.MsgCreateBTCDelegation{ + StakerAddr: staker.String(), + BtcPk: stPk, + FpBtcPkList: []bbn.BIP340PubKey{*fpBTCPK}, + Pop: pop, + StakingTime: uint32(stakingTimeBlocks), + StakingValue: stakingValue, + StakingTx: serializedStakingTx, + SlashingTx: testStakingInfo.SlashingTx, + DelegatorSlashingSig: delegatorSig, + UnbondingTx: serializedUnbondingTx, + UnbondingTime: uint32(unbondingTime), + UnbondingValue: unbondingValue, + UnbondingSlashingTx: testUnbondingInfo.SlashingTx, + DelegatorUnbondingSlashingSig: delSlashingTxSig, + MultisigInfo: multisigInfo, + } + + if !usePreApproval { + msgCreateBTCDel.StakingTxInclusionProof = txInclusionProof + } + + if addToAllowList { + h.BTCStakingKeeper.IndexAllowedStakingTransaction(h.Ctx, &stkTxHash) + } + + // mock for testing k-deep stuff + h.BTCLightClientKeeper.EXPECT().GetHeaderByHash(gomock.Eq(h.Ctx), gomock.Eq(btcHeader.Hash())).Return(btcHeaderInfo, nil).AnyTimes() + h.BTCLightClientKeeper.EXPECT().GetTipInfo(gomock.Eq(h.Ctx)).Return(&btclctypes.BTCHeaderInfo{Height: lightClientTipHeight}) + + _, err = h.MsgServer.CreateBTCDelegation(h.Ctx, msgCreateBTCDel) + if err != nil { + return "", nil, nil, nil, nil, nil, err + } + + stakingMsgTx, err := bbn.NewBTCTxFromBytes(msgCreateBTCDel.StakingTx) + h.NoError(err) + btcDel, err := h.BTCStakingKeeper.GetBTCDelegation(h.Ctx, stakingMsgTx.TxHash().String()) + h.NoError(err) + + // ensure the delegation is still pending + status, err := h.BTCStakingKeeper.BtcDelStatus(h.Ctx, btcDel, bsParams.CovenantQuorum, btcTipHeight) + require.NoError(h.t, err) + require.Equal(h.t, status, types.BTCDelegationStatus_PENDING) + + if usePreApproval { + // the BTC delegation does not have inclusion proof + require.False(h.t, btcDel.HasInclusionProof()) + } else { + // the BTC delegation has inclusion proof + require.True(h.t, btcDel.HasInclusionProof()) + } + + return stakingTxHash, msgCreateBTCDel, btcDel, btcHeaderInfo, txInclusionProof, &UnbondingTxInfo{ + UnbondingTxInclusionProof: unbondingTxInclusionProof, + UnbondingHeaderInfo: btcUnbondingHeaderInfo, + }, nil +} + func (h *Helper) GenerateCovenantSignaturesMessages( r *rand.Rand, covenantSKs []*btcec.PrivateKey, @@ -583,8 +807,14 @@ func (h *Helper) GenerateCovenantSignaturesMessages( vPKs, err := bbn.NewBTCPKsFromBIP340PKs(del.FpBtcPkList) h.NoError(err) - stakingInfo, err := del.GetStakingInfo(&bsParams, h.Net) - h.NoError(err) + var stakingInfo *btcstaking.StakingInfo + if del.IsMultisigBtcDel() { + stakingInfo, err = del.GetMultisigStakingInfo(&bsParams, h.Net) + h.NoError(err) + } else { + stakingInfo, err = del.GetStakingInfo(&bsParams, h.Net) + h.NoError(err) + } unbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() h.NoError(err) @@ -608,8 +838,14 @@ func (h *Helper) GenerateCovenantSignaturesMessages( // slash unbonding tx spends unbonding tx unbondingTx, err := bbn.NewBTCTxFromBytes(del.BtcUndelegation.UnbondingTx) h.NoError(err) - unbondingInfo, err := del.GetUnbondingInfo(&bsParams, h.Net) - h.NoError(err) + var unbondingInfo *btcstaking.UnbondingInfo + if del.IsMultisigBtcDel() { + unbondingInfo, err = del.GetMultisigUnbondingInfo(&bsParams, h.Net) + h.NoError(err) + } else { + unbondingInfo, err = del.GetUnbondingInfo(&bsParams, h.Net) + h.NoError(err) + } unbondingSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() h.NoError(err) @@ -634,8 +870,14 @@ func (h *Helper) GenerateCovenantSignaturesMessages( prevDel, err := h.BTCStakingKeeper.GetBTCDelegation(h.Ctx, prevTxHash.String()) h.NoError(err) params := h.BTCStakingKeeper.GetParams(h.Ctx) - prevDelStakingInfo, err := prevDel.GetStakingInfo(¶ms, h.Net) - h.NoError(err) + var prevDelStakingInfo *btcstaking.StakingInfo + if prevDel.IsMultisigBtcDel() { + prevDelStakingInfo, err = prevDel.GetMultisigStakingInfo(¶ms, h.Net) + h.NoError(err) + } else { + prevDelStakingInfo, err = prevDel.GetStakingInfo(¶ms, h.Net) + h.NoError(err) + } covStkExpSigs, err = datagen.GenCovenantStakeExpSig(covenantSKs, del, prevDelStakingInfo) h.NoError(err) } @@ -844,6 +1086,47 @@ func (h *Helper) CreateBtcStakeExpansionWithBtcTipHeight( return spendingTx, fundingTx, nil } +func (h *Helper) CreateMultisigBtcStakeExpansionWithBtcTipHeight( + r *rand.Rand, + delSKs []*btcec.PrivateKey, + delQuorum uint32, + fpPK *btcec.PublicKey, + stakingValue int64, + stakingTime uint16, + prevDel *types.BTCDelegation, + lightClientTipHeight uint32, +) (*wire.MsgTx, *wire.MsgTx, error) { + expandMsg := h.createMultisigBtcStakeExpandMessage( + r, + delSKs, + delQuorum, + fpPK, + stakingValue, + stakingTime, + prevDel, + ) + + h.BTCLightClientKeeper.EXPECT().GetTipInfo(gomock.Eq(h.Ctx)).Return(&btclctypes.BTCHeaderInfo{Height: lightClientTipHeight}).MaxTimes(3) + + // Submit the BtcStakeExpand message + _, err := h.MsgServer.BtcStakeExpand(h.Ctx, expandMsg) + if err != nil { + return nil, nil, err + } + + spendingTx, err := bbn.NewBTCTxFromBytes(expandMsg.StakingTx) + if err != nil { + return nil, nil, err + } + + fundingTx, err := bbn.NewBTCTxFromBytes(expandMsg.FundingTx) + if err != nil { + return nil, nil, err + } + + return spendingTx, fundingTx, nil +} + // Helper function to create a BtcStakeExpand message for testing func (h *Helper) createBtcStakeExpandMessage( r *rand.Rand, @@ -963,3 +1246,170 @@ func (h *Helper) createBtcStakeExpandMessage( FundingTx: fundingTxBz, } } + +// Helper function to create a BtcStakeExpand message for testing +func (h *Helper) createMultisigBtcStakeExpandMessage( + r *rand.Rand, + delSKs []*btcec.PrivateKey, + delQuorum uint32, + fpPK *btcec.PublicKey, + stakingValue int64, + stakingTime uint16, + prevDel *types.BTCDelegation, +) *types.MsgBtcStakeExpand { + // Get staking parameters + params := h.BTCStakingKeeper.GetParams(h.Ctx) + + // Convert fpPKs to BIP340PubKey format + fpBtcPk := bbn.NewBIP340PubKeyFromBTCPK(fpPK) + + // Convert covenant keys + var covenantPks []*btcec.PublicKey + for _, pk := range params.CovenantPks { + covenantPks = append(covenantPks, pk.MustToBTCPK()) + } + + // Create funding transaction + fundingTx := datagen.GenRandomTxWithOutputValue(r, 10000000) + + // Convert previousStakingTxHash to OutPoint + prevDelTxHash := prevDel.MustGetStakingTxHash() + prevStakingOutPoint := wire.NewOutPoint(&prevDelTxHash, datagen.StakingOutIdx) + + // Convert fundingTxHash to OutPoint + fundingTxHash := fundingTx.TxHash() + fundingOutPoint := wire.NewOutPoint(&fundingTxHash, 0) + outPoints := []*wire.OutPoint{prevStakingOutPoint, fundingOutPoint} + + // Generate staking slashing info using multiple inputs + stakingSlashingInfo := datagen.GenMultisigBTCStakingSlashingInfoWithInputs( + r, + h.T(), + h.Net, + outPoints, + delSKs, + delQuorum, + []*btcec.PublicKey{fpPK}, + covenantPks, + params.CovenantQuorum, + stakingTime, + stakingValue, + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + 10000, + ) + + slashingPathSpendInfo, err := stakingSlashingInfo.StakingInfo.SlashingPathSpendInfo() + h.NoError(err) + + // Sign the slashing tx with the primary delegator key + delegatorSig, err := stakingSlashingInfo.SlashingTx.Sign( + stakingSlashingInfo.StakingTx, + datagen.StakingOutIdx, + slashingPathSpendInfo.GetPkScriptPath(), + delSKs[0], + ) + h.NoError(err) + + // generate extra delegator signatures + var extraDelegatorSigs []*types.SignatureInfo + delSKList := delSKs[1:delQuorum] + for _, sk := range delSKList { + sig, err := stakingSlashingInfo.SlashingTx.Sign( + stakingSlashingInfo.StakingTx, + datagen.StakingOutIdx, + slashingPathSpendInfo.GetPkScriptPath(), + sk, + ) + require.NoError(h.t, err) + + extraDelegatorSigs = append(extraDelegatorSigs, &types.SignatureInfo{ + Pk: bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()), + Sig: sig, + }) + } + + // Serialize the staking tx bytes + serializedStakingTx, err := bbn.SerializeBTCTx(stakingSlashingInfo.StakingTx) + h.NoError(err) + + stkTxHash := stakingSlashingInfo.StakingTx.TxHash() + unbondingValue := uint64(stakingValue) - uint64(params.UnbondingFeeSat) + + // Generate unbonding slashing info + unbondingSlashingInfo := datagen.GenMultisigBTCUnbondingSlashingInfo( + r, + h.T(), + h.Net, + delSKs, + delQuorum, + []*btcec.PublicKey{fpPK}, + covenantPks, + params.CovenantQuorum, + wire.NewOutPoint(&stkTxHash, datagen.StakingOutIdx), + uint16(params.UnbondingTimeBlocks), + int64(unbondingValue), + params.SlashingPkScript, + params.SlashingRate, + uint16(params.UnbondingTimeBlocks), + ) + + unbondingTxBytes, err := bbn.SerializeBTCTx(unbondingSlashingInfo.UnbondingTx) + h.NoError(err) + + // Sign the unbonding slashing tx with the primary delegator key + delSlashingTxSig, err := unbondingSlashingInfo.GenDelSlashingTxSig(delSKs[0]) + h.NoError(err) + + // generate extra unbonding signatures + var extraUnbondingSigs []*types.SignatureInfo + for _, sk := range delSKList { + sig, err := unbondingSlashingInfo.GenDelSlashingTxSig(sk) + require.NoError(h.t, err) + + extraUnbondingSigs = append(extraUnbondingSigs, &types.SignatureInfo{ + Pk: bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()), + Sig: sig, + }) + } + + // Create proof of possession + stakerAddr := sdk.MustAccAddressFromBech32(prevDel.StakerAddr) + pop, err := datagen.NewPoPBTC(stakerAddr, delSKs[0]) + h.NoError(err) + + fundingTxBz, err := bbn.SerializeBTCTx(fundingTx) + h.NoError(err) + + // build extra staker pk list (all staker except the first one) + extraStakerPkList := make([]bbn.BIP340PubKey, len(delSKs[1:])) + for i, sk := range delSKs[1:] { + extraStakerPkList[i] = *bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()) + } + + return &types.MsgBtcStakeExpand{ + StakerAddr: prevDel.StakerAddr, + Pop: pop, + BtcPk: bbn.NewBIP340PubKeyFromBTCPK(delSKs[0].PubKey()), + FpBtcPkList: []bbn.BIP340PubKey{*fpBtcPk}, + StakingTime: uint32(stakingTime), + StakingValue: stakingValue, + StakingTx: serializedStakingTx, + SlashingTx: stakingSlashingInfo.SlashingTx, + DelegatorSlashingSig: delegatorSig, + UnbondingValue: int64(unbondingValue), + UnbondingTime: params.UnbondingTimeBlocks, + UnbondingTx: unbondingTxBytes, + UnbondingSlashingTx: unbondingSlashingInfo.SlashingTx, + DelegatorUnbondingSlashingSig: delSlashingTxSig, + PreviousStakingTxHash: prevDelTxHash.String(), + FundingTx: fundingTxBz, + MultisigInfo: &types.AdditionalStakerInfo{ + StakerBtcPkList: extraStakerPkList, + StakerQuorum: delQuorum, + DelegatorSlashingSigs: extraDelegatorSigs, + DelegatorUnbondingSlashingSigs: extraUnbondingSigs, + }, + } +} diff --git a/testutil/datagen/btcstaking.go b/testutil/datagen/btcstaking.go index eec9656e6..26b3195d1 100644 --- a/testutil/datagen/btcstaking.go +++ b/testutil/datagen/btcstaking.go @@ -742,6 +742,211 @@ func GenBTCUnbondingSlashingInfo( } } +func GenMultisigBTCStakingSlashingInfo( + r *rand.Rand, + t testing.TB, + btcNet *chaincfg.Params, + stakerSKs []*btcec.PrivateKey, + stakerQuorum uint32, + fpPKs []*btcec.PublicKey, + covenantPKs []*btcec.PublicKey, + covenantQuorum uint32, + stakingTimeBlocks uint16, + stakingValue int64, + slashingPkScript []byte, + slashingRate sdkmath.LegacyDec, + slashingChangeLockTime uint16, +) *TestStakingSlashingInfo { + // an arbitrary input + spend := makeSpendableOutWithRandOutPoint(r, btcutil.Amount(stakingValue+UnbondingTxFee)) + outPoint := &spend.prevOut + + return GenMultisigBTCStakingSlashingInfoWithOutpoint( + r, t, + btcNet, + outPoint, + stakerSKs, + stakerQuorum, + fpPKs, + covenantPKs, + covenantQuorum, + stakingTimeBlocks, + stakingValue, + slashingPkScript, + slashingRate, + slashingChangeLockTime, + ) +} + +func GenMultisigBTCStakingSlashingInfoWithOutpoint( + r *rand.Rand, + t testing.TB, + btcNet *chaincfg.Params, + outPoint *wire.OutPoint, + stakerSKs []*btcec.PrivateKey, + stakerQuorum uint32, + fpPKs []*btcec.PublicKey, + covenantPKs []*btcec.PublicKey, + covenantQuorum uint32, + stakingTimeBlocks uint16, + stakingValue int64, + slashingPkScript []byte, + slashingRate sdkmath.LegacyDec, + slashingChangeLockTime uint16, +) *TestStakingSlashingInfo { + return GenMultisigBTCStakingSlashingInfoWithInputs( + r, t, + btcNet, + []*wire.OutPoint{outPoint}, + stakerSKs, + stakerQuorum, + fpPKs, + covenantPKs, + covenantQuorum, + stakingTimeBlocks, + stakingValue, + slashingPkScript, + slashingRate, + slashingChangeLockTime, + 10000, + ) +} + +func GenMultisigBTCStakingSlashingInfoWithInputs( + r *rand.Rand, + t testing.TB, + btcNet *chaincfg.Params, + outPoints []*wire.OutPoint, + stakerSKs []*btcec.PrivateKey, + stakerQuorum uint32, + fpPKs []*btcec.PublicKey, + covenantPKs []*btcec.PublicKey, + covenantQuorum uint32, + stakingTimeBlocks uint16, + stakingValue int64, + slashingPkScript []byte, + slashingRate sdkmath.LegacyDec, + slashingChangeLockTime uint16, + changeAmt int64, +) *TestStakingSlashingInfo { + stakerPubKeys := make([]*btcec.PublicKey, len(stakerSKs)) + for i, sk := range stakerSKs { + stakerPubKeys[i] = sk.PubKey() + } + + stakingInfo, err := btcstaking.BuildMultisigStakingInfo( + stakerPubKeys, + stakerQuorum, + fpPKs, + covenantPKs, + covenantQuorum, + stakingTimeBlocks, + btcutil.Amount(stakingValue), + btcNet, + ) + require.NoError(t, err) + + tx := wire.NewMsgTx(2) + + // Add given input + for _, outPoint := range outPoints { + tx.AddTxIn(wire.NewTxIn(outPoint, nil, nil)) + } + + // Add staking output + tx.AddTxOut(stakingInfo.StakingOutput) + + // Add dummy change output + changeScript, err := GenRandomPubKeyHashScript(r, btcNet) + require.NoError(t, err) + require.False(t, txscript.GetScriptClass(changeScript) == txscript.NonStandardTy) + + tx.AddTxOut(wire.NewTxOut(changeAmt, changeScript)) + + // Build slashing tx + slashingMsgTx, err := btcstaking.BuildMultisigSlashingTxFromStakingTxStrict( + tx, + StakingOutIdx, + slashingPkScript, + stakerPubKeys, + stakerQuorum, + slashingChangeLockTime, + 2000, + slashingRate, + btcNet, + ) + require.NoError(t, err) + + slashingTx, err := bstypes.NewBTCSlashingTxFromMsgTx(slashingMsgTx) + require.NoError(t, err) + + return &TestStakingSlashingInfo{ + StakingTx: tx, + SlashingTx: slashingTx, + StakingInfo: stakingInfo, + } +} + +func GenMultisigBTCUnbondingSlashingInfo( + r *rand.Rand, + t testing.TB, + btcNet *chaincfg.Params, + stakerSKs []*btcec.PrivateKey, + stakerQuorum uint32, + fpPKs []*btcec.PublicKey, + covenantPKs []*btcec.PublicKey, + covenantQuorum uint32, + stakingTransactionOutpoint *wire.OutPoint, + stakingTimeBlocks uint16, + stakingValue int64, + slashingPkScript []byte, + slashingRate sdkmath.LegacyDec, + slashingChangeLockTime uint16, +) *TestUnbondingSlashingInfo { + stakerPubKeys := make([]*btcec.PublicKey, len(stakerSKs)) + for i, sk := range stakerSKs { + stakerPubKeys[i] = sk.PubKey() + } + + unbondingInfo, err := btcstaking.BuildMultisigUnbondingInfo( + stakerPubKeys, + stakerQuorum, + fpPKs, + covenantPKs, + covenantQuorum, + slashingChangeLockTime, + btcutil.Amount(stakingValue), + btcNet, + ) + + require.NoError(t, err) + tx := wire.NewMsgTx(2) + // add the given tx input + txIn := wire.NewTxIn(stakingTransactionOutpoint, nil, nil) + tx.AddTxIn(txIn) + tx.AddTxOut(unbondingInfo.UnbondingOutput) + + slashingMsgTx, err := btcstaking.BuildMultisigSlashingTxFromStakingTxStrict( + tx, + StakingOutIdx, + slashingPkScript, + stakerPubKeys, + stakerQuorum, + slashingChangeLockTime, + 2000, + slashingRate, + btcNet) + require.NoError(t, err) + slashingTx, err := bstypes.NewBTCSlashingTxFromMsgTx(slashingMsgTx) + require.NoError(t, err) + + return &TestUnbondingSlashingInfo{ + UnbondingTx: tx, + SlashingTx: slashingTx, + UnbondingInfo: unbondingInfo, + } +} + func (info *TestUnbondingSlashingInfo) GenDelSlashingTxSig(sk *btcec.PrivateKey) (*bbn.BIP340Signature, error) { unbondingTxMsg := info.UnbondingTx unbondingTxSlashingPathInfo, err := info.UnbondingInfo.SlashingPathSpendInfo() @@ -869,6 +1074,30 @@ func GenerateSignatures( return sigs } +// GenerateSignaturesInGivenOrder doesn't sort signatures, it returns signatures in given keys order +func GenerateSignaturesInGivenOrder( + t *testing.T, + keys []*btcec.PrivateKey, + tx *wire.MsgTx, + fundingOutput *wire.TxOut, + leaf txscript.TapLeaf, +) []*bbn.BIP340Signature { + sigs := make([]*bbn.BIP340Signature, 0, len(keys)) + + for _, key := range keys { + schnorrSig, err := btcstaking.SignTxWithOneScriptSpendInputFromTapLeaf( + tx, + fundingOutput, + key, + leaf, + ) + require.NoError(t, err) + sigs = append(sigs, bbn.NewBIP340SignatureFromBTCSig(schnorrSig)) + } + + return sigs +} + func GenerateSignaturesForStakeExpansion( t *testing.T, keys []*btcec.PrivateKey, @@ -1042,6 +1271,106 @@ func AddWitnessToStakeExpTx( return serializedUnbondingTxWithWitness, spendingTx } +func AddMultisigWitnessToStakeExpTx( + t *testing.T, + stakingOutput *wire.TxOut, + fundingOutput *wire.TxOut, + stakerSks []*btcec.PrivateKey, + stakerQuorum uint32, + covenantSks []*btcec.PrivateKey, + covenantQuorum uint32, + finalityProviderPKs []*btcec.PublicKey, + stakingTime uint16, + stakingValue int64, + spendingTx *wire.MsgTx, // this is the stake expansion transaction + net *chaincfg.Params, +) ([]byte, *wire.MsgTx) { + var covenatnPks []*btcec.PublicKey + for _, sk := range covenantSks { + covenatnPks = append(covenatnPks, sk.PubKey()) + } + + stakerPKs := make([]*btcec.PublicKey, 0, len(stakerSks)) + for _, sk := range stakerSks { + stakerPKs = append(stakerPKs, sk.PubKey()) + } + + stakingInfo, err := stk.BuildMultisigStakingInfo( + stakerPKs, + stakerQuorum, + finalityProviderPKs, + covenatnPks, + covenantQuorum, + stakingTime, + btcutil.Amount(stakingValue), + net, + ) + require.NoError(t, err) + + // sanity check that what we re-build is the same as what we have in the BTC delegation + require.Equal(t, stakingOutput, stakingInfo.StakingOutput) + + unbondingSpendInfo, err := stakingInfo.UnbondingPathSpendInfo() + require.NoError(t, err) + + unbondingScirpt := unbondingSpendInfo.RevealedLeaf.Script + require.NotNil(t, unbondingScirpt) + + covenantSigs := GenerateSignaturesForStakeExpansion( + t, + covenantSks, + spendingTx, + stakingOutput, + fundingOutput, + unbondingSpendInfo.RevealedLeaf, + ) + require.NoError(t, err) + + delPK2Sig := make(map[string]*bbn.BIP340Signature) + for _, stakerSk := range stakerSks { + stakerSchnorrSig, err := stk.SignTxForFirstScriptSpendWithTwoInputsFromTapLeaf( + spendingTx, + stakingOutput, + fundingOutput, + stakerSk, + unbondingSpendInfo.RevealedLeaf, + ) + require.NoError(t, err) + + stakerBIP340Sig := bbn.NewBIP340SignatureFromBTCSig(stakerSchnorrSig) + stakerPKHex := bbn.NewBIP340PubKeyFromBTCPK(stakerSk.PubKey()).MarshalHex() + delPK2Sig[stakerPKHex] = stakerBIP340Sig + } + + sortedBIP340StakerSigs, err := bstypes.GetOrderedDelegatorSignatures(delPK2Sig) + require.NoError(t, err) + + // only construct quorum number of staker signatures as witnesses + sortedStakerSigs := make([]*schnorr.Signature, len(sortedBIP340StakerSigs)) + numDelSigs := uint32(0) + for i, sig := range sortedBIP340StakerSigs { + if sig != nil { + sortedStakerSigs[i] = sig.MustToBTCSig() + numDelSigs++ + } else { + sortedStakerSigs[i] = nil + } + if numDelSigs == stakerQuorum { + break + } + } + + ubWitness, err := unbondingSpendInfo.CreateMultisigUnbondingPathWitness(covenantSigs, sortedStakerSigs) + require.NoError(t, err) + + spendingTx.TxIn[0].Witness = ubWitness + + serializedUnbondingTxWithWitness, err := bbn.SerializeBTCTx(spendingTx) + require.NoError(t, err) + + return serializedUnbondingTxWithWitness, spendingTx +} + func GenFundingTx( t *testing.T, r *rand.Rand, diff --git a/types/btc_schnorr_pk.go b/types/btc_schnorr_pk.go index dfcf1cb19..e942a9529 100644 --- a/types/btc_schnorr_pk.go +++ b/types/btc_schnorr_pk.go @@ -132,6 +132,7 @@ func NewBIP340PKsFromBTCPKs(btcPKs []*btcec.PublicKey) []BIP340PubKey { return pks } +// SortBIP340PKs sort pubkey in reverse lexicographic order func SortBIP340PKs(keys []BIP340PubKey) []BIP340PubKey { sortedPKs := make([]BIP340PubKey, len(keys)) copy(sortedPKs, keys) diff --git a/x/btcstaking/client/cli/tx.go b/x/btcstaking/client/cli/tx.go index 1fa209097..55bcab313 100644 --- a/x/btcstaking/client/cli/tx.go +++ b/x/btcstaking/client/cli/tx.go @@ -29,6 +29,8 @@ const ( FlagCommissionRate = "commission-rate" FlagCommissionMaxRate = "commission-max-rate" FlagCommissionMaxChangeRate = "commission-max-change-rate" + + FlagMultisigInfoJSON = "multisig-info-json" ) // GetTxCmd returns the transaction commands for this module @@ -202,7 +204,34 @@ func NewCreateBTCDelegationCmd() *cobra.Command { Args: cobra.ExactArgs(14), Short: "Create a BTC delegation", Long: strings.TrimSpace( - `Create a BTC delegation.`, // TODO: example + `Create a BTC delegation. +When btc staker is multisig, use the optional field --multisig-info-json [path/to/multisig.json]. Public keys are hex-encoded x-only BIP340 values; signatures are base64-encoded. + +Example: +$ babylond tx btcstaking create-btc-delegation [btc_pk] [pop_hex] [staking_tx] [inclusion_proof] [fp_pk] [staking_time] [staking_value] [slashing_tx] [delegator_slashing_sig] [unbonding_tx] [unbonding_slashing_tx] [unbonding_time] [unbonding_value] [delegator_unbonding_slashing_sig] --multisig-info-json ./temp/multisig.json + +Where multisig.json contains: + +{ + "staker_btc_pk_list": [ + "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659" + ], + "staker_quorum": 2, + "delegator_slashing_sigs": [ + { + "pk": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "sig": "BOf5A3ZYqSr+tPJbrlM5493KgaNTSTgn0m8W2SMI5J4qJekiCGeKLfhpcNqRsDqK+IFaimBJizWNr1YLNHqlVw==" + } + ], + "delegator_unbonding_slashing_sigs": [ + { + "pk": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "sig": "WDGq7te0S7dOXquUup1ClMSbzypgco2LTCAPUN0xPBurdFh5pa2VSnLEWpHDpR08et6pjYL4SB4OHgNnSm8/tw==" + } + ] +} +`, ), RunE: func(cmd *cobra.Command, args []string) error { clientCtx, err := client.GetClientTxContext(cmd) @@ -217,10 +246,21 @@ func NewCreateBTCDelegationCmd() *cobra.Command { msg.StakerAddr = clientCtx.FromAddress.String() + // parse multisig info json if exist + fs := cmd.Flags() + multisigInfo, err := parseMultisigInfoJSON(fs) + if err != nil { + return err + } + msg.MultisigInfo = multisigInfo + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } + // optional multisig info flag + cmd.Flags().String(FlagMultisigInfoJSON, "", "The btc staking multisig info (optional)") + flags.AddTxFlagsToCmd(cmd) return cmd @@ -232,7 +272,34 @@ func NewBTCStakeExpandCmd() *cobra.Command { Args: cobra.ExactArgs(16), Short: "Expand a BTC delegation", Long: strings.TrimSpace( - `Expand a BTC delegation.`, + `Expand a BTC delegation. +When btc staker is multisig, use the optional field --multisig-info-json [path/to/multisig.json]. Keys and signatures are hex-encoded x-only BIP340 values; signatures are base64-encoded. + +Example: +$ babylond tx btcstaking btc-stake-expand [btc_pk] [pop_hex] [staking_tx] [inclusion_proof] [fp_pk1],[fp_pk2],... [staking_time] [staking_value] [slashing_tx] [delegator_slashing_sig] [unbonding_tx] [unbonding_slashing_tx] [unbonding_time] [unbonding_value] [delegator_unbonding_slashing_sig] [previous_staking_tx_hash] [funding_tx] --multisig-info-json ./temp/multisig.json + +Where multisig.json contains: + +{ + "staker_btc_pk_list": [ + "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659" + ], + "staker_quorum": 2, + "delegator_slashing_sigs": [ + { + "pk": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "sig": "BOf5A3ZYqSr+tPJbrlM5493KgaNTSTgn0m8W2SMI5J4qJekiCGeKLfhpcNqRsDqK+IFaimBJizWNr1YLNHqlVw==" + } + ], + "delegator_unbonding_slashing_sigs": [ + { + "pk": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "sig": "WDGq7te0S7dOXquUup1ClMSbzypgco2LTCAPUN0xPBurdFh5pa2VSnLEWpHDpR08et6pjYL4SB4OHgNnSm8/tw==" + } + ] +} +`, ), RunE: func(cmd *cobra.Command, args []string) error { clientCtx, err := client.GetClientTxContext(cmd) @@ -250,6 +317,13 @@ func NewBTCStakeExpandCmd() *cobra.Command { return err } + // parse multisig info json if exist + fs := cmd.Flags() + multisigInfo, err := parseMultisigInfoJSON(fs) + if err != nil { + return err + } + msg := &types.MsgBtcStakeExpand{ StakerAddr: clientCtx.FromAddress.String(), BtcPk: parsed.BtcPk, @@ -267,12 +341,15 @@ func NewBTCStakeExpandCmd() *cobra.Command { DelegatorUnbondingSlashingSig: parsed.DelegatorUnbondingSlashingSig, PreviousStakingTxHash: args[14], FundingTx: fundingTx, + MultisigInfo: multisigInfo, } return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } + cmd.Flags().String(FlagMultisigInfoJSON, "", "The btc staking multisig info (optional)") + flags.AddTxFlagsToCmd(cmd) return cmd @@ -614,5 +691,6 @@ func parseArgsIntoMsgCreateBTCDelegation(args []string) (*types.MsgCreateBTCDele UnbondingValue: int64(unbondingValue), UnbondingSlashingTx: unbondingSlashingTx, DelegatorUnbondingSlashingSig: delegatorUnbondingSlashingSig, + MultisigInfo: nil, }, nil } diff --git a/x/btcstaking/client/cli/utils.go b/x/btcstaking/client/cli/utils.go index cec621388..2263a080f 100644 --- a/x/btcstaking/client/cli/utils.go +++ b/x/btcstaking/client/cli/utils.go @@ -3,9 +3,14 @@ package cli import ( "fmt" "math" + "os" + + "github.com/spf13/pflag" sdkmath "cosmossdk.io/math" "github.com/btcsuite/btcd/btcutil" + + "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" ) func parseLockTime(str string) (uint16, error) { @@ -47,3 +52,31 @@ func parseBtcAmount(str string) (btcutil.Amount, error) { return btcutil.Amount(asInt64), nil } + +// parseMultisigInfoJSON parse json multisig into AdditionalStakerInfo +// Note: it returns (nil, nil), if path/to/multisig.json is not provided (or empty string) +func parseMultisigInfoJSON(fs *pflag.FlagSet) (*types.AdditionalStakerInfo, error) { + var multisigInfo types.AdditionalStakerInfo + + path, err := fs.GetString(FlagMultisigInfoJSON) + if err != nil { + return nil, err + } + + // multisig info is not provided + if path == "" { + return nil, nil + } + + contents, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + err = types.ModuleCdc.UnmarshalJSON(contents, &multisigInfo) + if err != nil { + return nil, err + } + + return &multisigInfo, nil +} diff --git a/x/btcstaking/client/cli/utils_test.go b/x/btcstaking/client/cli/utils_test.go new file mode 100644 index 000000000..951e9d456 --- /dev/null +++ b/x/btcstaking/client/cli/utils_test.go @@ -0,0 +1,58 @@ +package cli + +import ( + "encoding/base64" + "os" + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" +) + +func TestParseMultisigInfoJSON_UnmarshalsBase64Sigs(t *testing.T) { + tmp := t.TempDir() + jsonData := `{ + "staker_btc_pk_list": [ + "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9" + ], + "staker_quorum": 1, + "delegator_slashing_sigs": [ + { + "pk": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "sig": "BOf5A3ZYqSr+tPJbrlM5493KgaNTSTgn0m8W2SMI5J4qJekiCGeKLfhpcNqRsDqK+IFaimBJizWNr1YLNHqlVw==" + } + ], + "delegator_unbonding_slashing_sigs": [ + { + "pk": "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", + "sig": "WDGq7te0S7dOXquUup1ClMSbzypgco2LTCAPUN0xPBurdFh5pa2VSnLEWpHDpR08et6pjYL4SB4OHgNnSm8/tw==" + } + ] +}` + path := tmp + "/multisig.json" + require.NoError(t, os.WriteFile(path, []byte(jsonData), 0o644)) + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.String(FlagMultisigInfoJSON, "", "") + require.NoError(t, fs.Parse([]string{"--" + FlagMultisigInfoJSON, path})) + + info, err := parseMultisigInfoJSON(fs) + require.NoError(t, err) + require.NotNil(t, info) + + expSig1, err := base64.StdEncoding.DecodeString("BOf5A3ZYqSr+tPJbrlM5493KgaNTSTgn0m8W2SMI5J4qJekiCGeKLfhpcNqRsDqK+IFaimBJizWNr1YLNHqlVw==") + require.NoError(t, err) + expSig2, err := base64.StdEncoding.DecodeString("WDGq7te0S7dOXquUup1ClMSbzypgco2LTCAPUN0xPBurdFh5pa2VSnLEWpHDpR08et6pjYL4SB4OHgNnSm8/tw==") + require.NoError(t, err) + + require.Len(t, info.StakerBtcPkList, 1) + require.Equal(t, "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", (&info.StakerBtcPkList[0]).MarshalHex()) + + require.Len(t, info.DelegatorSlashingSigs, 1) + require.Equal(t, expSig1, []byte(*info.DelegatorSlashingSigs[0].Sig)) + require.Equal(t, "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", info.DelegatorSlashingSigs[0].Pk.MarshalHex()) + + require.Len(t, info.DelegatorUnbondingSlashingSigs, 1) + require.Equal(t, expSig2, []byte(*info.DelegatorUnbondingSlashingSigs[0].Sig)) + require.Equal(t, "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9", info.DelegatorUnbondingSlashingSigs[0].Pk.MarshalHex()) +} diff --git a/x/btcstaking/keeper/btc_delegations.go b/x/btcstaking/keeper/btc_delegations.go index c6953b1de..349d90578 100644 --- a/x/btcstaking/keeper/btc_delegations.go +++ b/x/btcstaking/keeper/btc_delegations.go @@ -68,15 +68,15 @@ func (k Keeper) CreateBTCDelegation(ctx sdk.Context, parsedMsg *types.ParsedCrea } } - // everything is good, if the staking tx is not included on BTC consume additinal + // everything is good, if the staking tx is not included on BTC consume additional // gas if !parsedMsg.IsIncludedOnBTC() { ctx.GasMeter().ConsumeGas(params.DelegationCreationBaseGasFee, "delegation creation fee") } - // 7.all good, construct BTCDelegation and insert BTC delegation + // 7. all good, construct BTCDelegation and insert BTC delegation // NOTE: the BTC delegation does not have voting power yet. It will - // have voting power only when it receives a covenant signatures + // have voting power only when it receives covenant signatures newBTCDel := &types.BTCDelegation{ StakerAddr: parsedMsg.StakerAddress.String(), BtcPk: parsedMsg.StakerPK.BIP340PubKey, @@ -109,6 +109,8 @@ func (k Keeper) CreateBTCDelegation(ctx sdk.Context, parsedMsg *types.ParsedCrea return fmt.Errorf("error building stake expansion: %w", err) } + newBTCDel.MultisigInfo = buildMultisigInfo(parsedMsg.MultisigInfo) + // add this BTC delegation, and emit corresponding events if err := k.AddBTCDelegation(ctx, newBTCDel); err != nil { return fmt.Errorf("failed to add BTC delegation that has passed verification: %w", err) @@ -538,3 +540,33 @@ func buildStakeExpansion(stkExp *types.ParsedCreateDelStkExp) (*types.StakeExpan PreviousStkCovenantSigs: nil, }, nil } + +func buildMultisigInfo(multisigInfo *types.ParsedAdditionalStakerInfo) *types.AdditionalStakerInfo { + if multisigInfo == nil { + return nil + } + + slashingSignatureInfo := make([]*types.SignatureInfo, len(multisigInfo.StakerStakingSlashingSigs)) + unbondingSlashingSignatureInfo := make([]*types.SignatureInfo, len(multisigInfo.StakerUnbondingSlashingSigs)) + + for i, si := range multisigInfo.StakerStakingSlashingSigs { + slashingSignatureInfo[i] = &types.SignatureInfo{ + Pk: si.PublicKey.BIP340PubKey, + Sig: si.Sig.BIP340Signature, + } + } + + for i, si := range multisigInfo.StakerUnbondingSlashingSigs { + unbondingSlashingSignatureInfo[i] = &types.SignatureInfo{ + Pk: si.PublicKey.BIP340PubKey, + Sig: si.Sig.BIP340Signature, + } + } + + return &types.AdditionalStakerInfo{ + StakerBtcPkList: multisigInfo.StakerBTCPkList.PublicKeysBbnFormat, + StakerQuorum: multisigInfo.StakerQuorum, + DelegatorSlashingSigs: slashingSignatureInfo, + DelegatorUnbondingSlashingSigs: unbondingSlashingSignatureInfo, + } +} diff --git a/x/btcstaking/keeper/migrations.go b/x/btcstaking/keeper/migrations.go new file mode 100644 index 000000000..0cf8fa2cb --- /dev/null +++ b/x/btcstaking/keeper/migrations.go @@ -0,0 +1,25 @@ +package keeper + +import ( + v2 "github.com/babylonlabs-io/babylon/v4/x/btcstaking/migrations/v2" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Migrator is a struct for handling in-place store migrations. +type Migrator struct { + k Keeper +} + +// NewMigrator returns a new Migrator instance. +func NewMigrator(k Keeper) Migrator { + return Migrator{ + k: k, + } +} + +// Migrate1to2 migrates from version 1 to 2. +// This migration adds support for multisig BTC stakers by ensuring +// MaxStakerQuorum and MaxStakerNum parameters are available. +func (m Migrator) Migrate1to2(ctx sdk.Context) error { + return v2.MigrateStore(ctx, m.k) +} \ No newline at end of file diff --git a/x/btcstaking/keeper/msg_server.go b/x/btcstaking/keeper/msg_server.go index 9ba55e59f..b61ded894 100644 --- a/x/btcstaking/keeper/msg_server.go +++ b/x/btcstaking/keeper/msg_server.go @@ -165,6 +165,11 @@ func (ms msgServer) BtcStakeExpand(goCtx context.Context, req *types.MsgBtcStake return nil, status.Errorf(codes.InvalidArgument, "the previous BTC staking transaction FP: %+v is not the same as FP of the stake expansion %+v", prevBtcDel.FpBtcPkList, req.FpBtcPkList) } + // check that the previous delegation and the new expansion has the same staker btc pk + if err := validateStakerBtcPks(parsedMsg, prevBtcDel); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "%v", err) + } + // Ensure the finality provider is not deleted if ms.IsFinalityProviderDeleted(ctx, &req.FpBtcPkList[0]) { return nil, types.ErrFinalityProviderIsDeleted.Wrapf("finality provider pk %s has been deleted", req.FpBtcPkList[0].MarshalHex()) @@ -245,7 +250,7 @@ func (ms msgServer) AddBTCDelegationInclusionProof( return &types.MsgAddBTCDelegationInclusionProofResponse{}, nil } -// AddCovenantSig adds signatures from covenants to a BTC delegation +// AddCovenantSigs adds signatures from covenants to a BTC delegation // TODO: refactor this handler. Now it's too convoluted func (ms msgServer) AddCovenantSigs(goCtx context.Context, req *types.MsgAddCovenantSigs) (*types.MsgAddCovenantSigsResponse, error) { defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), types.MetricsKeyAddCovenantSigs) @@ -291,14 +296,22 @@ func (ms msgServer) AddCovenantSigs(goCtx context.Context, req *types.MsgAddCove /* Verify each covenant adaptor signature over slashing tx */ - stakingInfo, err := btcDel.GetStakingInfo(params, ms.btcNet) - if err != nil { - panic(fmt.Errorf("failed to get staking info from a verified delegation: %w", err)) + var stakingInfo *btcstaking.StakingInfo + if btcDel.IsMultisigBtcDel() { + stakingInfo, err = btcDel.GetMultisigStakingInfo(params, ms.btcNet) + if err != nil { + panic(fmt.Errorf("failed to get multisig staking info from a verified delegation: %w", err)) + } + } else { + stakingInfo, err = btcDel.GetStakingInfo(params, ms.btcNet) + if err != nil { + panic(fmt.Errorf("failed to get staking info from a verified delegation: %w", err)) + } } slashingSpendInfo, err := stakingInfo.SlashingPathSpendInfo() if err != nil { - // our staking info was constructed by using BuildStakingInfo constructor, so if - // this fails, it is a programming error + // our staking info was constructed by using BuildStakingInfo or + // BuildMultisigStakingInfo constructor, so if this fails, it is a programming error panic(err) } parsedSlashingAdaptorSignatures, err := btcDel.SlashingTx.ParseEncVerifyAdaptorSignatures( @@ -347,14 +360,22 @@ func (ms msgServer) AddCovenantSigs(goCtx context.Context, req *types.MsgAddCove verify each adaptor signature on slashing unbonding tx */ unbondingOutput := unbondingMsgTx.TxOut[0] // unbonding tx always have only one output - unbondingInfo, err := btcDel.GetUnbondingInfo(params, ms.btcNet) - if err != nil { - panic(err) + var unbondingInfo *btcstaking.UnbondingInfo + if btcDel.IsMultisigBtcDel() { + unbondingInfo, err = btcDel.GetMultisigUnbondingInfo(params, ms.btcNet) + if err != nil { + panic(err) + } + } else { + unbondingInfo, err = btcDel.GetUnbondingInfo(params, ms.btcNet) + if err != nil { + panic(err) + } } unbondingSlashingSpendInfo, err := unbondingInfo.SlashingPathSpendInfo() if err != nil { - // our unbonding info was constructed by using BuildStakingInfo constructor, so if - // this fails, it is a programming error + // our unbonding info was constructed by using BuildUnbondingInfo or + // BuildMultisigUnbondingInfo constructor, so if this fails, it is a programming error panic(err) } parsedUnbondingSlashingAdaptorSignatures, err := btcDel.BtcUndelegation.SlashingTx.ParseEncVerifyAdaptorSignatures( @@ -455,10 +476,19 @@ func (ms msgServer) validateStakeExpansionSig( return fmt.Errorf("failed to deserialize other funding txout: %w", err) } - // build staking info of prev delegation - prevDelStakingInfo, err := prevBtcDel.GetStakingInfo(prevParams, ms.btcNet) - if err != nil { - return fmt.Errorf("failed to get staking info of previous delegation: %w", err) + var prevDelStakingInfo *btcstaking.StakingInfo + // if prevBtcDel is a multisig btc delegation, we need to build multisig staking info + if prevBtcDel.IsMultisigBtcDel() { + prevDelStakingInfo, err = prevBtcDel.GetMultisigStakingInfo(prevParams, ms.btcNet) + if err != nil { + return fmt.Errorf("failed to get multisig staking info of previous delegation: %w", err) + } + } else { + // build staking info of prev delegation + prevDelStakingInfo, err = prevBtcDel.GetStakingInfo(prevParams, ms.btcNet) + if err != nil { + return fmt.Errorf("failed to get staking info of previous delegation: %w", err) + } } prevDelUnbondingPathSpendInfo, err := prevDelStakingInfo.UnbondingPathSpendInfo() if err != nil { @@ -592,9 +622,23 @@ func (ms msgServer) BTCUndelegate(goCtx context.Context, req *types.MsgBTCUndele return nil, types.ErrInvalidBTCUndelegateReq.Wrapf("failed to parse funding transactions: %s", err) } + // construct stakerPKs, if it's a single-sig btc delegation, len(stakerPKs) is 1, + // otherwise, it's a multisig btc delegation with non-one stakerCount. + var stakerPKs []*btcec.PublicKey + if btcDel.IsMultisigBtcDel() { + stakerPKs = append(stakerPKs, btcDel.BtcPk.MustToBTCPK()) + stakerBtcPKs, err := bbn.NewBTCPKsFromBIP340PKs(btcDel.MultisigInfo.StakerBtcPkList) + if err != nil { + return nil, err + } + stakerPKs = append(stakerPKs, stakerBtcPKs...) + } else { + stakerPKs = append(stakerPKs, btcDel.BtcPk.MustToBTCPK()) + } + // 4. Verify staker signature on stake spending tx if err := VerifySpendStakeTxStakerSig( - btcDel.BtcPk.MustToBTCPK(), + stakerPKs, stakingTx.TxOut[btcDel.StakingOutputIdx], stakingTxInputIdx, fundingTxs, @@ -765,3 +809,47 @@ func validateStakeExpansionAmt( return nil } + +func validateStakerBtcPks( + parsedMsg *types.ParsedCreateDelegationMessage, + prevBtcDel *types.BTCDelegation, +) error { + // check primary staker pk is the same + oldBtcPk := prevBtcDel.BtcPk.MarshalHex() + newBtcPk := parsedMsg.StakerPK.BIP340PubKey.MarshalHex() + if oldBtcPk != newBtcPk { + return fmt.Errorf("primary staker pk %s does not match previous primary staker pk %s", newBtcPk, oldBtcPk) + } + + // check multisig staker pks if prevBtcDel or new btc del is multisig + if prevBtcDel.IsMultisigBtcDel() || parsedMsg.MultisigInfo != nil { + // if prev btc del is multisig, new btc del must be multisig as well + if parsedMsg.MultisigInfo == nil { + return fmt.Errorf("new btc delegation is not multisig but previous one is") + } + + // if new btc del is multisig, prev btc del must be multisig as well + if prevBtcDel.IsMultisigBtcDel() == false { + return fmt.Errorf("previous btc delegation is not multisig but new one is") + } + + // check the length of old btc del and new btc del + if len(prevBtcDel.MultisigInfo.StakerBtcPkList) != len(parsedMsg.MultisigInfo.StakerBTCPkList.PublicKeysBbnFormat) { + return fmt.Errorf("number of staker pks in multisig delegation does not match") + } + + // sort staker pks in reverse lexicographical order to compare both staker pk list + // from old btc del and new btc del in equal level + sortedOldBtcPks := bbn.SortBIP340PKs(prevBtcDel.MultisigInfo.StakerBtcPkList) + sortedNewBtcPks := bbn.SortBIP340PKs(parsedMsg.MultisigInfo.StakerBTCPkList.PublicKeysBbnFormat) + + // compare both old and new staker btc pk list + for i, pk := range sortedOldBtcPks { + if pk.MarshalHex() != sortedNewBtcPks[i].MarshalHex() { + return fmt.Errorf("staker pk list in multisig delegation does not match") + } + } + } + + return nil +} diff --git a/x/btcstaking/keeper/msg_server_test.go b/x/btcstaking/keeper/msg_server_test.go index 3a525dcbe..00d8d6783 100644 --- a/x/btcstaking/keeper/msg_server_test.go +++ b/x/btcstaking/keeper/msg_server_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "encoding/hex" + "encoding/json" "errors" "fmt" "math" @@ -284,6 +285,8 @@ func FuzzCreateBTCDelegation(f *testing.F) { require.Equal(h.T(), msgCreateBTCDel.Pop, actualDel.Pop) require.Equal(h.T(), msgCreateBTCDel.StakingTx, actualDel.StakingTx) require.Equal(h.T(), msgCreateBTCDel.SlashingTx, actualDel.SlashingTx) + // actual btc delegation has a field `MultisigInfo` + require.Nil(h.T(), actualDel.MultisigInfo) // ensure the BTC delegation in DB is correctly formatted err = actualDel.ValidateBasic() h.NoError(err) @@ -1755,15 +1758,22 @@ func TestActiveAndExpiredEventsSameBlock(t *testing.T) { func TestBtcStakeExpansion(t *testing.T) { testCases := []struct { name string - setupOriginalDelegation func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSK *btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) + stakerCount uint32 // stakerCount is the total number of btc staker used for btc delegation + stakerQuorum uint32 // stakerQuorum is the threshold of multisig staker + expStkExpErr bool + ogDelCount int // ogDelCount is set when using different delegator SKs for original delegation than stake extension + setupOriginalDelegation func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSKs []*btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) }{ { - name: "expand regular delegation", - setupOriginalDelegation: func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSK *btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) { + name: "expand regular delegation", + stakerCount: 1, + stakerQuorum: 1, + expStkExpErr: false, + setupOriginalDelegation: func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSKs []*btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) { lcTip := uint32(30) prevDelStakingTxHash, prevMsgCreateBTCDel, prevDel, _, _, _, err := h.CreateDelegationWithBtcBlockHeight( r, - delSK, + delSKs[0], babylonFPPK, stakingValue, 1000, @@ -1790,12 +1800,15 @@ func TestBtcStakeExpansion(t *testing.T) { }, }, { - name: "expand expanded delegation", - setupOriginalDelegation: func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSK *btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) { + name: "expand expanded delegation", + stakerCount: 1, + stakerQuorum: 1, + expStkExpErr: false, + setupOriginalDelegation: func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSKs []*btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) { lcTip := uint32(30) originalDelStakingTxHash, originalMsgCreateBTCDel, originalDel, _, _, _, err := h.CreateDelegationWithBtcBlockHeight( r, - delSK, + delSKs[0], babylonFPPK, stakingValue, 1000, @@ -1820,7 +1833,7 @@ func TestBtcStakeExpansion(t *testing.T) { firstExpansionSpendingTx, firstExpansionFundingTx, err := h.CreateBtcStakeExpansionWithBtcTipHeight( r, - delSK, + delSKs[0], babylonFPPK, stakingValue, 1000, @@ -1842,7 +1855,7 @@ func TestBtcStakeExpansion(t *testing.T) { t, originalStkTx.TxOut[0], firstExpansionFundingTx.TxOut[0], - delSK, + delSKs[0], covenantSKs, bsParams.CovenantQuorum, []*btcec.PublicKey{babylonFPPK}, @@ -1878,6 +1891,114 @@ func TestBtcStakeExpansion(t *testing.T) { return firstExpandedDel, firstExpansionSpendingTx.TxHash().String(), lcTip }, }, + { + name: "expand multisig delegation", + stakerCount: 3, + stakerQuorum: 2, + expStkExpErr: false, + setupOriginalDelegation: func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSKs []*btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) { + lcTip := uint32(30) + prevDelStakingTxHash, prevMsgCreateBTCDel, prevDel, _, _, _, err := h.CreateMultisigDelegationWithBtcBlockHeight( + r, + delSKs, + 2, + babylonFPPK, + stakingValue, + 1000, + 0, + 0, + false, + true, + 10, + lcTip, + ) + h.NoError(err) + require.NotNil(t, prevMsgCreateBTCDel) + + h.CreateCovenantSigs(r, covenantSKs, prevMsgCreateBTCDel, prevDel, 10) + + bsParams := h.BTCStakingKeeper.GetParams(h.Ctx) + prevDel, err = h.BTCStakingKeeper.GetBTCDelegation(h.Ctx, prevDelStakingTxHash) + require.NoError(t, err) + status, err := h.BTCStakingKeeper.BtcDelStatus(h.Ctx, prevDel, bsParams.CovenantQuorum, lcTip) + h.NoError(err) + require.Equal(t, types.BTCDelegationStatus_ACTIVE, status) + + return prevDel, prevDelStakingTxHash, lcTip + }, + }, + { + name: "expand single sig delegation into multisig delegation", + stakerCount: 3, + stakerQuorum: 2, + expStkExpErr: true, + setupOriginalDelegation: func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSKs []*btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) { + lcTip := uint32(30) + prevDelStakingTxHash, prevMsgCreateBTCDel, prevDel, _, _, _, err := h.CreateDelegationWithBtcBlockHeight( + r, + delSKs[0], + babylonFPPK, + stakingValue, + 1000, + 0, + 0, + false, + true, + 10, + lcTip, + ) + h.NoError(err) + require.NotNil(t, prevMsgCreateBTCDel) + + h.CreateCovenantSigs(r, covenantSKs, prevMsgCreateBTCDel, prevDel, 10) + + bsParams := h.BTCStakingKeeper.GetParams(h.Ctx) + prevDel, err = h.BTCStakingKeeper.GetBTCDelegation(h.Ctx, prevDelStakingTxHash) + require.NoError(t, err) + status, err := h.BTCStakingKeeper.BtcDelStatus(h.Ctx, prevDel, bsParams.CovenantQuorum, lcTip) + h.NoError(err) + require.Equal(t, types.BTCDelegationStatus_ACTIVE, status) + + return prevDel, prevDelStakingTxHash, lcTip + }, + }, + { + name: "expand multisig delegation into single sig delegation", + stakerCount: 1, + stakerQuorum: 1, + expStkExpErr: true, + ogDelCount: 3, + setupOriginalDelegation: func(t *testing.T, h *testutil.Helper, r *rand.Rand, covenantSKs []*btcec.PrivateKey, babylonFPPK *btcec.PublicKey, delSKs []*btcec.PrivateKey, stakingValue int64) (*types.BTCDelegation, string, uint32) { + lcTip := uint32(30) + prevDelStakingTxHash, prevMsgCreateBTCDel, prevDel, _, _, _, err := h.CreateMultisigDelegationWithBtcBlockHeight( + r, + delSKs, + 2, + babylonFPPK, + stakingValue, + 1000, + 0, + 0, + false, + true, + 10, + lcTip, + ) + h.NoError(err) + require.NotNil(t, prevMsgCreateBTCDel) + + h.CreateCovenantSigs(r, covenantSKs, prevMsgCreateBTCDel, prevDel, 10) + + bsParams := h.BTCStakingKeeper.GetParams(h.Ctx) + prevDel, err = h.BTCStakingKeeper.GetBTCDelegation(h.Ctx, prevDelStakingTxHash) + require.NoError(t, err) + status, err := h.BTCStakingKeeper.BtcDelStatus(h.Ctx, prevDel, bsParams.CovenantQuorum, lcTip) + h.NoError(err) + require.Equal(t, types.BTCDelegationStatus_ACTIVE, status) + + return prevDel, prevDelStakingTxHash, lcTip + }, + }, } for _, tc := range testCases { @@ -1895,21 +2016,59 @@ func TestBtcStakeExpansion(t *testing.T) { _, babylonFPPK, _ := h.CreateFinalityProvider(r) stakingValue := int64(2 * 10e8) - delSK, _, err := datagen.GenRandomBTCKeyPair(r) - h.NoError(err) - prevDel, prevDelStakingTxHash, lcTip := tc.setupOriginalDelegation(t, h, r, covenantSKs, babylonFPPK, delSK, stakingValue) + var ( + delSKs []*btcec.PrivateKey + err error + ) + if tc.ogDelCount > 0 { + delSKs, _, err = datagen.GenRandomBTCKeyPairs(r, tc.ogDelCount) + h.NoError(err) + } else { + delSKs, _, err = datagen.GenRandomBTCKeyPairs(r, int(tc.stakerCount)) + h.NoError(err) + } - spendingTx, fundingTx, err := h.CreateBtcStakeExpansionWithBtcTipHeight( - r, - delSK, - babylonFPPK, - stakingValue, - 1000, - prevDel, - lcTip, + prevDel, prevDelStakingTxHash, lcTip := tc.setupOriginalDelegation(t, h, r, covenantSKs, babylonFPPK, delSKs, stakingValue) + + var ( + spendingTx *wire.MsgTx + fundingTx *wire.MsgTx ) - require.NoError(t, err) + + // handle single sig and multisig btc stake expansion separately + if int(tc.stakerCount) != 1 { + spendingTx, fundingTx, err = h.CreateMultisigBtcStakeExpansionWithBtcTipHeight( + r, + delSKs, + tc.stakerQuorum, + babylonFPPK, + stakingValue, + 1000, + prevDel, + lcTip, + ) + if tc.expStkExpErr { + require.Error(t, err) + return + } + require.NoError(t, err) + } else { + spendingTx, fundingTx, err = h.CreateBtcStakeExpansionWithBtcTipHeight( + r, + delSKs[0], + babylonFPPK, + stakingValue, + 1000, + prevDel, + lcTip, + ) + if tc.expStkExpErr { + require.Error(t, err) + return + } + require.NoError(t, err) + } expandedDel, err := h.BTCStakingKeeper.GetBTCDelegation(h.Ctx, spendingTx.TxHash().String()) require.NoError(t, err) @@ -1929,19 +2088,38 @@ func TestBtcStakeExpansion(t *testing.T) { prevStkTx, err := bbn.NewBTCTxFromBytes(prevDel.GetStakingTx()) require.NoError(t, err) - spendingTxWithWitnessBz, _ := datagen.AddWitnessToStakeExpTx( - t, - prevStkTx.TxOut[0], - fundingTx.TxOut[0], - delSK, - covenantSKs, - bsParams.CovenantQuorum, - []*btcec.PublicKey{babylonFPPK}, - uint16(1000), - stakingValue, - spendingTx, - h.Net, - ) + var spendingTxWithWitnessBz []byte + // handle single sig and multisig btc stake expansion separately + if int(tc.stakerCount) != 1 { + spendingTxWithWitnessBz, _ = datagen.AddMultisigWitnessToStakeExpTx( + t, + prevStkTx.TxOut[0], + fundingTx.TxOut[0], + delSKs, + tc.stakerQuorum, + covenantSKs, + bsParams.CovenantQuorum, + []*btcec.PublicKey{babylonFPPK}, + uint16(1000), + stakingValue, + spendingTx, + h.Net, + ) + } else { + spendingTxWithWitnessBz, _ = datagen.AddWitnessToStakeExpTx( + t, + prevStkTx.TxOut[0], + fundingTx.TxOut[0], + delSKs[0], + covenantSKs, + bsParams.CovenantQuorum, + []*btcec.PublicKey{babylonFPPK}, + uint16(1000), + stakingValue, + spendingTx, + h.Net, + ) + } expansionTxInclusionProof := h.BuildBTCInclusionProofForSpendingTx(r, spendingTx, lcTip) @@ -1992,3 +2170,158 @@ func TestBtcStakeExpansion(t *testing.T) { }) } } + +func TestMultisigCreateBTCDelegationWithMaxStakerParams(t *testing.T) { + // 1. create btc delegation with 2-of-3 multisig -> success + // 2. create btc delegation with 3-of-5 multisig -> fail since current test helper set 2-of-3 as a max + testCases := []struct { + name string + stakerQuorum uint32 + stakerNum uint32 + expErr error + }{ + { + name: "create btc delegation with 2-of-3 multisig - default max params: 2-of-3", + stakerQuorum: 2, + stakerNum: 3, + expErr: nil, + }, + { + name: "create btc delegation with 3-of-5 multisig - default max params: 2-of-3", + stakerQuorum: 3, + stakerNum: 5, + expErr: types.ErrInvalidMultisigInfo, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // mock BTC light client and BTC checkpoint modules + btclcKeeper := types.NewMockBTCLightClientKeeper(ctrl) + btccKeeper := types.NewMockBtcCheckpointKeeper(ctrl) + h := testutil.NewHelper(t, btclcKeeper, btccKeeper, nil) + + // set all parameters, default max 2-of-3 multisig + h.GenAndApplyParams(r) + + // generate and insert new finality provider + _, fpPK, _ := h.CreateFinalityProvider(r) + + usePreApproval := datagen.OneInN(r, 2) + + // generate and insert new BTC delegation + stakingValue := int64(2 * 10e8) + delSKs, _, err := datagen.GenRandomBTCKeyPairs(r, int(tc.stakerNum)) + h.NoError(err) + + var stakingTxHash string + var msgCreateBTCDel *types.MsgCreateBTCDelegation + if usePreApproval { + stakingTxHash, msgCreateBTCDel, _, _, _, _, err = h.CreateMultisigDelegationWithBtcBlockHeight( + r, + delSKs, + tc.stakerQuorum, + fpPK, + stakingValue, + 1000, + 0, + 0, + usePreApproval, + false, + 10, + 10, + ) + } else { + stakingTxHash, msgCreateBTCDel, _, _, _, _, err = h.CreateMultisigDelegationWithBtcBlockHeight( + r, + delSKs, + tc.stakerQuorum, + fpPK, + stakingValue, + 1000, + 0, + 0, + usePreApproval, + false, + 10, + 30, + ) + } + + // check error based on expectation + if tc.expErr != nil { + // expect error + require.Error(t, err) + require.ErrorIs(t, err, tc.expErr) + return // stop here for error cases + } + + // no error expected - continue with validation + h.NoError(err) + + // ensure consistency between the msg and the BTC delegation in DB + actualDel, err := h.BTCStakingKeeper.GetBTCDelegation(h.Ctx, stakingTxHash) + h.NoError(err) + require.Equal(h.T(), msgCreateBTCDel.StakerAddr, actualDel.StakerAddr) + require.Equal(h.T(), msgCreateBTCDel.Pop, actualDel.Pop) + require.Equal(h.T(), msgCreateBTCDel.StakingTx, actualDel.StakingTx) + require.Equal(h.T(), msgCreateBTCDel.SlashingTx, actualDel.SlashingTx) + require.Equal(h.T(), msgCreateBTCDel.MultisigInfo, actualDel.MultisigInfo) + + // MultisigInfo contains staker info except the one representative staker info, + // that is, for M-of-N multisig, length of StakerBtcPkList of MultisigInfo is N-1 + require.Equal(h.T(), int(tc.stakerNum), len(actualDel.MultisigInfo.StakerBtcPkList)+1) + require.Equal(h.T(), tc.stakerQuorum, actualDel.MultisigInfo.StakerQuorum) + + // ensure the BTC delegation in DB is correctly formatted + err = actualDel.ValidateBasic() + h.NoError(err) + // delegation is not activated by covenant yet + hasQuorum, err := h.BTCStakingKeeper.BtcDelHasCovenantQuorums(h.Ctx, actualDel, h.BTCStakingKeeper.GetParams(h.Ctx).CovenantQuorum) + h.NoError(err) + require.False(h.T(), hasQuorum) + + if usePreApproval { + require.Zero(h.T(), actualDel.StartHeight) + require.Zero(h.T(), actualDel.EndHeight) + } else { + require.Positive(h.T(), actualDel.StartHeight) + require.Positive(h.T(), actualDel.EndHeight) + } + + // check events emitted + events := h.Ctx.EventManager().Events() + var foundBtcDelCreatedEvent bool + + // build expected multisig staker pk hexs from delSKs (skip first key since it's the main staker key) + var expectedMultisigStakerPkHexs string + if tc.stakerNum > 1 { + multisigPkHexs := make([]string, 0, tc.stakerNum-1) + for i := 1; i < int(tc.stakerNum); i++ { + multisigPkHexs = append(multisigPkHexs, hex.EncodeToString(delSKs[i].PubKey().SerializeCompressed()[1:])) + } + + jsonBytes, err := json.Marshal(multisigPkHexs) + require.NoError(t, err) + expectedMultisigStakerPkHexs = string(jsonBytes) + } + + for _, event := range events { + if fmt.Sprintf("/%s", event.Type) == sdk.MsgTypeURL(&types.EventBTCDelegationCreated{}) { + foundBtcDelCreatedEvent = true + testutilevents.RequireEventAttribute(t, event, "staking_tx_hex", fmt.Sprintf("\"%s\"", hex.EncodeToString(actualDel.StakingTx)), "BTC Delegation created event should match the staking tx hash") + + if tc.stakerNum > 1 { + // for multisig, multisig_staker_btc_pk_hexs should have N-1 keys + testutilevents.RequireEventAttribute(t, event, "multisig_staker_btc_pk_hexs", expectedMultisigStakerPkHexs, "BTC Delegation Created event should have extra staker info field with correct keys") + } + } + } + require.True(t, foundBtcDelCreatedEvent, "EventBTCDelegationCreated should be emitted") + }) + } +} diff --git a/x/btcstaking/keeper/params.go b/x/btcstaking/keeper/params.go index c0343b4d1..9cd83e742 100644 --- a/x/btcstaking/keeper/params.go +++ b/x/btcstaking/keeper/params.go @@ -144,6 +144,21 @@ func (k Keeper) GetAllParams(ctx context.Context) []*types.Params { return p } +func (k Keeper) GetAllStoredParams(ctx context.Context) []*types.StoredParams { + paramsStore := k.paramsStore(ctx) + it := paramsStore.Iterator(nil, nil) + defer it.Close() + + var sps []*types.StoredParams + for ; it.Valid(); it.Next() { + var sp types.StoredParams + k.cdc.MustUnmarshal(it.Value(), &sp) + sps = append(sps, &sp) + } + + return sps +} + func (k Keeper) GetParamsByVersion(ctx context.Context, v uint32) *types.Params { paramsStore := k.paramsStore(ctx) spBytes := paramsStore.Get(uint32ToBytes(v)) diff --git a/x/btcstaking/keeper/unbonding_transaction_sig_validation.go b/x/btcstaking/keeper/unbonding_transaction_sig_validation.go index a0b066c30..7914cd8f8 100644 --- a/x/btcstaking/keeper/unbonding_transaction_sig_validation.go +++ b/x/btcstaking/keeper/unbonding_transaction_sig_validation.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + bbn "github.com/babylonlabs-io/babylon/v4/types" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -94,11 +95,12 @@ func buildOutputFetcher( // Funding transactions are necessary, as taproot signature commits to values and // pkScripts of all the inputs to the spend stake transaction. func VerifySpendStakeTxStakerSig( - stakerPubKey *btcec.PublicKey, + stakerPubKeys []*btcec.PublicKey, stakingOutput *wire.TxOut, stakingInputIdx uint32, fundingTransactions []*wire.MsgTx, - spendStakeTx *wire.MsgTx) error { + spendStakeTx *wire.MsgTx, +) error { // sanity check protecting against passing non-staking outputs if !txscript.IsPayToTaproot(stakingOutput.PkScript) { return fmt.Errorf("staking output must be a pay-to-taproot output") @@ -158,42 +160,59 @@ func VerifySpendStakeTxStakerSig( // Staker key is always first in the script, therefore signature will be last. // It is true regardless of the path used to spend the staking output (timelock, unbonding, slashing) - stakerRawSig := stakeSpendWitness[len(stakeSpendWitness)-3] - - stakerSig, sigHashType, err := parseSchnorrSigFromWitness(stakerRawSig) - - if err != nil { - return fmt.Errorf("failed to parse schnorr signature from witness: %w", err) - } + // index of stakerRawSigs started from len(stakeSpendWitness)-2-stakerCount to len(stakeSpendWitness)-3 + // NOTE: if single sig btc delegation, length of the stakerRawSigs is 1, otherwise (M-of-N multisig btc delegation), + // the length of stakerRawSigs is same with the length of stakerPubKeys (N) + stakerCount := len(stakerPubKeys) + stakerRawSigs := stakeSpendWitness[len(stakeSpendWitness)-2-stakerCount : len(stakeSpendWitness)-2] + + for i, stakerRawSig := range stakerRawSigs { + // stakerRawSig could be an empty byte to match the total length of stakerPubKeys + if len(stakerRawSig) == 0 { + continue + } + stakerSig, sigHashType, err := parseSchnorrSigFromWitness(stakerRawSig) + if err != nil { + return fmt.Errorf("failed to parse schnorr signature from witness: %w", err) + } + prevOuts, err := buildOutputFetcher(fundingTransactions, spendStakeTx) + if err != nil { + return fmt.Errorf("failed to build output fetcher from provided funding transactions: %w", err) + } - prevOuts, err := buildOutputFetcher(fundingTransactions, spendStakeTx) - if err != nil { - return fmt.Errorf("failed to build output fetcher from provided funding transactions: %w", err) - } + var opts []txscript.TaprootSigHashOption + if len(annex) > 0 { + opts = append(opts, txscript.WithAnnex(annex)) + } - var opts []txscript.TaprootSigHashOption - if len(annex) > 0 { - opts = append(opts, txscript.WithAnnex(annex)) - } + sigHash, err := txscript.CalcTapscriptSignaturehash( + txscript.NewTxSigHashes(spendStakeTx, prevOuts), + sigHashType, + spendStakeTx, + int(stakingInputIdx), + prevOuts, + txscript.NewBaseTapLeaf(witnessScript), + opts..., + ) - sigHash, err := txscript.CalcTapscriptSignaturehash( - txscript.NewTxSigHashes(spendStakeTx, prevOuts), - sigHashType, - spendStakeTx, - int(stakingInputIdx), - prevOuts, - txscript.NewBaseTapLeaf(witnessScript), - opts..., - ) + if err != nil { + return fmt.Errorf("failed to calculate tapscript signature hash: %w", err) + } - if err != nil { - return fmt.Errorf("failed to calculate tapscript signature hash: %w", err) - } + // sort stakerPubKeys in reverse lexicographical order before verify it with the given signature + // NOTE: staker signatures are also sorted in reverse lexicographical order + stakerBIP340PKs := bbn.NewBIP340PKsFromBTCPKs(stakerPubKeys) + sortedStakerBIP340PKs := bbn.SortBIP340PKs(stakerBIP340PKs) + sortedStakerBTCPks, err := bbn.NewBTCPKsFromBIP340PKs(sortedStakerBIP340PKs) + if err != nil { + return fmt.Errorf("failed to build bip340 pks: %w", err) + } - valid := stakerSig.Verify(sigHash, stakerPubKey) + valid := stakerSig.Verify(sigHash, sortedStakerBTCPks[i]) - if !valid { - return fmt.Errorf("failed to verify schnorr signature: %w", err) + if !valid { + return fmt.Errorf("failed to verify %d schnorr signature of %d: %w", i, stakerCount, err) + } } return nil diff --git a/x/btcstaking/keeper/unbonding_transaction_sig_validation_test.go b/x/btcstaking/keeper/unbonding_transaction_sig_validation_test.go index c42147ece..0c70a0055 100644 --- a/x/btcstaking/keeper/unbonding_transaction_sig_validation_test.go +++ b/x/btcstaking/keeper/unbonding_transaction_sig_validation_test.go @@ -168,7 +168,7 @@ func FuzzSigVerification(f *testing.F) { spendStakeTx.TxIn[stakingInputIdx].Witness = witness err = keeper.VerifySpendStakeTxStakerSig( - stakerPubKey, + []*btcec.PublicKey{stakerPubKey}, stakingInfo.StakingOutput, stakingInputIdx, fundingTxs, @@ -177,3 +177,143 @@ func FuzzSigVerification(f *testing.F) { require.NoError(t, err) }) } + +func TestVerifySpendStakeTxStakerSig(t *testing.T) { + t.Run("accepts 2-of-3 multisig staker witness", func(t *testing.T) { + fixture := newMultisigSpendStakeFixture(t, 3, 2) + + stakerSigs := datagen.GenerateSignatures( + t, + fixture.stakerPrivs, + fixture.spendStakeTx, + fixture.stakingInfo.StakingOutput, + fixture.unbondingSpendInfo.RevealedLeaf, + ) + stakerSigs[len(stakerSigs)-1] = nil + + covenantSig, err := btcstaking.SignTxWithOneScriptSpendInputFromTapLeaf( + fixture.spendStakeTx, + fixture.stakingInfo.StakingOutput, + covenantSk, + fixture.unbondingSpendInfo.RevealedLeaf, + ) + require.NoError(t, err) + + witness, err := fixture.unbondingSpendInfo.CreateMultisigUnbondingPathWitness( + []*schnorr.Signature{covenantSig}, + stakerSigs, + ) + require.NoError(t, err) + + fixture.spendStakeTx.TxIn[fixture.stakingInputIdx].Witness = witness + + err = keeper.VerifySpendStakeTxStakerSig( + fixture.stakerPubKeys, + fixture.stakingInfo.StakingOutput, + fixture.stakingInputIdx, + fixture.fundingTxs, + fixture.spendStakeTx, + ) + require.NoError(t, err) + }) + + t.Run("rejects witness with misordered staker signatures", func(t *testing.T) { + fixture := newMultisigSpendStakeFixture(t, 3, 2) + + validStakerSigs := datagen.GenerateSignatures( + t, + fixture.stakerPrivs, + fixture.spendStakeTx, + fixture.stakingInfo.StakingOutput, + fixture.unbondingSpendInfo.RevealedLeaf, + ) + + invalidOrderSigs := append([]*schnorr.Signature(nil), validStakerSigs...) + invalidOrderSigs[0], invalidOrderSigs[1] = invalidOrderSigs[1], invalidOrderSigs[0] + + covenantSig, err := btcstaking.SignTxWithOneScriptSpendInputFromTapLeaf( + fixture.spendStakeTx, + fixture.stakingInfo.StakingOutput, + covenantSk, + fixture.unbondingSpendInfo.RevealedLeaf, + ) + require.NoError(t, err) + + witness, err := fixture.unbondingSpendInfo.CreateMultisigUnbondingPathWitness( + []*schnorr.Signature{covenantSig}, + invalidOrderSigs, + ) + require.NoError(t, err) + fixture.spendStakeTx.TxIn[fixture.stakingInputIdx].Witness = witness + + err = keeper.VerifySpendStakeTxStakerSig( + fixture.stakerPubKeys, + fixture.stakingInfo.StakingOutput, + fixture.stakingInputIdx, + fixture.fundingTxs, + fixture.spendStakeTx, + ) + require.Error(t, err) + }) +} + +type multisigSpendStakeFixture struct { + stakerPrivs []*btcec.PrivateKey + stakerPubKeys []*btcec.PublicKey + stakingInfo *btcstaking.StakingInfo + fundingTxs []*wire.MsgTx + spendStakeTx *wire.MsgTx + stakingInputIdx uint32 + unbondingSpendInfo *btcstaking.SpendInfo +} + +func newMultisigSpendStakeFixture(t *testing.T, stakerCount int, stakerQuorum uint32) *multisigSpendStakeFixture { + t.Helper() + + stakerPrivs := make([]*btcec.PrivateKey, stakerCount) + stakerPubKeys := make([]*btcec.PublicKey, stakerCount) + for i := 0; i < stakerCount; i++ { + sk, err := btcec.NewPrivateKey() + require.NoError(t, err) + stakerPrivs[i] = sk + stakerPubKeys[i] = sk.PubKey() + } + + stakingInfo, err := btcstaking.BuildMultisigStakingInfo( + stakerPubKeys, + stakerQuorum, + []*btcec.PublicKey{finalityProviderSK.PubKey()}, + []*btcec.PublicKey{covenantSk.PubKey()}, + 1, + stakingTime, + stakingAmount, + &chainParams, + ) + require.NoError(t, err) + + fundingTx := wire.NewMsgTx(2) + fundingTx.AddTxOut(stakingInfo.StakingOutput) + + spendStakeTx := wire.NewMsgTx(2) + stakingOutPoint := wire.OutPoint{Hash: fundingTx.TxHash(), Index: 0} + spendStakeTx.AddTxIn(wire.NewTxIn(&stakingOutPoint, nil, nil)) + spendStakeTx.AddTxOut( + wire.NewTxOut( + stakingInfo.StakingOutput.Value-1000, + stakingInfo.StakingOutput.PkScript, + ), + ) + + unbondingSpendInfo, err := stakingInfo.UnbondingPathSpendInfo() + require.NoError(t, err) + + return &multisigSpendStakeFixture{ + stakerPrivs: stakerPrivs, + stakerPubKeys: stakerPubKeys, + stakingInfo: stakingInfo, + fundingTxs: []*wire.MsgTx{fundingTx}, + spendStakeTx: spendStakeTx, + stakingInputIdx: 0, + unbondingSpendInfo: unbondingSpendInfo, + } +} diff --git a/x/btcstaking/migrations/v2/store.go b/x/btcstaking/migrations/v2/store.go new file mode 100644 index 000000000..7154b85dc --- /dev/null +++ b/x/btcstaking/migrations/v2/store.go @@ -0,0 +1,31 @@ +package v2 + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" +) + +// Keeper exposes minimal methods used by the migration. +type Keeper interface { + GetAllStoredParams(ctx context.Context) []*types.StoredParams + OverwriteParamsAtVersion(ctx context.Context, version uint32, params types.Params) error +} + +// MigrateStore performs in-place store migrations from v1 to v2. +func MigrateStore(ctx sdk.Context, k Keeper) error { + allParams := k.GetAllStoredParams(ctx) + + for _, params := range allParams { + params.Params.MaxStakerNum = 1 + params.Params.MaxStakerQuorum = 1 + + if err := k.OverwriteParamsAtVersion(ctx, params.Version, params.Params); err != nil { + return err + } + } + + return nil +} diff --git a/x/btcstaking/module.go b/x/btcstaking/module.go index 5a5eab645..c4c57d7c8 100644 --- a/x/btcstaking/module.go +++ b/x/btcstaking/module.go @@ -29,6 +29,10 @@ var ( _ module.AppModuleBasic = AppModuleBasic{} ) +const ( + consensusVersion = 2 +) + // ---------------------------------------------------------------------------- // AppModuleBasic // ---------------------------------------------------------------------------- @@ -113,6 +117,12 @@ func NewAppModule( func (am AppModule) RegisterServices(cfg module.Configurator) { types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) types.RegisterQueryServer(cfg.QueryServer(), am.keeper) + + m := keeper.NewMigrator(am.keeper) + + if err := cfg.RegisterMigration(types.ModuleName, 1, m.Migrate1to2); err != nil { + panic(fmt.Sprintf("failed to migrate x/%s from version 1 to 2: %v", types.ModuleName, err)) + } } // RegisterInvariants registers the invariants of the module. If an invariant deviates from its predicted value, the InvariantRegistry triggers appropriate logic (most often the chain will be halted) @@ -135,7 +145,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw } // ConsensusVersion is a sequence number for state-breaking change of the module. It should be incremented on each consensus-breaking change introduced by the module. To avoid wrong/empty versions, the initial version should be set to 1 -func (AppModule) ConsensusVersion() uint64 { return 1 } +func (AppModule) ConsensusVersion() uint64 { return consensusVersion } func (am AppModule) BeginBlock(ctx context.Context) error { return BeginBlocker(ctx, am.keeper) diff --git a/x/btcstaking/types/btc_delegation.go b/x/btcstaking/types/btc_delegation.go index 6d5c2861c..97c39ff52 100644 --- a/x/btcstaking/types/btc_delegation.go +++ b/x/btcstaking/types/btc_delegation.go @@ -286,6 +286,12 @@ func (d *BTCDelegation) ValidateBasic() error { } } + if d.IsMultisigBtcDel() { + if err := d.MultisigInfo.Validate(); err != nil { + return err + } + } + return nil } @@ -367,6 +373,42 @@ func (d *BTCDelegation) GetStakingInfo(bsParams *Params, btcNet *chaincfg.Params return stakingInfo, nil } +// GetMultisigStakingInfo returns the staking info of the BTC delegation +// the multisig staking info can be used for constructing witness of slashing tx +// with access to a finality provider's SK +func (d *BTCDelegation) GetMultisigStakingInfo(bsParams *Params, btcNet *chaincfg.Params) (*btcstaking.StakingInfo, error) { + fpBtcPkList, err := bbn.NewBTCPKsFromBIP340PKs(d.FpBtcPkList) + if err != nil { + return nil, fmt.Errorf("failed to convert finality provider pks to BTC pks %v", err) + } + covenantBtcPkList, err := bbn.NewBTCPKsFromBIP340PKs(bsParams.CovenantPks) + if err != nil { + return nil, fmt.Errorf("failed to convert covenant pks to BTC pks %v", err) + } + + // construct stakerPKs from both multisig info and BtcPk + stakerPKs, err := bbn.NewBTCPKsFromBIP340PKs(d.MultisigInfo.StakerBtcPkList) + if err != nil { + return nil, fmt.Errorf("failed to BIP340 pks to BTC pks %v", err) + } + stakerPKs = append(stakerPKs, d.BtcPk.MustToBTCPK()) + + stakingInfo, err := btcstaking.BuildMultisigStakingInfo( + stakerPKs, + d.MultisigInfo.StakerQuorum, + fpBtcPkList, + covenantBtcPkList, + bsParams.CovenantQuorum, + d.MustGetValidStakingTime(), + btcutil.Amount(d.TotalSat), + btcNet, + ) + if err != nil { + return nil, fmt.Errorf("could not create BTC staking info: %v", err) + } + return stakingInfo, nil +} + func (d *BTCDelegation) SignUnbondingTx(bsParams *Params, btcNet *chaincfg.Params, sk *btcec.PrivateKey) (*schnorr.Signature, error) { stakingTx, err := bbn.NewBTCTxFromBytes(d.StakingTx) if err != nil { @@ -432,6 +474,48 @@ func (d *BTCDelegation) GetUnbondingInfo(bsParams *Params, btcNet *chaincfg.Para return unbondingInfo, nil } +// GetMultisigUnbondingInfo returns the multisig unbonding info of the BTC delegation +// the unbonding info can be used for constructing witness of unbonding slashing +// tx with access to a finality provider's SK +func (d *BTCDelegation) GetMultisigUnbondingInfo(bsParams *Params, btcNet *chaincfg.Params) (*btcstaking.UnbondingInfo, error) { + fpBtcPkList, err := bbn.NewBTCPKsFromBIP340PKs(d.FpBtcPkList) + if err != nil { + return nil, fmt.Errorf("failed to convert finality provider pks to BTC pks: %v", err) + } + + covenantBtcPkList, err := bbn.NewBTCPKsFromBIP340PKs(bsParams.CovenantPks) + if err != nil { + return nil, fmt.Errorf("failed to convert covenant pks to BTC pks: %v", err) + } + unbondingTx, err := bbn.NewBTCTxFromBytes(d.BtcUndelegation.UnbondingTx) + if err != nil { + return nil, fmt.Errorf("failed to parse unbonding transaction: %v", err) + } + + // construct stakerPKs from both multisig info and BtcPk + stakerPKs, err := bbn.NewBTCPKsFromBIP340PKs(d.MultisigInfo.StakerBtcPkList) + if err != nil { + return nil, fmt.Errorf("failed to convert staker pks to BTC pks: %v", err) + } + stakerPKs = append(stakerPKs, d.BtcPk.MustToBTCPK()) + + unbondingInfo, err := btcstaking.BuildMultisigUnbondingInfo( + stakerPKs, + d.MultisigInfo.StakerQuorum, + fpBtcPkList, + covenantBtcPkList, + bsParams.CovenantQuorum, + uint16(d.GetUnbondingTime()), + btcutil.Amount(unbondingTx.TxOut[0].Value), + btcNet, + ) + if err != nil { + return nil, fmt.Errorf("could not create BTC staking info: %v", err) + } + + return unbondingInfo, nil +} + // findFPIdx returns the index of the given finality provider // among all restaked finality providers func (d *BTCDelegation) findFPIdx(fpBTCPK *bbn.BIP340PubKey) (int, error) { @@ -554,6 +638,11 @@ func (d *BTCDelegation) IsStakeExpansion() bool { return d.StkExp != nil } +// IsMultisigBtcDel return true if the BTC delegation contains `MultisigInfo` +func (d *BTCDelegation) IsMultisigBtcDel() bool { + return d.MultisigInfo != nil +} + func (s *StakeExpansion) AddCovenantSigs( covPk *bbn.BIP340PubKey, stkExpSig *bbn.BIP340Signature, @@ -683,3 +772,50 @@ func (i *BTCDelegatorDelegationIndex) Add(stakingTxHash chainhash.Hash) error { return nil } + +func (a *AdditionalStakerInfo) ToResponse() *AdditionalStakerInfoResponse { + return &AdditionalStakerInfoResponse{ + StakerBtcPkList: a.StakerBtcPkList, + StakerQuorum: a.StakerQuorum, + DelegatorSlashingSigs: a.DelegatorSlashingSigs, + DelegatorUnbondingSlashingSigs: a.DelegatorUnbondingSlashingSigs, + } +} + +func (a *AdditionalStakerInfo) Validate() error { + if len(a.StakerBtcPkList) == 0 { + return fmt.Errorf("length of the stakerBtcPkList is zero") + } + + if a.StakerQuorum == 0 { + return fmt.Errorf("stakerQuorum is zero") + } + + if len(a.DelegatorSlashingSigs) == 0 { + return fmt.Errorf("length of the delegatorSlashingSigs is zero") + } + + if len(a.DelegatorUnbondingSlashingSigs) == 0 { + return fmt.Errorf("length of the delegatorUnbondingSlashingSigs is zero") + } + + for i, sig := range a.DelegatorSlashingSigs { + if sig == nil { + return errorsmod.Wrapf(ErrInvalidMultisigInfo, "DelegatorSlashingSigs[%d] is nil", i) + } + if err := sig.Validate(); err != nil { + return errorsmod.Wrapf(ErrInvalidMultisigInfo, "invalid signature at index %d: %v", i, err) + } + } + + for i, sig := range a.DelegatorUnbondingSlashingSigs { + if sig == nil { + return errorsmod.Wrapf(ErrInvalidMultisigInfo, "DelegatorUnbondingSlashingSigs[%d] is nil", i) + } + if err := sig.Validate(); err != nil { + return errorsmod.Wrapf(ErrInvalidMultisigInfo, "invalid signature at index %d: %v", i, err) + } + } + + return nil +} diff --git a/x/btcstaking/types/btc_slashing_tx.go b/x/btcstaking/types/btc_slashing_tx.go index 49385ab02..2d21b2762 100644 --- a/x/btcstaking/types/btc_slashing_tx.go +++ b/x/btcstaking/types/btc_slashing_tx.go @@ -286,6 +286,9 @@ func (tx *BTCSlashingTx) BuildSlashingTxWithWitness( return nil, fmt.Errorf("failed to get decryption key from BTC SK: %w", err) } // decrypt each covenant adaptor signature to Schnorr signature + // NOTE: some elements of the covenantSigs can be nil since it comes from the result + // of GetOrderedCovenantSignatures which outputs ordered covenant sigs in total size + // of covenant committee covSigs := make([]*schnorr.Signature, len(covenantSigs)) numSigs := uint32(0) for i, covenantSig := range covenantSigs { @@ -342,3 +345,111 @@ func (tx *BTCSlashingTx) BuildSlashingTxWithWitness( return slashingMsgTxWithWitness, nil } + +// BuildMultisigSlashingTxWithWitness builds the witness for the slashing tx, including +// - a (covenant_quorum, covenant_committee_size) multisig from covenant committee +// - a (1, num_restaked_finality_providers) multisig from the slashed finality provider +// - a (staker_quorum, staker_pks_list) multisig from the staker +func (tx *BTCSlashingTx) BuildMultisigSlashingTxWithWitness( + fpSK *btcec.PrivateKey, + fpBTCPKs []bbn.BIP340PubKey, + fundingMsgTx *wire.MsgTx, + outputIdx uint32, + delegatorSigs []*bbn.BIP340Signature, + delQuorum uint32, + covenantSigs []*asig.AdaptorSignature, + covenantQuorum uint32, + slashingPathSpendInfo *btcstaking.SpendInfo, +) (*wire.MsgTx, error) { + /* + construct covenant committee's part of witness, i.e., + a quorum number of covenant Schnorr signatures + */ + // decrypt covenant adaptor signature to Schnorr signature using finality provider's SK, + // then marshal + decKey, err := asig.NewDecryptionKeyFromBTCSK(fpSK) + if err != nil { + return nil, fmt.Errorf("failed to get decryption key from BTC SK: %w", err) + } + // decrypt each covenant adaptor signature to Schnorr signature + // NOTE: some elements of the covenantSigs can be nil since it comes from the result + // of GetOrderedCovenantSignatures which outputs ordered covenant sigs in total size + // of covenant committee + covSigs := make([]*schnorr.Signature, len(covenantSigs)) + numSigs := uint32(0) + for i, covenantSig := range covenantSigs { + if covenantSig != nil { + covSig, err := covenantSig.Decrypt(decKey) + if err != nil { + return nil, fmt.Errorf("failed to decrypt covenant adaptor signature: %w", err) + } + covSigs[i] = covSig + numSigs++ + } else { + covSigs[i] = nil + } + if numSigs == covenantQuorum { + break + } + } + // ensure the number of covenant signatures is at least the quorum number + if numSigs < covenantQuorum { + return nil, fmt.Errorf("not enough covenant signatures to reach quorum") + } + + /* + construct finality providers' part of witness, i.e., + 1 out of numRestakedFPs signature + */ + fpIdxInWitness, err := findFPIdxInWitness(fpSK, fpBTCPKs) + if err != nil { + return nil, err + } + fpSigs := make([]*schnorr.Signature, len(fpBTCPKs)) + fpSig, err := tx.Sign(fundingMsgTx, outputIdx, slashingPathSpendInfo.GetPkScriptPath(), fpSK) + if err != nil { + return nil, fmt.Errorf("failed to sign slashing tx for the finality provider: %w", err) + } + fpSigs[fpIdxInWitness] = fpSig.MustToBTCSig() + + /* + construct staker's part of witness, i.e., + a quorum number of staker Schnorr signatures + */ + delSigs := make([]*schnorr.Signature, len(delegatorSigs)) + numDelSigs := uint32(0) + for i, delSig := range delegatorSigs { + if delSig != nil { + delSigs[i] = delSig.MustToBTCSig() + numDelSigs++ + } else { + delSigs[i] = nil + } + if numDelSigs == delQuorum { + break + } + } + // ensure the number of delegator signatures is at least the quorum number + if numDelSigs < delQuorum { + return nil, fmt.Errorf("not enough delegator signatures to reach quorum") + } + + // construct witness + witness, err := slashingPathSpendInfo.CreateMultisigSlashingPathWitness( + covSigs, + fpSigs, + delSigs, + ) + if err != nil { + return nil, err + } + + // add witness to slashing tx + slashingMsgTxWithWitness, err := tx.ToMsgTx() + if err != nil { + return nil, err + } + slashingMsgTxWithWitness.TxIn[0].Witness = witness + + return slashingMsgTxWithWitness, nil +} diff --git a/x/btcstaking/types/btc_slashing_tx_test.go b/x/btcstaking/types/btc_slashing_tx_test.go index 115f35550..44f707cf5 100644 --- a/x/btcstaking/types/btc_slashing_tx_test.go +++ b/x/btcstaking/types/btc_slashing_tx_test.go @@ -247,3 +247,156 @@ func FuzzSlashingTxWithWitness(f *testing.F) { btctest.AssertSlashingTxExecution(t, testStakingInfo.StakingInfo.StakingOutput, slashingMsgTxWithWitness) }) } + +func FuzzMultisigSlashingTxWithWitness(f *testing.F) { + datagen.AddRandomSeedsToFuzzer(f, 10) + + f.Fuzz(func(t *testing.T, seed int64) { + r := rand.New(rand.NewSource(seed)) + var ( + stakingValue = int64(2 * 10e8) + stakingTimeBlocks = uint16(5) + net = &chaincfg.SimNetParams + ) + + // slashing address and key pairs + slashingAddress, err := datagen.GenRandomBTCAddress(r, net) + require.NoError(t, err) + slashingPkScript, err := txscript.PayToAddrScript(slashingAddress) + require.NoError(t, err) + + // Generate a slashing rate in the range [0.1, 0.50] i.e., 10-50%. + // NOTE - if the rate is higher or lower, it may produce slashing or change outputs + // with value below the dust threshold, causing test failure. + // Our goal is not to test failure due to such extreme cases here; + // this is already covered in FuzzGeneratingValidStakingSlashingTx + slashingRate := sdkmath.LegacyNewDecWithPrec(int64(datagen.RandomInt(r, 41)+10), 2) + + // restaked to a random number of finality providers + numRestakedFPs := int(datagen.RandomInt(r, 10) + 1) + fpSKs, fpPKs, err := datagen.GenRandomBTCKeyPairs(r, numRestakedFPs) + require.NoError(t, err) + fpBTCPKs := bbn.NewBIP340PKsFromBTCPKs(fpPKs) + + // a random finality provider gets slashed + fpIdx := int(datagen.RandomInt(r, numRestakedFPs)) + fpSK, fpPK := fpSKs[fpIdx], fpPKs[fpIdx] + encKey, err := asig.NewEncryptionKeyFromBTCPK(fpPK) + require.NoError(t, err) + decKey, err := asig.NewDecryptionKeyFromBTCSK(fpSK) + require.NoError(t, err) + + // (2, 3) multisig staker + delSKs, _, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + delQuorum := uint32(2) + + // (3, 5) covenant committee + covenantSKs, covenantPKs, err := datagen.GenRandomBTCKeyPairs(r, 5) + require.NoError(t, err) + covenantQuorum := uint32(3) + bsParams := types.Params{ + CovenantPks: bbn.NewBIP340PKsFromBTCPKs(covenantPKs), + CovenantQuorum: covenantQuorum, + } + slashingChangeLockTime := uint16(101) + + // generate staking/slashing tx + testStakingInfo := datagen.GenMultisigBTCStakingSlashingInfo( + r, + t, + net, + delSKs, + delQuorum, + fpPKs, + covenantPKs, + covenantQuorum, + stakingTimeBlocks, + stakingValue, + slashingPkScript, + slashingRate, + slashingChangeLockTime, + ) + + slashingTx := testStakingInfo.SlashingTx + slashingMsgTx, err := slashingTx.ToMsgTx() + require.NoError(t, err) + stakingMsgTx := testStakingInfo.StakingTx + + slashingSpendInfo, err := testStakingInfo.StakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + slashingPkScriptPath := slashingSpendInfo.GetPkScriptPath() + + // delegator signs slashing tx in given SKs order + delSigs := datagen.GenerateSignaturesInGivenOrder( + t, + delSKs, + slashingMsgTx, + stakingMsgTx.TxOut[0], + txscript.NewBaseTapLeaf(slashingPkScriptPath), + ) + require.NoError(t, err) + + // sort delegator PKs in reverse lexicographical order + delPK2Sig := make(map[string]*bbn.BIP340Signature) + for i, sk := range delSKs { + delPKHex := bbn.NewBIP340PubKeyFromBTCPK(sk.PubKey()).MarshalHex() + delPK2Sig[delPKHex] = delSigs[i] + } + // NOTE: we can simply use GenerateSignatures but here, intentionally use + // GenerateSignaturesInGivenOrder to test GenerateDelegatorSignatures + delSortedSigs, err := types.GetOrderedDelegatorSignatures(delPK2Sig) + require.NoError(t, err) + + // ensure that event if all covenant members provide covenant signatures, + // BuildSlashingTxWithWitness will only take a quorum number of signatures + // to construct the witness + covenantSigners := covenantSKs + // get covenant Schnorr signatures + covenantSigs, err := datagen.GenCovenantAdaptorSigs( + covenantSigners, + fpPKs, + stakingMsgTx, + slashingPkScriptPath, + slashingTx, + ) + require.NoError(t, err) + covSigsForFP, err := types.GetOrderedCovenantSignatures(fpIdx, covenantSigs, &bsParams) + require.NoError(t, err) + + // ensure all covenant signatures encrypted by the slashed + // finality provider's PK are verified + orderedCovenantPKs := bbn.SortBIP340PKs(bsParams.CovenantPks) + for i := range covSigsForFP { + if covSigsForFP[i] == nil { + continue + } + + err := slashingTx.EncVerifyAdaptorSignature( + testStakingInfo.StakingInfo.StakingOutput, + slashingPkScriptPath, + orderedCovenantPKs[i].MustToBTCPK(), + encKey, + covSigsForFP[i], + ) + require.NoError(t, err, "verifying covenant adaptor sig at %d", i) + + covSchnorrSig, err := covSigsForFP[i].Decrypt(decKey) + require.NoError(t, err) + err = slashingTx.VerifySignature( + testStakingInfo.StakingInfo.StakingOutput, + slashingPkScriptPath, + orderedCovenantPKs[i].MustToBTCPK(), + bbn.NewBIP340SignatureFromBTCSig(covSchnorrSig), + ) + require.NoError(t, err, "verifying covenant Schnorr sig at %d", i) + } + + // create slashing tx with witness + slashingMsgTxWithWitness, err := slashingTx.BuildMultisigSlashingTxWithWitness(fpSK, fpBTCPKs, stakingMsgTx, 0, delSortedSigs, delQuorum, covSigsForFP, covenantQuorum, slashingSpendInfo) + require.NoError(t, err) + + // verify slashing tx with witness + btctest.AssertSlashingTxExecution(t, testStakingInfo.StakingInfo.StakingOutput, slashingMsgTxWithWitness) + }) +} diff --git a/x/btcstaking/types/btcstaking.go b/x/btcstaking/types/btcstaking.go index c1af6b58d..36920339a 100644 --- a/x/btcstaking/types/btcstaking.go +++ b/x/btcstaking/types/btcstaking.go @@ -1,8 +1,10 @@ package types import ( + "bytes" "errors" "fmt" + "sort" time "time" "cosmossdk.io/math" @@ -99,6 +101,7 @@ func NewSignatureInfo(pk *bbn.BIP340PubKey, sig *bbn.BIP340Signature) *Signature // covenant signatures // the order of covenant adaptor signatures will follow the reverse lexicographical order // of signing public keys, in order to be used as tx witness +// NOTE: the number of covenant signatures returned as an output is the total size of covenant committee func GetOrderedCovenantSignatures(fpIdx int, covSigsList []*CovenantAdaptorSignatures, params *Params) ([]*asig.AdaptorSignature, error) { // construct the map where // - key is the covenant PK, and @@ -137,6 +140,36 @@ func GetOrderedCovenantSignatures(fpIdx int, covSigsList []*CovenantAdaptorSigna return orderedCovSigs, nil } +// GetOrderedDelegatorSignatures returns the ordered delegator Schnorr signatures. +// The order follows the reverse lexicographical order of delegator public keys so +// the resulting slice can be plugged directly into the BTC witness. +// NOTE: the returned slice must contain one slot per multisig participant (the full N), +// and entries can be nil for any delegator that failed to provide its signature. +func GetOrderedDelegatorSignatures(delPK2Sig map[string]*bbn.BIP340Signature) ([]*bbn.BIP340Signature, error) { + entries := make([]SignatureInfo, 0, len(delPK2Sig)) + for delPKStr, sig := range delPK2Sig { + delPK, err := bbn.NewBIP340PubKeyFromHex(delPKStr) + if err != nil { + return nil, err + } + entries = append(entries, SignatureInfo{Pk: delPK, Sig: sig}) + } + + // sort delegator PKs in reverse lexicographical order + sort.SliceStable(entries, func(i, j int) bool { + keyIBytes := entries[i].Pk.MustMarshal() + keyJBytes := entries[j].Pk.MustMarshal() + return bytes.Compare(keyIBytes, keyJBytes) == 1 + }) + + orderedDelSigs := make([]*bbn.BIP340Signature, len(entries)) + for i, entry := range entries { + orderedDelSigs[i] = entry.Sig + } + + return orderedDelSigs, nil +} + // NewLargestBtcReOrg creates a new Largest BTC reorg based on the rollback vars func NewLargestBtcReOrg(rollbackFrom, rollbackTo *btclightclienttypes.BTCHeaderInfo) LargestBtcReOrg { return LargestBtcReOrg{ diff --git a/x/btcstaking/types/btcstaking.pb.go b/x/btcstaking/types/btcstaking.pb.go index c60f819d1..791d1216b 100644 --- a/x/btcstaking/types/btcstaking.pb.go +++ b/x/btcstaking/types/btcstaking.pb.go @@ -418,6 +418,9 @@ type BTCDelegation struct { // stk_exp is contains the relevant information about the previous staking that // originated this stake. If nil it is NOT a stake expansion. StkExp *StakeExpansion `protobuf:"bytes,18,opt,name=stk_exp,json=stkExp,proto3" json:"stk_exp,omitempty"` + // multisig_info is used when the given btc delegation is M-of-N multisig. + // If nil, it is not a M-of-N multisig. + MultisigInfo *AdditionalStakerInfo `protobuf:"bytes,19,opt,name=multisig_info,json=multisigInfo,proto3" json:"multisig_info,omitempty"` } func (m *BTCDelegation) Reset() { *m = BTCDelegation{} } @@ -551,6 +554,13 @@ func (m *BTCDelegation) GetStkExp() *StakeExpansion { return nil } +func (m *BTCDelegation) GetMultisigInfo() *AdditionalStakerInfo { + if m != nil { + return m.MultisigInfo + } + return nil +} + // StakeExpansion stores information necessary to construct the expanded BTC staking // transaction created from a previous BTC staking. type StakeExpansion struct { @@ -1125,6 +1135,80 @@ func (m *LargestBtcReOrg) GetRollbackTo() *types2.BTCHeaderInfo { return nil } +// AdditionalStakerInfo is used when enabling multisig for btc staker +// NOTE: this structure doesn't contain original btc staker's signature, i.e., length of +// delegator_slashing_sigs and delegator_unbonding_slashing_sigs is M-1, and the length of +// staker_btc_pk_list is N-1 +type AdditionalStakerInfo struct { + // staker_btc_pk_list is the list of pubkeys of the btc staker that is using M-of-N multisig + // length of staker_btc_pk_list is N-1 + StakerBtcPkList []github_com_babylonlabs_io_babylon_v4_types.BIP340PubKey `protobuf:"bytes,1,rep,name=staker_btc_pk_list,json=stakerBtcPkList,proto3,customtype=github.com/babylonlabs-io/babylon/v4/types.BIP340PubKey" json:"staker_btc_pk_list,omitempty"` + // staker_quorum is threshold of M-of-N multisig, which value itself represent M + StakerQuorum uint32 `protobuf:"varint,2,opt,name=staker_quorum,json=stakerQuorum,proto3" json:"staker_quorum,omitempty"` + // delegator_slashing_sigs is the (btc_pk, signature) pair on the slashing tx + // by delegators (i.e., SK corresponding to btc_pk). + // It will be a part of the witness for the staking tx output. + DelegatorSlashingSigs []*SignatureInfo `protobuf:"bytes,3,rep,name=delegator_slashing_sigs,json=delegatorSlashingSigs,proto3" json:"delegator_slashing_sigs,omitempty"` + // delegator_unbonding_slashing_sigs is the (btc_pk, signature) pair on the slashing tx + // by delegators (i.e., SK corresponding to btc_pk). + // It will be a part of the witness for the unbonding tx output. + DelegatorUnbondingSlashingSigs []*SignatureInfo `protobuf:"bytes,4,rep,name=delegator_unbonding_slashing_sigs,json=delegatorUnbondingSlashingSigs,proto3" json:"delegator_unbonding_slashing_sigs,omitempty"` +} + +func (m *AdditionalStakerInfo) Reset() { *m = AdditionalStakerInfo{} } +func (m *AdditionalStakerInfo) String() string { return proto.CompactTextString(m) } +func (*AdditionalStakerInfo) ProtoMessage() {} +func (*AdditionalStakerInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_3851ae95ccfaf7db, []int{14} +} +func (m *AdditionalStakerInfo) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *AdditionalStakerInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_AdditionalStakerInfo.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *AdditionalStakerInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_AdditionalStakerInfo.Merge(m, src) +} +func (m *AdditionalStakerInfo) XXX_Size() int { + return m.Size() +} +func (m *AdditionalStakerInfo) XXX_DiscardUnknown() { + xxx_messageInfo_AdditionalStakerInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_AdditionalStakerInfo proto.InternalMessageInfo + +func (m *AdditionalStakerInfo) GetStakerQuorum() uint32 { + if m != nil { + return m.StakerQuorum + } + return 0 +} + +func (m *AdditionalStakerInfo) GetDelegatorSlashingSigs() []*SignatureInfo { + if m != nil { + return m.DelegatorSlashingSigs + } + return nil +} + +func (m *AdditionalStakerInfo) GetDelegatorUnbondingSlashingSigs() []*SignatureInfo { + if m != nil { + return m.DelegatorUnbondingSlashingSigs + } + return nil +} + func init() { proto.RegisterEnum("babylon.btcstaking.v1.BTCDelegationStatus", BTCDelegationStatus_name, BTCDelegationStatus_value) proto.RegisterType((*FinalityProvider)(nil), "babylon.btcstaking.v1.FinalityProvider") @@ -1141,6 +1225,7 @@ func init() { proto.RegisterType((*SelectiveSlashingEvidence)(nil), "babylon.btcstaking.v1.SelectiveSlashingEvidence") proto.RegisterType((*InclusionProof)(nil), "babylon.btcstaking.v1.InclusionProof") proto.RegisterType((*LargestBtcReOrg)(nil), "babylon.btcstaking.v1.LargestBtcReOrg") + proto.RegisterType((*AdditionalStakerInfo)(nil), "babylon.btcstaking.v1.AdditionalStakerInfo") } func init() { @@ -1148,118 +1233,124 @@ func init() { } var fileDescriptor_3851ae95ccfaf7db = []byte{ - // 1766 bytes of a gzipped FileDescriptorProto + // 1871 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x58, 0x4f, 0x6f, 0x1b, 0xc7, - 0x15, 0xf7, 0x92, 0x14, 0x25, 0x3d, 0xfe, 0x11, 0x35, 0x51, 0x94, 0xb5, 0x8c, 0x4a, 0x2a, 0xeb, - 0x04, 0x42, 0x6a, 0x91, 0xb6, 0x62, 0x34, 0xad, 0x8b, 0x1a, 0x30, 0x45, 0xaa, 0x66, 0x62, 0x4b, - 0xf4, 0x92, 0x76, 0xda, 0x1e, 0xba, 0x19, 0xee, 0x0e, 0x97, 0x5b, 0x92, 0x3b, 0x8b, 0x9d, 0x21, - 0x4b, 0x7d, 0x80, 0xde, 0x83, 0xa2, 0xbd, 0xf7, 0xd8, 0x53, 0xd1, 0x43, 0x4e, 0xfd, 0x04, 0x41, - 0x4f, 0x41, 0x2e, 0x2d, 0x0c, 0x54, 0x2d, 0xec, 0x43, 0xfa, 0x31, 0x8a, 0xf9, 0xb3, 0xe4, 0x52, - 0xb5, 0x1c, 0x39, 0xd1, 0x45, 0xe0, 0xbc, 0xff, 0xfb, 0xde, 0xef, 0xf7, 0x76, 0x56, 0xf0, 0x5e, - 0x17, 0x77, 0x4f, 0x87, 0x34, 0xa8, 0x76, 0xb9, 0xc3, 0x38, 0x1e, 0xf8, 0x81, 0x57, 0x9d, 0xdc, - 0x49, 0x9c, 0x2a, 0x61, 0x44, 0x39, 0x45, 0x6f, 0x6b, 0xbb, 0x4a, 0x42, 0x33, 0xb9, 0xb3, 0xb5, - 0xe1, 0x51, 0x8f, 0x4a, 0x8b, 0xaa, 0xf8, 0xa5, 0x8c, 0xb7, 0xae, 0x3b, 0x94, 0x8d, 0x28, 0xb3, - 0x95, 0x42, 0x1d, 0xb4, 0xea, 0xa6, 0x3a, 0x55, 0xe7, 0xb9, 0xba, 0x84, 0xe3, 0x3b, 0xd5, 0x85, - 0x6c, 0x5b, 0x3b, 0xaf, 0xae, 0x2a, 0xa4, 0xa1, 0x36, 0xb8, 0x95, 0x30, 0x70, 0xfa, 0xc4, 0x19, - 0x84, 0xd4, 0x0f, 0xb8, 0xae, 0x7c, 0x2e, 0xd0, 0xd6, 0x95, 0x84, 0xf5, 0xd0, 0xf7, 0xfa, 0xe2, - 0x2f, 0x99, 0x99, 0x27, 0x24, 0xda, 0x7e, 0x1d, 0x8f, 0xfc, 0x80, 0x56, 0xe5, 0xdf, 0xb8, 0x22, - 0x8f, 0x52, 0x6f, 0x48, 0xaa, 0xf2, 0xd4, 0x1d, 0xf7, 0xaa, 0xdc, 0x1f, 0x11, 0xc6, 0xf1, 0x48, - 0x57, 0x54, 0xfe, 0x3a, 0x03, 0xa5, 0x23, 0x3f, 0xc0, 0x43, 0x9f, 0x9f, 0xb6, 0x22, 0x3a, 0xf1, - 0x5d, 0x12, 0xa1, 0x5b, 0x90, 0xc1, 0xae, 0x1b, 0x99, 0xc6, 0xae, 0xb1, 0xb7, 0x5a, 0x33, 0xbf, - 0xfa, 0x7c, 0x7f, 0x43, 0x77, 0xe3, 0x81, 0xeb, 0x46, 0x84, 0xb1, 0x36, 0x8f, 0xfc, 0xc0, 0xb3, - 0xa4, 0x15, 0x6a, 0x40, 0xce, 0x25, 0xcc, 0x89, 0xfc, 0x90, 0xfb, 0x34, 0x30, 0x53, 0xbb, 0xc6, - 0x5e, 0xee, 0xe0, 0x07, 0x15, 0xed, 0x31, 0xef, 0xba, 0xec, 0x58, 0xa5, 0x3e, 0x37, 0xb5, 0x92, - 0x7e, 0xe8, 0x31, 0x80, 0x43, 0x47, 0x23, 0x9f, 0x31, 0x11, 0x25, 0x2d, 0x53, 0xef, 0x3f, 0x3f, - 0xdb, 0xb9, 0xa1, 0x02, 0x31, 0x77, 0x50, 0xf1, 0x69, 0x75, 0x84, 0x79, 0xbf, 0xf2, 0x88, 0x78, - 0xd8, 0x39, 0xad, 0x13, 0xe7, 0xab, 0xcf, 0xf7, 0x41, 0xe7, 0xa9, 0x13, 0xc7, 0x4a, 0x04, 0x40, - 0x16, 0x64, 0xbb, 0xdc, 0xb1, 0xc3, 0x81, 0x99, 0xd9, 0x35, 0xf6, 0xf2, 0xb5, 0x9f, 0x3e, 0x3f, - 0xdb, 0xf9, 0xd0, 0xf3, 0x79, 0x7f, 0xdc, 0xad, 0x38, 0x74, 0x54, 0xd5, 0xbd, 0x1d, 0xe2, 0x2e, - 0xdb, 0xf7, 0x69, 0x7c, 0xac, 0x4e, 0xee, 0x56, 0xf9, 0x69, 0x48, 0x58, 0xa5, 0xd6, 0x6c, 0x7d, - 0x70, 0xf7, 0x76, 0x6b, 0xdc, 0xfd, 0x98, 0x9c, 0x5a, 0x4b, 0x5d, 0xee, 0xb4, 0x06, 0xe8, 0x67, - 0x90, 0x0e, 0x69, 0x68, 0x2e, 0xc9, 0x27, 0xfc, 0x61, 0xe5, 0x95, 0xd8, 0xaa, 0xb4, 0x22, 0x4a, - 0x7b, 0x27, 0xbd, 0x16, 0x65, 0x8c, 0xc8, 0x52, 0x6a, 0x9d, 0x43, 0x4b, 0xf8, 0xa1, 0xbb, 0xb0, - 0xc9, 0x86, 0x98, 0xf5, 0x89, 0x6b, 0x6b, 0x57, 0xbb, 0x4f, 0xc4, 0x14, 0xcd, 0xec, 0xae, 0xb1, - 0x97, 0xb1, 0x36, 0xb4, 0xb6, 0xa6, 0x94, 0x0f, 0xa5, 0x0e, 0xdd, 0x02, 0x34, 0xf3, 0xe2, 0x4e, - 0xec, 0xb1, 0xbc, 0x6b, 0xec, 0x15, 0xac, 0x52, 0xec, 0xc1, 0x1d, 0x6d, 0xbd, 0x09, 0xd9, 0xdf, - 0x60, 0x7f, 0x48, 0x5c, 0x73, 0x65, 0xd7, 0xd8, 0x5b, 0xb1, 0xf4, 0x09, 0xdd, 0x86, 0x8d, 0xbe, - 0xef, 0xf5, 0x09, 0xe3, 0xf6, 0x84, 0x72, 0xe2, 0xc6, 0x71, 0x56, 0x65, 0x1c, 0xa4, 0x75, 0xcf, - 0x84, 0x4a, 0x47, 0x3a, 0x86, 0xb5, 0x79, 0x3b, 0x6d, 0x3f, 0xe8, 0x51, 0x33, 0x27, 0x1f, 0xfc, - 0xdd, 0x0b, 0x1e, 0xfc, 0x70, 0x66, 0xdd, 0x0c, 0x7a, 0xd4, 0x2a, 0x3a, 0x0b, 0xe7, 0xf2, 0x1f, - 0x53, 0x50, 0x5c, 0x34, 0x41, 0x4f, 0x60, 0x65, 0x84, 0xa7, 0x76, 0x84, 0x39, 0xd1, 0x58, 0xfb, - 0xd1, 0x17, 0x67, 0x3b, 0xd7, 0xde, 0x68, 0xe8, 0x7f, 0xfe, 0xfa, 0xaf, 0xef, 0x1b, 0xd6, 0xf2, - 0x08, 0x4f, 0x2d, 0xcc, 0x09, 0xfa, 0x35, 0xac, 0x89, 0x90, 0x4e, 0x1f, 0x07, 0x1e, 0x51, 0x91, - 0x53, 0xdf, 0x29, 0x72, 0x61, 0x84, 0xa7, 0x87, 0x32, 0x9a, 0x8c, 0xff, 0x11, 0xe4, 0xc6, 0xa1, - 0x8b, 0x39, 0xb1, 0x05, 0x93, 0x24, 0x4c, 0x73, 0x07, 0x5b, 0x15, 0x45, 0xb3, 0x4a, 0x4c, 0xb3, - 0x4a, 0x27, 0xa6, 0x59, 0xad, 0x20, 0xf2, 0x7e, 0xf6, 0xef, 0x1d, 0x43, 0x85, 0x03, 0xe5, 0x2d, - 0xf4, 0xf7, 0x32, 0xff, 0xfd, 0xd3, 0x8e, 0x51, 0xfe, 0x47, 0x0a, 0xcc, 0xf3, 0x0c, 0xfc, 0xc4, - 0xe7, 0xfd, 0xc7, 0x84, 0xe3, 0x04, 0x8a, 0x8d, 0x2b, 0x43, 0xf1, 0x26, 0x64, 0xf5, 0xf0, 0x53, - 0x12, 0x76, 0xfa, 0x84, 0xbe, 0x0f, 0xf9, 0x09, 0xe5, 0x7e, 0xe0, 0xd9, 0x21, 0xfd, 0x2d, 0x89, - 0xe4, 0xb3, 0x65, 0xac, 0x9c, 0x92, 0xb5, 0x84, 0xe8, 0x35, 0x08, 0xce, 0xbc, 0x31, 0x82, 0x97, - 0xbe, 0x11, 0xc1, 0xd9, 0x4b, 0x21, 0x78, 0xf9, 0x22, 0x04, 0x97, 0x7f, 0xb7, 0x02, 0x85, 0x5a, - 0xe7, 0xb0, 0x4e, 0x86, 0xc4, 0xc3, 0x72, 0xc7, 0xfc, 0x04, 0x72, 0x02, 0xb0, 0x24, 0xb2, 0x2f, - 0xb5, 0xdf, 0x40, 0x19, 0x0b, 0x61, 0x62, 0x12, 0xa9, 0xab, 0xde, 0x27, 0xe9, 0x6f, 0xb9, 0x4f, - 0x3e, 0x85, 0x62, 0x2f, 0xb4, 0x55, 0x55, 0xf6, 0xd0, 0x67, 0x62, 0x0a, 0xe9, 0xef, 0x5a, 0x5a, - 0xae, 0x17, 0xd6, 0x44, 0x71, 0x8f, 0x7c, 0x26, 0x21, 0xa1, 0x2b, 0x51, 0x70, 0x57, 0x33, 0xcb, - 0x69, 0x99, 0x00, 0xb1, 0x36, 0x89, 0x78, 0x72, 0x95, 0x29, 0x93, 0x88, 0xeb, 0x89, 0x7e, 0x0f, - 0x80, 0x04, 0xe7, 0xe6, 0xb5, 0x4a, 0x82, 0x78, 0xd1, 0xdc, 0x80, 0x55, 0x4e, 0x39, 0x1e, 0xda, - 0x0c, 0x73, 0xb9, 0xb5, 0x32, 0xd6, 0x8a, 0x14, 0xb4, 0xb1, 0xf4, 0x9d, 0x55, 0x30, 0x95, 0xdb, - 0x2a, 0x6f, 0xad, 0xc6, 0xf9, 0xa7, 0x12, 0x5a, 0x5a, 0x4d, 0xc7, 0x3c, 0x1c, 0x73, 0xdb, 0x77, - 0xa7, 0x26, 0x68, 0x68, 0x29, 0xcd, 0x89, 0x54, 0x34, 0xdd, 0x29, 0x3a, 0x80, 0x9c, 0x84, 0x9b, - 0x8e, 0x96, 0x93, 0x83, 0x5c, 0x7f, 0x7e, 0xb6, 0x23, 0x60, 0xd2, 0xd6, 0x9a, 0xce, 0xd4, 0x02, - 0x36, 0xfb, 0x8d, 0x1c, 0x28, 0xb8, 0x0a, 0x40, 0x34, 0xb2, 0x99, 0xef, 0x99, 0x79, 0xe9, 0x75, - 0xff, 0xf9, 0xd9, 0xce, 0xbd, 0x37, 0xee, 0x71, 0xdb, 0xf7, 0x02, 0xcc, 0xc7, 0x11, 0xb1, 0xf2, - 0xb3, 0xa0, 0x6d, 0xdf, 0x43, 0x4f, 0xa1, 0xe0, 0xd0, 0x09, 0x09, 0x70, 0xc0, 0x45, 0x0e, 0x66, - 0x16, 0x76, 0xd3, 0x7b, 0xb9, 0x83, 0xdb, 0x17, 0x6e, 0x5a, 0x65, 0xfb, 0xc0, 0xc5, 0xa1, 0x8a, - 0xa0, 0xa2, 0x32, 0x2b, 0x1f, 0x87, 0x69, 0xfb, 0x1e, 0x43, 0xef, 0x42, 0x71, 0x1c, 0x74, 0x69, - 0xe0, 0xce, 0x06, 0x58, 0x94, 0x9d, 0x29, 0xcc, 0xa4, 0x72, 0x84, 0x4f, 0xa0, 0x24, 0x40, 0x34, - 0x0e, 0xdc, 0x19, 0x53, 0xcc, 0x35, 0x89, 0xc9, 0xf7, 0x2e, 0x28, 0xa0, 0xd6, 0x39, 0x7c, 0x9a, - 0xb0, 0xb6, 0xd6, 0xba, 0xdc, 0x49, 0x0a, 0x44, 0xe6, 0x10, 0x47, 0x78, 0xc4, 0xec, 0x09, 0x89, - 0xe4, 0x0b, 0xbd, 0xa4, 0x32, 0x2b, 0xe9, 0x33, 0x25, 0x44, 0x37, 0xa1, 0x28, 0x32, 0x73, 0x3f, - 0x8c, 0xd1, 0xb1, 0x2e, 0xcd, 0xf2, 0x5d, 0xee, 0x74, 0xfc, 0x50, 0x03, 0xe4, 0x3e, 0x2c, 0x33, - 0x3e, 0xb0, 0xc9, 0x34, 0x34, 0xd1, 0x6b, 0xdf, 0x40, 0x6d, 0x41, 0xd7, 0xc6, 0x34, 0xc4, 0x81, - 0x88, 0x6e, 0x65, 0x19, 0x1f, 0x34, 0xa6, 0x61, 0xf9, 0x5f, 0x06, 0x14, 0x17, 0x55, 0xe8, 0x43, - 0x30, 0xc3, 0x88, 0x4c, 0x7c, 0x3a, 0x66, 0xf6, 0x1c, 0x5f, 0x76, 0x1f, 0xb3, 0xbe, 0xda, 0xb4, - 0xd6, 0xdb, 0xb1, 0xbe, 0x1d, 0x83, 0xed, 0x21, 0x66, 0x7d, 0x54, 0x85, 0x0d, 0xca, 0xfb, 0x24, - 0xb2, 0x7b, 0x63, 0xdd, 0xd6, 0xa9, 0x40, 0x9e, 0x5a, 0x0a, 0xd6, 0xba, 0xd4, 0x1d, 0x29, 0x55, - 0x67, 0x7a, 0x32, 0xe6, 0x08, 0xc3, 0x56, 0x22, 0xd3, 0xc0, 0x5e, 0x9c, 0x73, 0x5a, 0xce, 0xf9, - 0xe6, 0x45, 0xcf, 0x13, 0x0f, 0x56, 0xbe, 0x50, 0xdf, 0x99, 0x57, 0x34, 0x38, 0x4c, 0x8c, 0xb9, - 0x7c, 0x1f, 0x36, 0xeb, 0x31, 0x9a, 0x9e, 0xc6, 0x93, 0x95, 0x2f, 0xd8, 0x9b, 0x50, 0x64, 0xa1, - 0xe0, 0x9e, 0x5c, 0x64, 0x02, 0xf3, 0xea, 0xe1, 0xf2, 0x52, 0x2a, 0x7b, 0xd2, 0x99, 0x96, 0xff, - 0x90, 0x81, 0xb5, 0x73, 0x13, 0x15, 0xb4, 0x4e, 0x40, 0x27, 0xf6, 0xcb, 0xcd, 0x81, 0xf3, 0x7f, - 0x6c, 0x4a, 0x5d, 0x86, 0x4d, 0x1c, 0x36, 0x13, 0x6c, 0x8a, 0xbd, 0x05, 0xad, 0xd2, 0x57, 0x42, - 0xab, 0x8d, 0x39, 0xad, 0x74, 0x70, 0x41, 0xaf, 0x1e, 0x6c, 0xce, 0xdb, 0x9e, 0x48, 0xca, 0xe4, - 0xc2, 0xfc, 0x36, 0x3c, 0xdb, 0x98, 0xf1, 0x6c, 0x9e, 0x86, 0x21, 0x07, 0x6e, 0xcc, 0xf2, 0xcc, - 0xbb, 0xc7, 0x7c, 0x4f, 0x6d, 0xe7, 0xa5, 0x37, 0x18, 0xb6, 0x19, 0x07, 0x9a, 0x0d, 0xb4, 0xed, - 0x7b, 0x72, 0x27, 0x7b, 0x60, 0xce, 0x5b, 0x38, 0xcf, 0x22, 0x2f, 0x68, 0x59, 0x49, 0x8f, 0xfd, - 0x0b, 0x32, 0xbc, 0x1a, 0x24, 0xd6, 0x7c, 0x22, 0x0b, 0xf2, 0x72, 0x1b, 0xde, 0x99, 0xbf, 0x3d, - 0x69, 0x34, 0x7f, 0x8d, 0x32, 0xf4, 0x63, 0xc8, 0xb8, 0x64, 0xc8, 0x4c, 0xe3, 0xb5, 0x4f, 0xb4, - 0xf0, 0xee, 0xb5, 0xa4, 0x47, 0xf9, 0x18, 0x6e, 0xbc, 0x3a, 0x68, 0x33, 0x70, 0xc9, 0x54, 0xd0, - 0xeb, 0x1c, 0x1d, 0x55, 0xeb, 0x44, 0xa2, 0xbc, 0xb5, 0xce, 0x92, 0x5c, 0x14, 0xdd, 0x28, 0xff, - 0xc5, 0x80, 0xc2, 0x42, 0xe7, 0xd0, 0xc7, 0x90, 0xba, 0x9a, 0xeb, 0x52, 0x2a, 0x1c, 0xa0, 0x16, - 0xa4, 0x05, 0x38, 0x53, 0x57, 0x02, 0x4e, 0x11, 0xaa, 0xfc, 0x7b, 0x03, 0xae, 0x5f, 0x88, 0x2b, - 0x71, 0xcb, 0x70, 0xe8, 0xe4, 0xaa, 0xee, 0x7b, 0x0e, 0x9d, 0xb4, 0x06, 0x82, 0xca, 0x58, 0x25, - 0x52, 0x98, 0x4f, 0xc9, 0x5e, 0xe6, 0xf0, 0x2c, 0x39, 0x2b, 0xff, 0xcd, 0x80, 0xeb, 0x6d, 0x32, - 0x24, 0x0e, 0xf7, 0x27, 0x24, 0x86, 0x74, 0x43, 0x5c, 0x45, 0x03, 0x87, 0xa0, 0x4f, 0x60, 0x75, - 0x76, 0xcf, 0xb8, 0x8a, 0xdb, 0xcf, 0xb2, 0xbe, 0x62, 0xa0, 0x7d, 0x78, 0x2b, 0x22, 0x02, 0xe8, - 0x11, 0x71, 0x6d, 0x9d, 0x82, 0x0d, 0xd4, 0x2a, 0xb0, 0x4a, 0x33, 0xd5, 0x91, 0x30, 0x6f, 0x0f, - 0x3e, 0xca, 0xac, 0x18, 0xa5, 0x94, 0xb5, 0x76, 0x0e, 0x20, 0xe5, 0x2e, 0x14, 0x9b, 0x81, 0x33, - 0x1c, 0x8b, 0xc5, 0x2e, 0x2f, 0x4b, 0xe8, 0x1e, 0xa4, 0x07, 0xe4, 0x54, 0xb6, 0x30, 0x77, 0xb0, - 0x97, 0x44, 0x67, 0xe2, 0x1b, 0x7b, 0x72, 0xa7, 0xd2, 0x89, 0x70, 0xc0, 0xb0, 0x23, 0xe0, 0x27, - 0xea, 0x12, 0x4e, 0x68, 0x03, 0x96, 0x42, 0x11, 0x44, 0x6f, 0x74, 0x75, 0x28, 0xff, 0xdd, 0x80, - 0xb5, 0x47, 0x38, 0xf2, 0x08, 0xe3, 0x35, 0xee, 0x58, 0xe4, 0x24, 0xf2, 0xc4, 0xd5, 0xa4, 0x3b, - 0xa4, 0xce, 0xc0, 0x76, 0xfd, 0x5e, 0x4f, 0x26, 0x2b, 0x58, 0xab, 0x52, 0x52, 0xf7, 0x7b, 0x3d, - 0xf4, 0x18, 0x0a, 0x11, 0x1d, 0x0e, 0xbb, 0xd8, 0x19, 0xd8, 0xbd, 0x88, 0x8e, 0xf4, 0x87, 0xf1, - 0x42, 0x39, 0xc9, 0x6f, 0x78, 0x45, 0x98, 0x87, 0x04, 0xbb, 0x24, 0x92, 0xbc, 0xcc, 0xc7, 0xee, - 0x47, 0x11, 0x1d, 0xa1, 0x26, 0xe4, 0x66, 0xe1, 0x38, 0xd5, 0x77, 0xc6, 0xcb, 0x07, 0x83, 0xd8, - 0xb9, 0x43, 0xdf, 0xff, 0x14, 0xde, 0x5a, 0xa0, 0x66, 0x9b, 0x63, 0x3e, 0x66, 0x28, 0x07, 0xcb, - 0xad, 0xc6, 0x71, 0xbd, 0x79, 0xfc, 0xf3, 0xd2, 0x35, 0x94, 0x87, 0x95, 0x67, 0x0d, 0xab, 0x79, - 0xd4, 0x6c, 0xd4, 0x4b, 0x06, 0x02, 0xc8, 0x3e, 0x38, 0xec, 0x34, 0x9f, 0x35, 0x4a, 0x29, 0xa1, - 0x79, 0x7a, 0x5c, 0x3b, 0x39, 0xae, 0x37, 0xea, 0xa5, 0xb4, 0x70, 0x6a, 0xfc, 0xa2, 0xd5, 0xb4, - 0x1a, 0xf5, 0x52, 0x06, 0x2d, 0x43, 0xfa, 0xc1, 0xf1, 0x2f, 0x4b, 0x4b, 0xb5, 0x27, 0x5f, 0xbc, - 0xd8, 0x36, 0xbe, 0x7c, 0xb1, 0x6d, 0xfc, 0xe7, 0xc5, 0xb6, 0xf1, 0xd9, 0xcb, 0xed, 0x6b, 0x5f, - 0xbe, 0xdc, 0xbe, 0xf6, 0xcf, 0x97, 0xdb, 0xd7, 0x7e, 0x75, 0x39, 0xd0, 0x4c, 0x93, 0xff, 0x41, - 0x91, 0x08, 0xea, 0x66, 0xe5, 0xb7, 0xd5, 0x07, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xe4, 0x59, - 0xca, 0xc6, 0xfa, 0x11, 0x00, 0x00, + 0x15, 0xd7, 0x92, 0xd4, 0xbf, 0x47, 0x52, 0xa2, 0xc6, 0xb2, 0x4c, 0xcb, 0xa8, 0xa4, 0x30, 0x4e, + 0x20, 0xa4, 0x16, 0x69, 0x2b, 0x46, 0xd3, 0xba, 0xa8, 0x01, 0x51, 0xa4, 0x6a, 0x26, 0xb6, 0x44, + 0x2f, 0x69, 0xa7, 0x2d, 0x8a, 0x6e, 0x86, 0xbb, 0xc3, 0xe5, 0x96, 0xe4, 0xce, 0x76, 0x67, 0xc8, + 0x52, 0xdf, 0x22, 0x28, 0xda, 0x7b, 0x8f, 0x3d, 0x15, 0x3d, 0xe4, 0xd4, 0x4f, 0x10, 0xf4, 0x14, + 0xe4, 0x92, 0xc2, 0x40, 0xd5, 0xc2, 0x3e, 0xa4, 0x1f, 0xa0, 0x1f, 0xa0, 0x98, 0x3f, 0x4b, 0x2e, + 0x15, 0xc9, 0x91, 0x6c, 0x5d, 0x08, 0xce, 0xfb, 0x3b, 0xf3, 0xde, 0xef, 0xfd, 0x21, 0xe1, 0xfd, + 0x16, 0x6e, 0x1d, 0xf7, 0xa8, 0x5f, 0x6a, 0x71, 0x9b, 0x71, 0xdc, 0xf5, 0x7c, 0xb7, 0x34, 0xbc, + 0x17, 0x3b, 0x15, 0x83, 0x90, 0x72, 0x8a, 0xae, 0x6b, 0xb9, 0x62, 0x8c, 0x33, 0xbc, 0xb7, 0xbe, + 0xea, 0x52, 0x97, 0x4a, 0x89, 0x92, 0xf8, 0xa6, 0x84, 0xd7, 0x6f, 0xda, 0x94, 0xf5, 0x29, 0xb3, + 0x14, 0x43, 0x1d, 0x34, 0xeb, 0xb6, 0x3a, 0x95, 0x26, 0xbe, 0x5a, 0x84, 0xe3, 0x7b, 0xa5, 0x29, + 0x6f, 0xeb, 0x9b, 0x67, 0xdf, 0x2a, 0xa0, 0x81, 0x16, 0xb8, 0x13, 0x13, 0xb0, 0x3b, 0xc4, 0xee, + 0x06, 0xd4, 0xf3, 0xb9, 0xbe, 0xf9, 0x84, 0xa0, 0xa5, 0x8b, 0x31, 0xe9, 0x9e, 0xe7, 0x76, 0xc4, + 0x27, 0x19, 0x8b, 0xc7, 0x28, 0x5a, 0x7e, 0x05, 0xf7, 0x3d, 0x9f, 0x96, 0xe4, 0x67, 0x74, 0x23, + 0x97, 0x52, 0xb7, 0x47, 0x4a, 0xf2, 0xd4, 0x1a, 0xb4, 0x4b, 0xdc, 0xeb, 0x13, 0xc6, 0x71, 0x5f, + 0xdf, 0xa8, 0xf0, 0x6d, 0x0a, 0x72, 0x07, 0x9e, 0x8f, 0x7b, 0x1e, 0x3f, 0xae, 0x87, 0x74, 0xe8, + 0x39, 0x24, 0x44, 0x77, 0x20, 0x85, 0x1d, 0x27, 0xcc, 0x1b, 0x5b, 0xc6, 0xf6, 0x62, 0x39, 0xff, + 0xf5, 0x17, 0x3b, 0xab, 0x3a, 0x1a, 0x7b, 0x8e, 0x13, 0x12, 0xc6, 0x1a, 0x3c, 0xf4, 0x7c, 0xd7, + 0x94, 0x52, 0xa8, 0x0a, 0x69, 0x87, 0x30, 0x3b, 0xf4, 0x02, 0xee, 0x51, 0x3f, 0x9f, 0xd8, 0x32, + 0xb6, 0xd3, 0xbb, 0xef, 0x16, 0xb5, 0xc6, 0x24, 0xea, 0x32, 0x62, 0xc5, 0xca, 0x44, 0xd4, 0x8c, + 0xeb, 0xa1, 0x27, 0x00, 0x36, 0xed, 0xf7, 0x3d, 0xc6, 0x84, 0x95, 0xa4, 0x74, 0xbd, 0xf3, 0xe2, + 0x64, 0xf3, 0x96, 0x32, 0xc4, 0x9c, 0x6e, 0xd1, 0xa3, 0xa5, 0x3e, 0xe6, 0x9d, 0xe2, 0x63, 0xe2, + 0x62, 0xfb, 0xb8, 0x42, 0xec, 0xaf, 0xbf, 0xd8, 0x01, 0xed, 0xa7, 0x42, 0x6c, 0x33, 0x66, 0x00, + 0x99, 0x30, 0xd7, 0xe2, 0xb6, 0x15, 0x74, 0xf3, 0xa9, 0x2d, 0x63, 0x3b, 0x53, 0xfe, 0xe9, 0x8b, + 0x93, 0xcd, 0x8f, 0x5c, 0x8f, 0x77, 0x06, 0xad, 0xa2, 0x4d, 0xfb, 0x25, 0x1d, 0xdb, 0x1e, 0x6e, + 0xb1, 0x1d, 0x8f, 0x46, 0xc7, 0xd2, 0xf0, 0x7e, 0x89, 0x1f, 0x07, 0x84, 0x15, 0xcb, 0xb5, 0xfa, + 0x87, 0xf7, 0xef, 0xd6, 0x07, 0xad, 0x4f, 0xc8, 0xb1, 0x39, 0xdb, 0xe2, 0x76, 0xbd, 0x8b, 0x7e, + 0x06, 0xc9, 0x80, 0x06, 0xf9, 0x59, 0xf9, 0xc2, 0x1f, 0x16, 0xcf, 0xc4, 0x56, 0xb1, 0x1e, 0x52, + 0xda, 0x3e, 0x6a, 0xd7, 0x29, 0x63, 0x44, 0x5e, 0xa5, 0xdc, 0xdc, 0x37, 0x85, 0x1e, 0xba, 0x0f, + 0x6b, 0xac, 0x87, 0x59, 0x87, 0x38, 0x96, 0x56, 0xb5, 0x3a, 0x44, 0x64, 0x31, 0x3f, 0xb7, 0x65, + 0x6c, 0xa7, 0xcc, 0x55, 0xcd, 0x2d, 0x2b, 0xe6, 0x23, 0xc9, 0x43, 0x77, 0x00, 0x8d, 0xb5, 0xb8, + 0x1d, 0x69, 0xcc, 0x6f, 0x19, 0xdb, 0x59, 0x33, 0x17, 0x69, 0x70, 0x5b, 0x4b, 0xaf, 0xc1, 0xdc, + 0x6f, 0xb1, 0xd7, 0x23, 0x4e, 0x7e, 0x61, 0xcb, 0xd8, 0x5e, 0x30, 0xf5, 0x09, 0xdd, 0x85, 0xd5, + 0x8e, 0xe7, 0x76, 0x08, 0xe3, 0xd6, 0x90, 0x72, 0xe2, 0x44, 0x76, 0x16, 0xa5, 0x1d, 0xa4, 0x79, + 0xcf, 0x05, 0x4b, 0x5b, 0x3a, 0x84, 0xe5, 0x49, 0x38, 0x2d, 0xcf, 0x6f, 0xd3, 0x7c, 0x5a, 0x3e, + 0xfc, 0xbd, 0x73, 0x1e, 0xbe, 0x3f, 0x96, 0xae, 0xf9, 0x6d, 0x6a, 0x2e, 0xd9, 0x53, 0xe7, 0xc2, + 0x9f, 0x12, 0xb0, 0x34, 0x2d, 0x82, 0x9e, 0xc2, 0x42, 0x1f, 0x8f, 0xac, 0x10, 0x73, 0xa2, 0xb1, + 0xf6, 0xa3, 0x2f, 0x4f, 0x36, 0x67, 0x2e, 0x95, 0xf4, 0xbf, 0x7c, 0xfb, 0xb7, 0x0f, 0x0c, 0x73, + 0xbe, 0x8f, 0x47, 0x26, 0xe6, 0x04, 0xfd, 0x06, 0x96, 0x85, 0x49, 0xbb, 0x83, 0x7d, 0x97, 0x28, + 0xcb, 0x89, 0xb7, 0xb2, 0x9c, 0xed, 0xe3, 0xd1, 0xbe, 0xb4, 0x26, 0xed, 0x7f, 0x0c, 0xe9, 0x41, + 0xe0, 0x60, 0x4e, 0x2c, 0x51, 0x49, 0x12, 0xa6, 0xe9, 0xdd, 0xf5, 0xa2, 0x2a, 0xb3, 0x62, 0x54, + 0x66, 0xc5, 0x66, 0x54, 0x66, 0xe5, 0xac, 0xf0, 0xfb, 0xf9, 0xbf, 0x37, 0x0d, 0x65, 0x0e, 0x94, + 0xb6, 0xe0, 0x3f, 0x48, 0xfd, 0xf7, 0xcf, 0x9b, 0x46, 0xe1, 0x9b, 0x04, 0xe4, 0x4f, 0x57, 0xe0, + 0xa7, 0x1e, 0xef, 0x3c, 0x21, 0x1c, 0xc7, 0x50, 0x6c, 0x5c, 0x19, 0x8a, 0xd7, 0x60, 0x4e, 0x27, + 0x3f, 0x21, 0x61, 0xa7, 0x4f, 0xe8, 0x1d, 0xc8, 0x0c, 0x29, 0xf7, 0x7c, 0xd7, 0x0a, 0xe8, 0xef, + 0x49, 0x28, 0xdf, 0x96, 0x32, 0xd3, 0x8a, 0x56, 0x17, 0xa4, 0xd7, 0x20, 0x38, 0x75, 0x69, 0x04, + 0xcf, 0x7e, 0x2f, 0x82, 0xe7, 0x2e, 0x84, 0xe0, 0xf9, 0xf3, 0x10, 0x5c, 0xf8, 0x66, 0x01, 0xb2, + 0xe5, 0xe6, 0x7e, 0x85, 0xf4, 0x88, 0x8b, 0x65, 0x8f, 0xf9, 0x09, 0xa4, 0x05, 0x60, 0x49, 0x68, + 0x5d, 0xa8, 0xbf, 0x81, 0x12, 0x16, 0xc4, 0x58, 0x26, 0x12, 0x57, 0xdd, 0x4f, 0x92, 0x6f, 0xd8, + 0x4f, 0x3e, 0x83, 0xa5, 0x76, 0x60, 0xa9, 0x5b, 0x59, 0x3d, 0x8f, 0x89, 0x2c, 0x24, 0xdf, 0xf6, + 0x6a, 0xe9, 0x76, 0x50, 0x16, 0x97, 0x7b, 0xec, 0x31, 0x09, 0x09, 0x7d, 0x13, 0x05, 0x77, 0x95, + 0xb3, 0xb4, 0xa6, 0x09, 0x10, 0x6b, 0x91, 0x90, 0xc7, 0x5b, 0x99, 0x12, 0x09, 0xb9, 0xce, 0xe8, + 0x0f, 0x00, 0x88, 0x7f, 0x2a, 0x5f, 0x8b, 0xc4, 0x8f, 0x1a, 0xcd, 0x2d, 0x58, 0xe4, 0x94, 0xe3, + 0x9e, 0xc5, 0x30, 0x97, 0x5d, 0x2b, 0x65, 0x2e, 0x48, 0x42, 0x03, 0x4b, 0xdd, 0xf1, 0x0d, 0x46, + 0xb2, 0x5b, 0x65, 0xcc, 0xc5, 0xc8, 0xff, 0x48, 0x42, 0x4b, 0xb3, 0xe9, 0x80, 0x07, 0x03, 0x6e, + 0x79, 0xce, 0x28, 0x0f, 0x1a, 0x5a, 0x8a, 0x73, 0x24, 0x19, 0x35, 0x67, 0x84, 0x76, 0x21, 0x2d, + 0xe1, 0xa6, 0xad, 0xa5, 0x65, 0x22, 0x57, 0x5e, 0x9c, 0x6c, 0x0a, 0x98, 0x34, 0x34, 0xa7, 0x39, + 0x32, 0x81, 0x8d, 0xbf, 0x23, 0x1b, 0xb2, 0x8e, 0x02, 0x10, 0x0d, 0x2d, 0xe6, 0xb9, 0xf9, 0x8c, + 0xd4, 0x7a, 0xf8, 0xe2, 0x64, 0xf3, 0xc1, 0xa5, 0x63, 0xdc, 0xf0, 0x5c, 0x1f, 0xf3, 0x41, 0x48, + 0xcc, 0xcc, 0xd8, 0x68, 0xc3, 0x73, 0xd1, 0x33, 0xc8, 0xda, 0x74, 0x48, 0x7c, 0xec, 0x73, 0xe1, + 0x83, 0xe5, 0xb3, 0x5b, 0xc9, 0xed, 0xf4, 0xee, 0xdd, 0x73, 0x3b, 0xad, 0x92, 0xdd, 0x73, 0x70, + 0xa0, 0x2c, 0x28, 0xab, 0xcc, 0xcc, 0x44, 0x66, 0x1a, 0x9e, 0xcb, 0xd0, 0x7b, 0xb0, 0x34, 0xf0, + 0x5b, 0xd4, 0x77, 0xc6, 0x09, 0x5c, 0x92, 0x91, 0xc9, 0x8e, 0xa9, 0x32, 0x85, 0x4f, 0x21, 0x27, + 0x40, 0x34, 0xf0, 0x9d, 0x71, 0xa5, 0xe4, 0x97, 0x25, 0x26, 0xdf, 0x3f, 0xe7, 0x02, 0xe5, 0xe6, + 0xfe, 0xb3, 0x98, 0xb4, 0xb9, 0xdc, 0xe2, 0x76, 0x9c, 0x20, 0x3c, 0x07, 0x38, 0xc4, 0x7d, 0x66, + 0x0d, 0x49, 0x28, 0x07, 0x7a, 0x4e, 0x79, 0x56, 0xd4, 0xe7, 0x8a, 0x88, 0x6e, 0xc3, 0x92, 0xf0, + 0xcc, 0xbd, 0x20, 0x42, 0xc7, 0x8a, 0x14, 0xcb, 0xb4, 0xb8, 0xdd, 0xf4, 0x02, 0x0d, 0x90, 0x87, + 0x30, 0xcf, 0x78, 0xd7, 0x22, 0xa3, 0x20, 0x8f, 0x5e, 0x3b, 0x81, 0x1a, 0xa2, 0x5c, 0xab, 0xa3, + 0x00, 0xfb, 0xc2, 0xba, 0x39, 0xc7, 0x78, 0xb7, 0x3a, 0x0a, 0x50, 0x1d, 0xb2, 0xfd, 0x41, 0x8f, + 0x7b, 0xcc, 0x73, 0xd5, 0x1c, 0xbb, 0xf6, 0xda, 0x82, 0xdb, 0x73, 0x1c, 0x4f, 0x3c, 0x02, 0xf7, + 0xa4, 0xbd, 0x50, 0x4e, 0xb3, 0x4c, 0x64, 0x41, 0xce, 0xb2, 0x7f, 0x19, 0xb0, 0x34, 0xed, 0x0c, + 0x7d, 0x04, 0xf9, 0x20, 0x24, 0x43, 0x8f, 0x0e, 0x98, 0x35, 0x41, 0xac, 0xd5, 0xc1, 0xac, 0xa3, + 0x7a, 0xb7, 0x79, 0x3d, 0xe2, 0x37, 0x22, 0xf8, 0x3e, 0xc2, 0xac, 0x83, 0x4a, 0xb0, 0x4a, 0x79, + 0x87, 0x84, 0x56, 0x7b, 0xa0, 0x13, 0x35, 0x12, 0x58, 0x56, 0x6d, 0xc6, 0x5c, 0x91, 0xbc, 0x03, + 0xc5, 0x6a, 0x8e, 0x8e, 0x06, 0x1c, 0x61, 0x58, 0x8f, 0x79, 0xea, 0x5a, 0xd3, 0xc8, 0x49, 0x4a, + 0xe4, 0xdc, 0x3e, 0x2f, 0x42, 0x11, 0x54, 0xe4, 0xa3, 0x6e, 0x4c, 0x6e, 0xd4, 0xdd, 0x8f, 0x01, + 0xa7, 0xf0, 0x10, 0xd6, 0x2a, 0x11, 0x3e, 0x9f, 0x45, 0x58, 0x91, 0x23, 0xfb, 0x36, 0x2c, 0xb1, + 0x40, 0x54, 0xb3, 0x6c, 0x8d, 0xa2, 0x8a, 0xd4, 0xe3, 0x32, 0x92, 0x2a, 0x63, 0xd2, 0x1c, 0x15, + 0xfe, 0x98, 0x82, 0xe5, 0x53, 0x18, 0x11, 0x8d, 0x22, 0x06, 0xc6, 0x48, 0x2f, 0x3d, 0x81, 0xe2, + 0x77, 0xea, 0x33, 0x71, 0x91, 0xfa, 0xe4, 0xb0, 0x16, 0xab, 0xcf, 0x48, 0x5b, 0x14, 0x6a, 0xf2, + 0x4a, 0x0a, 0x75, 0x75, 0x52, 0xa8, 0xda, 0xb8, 0x28, 0xd8, 0x36, 0xac, 0x4d, 0xc2, 0x1e, 0x73, + 0xca, 0x64, 0x0b, 0x7e, 0x93, 0xca, 0x5d, 0x1d, 0x57, 0xee, 0xc4, 0x0d, 0x43, 0x36, 0xdc, 0x1a, + 0xfb, 0x99, 0x44, 0x4f, 0x00, 0x59, 0xf6, 0xfb, 0xd9, 0x4b, 0x24, 0x3b, 0x1f, 0x19, 0x1a, 0x27, + 0xb4, 0xe1, 0xb9, 0xb2, 0xcb, 0xbb, 0x90, 0x9f, 0x84, 0x70, 0xe2, 0x45, 0x96, 0xca, 0x9c, 0x2c, + 0x95, 0x9d, 0x73, 0x3c, 0x9c, 0x0d, 0x12, 0x73, 0x92, 0x91, 0x29, 0x7a, 0xa1, 0x01, 0x37, 0x26, + 0xf3, 0x98, 0x86, 0x93, 0xc1, 0xcc, 0xd0, 0x8f, 0x21, 0xe5, 0x90, 0x1e, 0xcb, 0x1b, 0xaf, 0x7d, + 0xd1, 0xd4, 0x34, 0x37, 0xa5, 0x46, 0xe1, 0x10, 0x6e, 0x9d, 0x6d, 0xb4, 0xe6, 0x3b, 0x64, 0x24, + 0xca, 0xeb, 0x54, 0x39, 0xaa, 0xd0, 0x09, 0x47, 0x19, 0x73, 0x85, 0xc5, 0x6b, 0x51, 0x44, 0xa3, + 0xf0, 0x57, 0x03, 0xb2, 0x53, 0x91, 0x43, 0x9f, 0x40, 0xe2, 0x6a, 0x16, 0xb0, 0x44, 0xd0, 0x45, + 0x75, 0x48, 0x0a, 0x70, 0x26, 0xae, 0x04, 0x9c, 0xc2, 0x54, 0xe1, 0x0f, 0x06, 0xdc, 0x3c, 0x17, + 0x57, 0x62, 0x6f, 0xb1, 0xe9, 0xf0, 0xaa, 0x36, 0x48, 0x9b, 0x0e, 0xeb, 0x5d, 0x51, 0xca, 0x58, + 0x39, 0x52, 0x98, 0x4f, 0xc8, 0x58, 0xa6, 0xf1, 0xd8, 0x39, 0x2b, 0xfc, 0xdd, 0x80, 0x9b, 0x0d, + 0xd2, 0x23, 0x36, 0xf7, 0x86, 0x24, 0x82, 0x74, 0x55, 0x2c, 0xb7, 0xbe, 0x4d, 0xd0, 0xa7, 0xb0, + 0x38, 0xde, 0x5c, 0xae, 0x62, 0x9f, 0x9a, 0xd7, 0x4b, 0x0b, 0xda, 0x81, 0x6b, 0x21, 0x11, 0x40, + 0x0f, 0x89, 0x63, 0x69, 0x17, 0xac, 0xab, 0x5a, 0x81, 0x99, 0x1b, 0xb3, 0x0e, 0x84, 0x78, 0xa3, + 0xfb, 0x71, 0x6a, 0xc1, 0xc8, 0x25, 0xcc, 0xe5, 0x53, 0x00, 0x29, 0xb4, 0x60, 0xa9, 0xe6, 0xdb, + 0xbd, 0x81, 0x68, 0xec, 0x72, 0xfd, 0x42, 0x0f, 0x20, 0xd9, 0x25, 0xc7, 0x32, 0x84, 0xe9, 0xdd, + 0xed, 0x38, 0x3a, 0x63, 0xbf, 0xda, 0x87, 0xf7, 0x8a, 0xcd, 0x10, 0xfb, 0x0c, 0xdb, 0x02, 0x7e, + 0xe2, 0x5e, 0x42, 0x09, 0xad, 0xc2, 0x6c, 0x20, 0x8c, 0xe8, 0x8e, 0xae, 0x0e, 0x85, 0x7f, 0x18, + 0xb0, 0xfc, 0x18, 0x87, 0x2e, 0x61, 0xbc, 0xcc, 0x6d, 0x93, 0x1c, 0x85, 0xae, 0x58, 0x76, 0x5a, + 0x3d, 0x6a, 0x77, 0x2d, 0xc7, 0x6b, 0xb7, 0xa5, 0xb3, 0xac, 0xb9, 0x28, 0x29, 0x15, 0xaf, 0xdd, + 0x46, 0x4f, 0x20, 0x1b, 0xd2, 0x5e, 0xaf, 0x85, 0xed, 0xae, 0xd5, 0x0e, 0x69, 0x5f, 0xff, 0xd4, + 0x9e, 0xba, 0x4e, 0xfc, 0x5f, 0x01, 0x55, 0x30, 0x8f, 0x08, 0x76, 0xa2, 0x21, 0x16, 0xa9, 0x1f, + 0x84, 0xb4, 0x8f, 0x6a, 0x90, 0x1e, 0x9b, 0xe3, 0x54, 0x6f, 0xa1, 0x17, 0x37, 0x06, 0x91, 0x72, + 0x93, 0x16, 0xfe, 0x97, 0x80, 0xd5, 0xb3, 0xc6, 0x26, 0xea, 0xa8, 0xfd, 0x8c, 0x84, 0x53, 0x6b, + 0xaa, 0xf1, 0xf6, 0x6b, 0xea, 0xb2, 0x32, 0x3b, 0x59, 0x55, 0xdf, 0x85, 0xac, 0xf6, 0xf4, 0xbb, + 0x01, 0x0d, 0x07, 0x2a, 0x38, 0x59, 0x33, 0xa3, 0x88, 0x4f, 0x25, 0x0d, 0xfd, 0x1a, 0x6e, 0x9c, + 0x3d, 0x2c, 0x2e, 0x37, 0x37, 0xaf, 0x9f, 0x35, 0x13, 0x18, 0xa2, 0xf0, 0xce, 0x59, 0x7d, 0xf4, + 0xac, 0xf9, 0x70, 0x31, 0x3f, 0x1b, 0xdf, 0xed, 0xa3, 0x71, 0x87, 0x1f, 0x7c, 0x06, 0xd7, 0xa6, + 0x3a, 0x62, 0x83, 0x63, 0x3e, 0x60, 0x28, 0x0d, 0xf3, 0xf5, 0xea, 0x61, 0xa5, 0x76, 0xf8, 0xf3, + 0xdc, 0x0c, 0xca, 0xc0, 0xc2, 0xf3, 0xaa, 0x59, 0x3b, 0xa8, 0x55, 0x2b, 0x39, 0x03, 0x01, 0xcc, + 0xed, 0xed, 0x37, 0x6b, 0xcf, 0xab, 0xb9, 0x84, 0xe0, 0x3c, 0x3b, 0x2c, 0x1f, 0x1d, 0x56, 0xaa, + 0x95, 0x5c, 0x52, 0x28, 0x55, 0x7f, 0x51, 0xaf, 0x99, 0xd5, 0x4a, 0x2e, 0x85, 0xe6, 0x21, 0xb9, + 0x77, 0xf8, 0xcb, 0xdc, 0x6c, 0xf9, 0xe9, 0x97, 0x2f, 0x37, 0x8c, 0xaf, 0x5e, 0x6e, 0x18, 0xff, + 0x79, 0xb9, 0x61, 0x7c, 0xfe, 0x6a, 0x63, 0xe6, 0xab, 0x57, 0x1b, 0x33, 0xff, 0x7c, 0xb5, 0x31, + 0xf3, 0xab, 0x8b, 0x65, 0x6e, 0x14, 0xff, 0x2b, 0x4c, 0xa6, 0xb1, 0x35, 0x27, 0x7f, 0x24, 0x7f, + 0xf8, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7d, 0x4a, 0x80, 0xd9, 0xc3, 0x13, 0x00, 0x00, } func (this *CommissionInfo) Equal(that interface{}) bool { @@ -1548,6 +1639,20 @@ func (m *BTCDelegation) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.MultisigInfo != nil { + { + size, err := m.MultisigInfo.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintBtcstaking(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x9a + } if m.StkExp != nil { { size, err := m.StkExp.MarshalToSizedBuffer(dAtA[:i]) @@ -2182,6 +2287,76 @@ func (m *LargestBtcReOrg) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *AdditionalStakerInfo) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *AdditionalStakerInfo) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *AdditionalStakerInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.DelegatorUnbondingSlashingSigs) > 0 { + for iNdEx := len(m.DelegatorUnbondingSlashingSigs) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.DelegatorUnbondingSlashingSigs[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintBtcstaking(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.DelegatorSlashingSigs) > 0 { + for iNdEx := len(m.DelegatorSlashingSigs) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.DelegatorSlashingSigs[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintBtcstaking(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if m.StakerQuorum != 0 { + i = encodeVarintBtcstaking(dAtA, i, uint64(m.StakerQuorum)) + i-- + dAtA[i] = 0x10 + } + if len(m.StakerBtcPkList) > 0 { + for iNdEx := len(m.StakerBtcPkList) - 1; iNdEx >= 0; iNdEx-- { + { + size := m.StakerBtcPkList[iNdEx].Size() + i -= size + if _, err := m.StakerBtcPkList[iNdEx].MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintBtcstaking(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func encodeVarintBtcstaking(dAtA []byte, offset int, v uint64) int { offset -= sovBtcstaking(v) base := offset @@ -2358,6 +2533,10 @@ func (m *BTCDelegation) Size() (n int) { l = m.StkExp.Size() n += 2 + l + sovBtcstaking(uint64(l)) } + if m.MultisigInfo != nil { + l = m.MultisigInfo.Size() + n += 2 + l + sovBtcstaking(uint64(l)) + } return n } @@ -2554,6 +2733,36 @@ func (m *LargestBtcReOrg) Size() (n int) { return n } +func (m *AdditionalStakerInfo) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.StakerBtcPkList) > 0 { + for _, e := range m.StakerBtcPkList { + l = e.Size() + n += 1 + l + sovBtcstaking(uint64(l)) + } + } + if m.StakerQuorum != 0 { + n += 1 + sovBtcstaking(uint64(m.StakerQuorum)) + } + if len(m.DelegatorSlashingSigs) > 0 { + for _, e := range m.DelegatorSlashingSigs { + l = e.Size() + n += 1 + l + sovBtcstaking(uint64(l)) + } + } + if len(m.DelegatorUnbondingSlashingSigs) > 0 { + for _, e := range m.DelegatorUnbondingSlashingSigs { + l = e.Size() + n += 1 + l + sovBtcstaking(uint64(l)) + } + } + return n +} + func sovBtcstaking(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -3778,6 +3987,42 @@ func (m *BTCDelegation) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 19: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MultisigInfo", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBtcstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthBtcstaking + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthBtcstaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MultisigInfo == nil { + m.MultisigInfo = &AdditionalStakerInfo{} + } + if err := m.MultisigInfo.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipBtcstaking(dAtA[iNdEx:]) @@ -5076,6 +5321,178 @@ func (m *LargestBtcReOrg) Unmarshal(dAtA []byte) error { } return nil } +func (m *AdditionalStakerInfo) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBtcstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: AdditionalStakerInfo: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: AdditionalStakerInfo: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StakerBtcPkList", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBtcstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthBtcstaking + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthBtcstaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var v github_com_babylonlabs_io_babylon_v4_types.BIP340PubKey + m.StakerBtcPkList = append(m.StakerBtcPkList, v) + if err := m.StakerBtcPkList[len(m.StakerBtcPkList)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field StakerQuorum", wireType) + } + m.StakerQuorum = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBtcstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.StakerQuorum |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DelegatorSlashingSigs", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBtcstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthBtcstaking + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthBtcstaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DelegatorSlashingSigs = append(m.DelegatorSlashingSigs, &SignatureInfo{}) + if err := m.DelegatorSlashingSigs[len(m.DelegatorSlashingSigs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DelegatorUnbondingSlashingSigs", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBtcstaking + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthBtcstaking + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthBtcstaking + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DelegatorUnbondingSlashingSigs = append(m.DelegatorUnbondingSlashingSigs, &SignatureInfo{}) + if err := m.DelegatorUnbondingSlashingSigs[len(m.DelegatorUnbondingSlashingSigs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipBtcstaking(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthBtcstaking + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipBtcstaking(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/btcstaking/types/btcstaking_test.go b/x/btcstaking/types/btcstaking_test.go index 035397227..0b180a403 100644 --- a/x/btcstaking/types/btcstaking_test.go +++ b/x/btcstaking/types/btcstaking_test.go @@ -1,10 +1,13 @@ package types_test import ( + bbn "github.com/babylonlabs-io/babylon/v4/types" "math/rand" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" "github.com/babylonlabs-io/babylon/v4/x/btcstaking/types" ) @@ -99,3 +102,19 @@ func TestLargestBtcReOrg_Validate(t *testing.T) { }) } } + +func TestExistDup(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().Unix())) + _, btcPks, err := datagen.GenRandomBTCKeyPairs(r, 3) + require.NoError(t, err) + + pks := bbn.NewBIP340PKsFromBTCPKs(btcPks) + duplicate, err := types.ExistsDup(pks) + require.NoError(t, err) + require.False(t, duplicate) + + pks = append(pks, pks[0]) + duplicate, err = types.ExistsDup(pks) + require.NoError(t, err) + require.True(t, duplicate) +} diff --git a/x/btcstaking/types/create_delegation_parser.go b/x/btcstaking/types/create_delegation_parser.go index 28c4eb16c..2e87cdcbc 100644 --- a/x/btcstaking/types/create_delegation_parser.go +++ b/x/btcstaking/types/create_delegation_parser.go @@ -123,6 +123,47 @@ func NewParsedProofOfInclusion( }, nil } +type ParsedSignatureInfo struct { + PublicKey *ParsedPublicKey + Sig *ParsedBIP340Signature +} + +func (si *SignatureInfo) ValidateBasic() error { + if si.Pk == nil { + return fmt.Errorf("cannot parse nil *bbn.BIP340PubKey") + } + if si.Sig == nil { + return fmt.Errorf("cannot parse nil *bbn.BIP340Signature") + } + + return nil +} + +func NewParsedSignatureInfo(si *SignatureInfo) (*ParsedSignatureInfo, error) { + if si == nil { + return nil, fmt.Errorf("cannot parse nil *SignatureInfo") + } + + if err := si.ValidateBasic(); err != nil { + return nil, err + } + + pk, err := si.Pk.ToBTCPK() + if err != nil { + return nil, fmt.Errorf("failed to parse *bbn.BIP340PubKey: %w", err) + } + + sig, err := si.Sig.ToBTCSig() + if err != nil { + return nil, fmt.Errorf("failed to parse *bbn.BIP340Signature: %w", err) + } + + return &ParsedSignatureInfo{ + PublicKey: &ParsedPublicKey{pk, si.Pk}, + Sig: &ParsedBIP340Signature{sig, si.Sig}, + }, nil +} + type ParsedCreateDelegationMessage struct { StakerAddress sdk.AccAddress StakingTx *ParsedBtcTransaction @@ -147,6 +188,10 @@ type ParsedCreateDelegationMessage struct { // contain the necessary information to validate and // create the BTC delegation as a stake expansion. StkExp *ParsedCreateDelStkExp + // MultisigInfo is an optional field. if this field is nil, + // this BTC delegation is not M-of-N multisig. else, it is an M-of-N multisig and + // should contain the necessary information to validate. + MultisigInfo *ParsedAdditionalStakerInfo } type ParsedCreateDelStkExp struct { @@ -164,6 +209,55 @@ type ParsedCreateDelStkExp struct { FundingOutputIndex uint32 } +type ParsedAdditionalStakerInfo struct { + StakerBTCPkList *ParsedPublicKeyList + StakerQuorum uint32 + StakerStakingSlashingSigs []*ParsedSignatureInfo + StakerUnbondingSlashingSigs []*ParsedSignatureInfo +} + +func parseAdditionalStakerInfo(asi *AdditionalStakerInfo) (*ParsedAdditionalStakerInfo, error) { + var ( + stakerStakingSlashingSigs, stakerUnbondingSlashingSigs []*ParsedSignatureInfo + ) + + stakerBTCPkList, err := NewParsedPublicKeyList(asi.StakerBtcPkList) + if err != nil { + return nil, err + } + + duplicate, err := ExistsDup(stakerBTCPkList.PublicKeysBbnFormat) + if err != nil { + return nil, fmt.Errorf("error in staker public keys: %v", err) + } + if duplicate { + return nil, ErrDuplicatedStakerKey + } + + for _, si := range asi.DelegatorSlashingSigs { + parsedSi, err := NewParsedSignatureInfo(si) + if err != nil { + return nil, err + } + stakerStakingSlashingSigs = append(stakerStakingSlashingSigs, parsedSi) + } + + for _, si := range asi.DelegatorUnbondingSlashingSigs { + parsedSi, err := NewParsedSignatureInfo(si) + if err != nil { + return nil, err + } + stakerUnbondingSlashingSigs = append(stakerUnbondingSlashingSigs, parsedSi) + } + + return &ParsedAdditionalStakerInfo{ + StakerBTCPkList: stakerBTCPkList, + StakerQuorum: asi.StakerQuorum, + StakerStakingSlashingSigs: stakerStakingSlashingSigs, + StakerUnbondingSlashingSigs: stakerUnbondingSlashingSigs, + }, nil +} + // parseCreateDelegationMessage parses MsgCreateBTCDelegation message and performs some basic // stateless checks: // - unbonding transaction is a simple transfer @@ -264,6 +358,15 @@ func parseCreateDelegationMessage(msg *MsgCreateBTCDelegation) (*ParsedCreateDel return nil, fmt.Errorf("unbonding value must be positive") } + // 9. Parse extra staker info + var parsedMultisigInfo *ParsedAdditionalStakerInfo + if msg.GetMultisigInfo() != nil { + parsedMultisigInfo, err = parseAdditionalStakerInfo(msg.GetMultisigInfo()) + if err != nil { + return nil, fmt.Errorf("failed to parse extra staker info: %v", err) + } + } + return &ParsedCreateDelegationMessage{ StakerAddress: stakerAddr, StakingTx: stakingTx, @@ -280,6 +383,7 @@ func parseCreateDelegationMessage(msg *MsgCreateBTCDelegation) (*ParsedCreateDel StakerUnbondingSlashingSig: stakerUnbondingSlashingSig, FinalityProviderKeys: fpPKs, ParsedPop: msg.GetPop(), + MultisigInfo: parsedMultisigInfo, }, nil } @@ -305,6 +409,7 @@ func parseBtcExpandMessage(msg *MsgBtcStakeExpand) (*ParsedCreateDelegationMessa UnbondingValue: msg.UnbondingValue, UnbondingSlashingTx: msg.UnbondingSlashingTx, DelegatorUnbondingSlashingSig: msg.DelegatorUnbondingSlashingSig, + MultisigInfo: msg.MultisigInfo, }) if err != nil { return nil, err diff --git a/x/btcstaking/types/errors.go b/x/btcstaking/types/errors.go index c9cbfe7ea..a6de846dc 100644 --- a/x/btcstaking/types/errors.go +++ b/x/btcstaking/types/errors.go @@ -35,4 +35,6 @@ var ( ErrLargestBtcReorgNotFound = errorsmod.Register(ModuleName, 1128, "there is no BTC reorg currently set") ErrInvalidStakeExpansion = errorsmod.Register(ModuleName, 1129, "invalid stake expansion") ErrFinalityProviderIsDeleted = errorsmod.Register(ModuleName, 1130, "the finality provider has been deleted") + ErrInvalidMultisigInfo = errorsmod.Register(ModuleName, 1131, "invalid multisig info") + ErrDuplicatedStakerKey = errorsmod.Register(ModuleName, 1132, "multisig staker key is duplicated") ) diff --git a/x/btcstaking/types/events.go b/x/btcstaking/types/events.go index adfc5170b..6fe44683d 100644 --- a/x/btcstaking/types/events.go +++ b/x/btcstaking/types/events.go @@ -105,6 +105,13 @@ func NewBtcDelCreationEvent( if btcDel.IsStakeExpansion() { e.PreviousStakingTxHashHex = btcDel.MustGetStakeExpansionTxHash().String() } + if btcDel.IsMultisigBtcDel() { + var multisigStakerBtcPkHexs []string + for _, btcPk := range btcDel.MultisigInfo.StakerBtcPkList { + multisigStakerBtcPkHexs = append(multisigStakerBtcPkHexs, btcPk.MarshalHex()) + } + e.MultisigStakerBtcPkHexs = multisigStakerBtcPkHexs + } return e } diff --git a/x/btcstaking/types/events.pb.go b/x/btcstaking/types/events.pb.go index fc2b91db2..9ebff97a7 100644 --- a/x/btcstaking/types/events.pb.go +++ b/x/btcstaking/types/events.pb.go @@ -725,6 +725,10 @@ type EventBTCDelegationCreated struct { // previous_staking_tx_hash_hex is the hex encoded of the hash of the staking tx // that was used as input to the stake expansion, if empty it is NOT a stake expansion. PreviousStakingTxHashHex string `protobuf:"bytes,11,opt,name=previous_staking_tx_hash_hex,json=previousStakingTxHashHex,proto3" json:"previous_staking_tx_hash_hex,omitempty"` + // multisig_staker_btc_pk_hexs is the hex str of Bitcoin secp256k1 PK of the multisig staker that + // create this BTC delegation. the PK follows encoding in BIP-340 spec. + // if empty, it is NOT a M-of-N multisig btc delegation. + MultisigStakerBtcPkHexs []string `protobuf:"bytes,12,rep,name=multisig_staker_btc_pk_hexs,json=multisigStakerBtcPkHexs,proto3" json:"multisig_staker_btc_pk_hexs,omitempty"` } func (m *EventBTCDelegationCreated) Reset() { *m = EventBTCDelegationCreated{} } @@ -837,6 +841,13 @@ func (m *EventBTCDelegationCreated) GetPreviousStakingTxHashHex() string { return "" } +func (m *EventBTCDelegationCreated) GetMultisigStakerBtcPkHexs() []string { + if m != nil { + return m.MultisigStakerBtcPkHexs + } + return nil +} + // EventCovenantSignatureReceived is the event emitted when a covenant committee // sends valid covenant signatures for a BTC delegation type EventCovenantSignatureReceived struct { @@ -1285,91 +1296,92 @@ func init() { } var fileDescriptor_74118427820fff75 = []byte{ - // 1334 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x57, 0x4f, 0x6f, 0x13, 0x47, - 0x14, 0x8f, 0x1d, 0xe7, 0xdf, 0x73, 0x08, 0x61, 0x9b, 0x52, 0xe3, 0x82, 0x09, 0x06, 0xa2, 0x94, - 0x16, 0x9b, 0x3f, 0xa9, 0x38, 0x54, 0xaa, 0x64, 0x27, 0x0e, 0x36, 0x44, 0x60, 0xec, 0x04, 0x89, - 0x5e, 0x56, 0xeb, 0xdd, 0x17, 0x7b, 0xf0, 0x7a, 0x76, 0xb5, 0x3b, 0xeb, 0xd8, 0x9f, 0xa0, 0xa7, - 0x4a, 0x9c, 0xfb, 0x09, 0x7a, 0x2b, 0xc7, 0x7e, 0x84, 0x5e, 0x2a, 0x71, 0xe8, 0xa1, 0xea, 0xa1, - 0xaa, 0xc8, 0xa1, 0xdf, 0xa2, 0xaa, 0x76, 0x66, 0xd6, 0xde, 0x75, 0xd6, 0x90, 0x54, 0x70, 0x89, - 0xb2, 0x33, 0xbf, 0xf7, 0x7b, 0x6f, 0x7e, 0xf3, 0xfe, 0x8c, 0x21, 0xdf, 0xd2, 0x5a, 0x43, 0xd3, - 0xa2, 0xc5, 0x16, 0xd3, 0x5d, 0xa6, 0x75, 0x09, 0x6d, 0x17, 0xfb, 0x77, 0x8b, 0xd8, 0x47, 0xca, - 0xdc, 0x82, 0xed, 0x58, 0xcc, 0x52, 0x3e, 0x95, 0x98, 0xc2, 0x18, 0x53, 0xe8, 0xdf, 0xcd, 0xae, - 0xb5, 0xad, 0xb6, 0xc5, 0x11, 0x45, 0xff, 0x3f, 0x01, 0xce, 0x6e, 0xc4, 0x13, 0x86, 0x4c, 0x05, - 0xee, 0x82, 0xd6, 0x23, 0xd4, 0x2a, 0xf2, 0xbf, 0x62, 0x29, 0xff, 0x63, 0x12, 0x2e, 0x57, 0x7c, - 0xc7, 0xbb, 0x84, 0x6a, 0x26, 0x61, 0xc3, 0xba, 0x63, 0xf5, 0x89, 0x81, 0xce, 0xb6, 0x83, 0x1a, - 0x43, 0x43, 0xb9, 0x0e, 0xd0, 0x62, 0xba, 0x6a, 0x77, 0xd5, 0x0e, 0x0e, 0x32, 0x89, 0xf5, 0xc4, - 0xe6, 0x52, 0x79, 0xee, 0xa7, 0x7f, 0x5e, 0xdf, 0x4a, 0x34, 0x16, 0x5b, 0x4c, 0xaf, 0x77, 0xab, - 0x38, 0x50, 0x2e, 0x41, 0x4a, 0x33, 0x0c, 0x27, 0x93, 0x0c, 0x6f, 0xf3, 0x25, 0xe5, 0x26, 0x80, - 0x6e, 0xf5, 0x7a, 0xc4, 0x75, 0x89, 0x45, 0x33, 0xb3, 0x61, 0x40, 0x68, 0x43, 0xc9, 0xc0, 0x42, - 0xcf, 0xa2, 0xa4, 0x8b, 0x4e, 0x26, 0xe5, 0x63, 0x1a, 0xc1, 0xa7, 0x92, 0x85, 0x45, 0x62, 0x20, - 0x65, 0x84, 0x0d, 0x33, 0x73, 0x7c, 0x6b, 0xf4, 0xed, 0x5b, 0x1d, 0x61, 0xcb, 0x25, 0x0c, 0x33, - 0xf3, 0xc2, 0x4a, 0x7e, 0x2a, 0x5f, 0xc0, 0xaa, 0x8b, 0xba, 0xe7, 0x10, 0x36, 0x54, 0x75, 0x8b, - 0x32, 0x4d, 0x67, 0x99, 0x05, 0x0e, 0x39, 0x1f, 0xac, 0x6f, 0x8b, 0x65, 0x9f, 0xc4, 0x40, 0xa6, - 0x11, 0xd3, 0xcd, 0x2c, 0x0a, 0x12, 0xf9, 0x99, 0xff, 0x37, 0x01, 0x9f, 0xc7, 0x8a, 0x53, 0x31, - 0xc8, 0xa9, 0xb5, 0x89, 0x0a, 0x90, 0x3c, 0x85, 0x00, 0xb3, 0xd3, 0x05, 0x48, 0x4d, 0x17, 0x60, - 0xee, 0xfd, 0x02, 0xcc, 0xbf, 0x57, 0x80, 0x85, 0xa8, 0x00, 0xaf, 0x12, 0x70, 0x85, 0x0b, 0x50, - 0xde, 0xdf, 0xde, 0x41, 0x13, 0xdb, 0x1a, 0x23, 0x16, 0x6d, 0x32, 0x8d, 0xe1, 0x81, 0x6d, 0x68, - 0x0c, 0x95, 0x0d, 0x38, 0x2f, 0x73, 0x4c, 0x65, 0x03, 0xb5, 0xa3, 0xb9, 0x1d, 0xa1, 0x43, 0xe3, - 0x9c, 0x5c, 0xde, 0x1f, 0x54, 0x35, 0xb7, 0xa3, 0x3c, 0x84, 0x25, 0x8a, 0x47, 0xaa, 0xeb, 0x9b, - 0x72, 0x11, 0x56, 0xee, 0xdd, 0x2a, 0xc4, 0xe6, 0x78, 0xe1, 0x84, 0x2f, 0xcf, 0x6d, 0x2c, 0x52, - 0x3c, 0xe2, 0x6e, 0xf3, 0x87, 0x70, 0x91, 0x47, 0xd4, 0x44, 0x13, 0x75, 0x46, 0xfa, 0xd8, 0x34, - 0x35, 0xb7, 0x43, 0x68, 0x5b, 0xd9, 0x83, 0x45, 0xf4, 0x6f, 0x87, 0xea, 0xc8, 0x63, 0x48, 0xdf, - 0xbb, 0x33, 0xc5, 0xc3, 0x09, 0xdb, 0x8a, 0xb4, 0x6b, 0x8c, 0x18, 0xf2, 0x3f, 0xcc, 0xc3, 0x1a, - 0x77, 0x54, 0xb7, 0x8e, 0xd0, 0xd9, 0x21, 0x2e, 0x93, 0x27, 0x26, 0x00, 0xae, 0x6f, 0x86, 0x86, - 0x7a, 0x68, 0x4b, 0x47, 0xd5, 0x29, 0x8e, 0xe2, 0x08, 0xc4, 0x62, 0x53, 0x50, 0x4c, 0x26, 0x56, - 0x75, 0xa6, 0xb1, 0x24, 0xd9, 0x77, 0x6d, 0xe5, 0x10, 0x96, 0x5e, 0x6a, 0xc4, 0x14, 0x9e, 0x92, - 0xdc, 0xd3, 0xc3, 0x33, 0x7b, 0x7a, 0xc4, 0x19, 0x62, 0x1c, 0x2d, 0x0a, 0xee, 0x5d, 0x5b, 0x31, - 0x21, 0xed, 0xd1, 0xb1, 0xa7, 0x59, 0xee, 0xa9, 0x76, 0x66, 0x4f, 0x07, 0x92, 0x23, 0xc6, 0x17, - 0x04, 0xfc, 0xbb, 0xb6, 0xd2, 0x86, 0x35, 0xbf, 0x6a, 0x0c, 0x34, 0x45, 0x3a, 0xa8, 0x1e, 0xe7, - 0xe0, 0xb9, 0x9d, 0xbe, 0xb7, 0xf5, 0x2e, 0xb7, 0xd3, 0xd2, 0xb0, 0x3a, 0xd3, 0xb8, 0xd0, 0x62, - 0xfa, 0x0e, 0x9a, 0xa1, 0xc5, 0x6c, 0x57, 0xb6, 0xb6, 0x29, 0x5a, 0x2b, 0x8f, 0x21, 0x69, 0x77, - 0xf9, 0x0d, 0x2e, 0x97, 0xbf, 0xf9, 0xf3, 0xaf, 0xab, 0x0f, 0xda, 0x84, 0x75, 0xbc, 0x56, 0x41, - 0xb7, 0x7a, 0x45, 0x19, 0x84, 0xa9, 0xb5, 0xdc, 0xdb, 0xc4, 0x0a, 0x3e, 0x8b, 0xfd, 0xad, 0x22, - 0x1b, 0xda, 0xe8, 0x16, 0xca, 0xb5, 0xfa, 0xfd, 0xad, 0x3b, 0x75, 0xaf, 0xf5, 0x18, 0x87, 0x8d, - 0xa4, 0xdd, 0xcd, 0xbe, 0x94, 0xad, 0x22, 0x5e, 0xee, 0x0f, 0xeb, 0xcb, 0x94, 0x55, 0x39, 0x4d, - 0xf0, 0x0f, 0xea, 0xad, 0x9c, 0x82, 0x24, 0xf6, 0xf3, 0x08, 0xd7, 0x62, 0x5b, 0xa1, 0x28, 0xd0, - 0xed, 0x8e, 0x46, 0xdb, 0xa8, 0x5c, 0x86, 0x79, 0xd1, 0x10, 0xa3, 0xcd, 0x70, 0x8e, 0x37, 0x43, - 0x25, 0x3f, 0xd9, 0x03, 0xc6, 0xdd, 0x72, 0x54, 0xde, 0xbf, 0xa4, 0xe0, 0xd2, 0xc9, 0xab, 0x0e, - 0x86, 0xd1, 0x97, 0xb0, 0x12, 0xee, 0x36, 0x93, 0x4d, 0x77, 0x79, 0xdc, 0x73, 0x70, 0xa0, 0x3c, - 0x80, 0xb5, 0x00, 0x6c, 0x79, 0xcc, 0xf6, 0x98, 0x4a, 0xa8, 0x81, 0x83, 0xa8, 0x67, 0x45, 0x42, - 0x9e, 0x72, 0x44, 0xcd, 0x07, 0x28, 0x5f, 0xc1, 0x8a, 0xad, 0x39, 0x5a, 0xcf, 0x55, 0xfb, 0xe8, - 0x9c, 0x1c, 0x5b, 0xe7, 0xc4, 0xe6, 0x73, 0xb1, 0xa7, 0x3c, 0x84, 0x2b, 0x87, 0x52, 0x13, 0xd5, - 0x96, 0xa2, 0xa8, 0x42, 0x05, 0x97, 0x87, 0x98, 0x5a, 0x9f, 0x1d, 0x1b, 0x5f, 0x3a, 0x9c, 0xd0, - 0xaf, 0xec, 0x4b, 0xe3, 0xfa, 0xf1, 0xde, 0x81, 0x0b, 0x7e, 0x30, 0x23, 0x6b, 0x6e, 0x3c, 0x17, - 0xf6, 0xbc, 0x22, 0xf6, 0xcb, 0xc1, 0x68, 0xd9, 0x84, 0xe5, 0x91, 0x1c, 0xa4, 0x27, 0x67, 0x60, - 0x00, 0x4e, 0x07, 0x62, 0x90, 0x1e, 0xfa, 0x47, 0xf2, 0x68, 0xcb, 0xa2, 0xc6, 0x08, 0xbb, 0x10, - 0x39, 0xd2, 0x68, 0x93, 0xa3, 0x37, 0x61, 0x39, 0x84, 0x1e, 0x88, 0xb1, 0x38, 0xe2, 0x1d, 0x63, - 0x07, 0xd1, 0x2b, 0x5d, 0x8a, 0xbd, 0x52, 0x65, 0x03, 0xd2, 0xf2, 0x5c, 0xfc, 0x8d, 0x00, 0x91, - 0x09, 0x28, 0x76, 0x4a, 0xfe, 0x4b, 0xe1, 0x5b, 0xb8, 0x6c, 0x3b, 0xd8, 0x27, 0x96, 0xe7, 0xaa, - 0x13, 0x33, 0x85, 0x4b, 0x91, 0xe6, 0x73, 0x25, 0x13, 0x60, 0x9a, 0xe1, 0xf9, 0x52, 0xc5, 0x41, - 0xfe, 0x75, 0x12, 0x72, 0x3c, 0x75, 0xb6, 0xad, 0x3e, 0x52, 0x8d, 0xb2, 0x26, 0x69, 0x53, 0x8d, - 0x79, 0x0e, 0x36, 0x50, 0x47, 0xd2, 0x47, 0x43, 0xb9, 0x3d, 0x65, 0x5a, 0x8d, 0x74, 0x88, 0x0e, - 0xad, 0x2d, 0xf8, 0x44, 0x97, 0x5c, 0xe1, 0x3b, 0x89, 0x24, 0xd0, 0x6a, 0x80, 0x18, 0xdd, 0xca, - 0x13, 0x58, 0x1f, 0x59, 0x8d, 0x65, 0x74, 0x83, 0x60, 0x38, 0x45, 0x24, 0xa1, 0xae, 0x04, 0xf0, - 0x83, 0x00, 0x3d, 0x8a, 0xdc, 0xe7, 0x7b, 0x01, 0x1b, 0x23, 0x3e, 0x2e, 0x97, 0x8a, 0x03, 0x5b, - 0xa3, 0x7e, 0xf2, 0x4d, 0xb0, 0xa6, 0xc2, 0xac, 0xf9, 0xc0, 0xc8, 0x17, 0x0a, 0x2b, 0x81, 0x49, - 0x98, 0x3a, 0x6f, 0x41, 0x36, 0xa2, 0xd8, 0x33, 0xcf, 0x72, 0xbc, 0x5e, 0x03, 0x35, 0xbd, 0x73, - 0x76, 0xb5, 0x4e, 0x53, 0xde, 0xbf, 0x25, 0x60, 0xf3, 0x64, 0x79, 0xd7, 0xa8, 0x6e, 0x7a, 0x7e, - 0x70, 0x75, 0xc7, 0xb2, 0x0e, 0xff, 0xef, 0x6d, 0x89, 0x6a, 0x70, 0x98, 0xda, 0x41, 0xd2, 0xee, - 0xb0, 0x68, 0x08, 0x69, 0xbe, 0x55, 0xe5, 0x3b, 0xca, 0x0d, 0x00, 0xa4, 0x46, 0x80, 0x8b, 0xdc, - 0xc5, 0x12, 0x52, 0x43, 0xa2, 0x22, 0xe7, 0x49, 0xc5, 0x9f, 0xe7, 0xf7, 0x84, 0xcc, 0x39, 0x71, - 0x1e, 0x71, 0x1c, 0x71, 0x8d, 0x68, 0x54, 0x34, 0xc7, 0x1c, 0x7e, 0xbc, 0x53, 0x44, 0xe2, 0x9b, - 0x8d, 0xaf, 0xbd, 0xaf, 0xe1, 0xb3, 0xc9, 0x94, 0x09, 0x82, 0x10, 0x4f, 0x49, 0xde, 0x22, 0xc7, - 0xd9, 0x21, 0x82, 0xc8, 0xd3, 0xb8, 0x26, 0x5c, 0x19, 0xd8, 0xc4, 0xf9, 0x38, 0x69, 0xf1, 0x7d, - 0x52, 0x26, 0xe2, 0x01, 0xc5, 0x81, 0x8d, 0x3a, 0x43, 0xe3, 0x20, 0xd4, 0x65, 0xce, 0x5e, 0xb6, - 0xae, 0xed, 0x5f, 0xb0, 0x38, 0x7a, 0x60, 0x12, 0x2d, 0x5b, 0x8e, 0xe0, 0xa5, 0x21, 0xad, 0x4a, - 0x90, 0x9d, 0xb4, 0x42, 0xcd, 0xef, 0xe5, 0xdc, 0x38, 0xa2, 0xef, 0xc5, 0x88, 0x31, 0x47, 0x4d, - 0xa1, 0x68, 0x99, 0x96, 0xde, 0x95, 0x73, 0xc7, 0x17, 0xfc, 0x5c, 0x2c, 0x45, 0xd9, 0x47, 0xf1, - 0xd9, 0x73, 0xeb, 0xe7, 0x04, 0x5c, 0x8c, 0x1f, 0xb1, 0xca, 0x4d, 0xb8, 0xb6, 0x5b, 0x7b, 0x52, - 0xda, 0xab, 0xed, 0xbf, 0x50, 0xeb, 0x8d, 0xa7, 0xcf, 0x6b, 0x3b, 0x95, 0x86, 0xda, 0xdc, 0x2f, - 0xed, 0x1f, 0x34, 0xd5, 0xda, 0x93, 0xd2, 0xf6, 0x7e, 0xed, 0x79, 0x65, 0x75, 0x46, 0xb9, 0x0e, - 0x57, 0xa7, 0xc2, 0x24, 0x28, 0xf1, 0x4e, 0xd0, 0xa3, 0x52, 0x6d, 0xaf, 0xb2, 0xb3, 0x9a, 0x54, - 0x6e, 0xc0, 0xfa, 0x54, 0x50, 0x73, 0xaf, 0xd4, 0xac, 0x56, 0x76, 0x56, 0x67, 0xcb, 0xcf, 0x7e, - 0x7d, 0x9b, 0x4b, 0xbc, 0x79, 0x9b, 0x4b, 0xfc, 0xfd, 0x36, 0x97, 0x78, 0x75, 0x9c, 0x9b, 0x79, - 0x73, 0x9c, 0x9b, 0xf9, 0xe3, 0x38, 0x37, 0xf3, 0xdd, 0xe9, 0x5e, 0x1d, 0x83, 0xf0, 0xaf, 0x56, - 0xfe, 0x04, 0x69, 0xcd, 0xf3, 0xdf, 0xa6, 0xf7, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xcb, 0x99, - 0x45, 0x88, 0x29, 0x0f, 0x00, 0x00, + // 1360 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x57, 0xcf, 0x6e, 0xdb, 0xc6, + 0x13, 0xb6, 0x64, 0xf9, 0xdf, 0xc8, 0x71, 0x1c, 0xfe, 0xfc, 0x4b, 0x14, 0x25, 0x51, 0x1c, 0x25, + 0x31, 0xdc, 0xb4, 0x91, 0xf2, 0xc7, 0x45, 0x0e, 0x2d, 0x0a, 0x48, 0xb6, 0x1c, 0x29, 0x31, 0x12, + 0x45, 0xb2, 0x03, 0xa4, 0x17, 0x82, 0x22, 0xc7, 0xd2, 0x46, 0xd4, 0x92, 0x20, 0x97, 0xb2, 0xf4, + 0x04, 0x3d, 0x15, 0xc8, 0xb9, 0x4f, 0xd0, 0x5b, 0xf3, 0x18, 0xbd, 0x14, 0xc8, 0xa1, 0x87, 0xa2, + 0x87, 0xa2, 0x48, 0x0e, 0x7d, 0x83, 0x1e, 0x8b, 0x82, 0xbb, 0x4b, 0x89, 0x94, 0xa9, 0xc4, 0x2e, + 0x92, 0x8b, 0x61, 0xee, 0x7e, 0xf3, 0xcd, 0xec, 0x37, 0xb3, 0x33, 0x2b, 0xc8, 0xb7, 0xb4, 0xd6, + 0xd0, 0xb4, 0x68, 0xb1, 0xc5, 0x74, 0x97, 0x69, 0x5d, 0x42, 0xdb, 0xc5, 0xfe, 0xdd, 0x22, 0xf6, + 0x91, 0x32, 0xb7, 0x60, 0x3b, 0x16, 0xb3, 0x94, 0xff, 0x4b, 0x4c, 0x61, 0x8c, 0x29, 0xf4, 0xef, + 0x66, 0xd7, 0xda, 0x56, 0xdb, 0xe2, 0x88, 0xa2, 0xff, 0x9f, 0x00, 0x67, 0x37, 0xe2, 0x09, 0x43, + 0xa6, 0x02, 0x77, 0x4e, 0xeb, 0x11, 0x6a, 0x15, 0xf9, 0x5f, 0xb1, 0x94, 0xff, 0x21, 0x09, 0x97, + 0x2b, 0xbe, 0xe3, 0x5d, 0x42, 0x35, 0x93, 0xb0, 0x61, 0xdd, 0xb1, 0xfa, 0xc4, 0x40, 0x67, 0xdb, + 0x41, 0x8d, 0xa1, 0xa1, 0x5c, 0x07, 0x68, 0x31, 0x5d, 0xb5, 0xbb, 0x6a, 0x07, 0x07, 0x99, 0xc4, + 0x7a, 0x62, 0x73, 0xa9, 0x3c, 0xf7, 0xe3, 0x5f, 0xaf, 0x6f, 0x25, 0x1a, 0x8b, 0x2d, 0xa6, 0xd7, + 0xbb, 0x55, 0x1c, 0x28, 0x17, 0x21, 0xa5, 0x19, 0x86, 0x93, 0x49, 0x86, 0xb7, 0xf9, 0x92, 0x72, + 0x13, 0x40, 0xb7, 0x7a, 0x3d, 0xe2, 0xba, 0xc4, 0xa2, 0x99, 0xd9, 0x30, 0x20, 0xb4, 0xa1, 0x64, + 0x60, 0xa1, 0x67, 0x51, 0xd2, 0x45, 0x27, 0x93, 0xf2, 0x31, 0x8d, 0xe0, 0x53, 0xc9, 0xc2, 0x22, + 0x31, 0x90, 0x32, 0xc2, 0x86, 0x99, 0x39, 0xbe, 0x35, 0xfa, 0xf6, 0xad, 0x8e, 0xb0, 0xe5, 0x12, + 0x86, 0x99, 0x79, 0x61, 0x25, 0x3f, 0x95, 0xcf, 0x60, 0xd5, 0x45, 0xdd, 0x73, 0x08, 0x1b, 0xaa, + 0xba, 0x45, 0x99, 0xa6, 0xb3, 0xcc, 0x02, 0x87, 0x9c, 0x0d, 0xd6, 0xb7, 0xc5, 0xb2, 0x4f, 0x62, + 0x20, 0xd3, 0x88, 0xe9, 0x66, 0x16, 0x05, 0x89, 0xfc, 0xcc, 0xff, 0x93, 0x80, 0x4b, 0xb1, 0xe2, + 0x54, 0x0c, 0x72, 0x62, 0x6d, 0xa2, 0x02, 0x24, 0x4f, 0x20, 0xc0, 0xec, 0x74, 0x01, 0x52, 0xd3, + 0x05, 0x98, 0xfb, 0xb0, 0x00, 0xf3, 0x1f, 0x14, 0x60, 0x21, 0x2a, 0xc0, 0xab, 0x04, 0x5c, 0xe1, + 0x02, 0x94, 0xf7, 0xb7, 0x77, 0xd0, 0xc4, 0xb6, 0xc6, 0x88, 0x45, 0x9b, 0x4c, 0x63, 0x78, 0x60, + 0x1b, 0x1a, 0x43, 0x65, 0x03, 0xce, 0xca, 0x1a, 0x53, 0xd9, 0x40, 0xed, 0x68, 0x6e, 0x47, 0xe8, + 0xd0, 0x38, 0x23, 0x97, 0xf7, 0x07, 0x55, 0xcd, 0xed, 0x28, 0x0f, 0x61, 0x89, 0xe2, 0x91, 0xea, + 0xfa, 0xa6, 0x5c, 0x84, 0x95, 0x7b, 0xb7, 0x0a, 0xb1, 0x35, 0x5e, 0x38, 0xe6, 0xcb, 0x73, 0x1b, + 0x8b, 0x14, 0x8f, 0xb8, 0xdb, 0xfc, 0x21, 0x9c, 0xe7, 0x11, 0x35, 0xd1, 0x44, 0x9d, 0x91, 0x3e, + 0x36, 0x4d, 0xcd, 0xed, 0x10, 0xda, 0x56, 0xf6, 0x60, 0x11, 0xfd, 0xec, 0x50, 0x1d, 0x79, 0x0c, + 0xe9, 0x7b, 0x77, 0xa6, 0x78, 0x38, 0x66, 0x5b, 0x91, 0x76, 0x8d, 0x11, 0x43, 0xfe, 0xfb, 0x79, + 0x58, 0xe3, 0x8e, 0xea, 0xd6, 0x11, 0x3a, 0x3b, 0xc4, 0x65, 0xf2, 0xc4, 0x04, 0xc0, 0xf5, 0xcd, + 0xd0, 0x50, 0x0f, 0x6d, 0xe9, 0xa8, 0x3a, 0xc5, 0x51, 0x1c, 0x81, 0x58, 0x6c, 0x0a, 0x8a, 0xc9, + 0xc2, 0xaa, 0xce, 0x34, 0x96, 0x24, 0xfb, 0xae, 0xad, 0x1c, 0xc2, 0xd2, 0x4b, 0x8d, 0x98, 0xc2, + 0x53, 0x92, 0x7b, 0x7a, 0x78, 0x6a, 0x4f, 0x8f, 0x38, 0x43, 0x8c, 0xa3, 0x45, 0xc1, 0xbd, 0x6b, + 0x2b, 0x26, 0xa4, 0x3d, 0x3a, 0xf6, 0x34, 0xcb, 0x3d, 0xd5, 0x4e, 0xed, 0xe9, 0x40, 0x72, 0xc4, + 0xf8, 0x82, 0x80, 0x7f, 0xd7, 0x56, 0xda, 0xb0, 0xe6, 0xdf, 0x1a, 0x03, 0x4d, 0x51, 0x0e, 0xaa, + 0xc7, 0x39, 0x78, 0x6d, 0xa7, 0xef, 0x6d, 0xbd, 0xcf, 0xed, 0xb4, 0x32, 0xac, 0xce, 0x34, 0xce, + 0xb5, 0x98, 0xbe, 0x83, 0x66, 0x68, 0x31, 0xdb, 0x95, 0xad, 0x6d, 0x8a, 0xd6, 0xca, 0x63, 0x48, + 0xda, 0x5d, 0x9e, 0xc1, 0xe5, 0xf2, 0x57, 0xbf, 0xff, 0x71, 0xf5, 0x41, 0x9b, 0xb0, 0x8e, 0xd7, + 0x2a, 0xe8, 0x56, 0xaf, 0x28, 0x83, 0x30, 0xb5, 0x96, 0x7b, 0x9b, 0x58, 0xc1, 0x67, 0xb1, 0xbf, + 0x55, 0x64, 0x43, 0x1b, 0xdd, 0x42, 0xb9, 0x56, 0xbf, 0xbf, 0x75, 0xa7, 0xee, 0xb5, 0x1e, 0xe3, + 0xb0, 0x91, 0xb4, 0xbb, 0xd9, 0x97, 0xb2, 0x55, 0xc4, 0xcb, 0xfd, 0x71, 0x7d, 0x99, 0xf2, 0x56, + 0x4e, 0x13, 0xfc, 0xa3, 0x7a, 0x2b, 0xa7, 0x20, 0x89, 0xfd, 0x3c, 0xc2, 0xb5, 0xd8, 0x56, 0x28, + 0x2e, 0xe8, 0x76, 0x47, 0xa3, 0x6d, 0x54, 0x2e, 0xc3, 0xbc, 0x68, 0x88, 0xd1, 0x66, 0x38, 0xc7, + 0x9b, 0xa1, 0x92, 0x9f, 0xec, 0x01, 0xe3, 0x6e, 0x39, 0xba, 0xde, 0x7f, 0xa7, 0xe0, 0xe2, 0xf1, + 0x54, 0x07, 0xc3, 0xe8, 0x73, 0x58, 0x09, 0x77, 0x9b, 0xc9, 0xa6, 0xbb, 0x3c, 0xee, 0x39, 0x38, + 0x50, 0x1e, 0xc0, 0x5a, 0x00, 0xb6, 0x3c, 0x66, 0x7b, 0x4c, 0x25, 0xd4, 0xc0, 0x41, 0xd4, 0xb3, + 0x22, 0x21, 0x4f, 0x39, 0xa2, 0xe6, 0x03, 0x94, 0x2f, 0x60, 0xc5, 0xd6, 0x1c, 0xad, 0xe7, 0xaa, + 0x7d, 0x74, 0x8e, 0x8f, 0xad, 0x33, 0x62, 0xf3, 0xb9, 0xd8, 0x53, 0x1e, 0xc2, 0x95, 0x43, 0xa9, + 0x89, 0x6a, 0x4b, 0x51, 0x54, 0xa1, 0x82, 0xcb, 0x43, 0x4c, 0xad, 0xcf, 0x8e, 0x8d, 0x2f, 0x1e, + 0x4e, 0xe8, 0x57, 0xf6, 0xa5, 0x71, 0xfd, 0x78, 0xef, 0xc0, 0x39, 0x3f, 0x98, 0x91, 0x35, 0x37, + 0x9e, 0x0b, 0x7b, 0x5e, 0x11, 0xfb, 0xe5, 0x60, 0xb4, 0x6c, 0xc2, 0xf2, 0x48, 0x0e, 0xd2, 0x93, + 0x33, 0x30, 0x00, 0xa7, 0x03, 0x31, 0x48, 0x0f, 0xfd, 0x23, 0x79, 0xb4, 0x65, 0x51, 0x63, 0x84, + 0x5d, 0x88, 0x1c, 0x69, 0xb4, 0xc9, 0xd1, 0x9b, 0xb0, 0x1c, 0x42, 0x0f, 0xc4, 0x58, 0x1c, 0xf1, + 0x8e, 0xb1, 0x83, 0x68, 0x4a, 0x97, 0x62, 0x53, 0xaa, 0x6c, 0x40, 0x5a, 0x9e, 0x8b, 0xbf, 0x11, + 0x20, 0x32, 0x01, 0xc5, 0x4e, 0xc9, 0x7f, 0x29, 0x7c, 0x03, 0x97, 0x6d, 0x07, 0xfb, 0xc4, 0xf2, + 0x5c, 0x75, 0x62, 0xa6, 0x70, 0x29, 0xd2, 0x7c, 0xae, 0x64, 0x02, 0x4c, 0x33, 0x3c, 0x5f, 0x7c, + 0x35, 0xbe, 0x86, 0x4b, 0x3d, 0xcf, 0x64, 0xc4, 0x25, 0x6d, 0xf5, 0x98, 0x90, 0x6e, 0x66, 0xd9, + 0x4f, 0x43, 0xe3, 0x42, 0x00, 0x69, 0x46, 0xa4, 0x74, 0xf3, 0xaf, 0x93, 0x90, 0xe3, 0x85, 0xb7, + 0x6d, 0xf5, 0x91, 0x6a, 0x94, 0x35, 0x49, 0x9b, 0x6a, 0xcc, 0x73, 0xb0, 0x81, 0x3a, 0x92, 0x3e, + 0x1a, 0xca, 0xed, 0x29, 0xb3, 0x6e, 0xa4, 0x62, 0x74, 0xe4, 0x6d, 0xc1, 0xff, 0x74, 0xc9, 0x15, + 0xce, 0x68, 0xa4, 0xfc, 0x56, 0x03, 0xc4, 0x28, 0xa7, 0x4f, 0x60, 0x7d, 0x64, 0x35, 0x4e, 0x82, + 0x1b, 0x04, 0xc3, 0x29, 0x22, 0xe5, 0x78, 0x25, 0x80, 0x1f, 0x04, 0xe8, 0x51, 0xe4, 0x3e, 0xdf, + 0x0b, 0xd8, 0x18, 0xf1, 0x71, 0x55, 0x54, 0x1c, 0xd8, 0x1a, 0xf5, 0x4b, 0x77, 0x82, 0x35, 0x15, + 0x66, 0xcd, 0x07, 0x46, 0x5c, 0xa7, 0x4a, 0x60, 0x12, 0xa6, 0xce, 0x5b, 0x90, 0x8d, 0x28, 0xf6, + 0xcc, 0xb3, 0x1c, 0xaf, 0xd7, 0x40, 0x4d, 0xef, 0x9c, 0x5e, 0xad, 0x93, 0x34, 0x87, 0x5f, 0x12, + 0xb0, 0x79, 0xbc, 0x39, 0xd4, 0xa8, 0x6e, 0x7a, 0x7e, 0x70, 0x75, 0xc7, 0xb2, 0x0e, 0xff, 0x6b, + 0xb6, 0xc4, 0x5d, 0x72, 0x98, 0xda, 0x41, 0xd2, 0xee, 0xb0, 0x68, 0x08, 0x69, 0xbe, 0x55, 0xe5, + 0x3b, 0xca, 0x0d, 0x00, 0xa4, 0x46, 0x80, 0x8b, 0xe4, 0x62, 0x09, 0xa9, 0x21, 0x51, 0x91, 0xf3, + 0xa4, 0xe2, 0xcf, 0xf3, 0x6b, 0x42, 0xd6, 0x9c, 0x38, 0x8f, 0x38, 0x8e, 0x48, 0x23, 0x1a, 0x15, + 0xcd, 0x31, 0x87, 0x9f, 0xee, 0x14, 0x91, 0xf8, 0x66, 0xe3, 0x6f, 0xee, 0x97, 0x70, 0x61, 0xb2, + 0x64, 0x82, 0x20, 0xc4, 0x43, 0x94, 0x37, 0xd8, 0x71, 0x75, 0x88, 0x20, 0xf2, 0x34, 0xae, 0x85, + 0x57, 0x06, 0x36, 0x71, 0x3e, 0x4d, 0x59, 0x7c, 0x97, 0x94, 0x85, 0x78, 0x40, 0x71, 0x60, 0xa3, + 0xce, 0xd0, 0x38, 0x08, 0xf5, 0xa8, 0xd3, 0x5f, 0x5b, 0xd7, 0xf6, 0x13, 0x2c, 0x8e, 0x1e, 0x98, + 0x44, 0xaf, 0x2d, 0x47, 0xf0, 0xab, 0x21, 0xad, 0x4a, 0x90, 0x9d, 0xb4, 0x42, 0xcd, 0x9f, 0x04, + 0xdc, 0x38, 0xa2, 0xef, 0xf9, 0x88, 0x31, 0x47, 0x4d, 0xa1, 0x68, 0x99, 0x96, 0xde, 0x95, 0x53, + 0xcb, 0x17, 0xfc, 0x4c, 0x2c, 0x45, 0xd9, 0x47, 0xf1, 0xc9, 0x75, 0xeb, 0xa7, 0x04, 0x9c, 0x8f, + 0x1f, 0xd0, 0xca, 0x4d, 0xb8, 0xb6, 0x5b, 0x7b, 0x52, 0xda, 0xab, 0xed, 0xbf, 0x50, 0xeb, 0x8d, + 0xa7, 0xcf, 0x6b, 0x3b, 0x95, 0x86, 0xda, 0xdc, 0x2f, 0xed, 0x1f, 0x34, 0xd5, 0xda, 0x93, 0xd2, + 0xf6, 0x7e, 0xed, 0x79, 0x65, 0x75, 0x46, 0xb9, 0x0e, 0x57, 0xa7, 0xc2, 0x24, 0x28, 0xf1, 0x5e, + 0xd0, 0xa3, 0x52, 0x6d, 0xaf, 0xb2, 0xb3, 0x9a, 0x54, 0x6e, 0xc0, 0xfa, 0x54, 0x50, 0x73, 0xaf, + 0xd4, 0xac, 0x56, 0x76, 0x56, 0x67, 0xcb, 0xcf, 0x7e, 0x7e, 0x9b, 0x4b, 0xbc, 0x79, 0x9b, 0x4b, + 0xfc, 0xf9, 0x36, 0x97, 0x78, 0xf5, 0x2e, 0x37, 0xf3, 0xe6, 0x5d, 0x6e, 0xe6, 0xb7, 0x77, 0xb9, + 0x99, 0x6f, 0x4f, 0xf6, 0x66, 0x19, 0x84, 0x7f, 0xf3, 0xf2, 0x07, 0x4c, 0x6b, 0x9e, 0xff, 0xb2, + 0xbd, 0xff, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x05, 0xfa, 0x07, 0xf1, 0x67, 0x0f, 0x00, 0x00, } func (m *EventFinalityProviderCreated) Marshal() (dAtA []byte, err error) { @@ -1871,6 +1883,15 @@ func (m *EventBTCDelegationCreated) MarshalToSizedBuffer(dAtA []byte) (int, erro _ = i var l int _ = l + if len(m.MultisigStakerBtcPkHexs) > 0 { + for iNdEx := len(m.MultisigStakerBtcPkHexs) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.MultisigStakerBtcPkHexs[iNdEx]) + copy(dAtA[i:], m.MultisigStakerBtcPkHexs[iNdEx]) + i = encodeVarintEvents(dAtA, i, uint64(len(m.MultisigStakerBtcPkHexs[iNdEx]))) + i-- + dAtA[i] = 0x62 + } + } if len(m.PreviousStakingTxHashHex) > 0 { i -= len(m.PreviousStakingTxHashHex) copy(dAtA[i:], m.PreviousStakingTxHashHex) @@ -2515,6 +2536,12 @@ func (m *EventBTCDelegationCreated) Size() (n int) { if l > 0 { n += 1 + l + sovEvents(uint64(l)) } + if len(m.MultisigStakerBtcPkHexs) > 0 { + for _, s := range m.MultisigStakerBtcPkHexs { + l = len(s) + n += 1 + l + sovEvents(uint64(l)) + } + } return n } @@ -4364,6 +4391,38 @@ func (m *EventBTCDelegationCreated) Unmarshal(dAtA []byte) error { } m.PreviousStakingTxHashHex = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 12: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MultisigStakerBtcPkHexs", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvents + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvents + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvents + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MultisigStakerBtcPkHexs = append(m.MultisigStakerBtcPkHexs, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipEvents(dAtA[iNdEx:]) diff --git a/x/btcstaking/types/genesis_test.go b/x/btcstaking/types/genesis_test.go index 0ad203c8a..2b4cb2331 100644 --- a/x/btcstaking/types/genesis_test.go +++ b/x/btcstaking/types/genesis_test.go @@ -60,6 +60,8 @@ func TestGenesisState_Validate(t *testing.T) { MinCommissionRate: sdkmath.LegacyMustNewDecFromStr("0.5"), SlashingRate: sdkmath.LegacyMustNewDecFromStr("0.1"), UnbondingFeeSat: types.DefaultParams().UnbondingFeeSat, + MaxStakerQuorum: 2, + MaxStakerNum: 3, }, }, AllowedStakingTxHashes: txHashes, diff --git a/x/btcstaking/types/msg_test.go b/x/btcstaking/types/msg_test.go index 27d37d2ae..b25e91ace 100644 --- a/x/btcstaking/types/msg_test.go +++ b/x/btcstaking/types/msg_test.go @@ -303,11 +303,15 @@ func TestStructFieldConsistency(t *testing.T) { } } - // Reverse check: all fields in MsgBtcStakeExpand (except last two: PreviousStakingTxHash and FundingTx) must be in MsgCreateBTCDelegation + // Reverse check: all fields in MsgBtcStakeExpand (except unique two: PreviousStakingTxHash and FundingTx) must be in MsgCreateBTCDelegation var missingFromCreate []string - for i := 0; i < expandType.NumField()-2; i++ { + for i := 0; i < expandType.NumField(); i++ { expandField := expandType.Field(i) createField, ok := createType.FieldByName(expandField.Name) + // skip the two fields unique to MsgBtcStakeExpand + if expandField.Name == "PreviousStakingTxHash" || expandField.Name == "FundingTx" { + continue + } if !ok { missingFromCreate = append(missingFromCreate, expandField.Name) continue diff --git a/x/btcstaking/types/params.go b/x/btcstaking/types/params.go index f78087c91..09d895f76 100644 --- a/x/btcstaking/types/params.go +++ b/x/btcstaking/types/params.go @@ -84,6 +84,9 @@ func DefaultParams() Params { // Allow list can only be enabled by upgrade AllowListExpirationHeight: 0, BtcActivationHeight: 0, + // The default multisig scheme is 1-of-1 multisig, which is equivalent to single-sig + MaxStakerQuorum: 1, + MaxStakerNum: 1, } } @@ -198,6 +201,21 @@ func validateNoDustSlashingOutput(p *Params) error { return nil } +func validateMaxStakerQuorumAndNum(maxStakerQuorum, maxStakerNum uint32) error { + // if either is non-zero, both must be positive and satisfy quorum rules + if maxStakerQuorum == 0 { + return fmt.Errorf("max staker quorum has to be positive when max staker num is non-zero") + } + if maxStakerNum == 0 { + return fmt.Errorf("max staker num has to be positive when max staker quorum is non-zero") + } + if maxStakerQuorum*2 <= maxStakerNum { + return fmt.Errorf("max staker quorum size has to be more than 1/2 of the max staker num") + } + + return nil +} + // Validate validates the set of params func (p Params) Validate() error { if p.CovenantQuorum == 0 { @@ -243,6 +261,10 @@ func (p Params) Validate() error { return err } + if err := validateMaxStakerQuorumAndNum(p.MaxStakerQuorum, p.MaxStakerNum); err != nil { + return err + } + return nil } diff --git a/x/btcstaking/types/params.pb.go b/x/btcstaking/types/params.pb.go index cc94dfa92..e0045c234 100644 --- a/x/btcstaking/types/params.pb.go +++ b/x/btcstaking/types/params.pb.go @@ -78,6 +78,10 @@ type Params struct { // btc_activation_height is the btc height from which parameters are activated // (inclusive) BtcActivationHeight uint32 `protobuf:"varint,15,opt,name=btc_activation_height,json=btcActivationHeight,proto3" json:"btc_activation_height,omitempty"` + // max_staker_quorum is the max M from M-of-N multisig + MaxStakerQuorum uint32 `protobuf:"varint,16,opt,name=max_staker_quorum,json=maxStakerQuorum,proto3" json:"max_staker_quorum,omitempty"` + // max_staker_num is the max N from M-of-N multisig + MaxStakerNum uint32 `protobuf:"varint,17,opt,name=max_staker_num,json=maxStakerNum,proto3" json:"max_staker_num,omitempty"` } func (m *Params) Reset() { *m = Params{} } @@ -196,6 +200,20 @@ func (m *Params) GetBtcActivationHeight() uint32 { return 0 } +func (m *Params) GetMaxStakerQuorum() uint32 { + if m != nil { + return m.MaxStakerQuorum + } + return 0 +} + +func (m *Params) GetMaxStakerNum() uint32 { + if m != nil { + return m.MaxStakerNum + } + return 0 +} + // HeightVersionPair pairs a btc height with a version of the parameters type HeightVersionPair struct { // start_height is the height from which the parameters are activated @@ -367,53 +385,56 @@ func init() { } var fileDescriptor_8d1392776a3e15b9 = []byte{ - // 733 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x5f, 0x4f, 0xd3, 0x5c, - 0x18, 0x5f, 0x5f, 0xc6, 0x78, 0x39, 0x1b, 0xf0, 0x52, 0x20, 0x6f, 0x41, 0xdc, 0xe6, 0xbc, 0x70, - 0x31, 0xd2, 0x3a, 0xc0, 0x98, 0x48, 0xa2, 0x71, 0x20, 0x6a, 0xc4, 0x64, 0x74, 0x0b, 0x17, 0x5e, - 0xd8, 0x9c, 0x76, 0xc7, 0xee, 0x64, 0x6d, 0x4f, 0xed, 0x39, 0x9b, 0xdb, 0xb7, 0xf0, 0x52, 0xef, - 0xfc, 0x10, 0x7e, 0x08, 0x2e, 0x89, 0x57, 0x86, 0x0b, 0x62, 0xe0, 0x8b, 0x98, 0x9e, 0x73, 0xda, - 0x15, 0xc4, 0x84, 0xbb, 0x9d, 0xf3, 0xfb, 0xfb, 0xb4, 0xeb, 0x03, 0x6a, 0x36, 0xb4, 0xc7, 0x1e, - 0x09, 0x0c, 0x9b, 0x39, 0x94, 0xc1, 0x3e, 0x0e, 0x5c, 0x63, 0xd8, 0x30, 0x42, 0x18, 0x41, 0x9f, - 0xea, 0x61, 0x44, 0x18, 0x51, 0x57, 0x24, 0x47, 0x9f, 0x70, 0xf4, 0x61, 0x63, 0x6d, 0xd9, 0x25, - 0x2e, 0xe1, 0x0c, 0x23, 0xfe, 0x25, 0xc8, 0x6b, 0xab, 0x0e, 0xa1, 0x3e, 0xa1, 0x96, 0x00, 0xc4, - 0x41, 0x40, 0xb5, 0xaf, 0x33, 0xa0, 0xd0, 0xe2, 0xc6, 0xea, 0x7b, 0x50, 0x72, 0xc8, 0x10, 0x05, - 0x30, 0x60, 0x56, 0xd8, 0xa7, 0x9a, 0x52, 0x9d, 0xaa, 0x97, 0x9a, 0x3b, 0xa7, 0x67, 0x95, 0xc7, - 0x2e, 0x66, 0xbd, 0x81, 0xad, 0x3b, 0xc4, 0x37, 0x64, 0xae, 0x07, 0x6d, 0xba, 0x81, 0x49, 0x72, - 0x34, 0x86, 0xdb, 0x06, 0x1b, 0x87, 0x88, 0xea, 0xcd, 0xd7, 0xad, 0xad, 0xed, 0x87, 0xad, 0x81, - 0xfd, 0x06, 0x8d, 0xcd, 0x62, 0x62, 0xd8, 0xea, 0x53, 0xf5, 0x1e, 0x58, 0x48, 0xfd, 0x3f, 0x0e, - 0x48, 0x34, 0xf0, 0xb5, 0x7f, 0xaa, 0x4a, 0x7d, 0xce, 0x9c, 0x4f, 0xae, 0x0f, 0xf9, 0xad, 0xda, - 0x00, 0x2b, 0x3e, 0x0e, 0x2c, 0x39, 0x96, 0x35, 0x84, 0xde, 0x00, 0x59, 0x14, 0x32, 0x6d, 0xaa, - 0xaa, 0xd4, 0xa7, 0x4c, 0xd5, 0xc7, 0x41, 0x5b, 0x60, 0x47, 0x31, 0xd4, 0x86, 0x8c, 0x4b, 0xe0, - 0xe8, 0x1a, 0x49, 0x5e, 0x4a, 0xe0, 0xe8, 0xaa, 0xe4, 0x11, 0xf8, 0x3f, 0x9b, 0xc2, 0xb0, 0x8f, - 0x2c, 0xdb, 0x23, 0x4e, 0x9f, 0x6a, 0xd3, 0xbc, 0xd6, 0xf2, 0x24, 0xa7, 0x83, 0x7d, 0xd4, 0xe4, - 0x18, 0x97, 0x65, 0x92, 0xb2, 0xb2, 0x82, 0x94, 0xa5, 0x59, 0x19, 0xd9, 0x03, 0xa0, 0x52, 0x0f, - 0xd2, 0x5e, 0xac, 0x09, 0xfb, 0x16, 0x75, 0x22, 0x1c, 0x32, 0x6d, 0xa6, 0xaa, 0xd4, 0x4b, 0xe6, - 0x7f, 0x09, 0xd2, 0xea, 0xb7, 0xf9, 0xbd, 0xba, 0x2d, 0xbb, 0x25, 0x0a, 0x36, 0xb2, 0x3e, 0x20, - 0x31, 0xd0, 0xbf, 0x7c, 0xa0, 0xa5, 0xb8, 0x9b, 0x44, 0x3b, 0xa3, 0x7d, 0xc4, 0x27, 0x3a, 0x02, - 0x73, 0xa9, 0x22, 0x82, 0x0c, 0x69, 0xb3, 0x55, 0xa5, 0x3e, 0xdb, 0x6c, 0x1c, 0x9f, 0x55, 0x72, - 0xa7, 0x67, 0x95, 0x5b, 0xe2, 0xc5, 0xd3, 0x6e, 0x5f, 0xc7, 0xc4, 0xf0, 0x21, 0xeb, 0xe9, 0x07, - 0xc8, 0x85, 0xce, 0x78, 0x0f, 0x39, 0x3f, 0xbe, 0x6f, 0x00, 0xf9, 0xbf, 0xd8, 0x43, 0x8e, 0x59, - 0x4a, 0x7c, 0x4c, 0xc8, 0x90, 0xba, 0x09, 0x56, 0x06, 0x81, 0x4d, 0x82, 0xee, 0xd5, 0x81, 0x01, - 0x1f, 0x78, 0x29, 0x05, 0x33, 0xf3, 0xde, 0x07, 0x8b, 0x13, 0x4d, 0xd2, 0xbd, 0xc8, 0xbb, 0x2f, - 0xa4, 0x80, 0xec, 0xdd, 0x06, 0xf1, 0x38, 0x96, 0x43, 0x7c, 0x1f, 0x53, 0x8a, 0x49, 0x20, 0xda, - 0x97, 0x78, 0xfb, 0xbb, 0x37, 0x68, 0x6f, 0x2e, 0xfa, 0x38, 0xd8, 0x4d, 0xe5, 0xbc, 0xf4, 0x3e, - 0xa8, 0x76, 0x91, 0x87, 0x5c, 0xc8, 0x62, 0x43, 0x27, 0x42, 0xe2, 0x87, 0x0d, 0x29, 0xb2, 0x5c, - 0x48, 0xe3, 0x4e, 0xda, 0x5c, 0x55, 0xa9, 0xe7, 0xcd, 0xf5, 0x09, 0x6f, 0x57, 0xd2, 0x9a, 0x90, - 0xa2, 0x97, 0x90, 0xee, 0x23, 0xa4, 0x3e, 0x03, 0xeb, 0xd0, 0xf3, 0xc8, 0x27, 0xcb, 0xc3, 0x94, - 0x59, 0x68, 0x14, 0xe2, 0x48, 0x38, 0xf5, 0x10, 0x76, 0x7b, 0x4c, 0x9b, 0xe7, 0x1e, 0xab, 0x9c, - 0x73, 0x80, 0x29, 0x7b, 0x91, 0x32, 0x5e, 0x71, 0x42, 0xfc, 0xf4, 0x6c, 0xe6, 0x58, 0xd0, 0x61, - 0x78, 0x78, 0x49, 0xb9, 0x20, 0x9e, 0x9e, 0xcd, 0x9c, 0xe7, 0x29, 0x26, 0x34, 0x4f, 0xf2, 0x5f, - 0xbe, 0x55, 0x72, 0xb5, 0x16, 0x58, 0x14, 0xe7, 0x23, 0x14, 0xc5, 0x73, 0xb5, 0x20, 0x8e, 0xd4, - 0x3b, 0xa0, 0x44, 0x19, 0x8c, 0x58, 0xe2, 0xa2, 0xf0, 0xfc, 0x22, 0xbf, 0x93, 0x89, 0x1a, 0x98, - 0x19, 0x0a, 0x85, 0xfc, 0xc0, 0x92, 0x63, 0xad, 0x03, 0x54, 0xc1, 0xe9, 0x10, 0xe9, 0xf9, 0x16, - 0x86, 0xea, 0x53, 0x30, 0x1d, 0x42, 0x1c, 0x89, 0x2f, 0xbe, 0xb8, 0x59, 0xd7, 0xaf, 0xdd, 0x2d, - 0xfa, 0x1f, 0x5d, 0x4c, 0x21, 0xab, 0x21, 0x50, 0x6a, 0x33, 0x12, 0xa1, 0xae, 0x5c, 0x24, 0x99, - 0x7c, 0xe5, 0x52, 0xbe, 0xba, 0x03, 0x0a, 0x62, 0x8b, 0xf1, 0x62, 0xc5, 0xcd, 0xdb, 0x7f, 0x89, - 0x12, 0x46, 0xcd, 0x7c, 0xfc, 0xee, 0x4d, 0x29, 0x69, 0x1e, 0x1e, 0x9f, 0x97, 0x95, 0x93, 0xf3, - 0xb2, 0xf2, 0xeb, 0xbc, 0xac, 0x7c, 0xbe, 0x28, 0xe7, 0x4e, 0x2e, 0xca, 0xb9, 0x9f, 0x17, 0xe5, - 0xdc, 0xbb, 0x9b, 0xed, 0xa7, 0x51, 0x76, 0x9f, 0xf2, 0x65, 0x65, 0x17, 0xf8, 0x12, 0xdc, 0xfa, - 0x1d, 0x00, 0x00, 0xff, 0xff, 0x24, 0x66, 0xd0, 0xe3, 0x72, 0x05, 0x00, 0x00, + // 772 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0xc1, 0x6e, 0xeb, 0x44, + 0x14, 0x8d, 0x69, 0x9a, 0xd2, 0x89, 0xdb, 0x34, 0x6e, 0x2b, 0xdc, 0x52, 0x92, 0x10, 0x90, 0x88, + 0x2a, 0x6a, 0x93, 0xb6, 0x08, 0x89, 0x4a, 0x20, 0xd2, 0x52, 0x40, 0x14, 0x94, 0x3a, 0x51, 0x17, + 0x2c, 0xb0, 0xc6, 0xce, 0xe0, 0x8c, 0x62, 0x7b, 0x8c, 0x67, 0x1c, 0x92, 0xbf, 0x60, 0xc9, 0x92, + 0x8f, 0xe0, 0x23, 0xba, 0xac, 0x58, 0xa1, 0x2e, 0x2a, 0xd4, 0x2e, 0xdf, 0x4f, 0x3c, 0x79, 0x66, + 0xec, 0xb8, 0x7d, 0x7d, 0x52, 0x77, 0x9e, 0x39, 0xe7, 0xdc, 0x7b, 0xce, 0x78, 0xe6, 0x82, 0xb6, + 0x03, 0x9d, 0xb9, 0x4f, 0x42, 0xd3, 0x61, 0x2e, 0x65, 0x70, 0x82, 0x43, 0xcf, 0x9c, 0x76, 0xcd, + 0x08, 0xc6, 0x30, 0xa0, 0x46, 0x14, 0x13, 0x46, 0xb4, 0x6d, 0xc9, 0x31, 0x16, 0x1c, 0x63, 0xda, + 0xdd, 0xdd, 0xf2, 0x88, 0x47, 0x38, 0xc3, 0x4c, 0xbf, 0x04, 0x79, 0x77, 0xc7, 0x25, 0x34, 0x20, + 0xd4, 0x16, 0x80, 0x58, 0x08, 0xa8, 0xfd, 0x6a, 0x05, 0x54, 0xfa, 0xbc, 0xb0, 0xf6, 0x2b, 0x50, + 0x5d, 0x32, 0x45, 0x21, 0x0c, 0x99, 0x1d, 0x4d, 0xa8, 0xae, 0xb4, 0x96, 0x3a, 0x6a, 0xef, 0xe4, + 0xf6, 0xae, 0xf9, 0x85, 0x87, 0xd9, 0x38, 0x71, 0x0c, 0x97, 0x04, 0xa6, 0xec, 0xeb, 0x43, 0x87, + 0x1e, 0x60, 0x92, 0x2d, 0xcd, 0xe9, 0xb1, 0xc9, 0xe6, 0x11, 0xa2, 0x46, 0xef, 0x87, 0xfe, 0xd1, + 0xf1, 0x67, 0xfd, 0xc4, 0xf9, 0x11, 0xcd, 0xad, 0x6a, 0x56, 0xb0, 0x3f, 0xa1, 0xda, 0x27, 0xa0, + 0x96, 0xd7, 0xff, 0x3d, 0x21, 0x71, 0x12, 0xe8, 0xef, 0xb4, 0x94, 0xce, 0x9a, 0xb5, 0x9e, 0x6d, + 0x5f, 0xf2, 0x5d, 0xad, 0x0b, 0xb6, 0x03, 0x1c, 0xda, 0x32, 0x96, 0x3d, 0x85, 0x7e, 0x82, 0x6c, + 0x0a, 0x99, 0xbe, 0xd4, 0x52, 0x3a, 0x4b, 0x96, 0x16, 0xe0, 0x70, 0x20, 0xb0, 0xab, 0x14, 0x1a, + 0x40, 0xc6, 0x25, 0x70, 0xf6, 0x8c, 0xa4, 0x2c, 0x25, 0x70, 0xf6, 0x54, 0xf2, 0x39, 0x78, 0xaf, + 0xd8, 0x85, 0xe1, 0x00, 0xd9, 0x8e, 0x4f, 0xdc, 0x09, 0xd5, 0x97, 0xb9, 0xad, 0xad, 0x45, 0x9f, + 0x21, 0x0e, 0x50, 0x8f, 0x63, 0x5c, 0x56, 0xe8, 0x54, 0x94, 0x55, 0xa4, 0x2c, 0xef, 0x55, 0x90, + 0x7d, 0x0a, 0x34, 0xea, 0x43, 0x3a, 0x4e, 0x35, 0xd1, 0xc4, 0xa6, 0x6e, 0x8c, 0x23, 0xa6, 0xaf, + 0xb4, 0x94, 0x8e, 0x6a, 0x6d, 0x64, 0x48, 0x7f, 0x32, 0xe0, 0xfb, 0xda, 0xb1, 0xf4, 0x96, 0x29, + 0xd8, 0xcc, 0xfe, 0x0d, 0x89, 0x40, 0xef, 0xf2, 0x40, 0x9b, 0xa9, 0x37, 0x89, 0x0e, 0x67, 0xe7, + 0x88, 0x27, 0xba, 0x02, 0x6b, 0xb9, 0x22, 0x86, 0x0c, 0xe9, 0xab, 0x2d, 0xa5, 0xb3, 0xda, 0xeb, + 0x5e, 0xdf, 0x35, 0x4b, 0xb7, 0x77, 0xcd, 0xf7, 0xc5, 0x8f, 0xa7, 0xa3, 0x89, 0x81, 0x89, 0x19, + 0x40, 0x36, 0x36, 0x2e, 0x90, 0x07, 0xdd, 0xf9, 0x19, 0x72, 0xff, 0xfd, 0xe7, 0x00, 0xc8, 0x7b, + 0x71, 0x86, 0x5c, 0x4b, 0xcd, 0xea, 0x58, 0x90, 0x21, 0xed, 0x10, 0x6c, 0x27, 0xa1, 0x43, 0xc2, + 0xd1, 0xd3, 0xc0, 0x80, 0x07, 0xde, 0xcc, 0xc1, 0x42, 0xde, 0x7d, 0x50, 0x5f, 0x68, 0x32, 0xef, + 0x55, 0xee, 0xbd, 0x96, 0x03, 0xd2, 0xf7, 0x00, 0xa4, 0x71, 0x6c, 0x97, 0x04, 0x01, 0xa6, 0x14, + 0x93, 0x50, 0xb8, 0x57, 0xb9, 0xfb, 0x8f, 0x5e, 0xe0, 0xde, 0xaa, 0x07, 0x38, 0x3c, 0xcd, 0xe5, + 0xdc, 0xf4, 0x39, 0x68, 0x8d, 0x90, 0x8f, 0x3c, 0xc8, 0xd2, 0x82, 0x6e, 0x8c, 0xc4, 0x87, 0x03, + 0x29, 0xb2, 0x3d, 0x48, 0x53, 0x4f, 0xfa, 0x5a, 0x4b, 0xe9, 0x94, 0xad, 0xbd, 0x05, 0xef, 0x54, + 0xd2, 0x7a, 0x90, 0xa2, 0xef, 0x20, 0x3d, 0x47, 0x48, 0xfb, 0x1a, 0xec, 0x41, 0xdf, 0x27, 0x7f, + 0xd8, 0x3e, 0xa6, 0xcc, 0x46, 0xb3, 0x08, 0xc7, 0xa2, 0xd2, 0x18, 0x61, 0x6f, 0xcc, 0xf4, 0x75, + 0x5e, 0x63, 0x87, 0x73, 0x2e, 0x30, 0x65, 0xdf, 0xe6, 0x8c, 0xef, 0x39, 0x21, 0x3d, 0x3d, 0x87, + 0xb9, 0x36, 0x74, 0x19, 0x9e, 0x3e, 0x52, 0xd6, 0xc4, 0xe9, 0x39, 0xcc, 0xfd, 0x26, 0xc7, 0xa4, + 0x66, 0x1f, 0xd4, 0xb3, 0x4b, 0x86, 0xe2, 0xec, 0xb1, 0x6c, 0x70, 0x7e, 0x4d, 0x5e, 0x2f, 0x14, + 0xcb, 0xd7, 0xf2, 0x31, 0x58, 0x2f, 0x70, 0xc3, 0x24, 0xd0, 0xeb, 0x9c, 0xa8, 0xe6, 0xc4, 0x9f, + 0x93, 0xe0, 0xcb, 0xf2, 0x5f, 0x7f, 0x37, 0x4b, 0xed, 0x3e, 0xa8, 0x8b, 0x0e, 0x57, 0x28, 0x4e, + 0x4f, 0xaa, 0x0f, 0x71, 0xac, 0x7d, 0x08, 0x54, 0xca, 0x60, 0xcc, 0x32, 0x5f, 0x0a, 0x4f, 0x54, + 0xe5, 0x7b, 0xd2, 0x8f, 0x0e, 0x56, 0xa6, 0x42, 0x21, 0x9f, 0x6c, 0xb6, 0x6c, 0x0f, 0x81, 0x26, + 0x38, 0x43, 0x22, 0x6b, 0xfe, 0x04, 0x23, 0xed, 0x2b, 0xb0, 0x1c, 0x41, 0x1c, 0x8b, 0x19, 0x52, + 0x3d, 0xec, 0x18, 0xcf, 0x4e, 0x2b, 0xe3, 0x0d, 0x2f, 0x96, 0x90, 0xb5, 0x11, 0x50, 0x07, 0x8c, + 0xc4, 0x68, 0x24, 0x47, 0x53, 0xa1, 0xbf, 0xf2, 0xa8, 0xbf, 0x76, 0x02, 0x2a, 0x62, 0x2e, 0x72, + 0x63, 0xd5, 0xc3, 0x0f, 0xde, 0xd2, 0x4a, 0x14, 0xea, 0x95, 0xd3, 0xdb, 0x64, 0x49, 0x49, 0xef, + 0xf2, 0xfa, 0xbe, 0xa1, 0xdc, 0xdc, 0x37, 0x94, 0xff, 0xef, 0x1b, 0xca, 0x9f, 0x0f, 0x8d, 0xd2, + 0xcd, 0x43, 0xa3, 0xf4, 0xdf, 0x43, 0xa3, 0xf4, 0xcb, 0xcb, 0x26, 0xde, 0xac, 0x38, 0xa1, 0xf9, + 0xf8, 0x73, 0x2a, 0x7c, 0xac, 0x1e, 0xbd, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x0b, 0x54, 0xe9, 0x23, + 0xc4, 0x05, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -436,6 +457,20 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.MaxStakerNum != 0 { + i = encodeVarintParams(dAtA, i, uint64(m.MaxStakerNum)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x88 + } + if m.MaxStakerQuorum != 0 { + i = encodeVarintParams(dAtA, i, uint64(m.MaxStakerQuorum)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x80 + } if m.BtcActivationHeight != 0 { i = encodeVarintParams(dAtA, i, uint64(m.BtcActivationHeight)) i-- @@ -707,6 +742,12 @@ func (m *Params) Size() (n int) { if m.BtcActivationHeight != 0 { n += 1 + sovParams(uint64(m.BtcActivationHeight)) } + if m.MaxStakerQuorum != 0 { + n += 2 + sovParams(uint64(m.MaxStakerQuorum)) + } + if m.MaxStakerNum != 0 { + n += 2 + sovParams(uint64(m.MaxStakerNum)) + } return n } @@ -1135,6 +1176,44 @@ func (m *Params) Unmarshal(dAtA []byte) error { break } } + case 16: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxStakerQuorum", wireType) + } + m.MaxStakerQuorum = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxStakerQuorum |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 17: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxStakerNum", wireType) + } + m.MaxStakerNum = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxStakerNum |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipParams(dAtA[iNdEx:]) diff --git a/x/btcstaking/types/params_test.go b/x/btcstaking/types/params_test.go index 956559c9c..4a458efee 100644 --- a/x/btcstaking/types/params_test.go +++ b/x/btcstaking/types/params_test.go @@ -230,3 +230,100 @@ func TestDefaultParamsAreValid(t *testing.T) { params := types.DefaultParams() require.NoError(t, params.Validate()) } + +func TestParamsValidateMaxStakerQuorumAndNum(t *testing.T) { + testCases := []struct { + name string + modifyParams func(p *types.Params) + expectedError string + }{ + { + name: "valid 2-of-3 multisig (default)", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 2 + p.MaxStakerNum = 3 + }, + expectedError: "", + }, + { + name: "valid 3-of-5 multisig", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 3 + p.MaxStakerNum = 5 + }, + expectedError: "", + }, + { + name: "valid 5-of-9 multisig", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 5 + p.MaxStakerNum = 9 + }, + expectedError: "", + }, + { + name: "invalid: max staker quorum and num both zero", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 0 + p.MaxStakerNum = 0 + }, + expectedError: "max staker quorum has to be positive when max staker num is non-zero", + }, + { + name: "invalid: quorum is 0 but num is non-zero", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 0 + p.MaxStakerNum = 3 + }, + expectedError: "max staker quorum has to be positive when max staker num is non-zero", + }, + { + name: "invalid: num is 0 but quorum is non-zero", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 2 + p.MaxStakerNum = 0 + }, + expectedError: "max staker num has to be positive when max staker quorum is non-zero", + }, + { + name: "invalid: quorum not more than 1/2", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 2 + p.MaxStakerNum = 4 + }, + expectedError: "max staker quorum size has to be more than 1/2 of the max staker num", + }, + { + name: "invalid: quorum equals num/2", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 3 + p.MaxStakerNum = 6 + }, + expectedError: "max staker quorum size has to be more than 1/2 of the max staker num", + }, + { + name: "invalid: quorum less than num/2", + modifyParams: func(p *types.Params) { + p.MaxStakerQuorum = 1 + p.MaxStakerNum = 5 + }, + expectedError: "max staker quorum size has to be more than 1/2 of the max staker num", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + params := types.DefaultParams() + tc.modifyParams(¶ms) + + err := params.Validate() + + if tc.expectedError == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedError) + } + }) + } +} diff --git a/x/btcstaking/types/query.go b/x/btcstaking/types/query.go index e64e69ee5..32c13f617 100644 --- a/x/btcstaking/types/query.go +++ b/x/btcstaking/types/query.go @@ -49,6 +49,10 @@ func NewBTCDelegationResponse(btcDel *BTCDelegation, status BTCDelegationStatus) resp.StkExp = btcDel.StkExp.ToResponse() } + if btcDel.IsMultisigBtcDel() { + resp.MultisigInfo = btcDel.MultisigInfo.ToResponse() + } + return resp } diff --git a/x/btcstaking/types/query.pb.go b/x/btcstaking/types/query.pb.go index 7ca1aa0d4..285400649 100644 --- a/x/btcstaking/types/query.pb.go +++ b/x/btcstaking/types/query.pb.go @@ -876,6 +876,9 @@ type BTCDelegationResponse struct { ParamsVersion uint32 `protobuf:"varint,17,opt,name=params_version,json=paramsVersion,proto3" json:"params_version,omitempty"` // stk_exp contains the stake expansion information, if nil it is NOT a stake expansion. StkExp *StakeExpansionResponse `protobuf:"bytes,18,opt,name=stk_exp,json=stkExp,proto3" json:"stk_exp,omitempty"` + // multisig_info is used when the given btc delegation is M-of-N multisig. + // If nil, it is not a M-of-N multisig. + MultisigInfo *AdditionalStakerInfoResponse `protobuf:"bytes,19,opt,name=multisig_info,json=multisigInfo,proto3" json:"multisig_info,omitempty"` } func (m *BTCDelegationResponse) Reset() { *m = BTCDelegationResponse{} } @@ -1023,6 +1026,87 @@ func (m *BTCDelegationResponse) GetStkExp() *StakeExpansionResponse { return nil } +func (m *BTCDelegationResponse) GetMultisigInfo() *AdditionalStakerInfoResponse { + if m != nil { + return m.MultisigInfo + } + return nil +} + +// AdditionalStakerInfoResponse provides multisig info for the given btc staker +// NOTE: this structure doesn't contain original btc staker's signature, i.e., length of +// delegator_slashing_sigs and delegator_unbonding_slashing_sigs is M-1, and the length of +// staker_btc_pk_list is N-1 +type AdditionalStakerInfoResponse struct { + // staker_btc_pk_list is the list of pubkeys of the btc staker that is using M-of-N multisig + // length of staker_btc_pk_list is N-1 + StakerBtcPkList []github_com_babylonlabs_io_babylon_v4_types.BIP340PubKey `protobuf:"bytes,1,rep,name=staker_btc_pk_list,json=stakerBtcPkList,proto3,customtype=github.com/babylonlabs-io/babylon/v4/types.BIP340PubKey" json:"staker_btc_pk_list,omitempty"` + // staker_quorum is threshold of M-of-N multisig, which value itself represent M + StakerQuorum uint32 `protobuf:"varint,2,opt,name=staker_quorum,json=stakerQuorum,proto3" json:"staker_quorum,omitempty"` + // delegator_slashing_sigs is the (btc_pk, signature) pair on the slashing tx + // by delegators (i.e., SK corresponding to btc_pk). + // It will be a part of the witness for the staking tx output. + DelegatorSlashingSigs []*SignatureInfo `protobuf:"bytes,3,rep,name=delegator_slashing_sigs,json=delegatorSlashingSigs,proto3" json:"delegator_slashing_sigs,omitempty"` + // delegator_unbonding_slashing_sigs is the (btc_pk, signature) pair on the slashing tx + // by delegators (i.e., SK corresponding to btc_pk). + // It will be a part of the witness for the unbonding tx output. + DelegatorUnbondingSlashingSigs []*SignatureInfo `protobuf:"bytes,4,rep,name=delegator_unbonding_slashing_sigs,json=delegatorUnbondingSlashingSigs,proto3" json:"delegator_unbonding_slashing_sigs,omitempty"` +} + +func (m *AdditionalStakerInfoResponse) Reset() { *m = AdditionalStakerInfoResponse{} } +func (m *AdditionalStakerInfoResponse) String() string { return proto.CompactTextString(m) } +func (*AdditionalStakerInfoResponse) ProtoMessage() {} +func (*AdditionalStakerInfoResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_74d49d26f7429697, []int{17} +} +func (m *AdditionalStakerInfoResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *AdditionalStakerInfoResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_AdditionalStakerInfoResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *AdditionalStakerInfoResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_AdditionalStakerInfoResponse.Merge(m, src) +} +func (m *AdditionalStakerInfoResponse) XXX_Size() int { + return m.Size() +} +func (m *AdditionalStakerInfoResponse) XXX_DiscardUnknown() { + xxx_messageInfo_AdditionalStakerInfoResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_AdditionalStakerInfoResponse proto.InternalMessageInfo + +func (m *AdditionalStakerInfoResponse) GetStakerQuorum() uint32 { + if m != nil { + return m.StakerQuorum + } + return 0 +} + +func (m *AdditionalStakerInfoResponse) GetDelegatorSlashingSigs() []*SignatureInfo { + if m != nil { + return m.DelegatorSlashingSigs + } + return nil +} + +func (m *AdditionalStakerInfoResponse) GetDelegatorUnbondingSlashingSigs() []*SignatureInfo { + if m != nil { + return m.DelegatorUnbondingSlashingSigs + } + return nil +} + // StakeExpansionResponse stores information necessary to construct the expanded BTC staking // transaction created from a previous BTC staking. type StakeExpansionResponse struct { @@ -1047,7 +1131,7 @@ func (m *StakeExpansionResponse) Reset() { *m = StakeExpansionResponse{} func (m *StakeExpansionResponse) String() string { return proto.CompactTextString(m) } func (*StakeExpansionResponse) ProtoMessage() {} func (*StakeExpansionResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{17} + return fileDescriptor_74d49d26f7429697, []int{18} } func (m *StakeExpansionResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1109,7 +1193,7 @@ func (m *DelegatorUnbondingInfoResponse) Reset() { *m = DelegatorUnbondi func (m *DelegatorUnbondingInfoResponse) String() string { return proto.CompactTextString(m) } func (*DelegatorUnbondingInfoResponse) ProtoMessage() {} func (*DelegatorUnbondingInfoResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{18} + return fileDescriptor_74d49d26f7429697, []int{19} } func (m *DelegatorUnbondingInfoResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1174,7 +1258,7 @@ func (m *BTCUndelegationResponse) Reset() { *m = BTCUndelegationResponse func (m *BTCUndelegationResponse) String() string { return proto.CompactTextString(m) } func (*BTCUndelegationResponse) ProtoMessage() {} func (*BTCUndelegationResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{19} + return fileDescriptor_74d49d26f7429697, []int{20} } func (m *BTCUndelegationResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1254,7 +1338,7 @@ func (m *BTCDelegatorDelegationsResponse) Reset() { *m = BTCDelegatorDel func (m *BTCDelegatorDelegationsResponse) String() string { return proto.CompactTextString(m) } func (*BTCDelegatorDelegationsResponse) ProtoMessage() {} func (*BTCDelegatorDelegationsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{20} + return fileDescriptor_74d49d26f7429697, []int{21} } func (m *BTCDelegatorDelegationsResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1330,7 +1414,7 @@ func (m *FinalityProviderResponse) Reset() { *m = FinalityProviderRespon func (m *FinalityProviderResponse) String() string { return proto.CompactTextString(m) } func (*FinalityProviderResponse) ProtoMessage() {} func (*FinalityProviderResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{21} + return fileDescriptor_74d49d26f7429697, []int{22} } func (m *FinalityProviderResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1437,7 +1521,7 @@ func (m *QueryLargestBtcReOrgRequest) Reset() { *m = QueryLargestBtcReOr func (m *QueryLargestBtcReOrgRequest) String() string { return proto.CompactTextString(m) } func (*QueryLargestBtcReOrgRequest) ProtoMessage() {} func (*QueryLargestBtcReOrgRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{22} + return fileDescriptor_74d49d26f7429697, []int{23} } func (m *QueryLargestBtcReOrgRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1481,7 +1565,7 @@ func (m *QueryLargestBtcReOrgResponse) Reset() { *m = QueryLargestBtcReO func (m *QueryLargestBtcReOrgResponse) String() string { return proto.CompactTextString(m) } func (*QueryLargestBtcReOrgResponse) ProtoMessage() {} func (*QueryLargestBtcReOrgResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{23} + return fileDescriptor_74d49d26f7429697, []int{24} } func (m *QueryLargestBtcReOrgResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1542,7 +1626,7 @@ func (m *QueryParamsVersionsRequest) Reset() { *m = QueryParamsVersionsR func (m *QueryParamsVersionsRequest) String() string { return proto.CompactTextString(m) } func (*QueryParamsVersionsRequest) ProtoMessage() {} func (*QueryParamsVersionsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{24} + return fileDescriptor_74d49d26f7429697, []int{25} } func (m *QueryParamsVersionsRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1583,7 +1667,7 @@ func (m *QueryParamsVersionsResponse) Reset() { *m = QueryParamsVersions func (m *QueryParamsVersionsResponse) String() string { return proto.CompactTextString(m) } func (*QueryParamsVersionsResponse) ProtoMessage() {} func (*QueryParamsVersionsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_74d49d26f7429697, []int{25} + return fileDescriptor_74d49d26f7429697, []int{26} } func (m *QueryParamsVersionsResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1630,6 +1714,7 @@ func init() { proto.RegisterType((*QueryBTCDelegationRequest)(nil), "babylon.btcstaking.v1.QueryBTCDelegationRequest") proto.RegisterType((*QueryBTCDelegationResponse)(nil), "babylon.btcstaking.v1.QueryBTCDelegationResponse") proto.RegisterType((*BTCDelegationResponse)(nil), "babylon.btcstaking.v1.BTCDelegationResponse") + proto.RegisterType((*AdditionalStakerInfoResponse)(nil), "babylon.btcstaking.v1.AdditionalStakerInfoResponse") proto.RegisterType((*StakeExpansionResponse)(nil), "babylon.btcstaking.v1.StakeExpansionResponse") proto.RegisterType((*DelegatorUnbondingInfoResponse)(nil), "babylon.btcstaking.v1.DelegatorUnbondingInfoResponse") proto.RegisterType((*BTCUndelegationResponse)(nil), "babylon.btcstaking.v1.BTCUndelegationResponse") @@ -1644,143 +1729,149 @@ func init() { func init() { proto.RegisterFile("babylon/btcstaking/v1/query.proto", fileDescriptor_74d49d26f7429697) } var fileDescriptor_74d49d26f7429697 = []byte{ - // 2164 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x59, 0xcf, 0x6f, 0xdb, 0xc8, - 0xf5, 0x37, 0x6d, 0xc7, 0xb1, 0x9f, 0x6c, 0xd9, 0x99, 0x75, 0x12, 0xc6, 0x89, 0x7f, 0x84, 0xdf, - 0xfc, 0x70, 0x7e, 0x58, 0xb4, 0x1d, 0x67, 0x83, 0x6f, 0x83, 0xa4, 0x8d, 0xec, 0x78, 0x93, 0xdd, - 0xa4, 0x71, 0x28, 0x67, 0x0f, 0x6d, 0x51, 0x96, 0x22, 0x47, 0x14, 0x2b, 0x89, 0xc3, 0x70, 0x46, - 0xae, 0x8c, 0xc0, 0x40, 0xd1, 0x43, 0xd1, 0x63, 0x81, 0xf6, 0x0f, 0xd8, 0x5b, 0x0b, 0xb4, 0x87, - 0x05, 0x76, 0x2f, 0x3d, 0x14, 0x3d, 0xf4, 0x92, 0xde, 0x16, 0xe9, 0xa5, 0xd8, 0x43, 0x50, 0x24, - 0x05, 0xb6, 0x97, 0x9e, 0x7a, 0xe9, 0xb1, 0xe0, 0x70, 0x28, 0x52, 0x32, 0x29, 0x5b, 0xae, 0x2f, - 0x86, 0x67, 0xe6, 0xfd, 0x9e, 0xcf, 0x7b, 0xf3, 0xf8, 0x04, 0x17, 0xcb, 0x46, 0x79, 0xb7, 0x4e, - 0x5c, 0xb5, 0xcc, 0x4c, 0xca, 0x8c, 0x9a, 0xe3, 0xda, 0xea, 0xce, 0x8a, 0xfa, 0xb2, 0x89, 0xfd, - 0xdd, 0x82, 0xe7, 0x13, 0x46, 0xd0, 0x69, 0x41, 0x52, 0x88, 0x49, 0x0a, 0x3b, 0x2b, 0x33, 0xd3, - 0x36, 0xb1, 0x09, 0xa7, 0x50, 0x83, 0xff, 0x42, 0xe2, 0x99, 0x0b, 0x36, 0x21, 0x76, 0x1d, 0xab, - 0x86, 0xe7, 0xa8, 0x86, 0xeb, 0x12, 0x66, 0x30, 0x87, 0xb8, 0x54, 0x9c, 0x9e, 0x33, 0x09, 0x6d, - 0x10, 0xaa, 0x87, 0x6c, 0xe1, 0x42, 0x1c, 0x5d, 0x0a, 0x57, 0x6a, 0x6c, 0x44, 0x19, 0x33, 0x63, - 0x25, 0x5a, 0x0b, 0xaa, 0xeb, 0x82, 0xaa, 0x6c, 0x50, 0x1c, 0x1a, 0xd9, 0x26, 0xf4, 0x0c, 0xdb, - 0x71, 0xb9, 0x36, 0x41, 0xab, 0xa4, 0xbb, 0xe6, 0x19, 0xbe, 0xd1, 0x88, 0xb4, 0x5e, 0x49, 0xa7, - 0x49, 0x78, 0x1a, 0xd2, 0xcd, 0x67, 0xc8, 0x22, 0x9e, 0x20, 0xb8, 0x9c, 0x20, 0xa8, 0x3b, 0x76, - 0x35, 0xf8, 0x8b, 0x5d, 0xd6, 0x15, 0x4b, 0x65, 0x1a, 0xd0, 0xf3, 0x60, 0xb9, 0xc5, 0x8d, 0xd0, - 0xf0, 0xcb, 0x26, 0xa6, 0x4c, 0xd1, 0xe0, 0x83, 0x8e, 0x5d, 0xea, 0x11, 0x97, 0x62, 0x74, 0x17, - 0x46, 0x42, 0x63, 0x65, 0x69, 0x41, 0x5a, 0xcc, 0xad, 0xce, 0x16, 0x52, 0x6f, 0xa2, 0x10, 0xb2, - 0x15, 0x87, 0x5f, 0xbf, 0x9d, 0x1f, 0xd0, 0x04, 0x8b, 0x72, 0x07, 0xce, 0x27, 0x64, 0x16, 0x77, - 0x3f, 0xc5, 0x3e, 0x75, 0x88, 0x2b, 0x54, 0x22, 0x19, 0x4e, 0xee, 0x84, 0x3b, 0x5c, 0xf8, 0x84, - 0x16, 0x2d, 0x95, 0xef, 0xc3, 0x85, 0x74, 0xc6, 0xe3, 0xb0, 0xea, 0x3e, 0xcc, 0x76, 0x08, 0x2f, - 0x6e, 0xaf, 0x3f, 0xc2, 0x41, 0xb8, 0x22, 0xbb, 0x66, 0x01, 0xca, 0xcc, 0xd4, 0xab, 0x7c, 0x53, - 0x98, 0x36, 0x56, 0x66, 0x66, 0x48, 0xa5, 0xfc, 0x04, 0xe6, 0xb2, 0xf8, 0x8f, 0xc1, 0xbc, 0x64, - 0x54, 0x06, 0x3b, 0xa3, 0x62, 0x0b, 0xc3, 0x37, 0x1d, 0xd7, 0xa8, 0x3b, 0x6c, 0x77, 0xcb, 0x27, - 0x3b, 0x8e, 0x85, 0xfd, 0xe8, 0x0e, 0xd1, 0x26, 0x40, 0x8c, 0x40, 0xa1, 0xfb, 0x4a, 0x41, 0x40, - 0x3c, 0x80, 0x6b, 0x21, 0xc4, 0x81, 0x80, 0x6b, 0x61, 0xcb, 0xb0, 0xb1, 0xe0, 0xd5, 0x12, 0x9c, - 0xca, 0x5f, 0x24, 0xe1, 0x62, 0x8a, 0x26, 0xe1, 0xe2, 0x0f, 0x01, 0x55, 0xc4, 0x61, 0x90, 0x49, - 0xe1, 0xa9, 0x2c, 0x2d, 0x0c, 0x2d, 0xe6, 0x56, 0xd5, 0x0c, 0x77, 0xbb, 0xa5, 0x45, 0xc2, 0xb4, - 0x53, 0x95, 0x6e, 0x3d, 0xe8, 0xa3, 0x0e, 0x57, 0x06, 0xb9, 0x2b, 0x57, 0x0f, 0x74, 0x45, 0xc8, - 0x4b, 0xfa, 0xf2, 0x40, 0x40, 0x69, 0xbf, 0xf2, 0x30, 0x66, 0x17, 0x61, 0xa2, 0xe2, 0xe9, 0xc1, - 0x7d, 0x7b, 0x35, 0xbd, 0x8a, 0x5b, 0x3c, 0x6c, 0x63, 0x1a, 0x54, 0xbc, 0x22, 0x33, 0xb7, 0x6a, - 0x8f, 0x70, 0x4b, 0xd9, 0xcb, 0x88, 0x7b, 0x3b, 0x18, 0x3f, 0x80, 0x53, 0xfb, 0x82, 0x21, 0xc2, - 0xdf, 0x77, 0x2c, 0xa6, 0xba, 0x63, 0xa1, 0xfc, 0x56, 0x82, 0x19, 0xae, 0xbf, 0xb8, 0xbd, 0xbe, - 0x81, 0xeb, 0xd8, 0x0e, 0xcb, 0x59, 0xe4, 0x40, 0x11, 0x46, 0x28, 0x33, 0x58, 0x33, 0x04, 0x5b, - 0x7e, 0xf5, 0x7a, 0x86, 0xc6, 0x0e, 0xee, 0x12, 0xe7, 0xd0, 0x04, 0x67, 0x17, 0x70, 0x06, 0x8f, - 0x0c, 0x9c, 0x3f, 0x4a, 0x22, 0xe3, 0xbb, 0x4d, 0x15, 0x81, 0x7a, 0x01, 0x93, 0x41, 0xa4, 0xad, - 0xf8, 0x48, 0x40, 0xe6, 0xe6, 0x61, 0x8c, 0x6e, 0xc7, 0x28, 0x5f, 0x66, 0x66, 0x42, 0xfc, 0xf1, - 0x81, 0xe5, 0xd7, 0x12, 0x5c, 0x4d, 0xbd, 0xea, 0x94, 0xb8, 0x1f, 0x0c, 0x9c, 0x63, 0x0b, 0xeb, - 0x37, 0x12, 0x2c, 0x1e, 0x6c, 0x96, 0x88, 0xb1, 0x0f, 0xe7, 0x12, 0x31, 0x26, 0x7e, 0x4a, 0xb4, - 0x3f, 0x3c, 0x30, 0xda, 0x24, 0x4d, 0xb4, 0x76, 0x36, 0x8e, 0x7b, 0x07, 0xc1, 0xf1, 0x5d, 0xc0, - 0xc7, 0x70, 0x6e, 0x3f, 0x7e, 0xa2, 0x88, 0x2f, 0xc1, 0x07, 0xc2, 0x58, 0x9d, 0xb5, 0xf4, 0xaa, - 0x41, 0xab, 0x89, 0xb8, 0x4f, 0x89, 0xa3, 0xed, 0xd6, 0x23, 0x83, 0x56, 0x83, 0xb4, 0x7d, 0x99, - 0x96, 0x36, 0xed, 0x30, 0x95, 0x20, 0xdf, 0x09, 0x45, 0x91, 0xb0, 0xfd, 0x21, 0x71, 0xa2, 0x03, - 0x89, 0xca, 0xeb, 0x93, 0x70, 0x3a, 0x5d, 0xdd, 0xff, 0x43, 0x2e, 0x10, 0x86, 0x7d, 0xdd, 0xb0, - 0xac, 0xb0, 0x38, 0x8c, 0x15, 0xe5, 0x37, 0x5f, 0x2e, 0x4d, 0x8b, 0x28, 0x3d, 0xb0, 0x2c, 0x1f, - 0x53, 0x5a, 0x62, 0xbe, 0xe3, 0xda, 0x1a, 0x84, 0xc4, 0xc1, 0x26, 0xd2, 0x60, 0x24, 0x44, 0x19, - 0x0f, 0xec, 0x78, 0xf1, 0xee, 0xd7, 0x6f, 0xe7, 0xef, 0xd8, 0x0e, 0xab, 0x36, 0xcb, 0x05, 0x93, - 0x34, 0x54, 0x61, 0x6f, 0xdd, 0x28, 0xd3, 0x25, 0x87, 0x44, 0x4b, 0x75, 0x67, 0x4d, 0x65, 0xbb, - 0x1e, 0xa6, 0x85, 0xe2, 0xe3, 0xad, 0x5b, 0x6b, 0xcb, 0x5b, 0xcd, 0xf2, 0x27, 0x78, 0x57, 0x3b, - 0x51, 0x0e, 0xc0, 0x89, 0x7e, 0x04, 0xf9, 0x18, 0xbc, 0x75, 0x87, 0x32, 0x79, 0x68, 0x61, 0xe8, - 0x7f, 0x95, 0x9d, 0x13, 0xd0, 0x7f, 0xe2, 0xf0, 0xf4, 0x18, 0x6f, 0x5f, 0x96, 0xd3, 0xc0, 0xf2, - 0x30, 0x7f, 0xcb, 0x72, 0xd1, 0x2d, 0x39, 0x0d, 0x2c, 0x48, 0x7c, 0x16, 0xbd, 0xb4, 0x27, 0xda, - 0x24, 0x3e, 0x0b, 0x5f, 0xd4, 0xe0, 0x29, 0xc6, 0xae, 0x15, 0x11, 0x8c, 0x84, 0x4f, 0x31, 0x76, - 0x2d, 0x71, 0x7c, 0x1e, 0xc6, 0x18, 0x61, 0x46, 0x5d, 0xa7, 0x06, 0x93, 0x4f, 0x2e, 0x48, 0x8b, - 0xc3, 0xda, 0x28, 0xdf, 0x28, 0x19, 0x0c, 0x5d, 0x82, 0x7c, 0x12, 0x2e, 0xb8, 0x25, 0x8f, 0x72, - 0xa4, 0x8c, 0xc7, 0x48, 0xc1, 0x2d, 0x74, 0x05, 0x26, 0x69, 0xdd, 0xa0, 0xd5, 0x04, 0xd9, 0x18, - 0x27, 0x9b, 0x88, 0xb6, 0x43, 0xba, 0xdb, 0x70, 0x36, 0x4e, 0x29, 0x7e, 0xa4, 0x53, 0xc7, 0xe6, - 0xf4, 0xc0, 0xe9, 0xa7, 0xdb, 0xc7, 0xa5, 0xe0, 0xb4, 0xe4, 0xd8, 0x01, 0xdb, 0x0b, 0x98, 0x30, - 0xc9, 0x0e, 0x76, 0x0d, 0x97, 0x05, 0xf4, 0x54, 0xce, 0xf1, 0x0c, 0x5c, 0xce, 0x40, 0xd9, 0xba, - 0xa0, 0x7d, 0x60, 0x19, 0x5e, 0x20, 0xc9, 0xb1, 0x5d, 0x83, 0x35, 0x7d, 0x4c, 0xb5, 0xf1, 0x48, - 0x4c, 0xc9, 0xb1, 0x29, 0xba, 0x09, 0x28, 0xf2, 0x8d, 0x34, 0x99, 0xd7, 0x64, 0xba, 0x63, 0xb5, - 0xe4, 0x71, 0x1e, 0x9f, 0x28, 0x13, 0x9e, 0xf1, 0x83, 0xc7, 0x56, 0x0b, 0x9d, 0x81, 0x11, 0xc3, - 0x64, 0xce, 0x0e, 0x96, 0x27, 0x16, 0xa4, 0xc5, 0x51, 0x4d, 0xac, 0xd0, 0x3c, 0x07, 0x25, 0x6b, - 0x52, 0xdd, 0xc2, 0xd4, 0x94, 0xf3, 0x61, 0x01, 0x0b, 0xb7, 0x36, 0x30, 0x35, 0xd1, 0x65, 0xc8, - 0x37, 0xdd, 0x32, 0x71, 0xad, 0xf6, 0x35, 0x4e, 0x72, 0x15, 0x13, 0xed, 0x5d, 0x7e, 0x91, 0x26, - 0x9c, 0x6e, 0xba, 0x71, 0x26, 0xe9, 0xbe, 0x40, 0xbd, 0x3c, 0xc5, 0x53, 0xaa, 0x90, 0x9d, 0x52, - 0x2f, 0x12, 0x6c, 0xed, 0xa4, 0x9a, 0x6e, 0xa6, 0xec, 0x06, 0xb6, 0x84, 0x1d, 0x92, 0x1e, 0xb5, - 0x47, 0xa7, 0x42, 0x5b, 0xc2, 0x5d, 0xd1, 0x22, 0xa2, 0x4d, 0x38, 0x49, 0x59, 0x4d, 0xc7, 0x2d, - 0x4f, 0x46, 0x5c, 0xfb, 0x52, 0x86, 0xf6, 0x52, 0x90, 0x61, 0x0f, 0x5b, 0x9e, 0xe1, 0x26, 0x5b, - 0xcb, 0xe0, 0x49, 0xac, 0x3d, 0x6c, 0x79, 0xca, 0xbf, 0x25, 0x38, 0x93, 0x4e, 0x82, 0xee, 0xc3, - 0x05, 0xcf, 0xc7, 0x3b, 0x0e, 0x69, 0x52, 0x3d, 0xbb, 0x20, 0xc9, 0x11, 0x4d, 0xa9, 0xab, 0x30, - 0xa1, 0x0f, 0x41, 0x26, 0xac, 0x8a, 0x7d, 0xbd, 0xd2, 0x14, 0x91, 0x6d, 0x05, 0xb7, 0xc8, 0x79, - 0x07, 0x43, 0x2c, 0xf1, 0xf3, 0xcd, 0xf0, 0x78, 0xbb, 0xf5, 0xac, 0xc9, 0x02, 0x3e, 0x03, 0x66, - 0x12, 0x7a, 0x6b, 0x7a, 0x27, 0xb0, 0x86, 0x38, 0xb0, 0x2e, 0x65, 0x79, 0x1b, 0x21, 0xe9, 0xb1, - 0x5b, 0x21, 0xda, 0xd9, 0xd8, 0xb6, 0xda, 0x7a, 0x02, 0x57, 0xca, 0x53, 0x98, 0x6b, 0x17, 0xf8, - 0x17, 0xd1, 0x1d, 0x73, 0x96, 0xc8, 0xf9, 0x1b, 0x80, 0xa8, 0x17, 0xe4, 0x24, 0xaf, 0x50, 0x51, - 0xca, 0x84, 0x2e, 0x4f, 0xf2, 0x13, 0x1e, 0x35, 0x9e, 0x34, 0xca, 0x7f, 0x86, 0xe0, 0x6c, 0xc6, - 0x2d, 0xa3, 0x45, 0x98, 0x4a, 0x60, 0x2b, 0x29, 0x26, 0xc6, 0x5c, 0x98, 0x7a, 0x26, 0x9c, 0x6f, - 0xbb, 0x1a, 0xb3, 0x04, 0xd9, 0xc7, 0x2b, 0xd7, 0x60, 0x1f, 0x8e, 0xcb, 0x91, 0xa0, 0xb6, 0x73, - 0x25, 0xc7, 0xe6, 0xf5, 0x2a, 0xa5, 0x0e, 0x0c, 0xa5, 0xd5, 0x81, 0xbb, 0x30, 0xd3, 0x55, 0x07, - 0x22, 0x63, 0x02, 0x96, 0x61, 0xce, 0x72, 0xb6, 0xb3, 0x14, 0x84, 0x5a, 0x02, 0xe6, 0x0a, 0x9c, - 0x89, 0x2f, 0x2d, 0xc1, 0x4b, 0xe5, 0x13, 0x47, 0x2c, 0x0b, 0xd3, 0xed, 0xb2, 0x10, 0x6b, 0xa2, - 0xe8, 0xa7, 0x12, 0x5c, 0x8c, 0xad, 0x8c, 0x63, 0xe6, 0xb8, 0x15, 0x12, 0x67, 0xe7, 0x08, 0xcf, - 0x8f, 0xdb, 0x19, 0x3a, 0x7b, 0xe3, 0x40, 0x9b, 0xb3, 0x7a, 0x9e, 0x2b, 0x26, 0xcc, 0x1f, 0xd0, - 0x4e, 0xa0, 0xef, 0xc0, 0xb0, 0x85, 0xeb, 0x47, 0x6b, 0x01, 0x39, 0xa7, 0xf2, 0xf9, 0x09, 0x90, - 0x33, 0xbb, 0xf2, 0x87, 0x90, 0x0b, 0xca, 0x9a, 0xef, 0x78, 0x89, 0xe7, 0xfd, 0xff, 0xa2, 0xae, - 0x24, 0xd6, 0x10, 0xb6, 0x24, 0x1b, 0x31, 0xa9, 0x96, 0xe4, 0x43, 0x4f, 0x01, 0x4c, 0xd2, 0x68, - 0x38, 0xb4, 0xfd, 0x49, 0x36, 0x56, 0x5c, 0xfa, 0xfa, 0xed, 0xfc, 0xf9, 0x50, 0x10, 0xb5, 0x6a, - 0x05, 0x87, 0xa8, 0x0d, 0x83, 0x55, 0x0b, 0x4f, 0xb0, 0x6d, 0x98, 0xbb, 0x1b, 0xd8, 0x7c, 0xf3, - 0xe5, 0x12, 0x08, 0x3d, 0x1b, 0xd8, 0xd4, 0x12, 0x02, 0xd0, 0x4d, 0x18, 0xe6, 0x1d, 0xc0, 0xd0, - 0x01, 0x1d, 0x00, 0xa7, 0x4a, 0xbc, 0xfd, 0xc3, 0xc7, 0xf6, 0xf6, 0xdf, 0x83, 0x21, 0x8f, 0x78, - 0xfc, 0xb5, 0xcd, 0xad, 0xde, 0xc8, 0xfa, 0x34, 0xf5, 0x09, 0xa9, 0x3c, 0xab, 0x6c, 0x11, 0x4a, - 0x31, 0x37, 0xbc, 0xb8, 0xbd, 0xae, 0x05, 0x7c, 0x68, 0x0d, 0xce, 0x70, 0xe8, 0x62, 0x4b, 0x17, - 0xac, 0xc9, 0xe7, 0x79, 0x58, 0x9b, 0x16, 0xa7, 0xc5, 0xf0, 0x50, 0xbc, 0xd4, 0xc1, 0x83, 0x15, - 0x71, 0xc5, 0xdf, 0xd6, 0x27, 0xc5, 0x83, 0x25, 0x38, 0xa2, 0x4f, 0xec, 0xe0, 0xc1, 0x12, 0x14, - 0xa3, 0x5c, 0xa6, 0x58, 0x05, 0xfb, 0x3f, 0x36, 0x9c, 0x3a, 0xb6, 0xf8, 0x1b, 0x3d, 0xaa, 0x89, - 0x15, 0x5a, 0x86, 0xe9, 0xaa, 0x63, 0x57, 0x31, 0x65, 0xfa, 0x0e, 0x61, 0xb8, 0xdd, 0x30, 0x00, - 0x97, 0x8f, 0xc4, 0xd9, 0xa7, 0xc1, 0x91, 0xd0, 0xf0, 0x5d, 0x98, 0x8c, 0x2f, 0x85, 0xe7, 0x85, - 0x9c, 0xe3, 0x01, 0xb9, 0x9c, 0x99, 0x82, 0x11, 0x35, 0x87, 0x79, 0xde, 0xec, 0x58, 0xf3, 0x5e, - 0x86, 0x54, 0x18, 0xef, 0x27, 0x19, 0xb6, 0xc4, 0x43, 0x9b, 0x0b, 0xf6, 0x36, 0xc2, 0xad, 0x8f, - 0x87, 0x47, 0xc7, 0xa7, 0x26, 0x94, 0x59, 0xf1, 0x85, 0xf4, 0xc4, 0xf0, 0x6d, 0x4c, 0x59, 0x91, - 0x99, 0x1a, 0x7e, 0xe6, 0xdb, 0xd1, 0x18, 0xe6, 0x1b, 0x49, 0x7c, 0xaf, 0xee, 0x3b, 0x17, 0xa8, - 0x9e, 0x05, 0x28, 0xd7, 0x89, 0x59, 0xd3, 0x2d, 0xa7, 0x52, 0x69, 0x0f, 0x27, 0x82, 0x9d, 0x0d, - 0xa7, 0x52, 0x09, 0xfa, 0x0d, 0x9f, 0xd4, 0xeb, 0x65, 0xc3, 0xac, 0xe9, 0x15, 0x9f, 0x34, 0x44, - 0x33, 0xde, 0x51, 0x58, 0x12, 0xb3, 0x21, 0x91, 0x60, 0x8f, 0xb0, 0x61, 0x61, 0xbf, 0x23, 0xbf, - 0xc7, 0x23, 0x31, 0x9b, 0x3e, 0x69, 0xa0, 0xe7, 0x90, 0x6b, 0x8b, 0x65, 0x84, 0x83, 0xf7, 0x28, - 0x42, 0x21, 0x12, 0xb2, 0x4d, 0x14, 0x57, 0xb4, 0xe7, 0x5b, 0xc9, 0xe7, 0xfb, 0xb8, 0x47, 0x19, - 0xdf, 0x1a, 0xfd, 0xc5, 0x67, 0xf3, 0x03, 0xff, 0xfc, 0x6c, 0x7e, 0x40, 0xf9, 0x42, 0xea, 0x98, - 0x46, 0xc5, 0x0a, 0x45, 0x60, 0x1f, 0x24, 0x86, 0x36, 0x43, 0xbc, 0x52, 0x64, 0xf5, 0x0d, 0xc4, - 0xc7, 0x56, 0xea, 0xe8, 0xe6, 0xb8, 0x3e, 0x83, 0x62, 0xab, 0x57, 0xff, 0x9c, 0x87, 0x13, 0xdc, - 0x6a, 0xf4, 0x73, 0x09, 0x46, 0x42, 0xad, 0xe8, 0x5a, 0x86, 0x69, 0xfb, 0xc7, 0x7a, 0x33, 0xd7, - 0x0f, 0x43, 0x2a, 0x4a, 0xf6, 0xe5, 0x9f, 0xfd, 0xf5, 0x1f, 0xbf, 0x1a, 0x9c, 0x47, 0xb3, 0x6a, - 0xaf, 0xa9, 0x25, 0xfa, 0x8d, 0x04, 0xf9, 0xce, 0x18, 0xa2, 0x95, 0x83, 0xb5, 0x74, 0x5d, 0xf0, - 0xcc, 0x6a, 0x3f, 0x2c, 0xc2, 0xc0, 0x02, 0x37, 0x70, 0x11, 0x5d, 0xe9, 0x69, 0x60, 0xd4, 0x1f, - 0x52, 0xf4, 0x3b, 0x09, 0x26, 0xbb, 0x46, 0x88, 0xe8, 0x10, 0x7a, 0xbb, 0x07, 0x95, 0x33, 0xb7, - 0xfa, 0xe2, 0x11, 0xc6, 0xaa, 0xdc, 0xd8, 0x6b, 0xe8, 0x6a, 0x4f, 0x63, 0xd5, 0x57, 0xc2, 0xda, - 0x3d, 0xf4, 0x27, 0x09, 0x4e, 0xed, 0x9b, 0x29, 0xa2, 0xb5, 0xc3, 0xe8, 0xee, 0x1e, 0x61, 0xce, - 0xdc, 0xee, 0x93, 0x4b, 0xd8, 0x7c, 0x8f, 0xdb, 0x7c, 0x07, 0xdd, 0xee, 0x6d, 0x73, 0x5c, 0xc1, - 0xd5, 0x57, 0xf1, 0xff, 0x7b, 0xe8, 0x0b, 0x09, 0x4e, 0xed, 0x1b, 0x19, 0xf6, 0xf6, 0x20, 0x6b, - 0x96, 0xd9, 0xdb, 0x83, 0xcc, 0xb9, 0xa4, 0xb2, 0xc2, 0x3d, 0xb8, 0x81, 0xae, 0x65, 0x78, 0xb0, - 0x7f, 0x68, 0x89, 0xde, 0x48, 0x30, 0xd5, 0x2d, 0x10, 0xdd, 0xea, 0x47, 0x7d, 0x64, 0xf3, 0x5a, - 0x7f, 0x4c, 0xc2, 0xe4, 0x12, 0x37, 0xf9, 0x29, 0xfa, 0xe4, 0xd0, 0x26, 0xab, 0xaf, 0x3a, 0x26, - 0x4f, 0x7b, 0xfb, 0x49, 0xd0, 0xe7, 0x12, 0xe4, 0x3b, 0x87, 0x70, 0xbd, 0x93, 0x34, 0x75, 0xb6, - 0xd8, 0x3b, 0x49, 0xd3, 0x67, 0x7c, 0xca, 0x1d, 0xee, 0xce, 0x0a, 0x52, 0xd5, 0xcc, 0xdf, 0x35, - 0x92, 0x23, 0x29, 0xf5, 0x55, 0xf8, 0xbd, 0xb9, 0x87, 0xfe, 0x25, 0xc1, 0xf9, 0x1e, 0x03, 0x2e, - 0x74, 0xbf, 0x9f, 0xe8, 0xa6, 0x38, 0xf3, 0xed, 0x23, 0xf3, 0x0b, 0xcf, 0x9e, 0x72, 0xcf, 0x3e, - 0x42, 0x0f, 0x8f, 0x7e, 0x51, 0x09, 0xc7, 0xd1, 0x1f, 0x24, 0x98, 0xe8, 0x88, 0x21, 0x5a, 0x3e, - 0x74, 0xb8, 0x23, 0x9f, 0x56, 0xfa, 0xe0, 0x10, 0x5e, 0xac, 0x73, 0x2f, 0xee, 0xa1, 0xbb, 0x87, - 0xba, 0x1f, 0x7e, 0x3d, 0xdd, 0x5f, 0xb8, 0x7b, 0xe8, 0xf7, 0x12, 0x4c, 0x76, 0x75, 0x28, 0xbd, - 0x2b, 0x6b, 0x7a, 0xbb, 0xd3, 0xbb, 0xb2, 0x66, 0xb4, 0x40, 0xca, 0x32, 0xf7, 0xe0, 0x3a, 0x5a, - 0xcc, 0xf0, 0xa0, 0x1e, 0xf2, 0xf1, 0xc0, 0xfb, 0x98, 0xf8, 0x76, 0xf1, 0xf9, 0xeb, 0x77, 0x73, - 0xd2, 0x57, 0xef, 0xe6, 0xa4, 0xbf, 0xbf, 0x9b, 0x93, 0x7e, 0xf9, 0x7e, 0x6e, 0xe0, 0xab, 0xf7, - 0x73, 0x03, 0x7f, 0x7b, 0x3f, 0x37, 0xf0, 0xbd, 0xc3, 0x35, 0xd3, 0xad, 0xa4, 0x06, 0xde, 0x59, - 0x97, 0x47, 0xf8, 0x8f, 0x69, 0xb7, 0xfe, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x72, 0xc0, 0xd2, 0xab, - 0xbd, 0x1c, 0x00, 0x00, + // 2267 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x59, 0xcd, 0x6f, 0xdb, 0xc8, + 0x15, 0x37, 0x6d, 0xc7, 0xb1, 0x9f, 0x2c, 0xd9, 0x99, 0x38, 0x89, 0xe2, 0xc4, 0x76, 0xc2, 0xcd, + 0x87, 0xf3, 0x61, 0x31, 0x76, 0x92, 0x0d, 0xda, 0x20, 0x69, 0x23, 0x3b, 0xd9, 0x64, 0x37, 0x69, + 0x1c, 0xda, 0x59, 0x14, 0xed, 0xa2, 0x2c, 0x45, 0x8e, 0x28, 0x56, 0x12, 0x87, 0x21, 0x47, 0xae, + 0x8c, 0xc0, 0x40, 0xd1, 0x43, 0xd1, 0x63, 0x81, 0xf6, 0x0f, 0x58, 0xf4, 0xd2, 0x02, 0xed, 0x61, + 0x81, 0xdd, 0x4b, 0x0f, 0x45, 0x0f, 0xbd, 0x6c, 0x6f, 0x8b, 0xf4, 0x52, 0xec, 0x21, 0x28, 0x92, + 0x02, 0xdb, 0x4b, 0x4f, 0xbd, 0xf4, 0xd0, 0xc3, 0x82, 0x33, 0x43, 0x91, 0x94, 0x48, 0xd9, 0xf2, + 0xfa, 0x62, 0x98, 0x33, 0xef, 0xfb, 0xfd, 0xde, 0xe3, 0xe3, 0x13, 0x9c, 0xad, 0xe8, 0x95, 0xed, + 0x06, 0x71, 0x94, 0x0a, 0x35, 0x7c, 0xaa, 0xd7, 0x6d, 0xc7, 0x52, 0xb6, 0x96, 0x95, 0x17, 0x2d, + 0xec, 0x6d, 0x97, 0x5c, 0x8f, 0x50, 0x82, 0x8e, 0x09, 0x92, 0x52, 0x44, 0x52, 0xda, 0x5a, 0x9e, + 0x9d, 0xb1, 0x88, 0x45, 0x18, 0x85, 0x12, 0xfc, 0xc7, 0x89, 0x67, 0x4f, 0x5b, 0x84, 0x58, 0x0d, + 0xac, 0xe8, 0xae, 0xad, 0xe8, 0x8e, 0x43, 0xa8, 0x4e, 0x6d, 0xe2, 0xf8, 0xe2, 0xf6, 0xa4, 0x41, + 0xfc, 0x26, 0xf1, 0x35, 0xce, 0xc6, 0x1f, 0xc4, 0xd5, 0x39, 0xfe, 0xa4, 0x44, 0x46, 0x54, 0x30, + 0xd5, 0x97, 0xc3, 0x67, 0x41, 0x75, 0x59, 0x50, 0x55, 0x74, 0x1f, 0x73, 0x23, 0x3b, 0x84, 0xae, + 0x6e, 0xd9, 0x0e, 0xd3, 0x26, 0x68, 0xe5, 0x74, 0xd7, 0x5c, 0xdd, 0xd3, 0x9b, 0xa1, 0xd6, 0x0b, + 0xe9, 0x34, 0x31, 0x4f, 0x39, 0xdd, 0x42, 0x86, 0x2c, 0xe2, 0x0a, 0x82, 0xf3, 0x31, 0x82, 0x86, + 0x6d, 0xd5, 0x82, 0xbf, 0xd8, 0xa1, 0x5d, 0xb1, 0x94, 0x67, 0x00, 0x3d, 0x0b, 0x1e, 0xd7, 0x99, + 0x11, 0x2a, 0x7e, 0xd1, 0xc2, 0x3e, 0x95, 0x55, 0x38, 0x9a, 0x38, 0xf5, 0x5d, 0xe2, 0xf8, 0x18, + 0xdd, 0x86, 0x31, 0x6e, 0x6c, 0x51, 0x3a, 0x23, 0x2d, 0xe6, 0x56, 0xe6, 0x4a, 0xa9, 0x99, 0x28, + 0x71, 0xb6, 0xf2, 0xe8, 0xe7, 0xaf, 0x17, 0x86, 0x54, 0xc1, 0x22, 0xdf, 0x82, 0x53, 0x31, 0x99, + 0xe5, 0xed, 0x0f, 0xb1, 0xe7, 0xdb, 0xc4, 0x11, 0x2a, 0x51, 0x11, 0x0e, 0x6f, 0xf1, 0x13, 0x26, + 0x3c, 0xaf, 0x86, 0x8f, 0xf2, 0x0f, 0xe1, 0x74, 0x3a, 0xe3, 0x41, 0x58, 0x75, 0x17, 0xe6, 0x12, + 0xc2, 0xcb, 0x9b, 0xab, 0x0f, 0x71, 0x10, 0xae, 0xd0, 0xae, 0x39, 0x80, 0x0a, 0x35, 0xb4, 0x1a, + 0x3b, 0x14, 0xa6, 0x4d, 0x54, 0xa8, 0xc1, 0xa9, 0xe4, 0x9f, 0xc2, 0x7c, 0x16, 0xff, 0x01, 0x98, + 0x17, 0x8f, 0xca, 0x70, 0x32, 0x2a, 0x96, 0x30, 0xfc, 0x81, 0xed, 0xe8, 0x0d, 0x9b, 0x6e, 0xaf, + 0x7b, 0x64, 0xcb, 0x36, 0xb1, 0x17, 0xe6, 0x10, 0x3d, 0x00, 0x88, 0x10, 0x28, 0x74, 0x5f, 0x28, + 0x09, 0x88, 0x07, 0x70, 0x2d, 0x71, 0x1c, 0x08, 0xb8, 0x96, 0xd6, 0x75, 0x0b, 0x0b, 0x5e, 0x35, + 0xc6, 0x29, 0xff, 0x4d, 0x12, 0x2e, 0xa6, 0x68, 0x12, 0x2e, 0xfe, 0x08, 0x50, 0x55, 0x5c, 0x06, + 0x95, 0xc4, 0x6f, 0x8b, 0xd2, 0x99, 0x91, 0xc5, 0xdc, 0x8a, 0x92, 0xe1, 0x6e, 0xb7, 0xb4, 0x50, + 0x98, 0x7a, 0xa4, 0xda, 0xad, 0x07, 0xbd, 0x97, 0x70, 0x65, 0x98, 0xb9, 0x72, 0x71, 0x57, 0x57, + 0x84, 0xbc, 0xb8, 0x2f, 0xf7, 0x04, 0x94, 0x7a, 0x95, 0xf3, 0x98, 0x9d, 0x85, 0x7c, 0xd5, 0xd5, + 0x82, 0x7c, 0xbb, 0x75, 0xad, 0x86, 0xdb, 0x2c, 0x6c, 0x13, 0x2a, 0x54, 0xdd, 0x32, 0x35, 0xd6, + 0xeb, 0x0f, 0x71, 0x5b, 0xde, 0xc9, 0x88, 0x7b, 0x27, 0x18, 0x1f, 0xc1, 0x91, 0x9e, 0x60, 0x88, + 0xf0, 0x0f, 0x1c, 0x8b, 0xe9, 0xee, 0x58, 0xc8, 0xbf, 0x97, 0x60, 0x96, 0xe9, 0x2f, 0x6f, 0xae, + 0xae, 0xe1, 0x06, 0xb6, 0x78, 0x3b, 0x0b, 0x1d, 0x28, 0xc3, 0x98, 0x4f, 0x75, 0xda, 0xe2, 0x60, + 0x2b, 0xac, 0x5c, 0xce, 0xd0, 0x98, 0xe0, 0xde, 0x60, 0x1c, 0xaa, 0xe0, 0xec, 0x02, 0xce, 0xf0, + 0xbe, 0x81, 0xf3, 0x67, 0x49, 0x54, 0x7c, 0xb7, 0xa9, 0x22, 0x50, 0xcf, 0x61, 0x2a, 0x88, 0xb4, + 0x19, 0x5d, 0x09, 0xc8, 0x5c, 0xdd, 0x8b, 0xd1, 0x9d, 0x18, 0x15, 0x2a, 0xd4, 0x88, 0x89, 0x3f, + 0x38, 0xb0, 0xfc, 0x46, 0x82, 0x8b, 0xa9, 0xa9, 0x4e, 0x89, 0xfb, 0xee, 0xc0, 0x39, 0xb0, 0xb0, + 0x7e, 0x25, 0xc1, 0xe2, 0xee, 0x66, 0x89, 0x18, 0x7b, 0x70, 0x32, 0x16, 0x63, 0xe2, 0xa5, 0x44, + 0xfb, 0xdd, 0x5d, 0xa3, 0x4d, 0xd2, 0x44, 0xab, 0x27, 0xa2, 0xb8, 0x27, 0x08, 0x0e, 0x2e, 0x01, + 0xef, 0xc3, 0xc9, 0x5e, 0xfc, 0x84, 0x11, 0x5f, 0x82, 0xa3, 0xc2, 0x58, 0x8d, 0xb6, 0xb5, 0x9a, + 0xee, 0xd7, 0x62, 0x71, 0x9f, 0x16, 0x57, 0x9b, 0xed, 0x87, 0xba, 0x5f, 0x0b, 0xca, 0xf6, 0x45, + 0x5a, 0xd9, 0x74, 0xc2, 0xb4, 0x01, 0x85, 0x24, 0x14, 0x45, 0xc1, 0x0e, 0x86, 0xc4, 0x7c, 0x02, + 0x89, 0xf2, 0x6f, 0xc7, 0xe1, 0x58, 0xba, 0xba, 0x6f, 0x41, 0x2e, 0x10, 0x86, 0x3d, 0x4d, 0x37, + 0x4d, 0xde, 0x1c, 0x26, 0xca, 0xc5, 0x57, 0x9f, 0x2d, 0xcd, 0x88, 0x28, 0xdd, 0x33, 0x4d, 0x0f, + 0xfb, 0xfe, 0x06, 0xf5, 0x6c, 0xc7, 0x52, 0x81, 0x13, 0x07, 0x87, 0x48, 0x85, 0x31, 0x8e, 0x32, + 0x16, 0xd8, 0xc9, 0xf2, 0xed, 0x2f, 0x5f, 0x2f, 0xdc, 0xb2, 0x6c, 0x5a, 0x6b, 0x55, 0x4a, 0x06, + 0x69, 0x2a, 0xc2, 0xde, 0x86, 0x5e, 0xf1, 0x97, 0x6c, 0x12, 0x3e, 0x2a, 0x5b, 0x37, 0x14, 0xba, + 0xed, 0x62, 0xbf, 0x54, 0x7e, 0xb4, 0x7e, 0xfd, 0xc6, 0xb5, 0xf5, 0x56, 0xe5, 0x03, 0xbc, 0xad, + 0x1e, 0xaa, 0x04, 0xe0, 0x44, 0x3f, 0x86, 0x42, 0x04, 0xde, 0x86, 0xed, 0xd3, 0xe2, 0xc8, 0x99, + 0x91, 0x6f, 0x2a, 0x3b, 0x27, 0xa0, 0xff, 0xd8, 0x66, 0xe5, 0x31, 0xd9, 0x49, 0x96, 0xdd, 0xc4, + 0xc5, 0x51, 0xf6, 0x2e, 0xcb, 0x85, 0x59, 0xb2, 0x9b, 0x58, 0x90, 0x78, 0x34, 0x7c, 0xd3, 0x1e, + 0xea, 0x90, 0x78, 0x94, 0xbf, 0x51, 0x83, 0x57, 0x31, 0x76, 0xcc, 0x90, 0x60, 0x8c, 0xbf, 0x8a, + 0xb1, 0x63, 0x8a, 0xeb, 0x53, 0x30, 0x41, 0x09, 0xd5, 0x1b, 0x9a, 0xaf, 0xd3, 0xe2, 0xe1, 0x33, + 0xd2, 0xe2, 0xa8, 0x3a, 0xce, 0x0e, 0x36, 0x74, 0x8a, 0xce, 0x41, 0x21, 0x0e, 0x17, 0xdc, 0x2e, + 0x8e, 0x33, 0xa4, 0x4c, 0x46, 0x48, 0xc1, 0x6d, 0x74, 0x01, 0xa6, 0xfc, 0x86, 0xee, 0xd7, 0x62, + 0x64, 0x13, 0x8c, 0x2c, 0x1f, 0x1e, 0x73, 0xba, 0x9b, 0x70, 0x22, 0x2a, 0x29, 0x76, 0xa5, 0xf9, + 0xb6, 0xc5, 0xe8, 0x81, 0xd1, 0xcf, 0x74, 0xae, 0x37, 0x82, 0xdb, 0x0d, 0xdb, 0x0a, 0xd8, 0x9e, + 0x43, 0xde, 0x20, 0x5b, 0xd8, 0xd1, 0x1d, 0x1a, 0xd0, 0xfb, 0xc5, 0x1c, 0xab, 0xc0, 0x6b, 0x19, + 0x28, 0x5b, 0x15, 0xb4, 0xf7, 0x4c, 0xdd, 0x0d, 0x24, 0xd9, 0x96, 0xa3, 0xd3, 0x96, 0x87, 0x7d, + 0x75, 0x32, 0x14, 0xb3, 0x61, 0x5b, 0x3e, 0xba, 0x0a, 0x28, 0xf4, 0x8d, 0xb4, 0xa8, 0xdb, 0xa2, + 0x9a, 0x6d, 0xb6, 0x8b, 0x93, 0x2c, 0x3e, 0x61, 0x25, 0x3c, 0x65, 0x17, 0x8f, 0xcc, 0x36, 0x3a, + 0x0e, 0x63, 0xba, 0x41, 0xed, 0x2d, 0x5c, 0xcc, 0x9f, 0x91, 0x16, 0xc7, 0x55, 0xf1, 0x84, 0x16, + 0x18, 0x28, 0x69, 0xcb, 0xd7, 0x4c, 0xec, 0x1b, 0xc5, 0x02, 0x6f, 0x60, 0xfc, 0x68, 0x0d, 0xfb, + 0x06, 0x3a, 0x0f, 0x85, 0x96, 0x53, 0x21, 0x8e, 0xd9, 0x49, 0xe3, 0x14, 0x53, 0x91, 0xef, 0x9c, + 0xb2, 0x44, 0x1a, 0x70, 0xac, 0xe5, 0x44, 0x95, 0xa4, 0x79, 0x02, 0xf5, 0xc5, 0x69, 0x56, 0x52, + 0xa5, 0xec, 0x92, 0x7a, 0x1e, 0x63, 0xeb, 0x14, 0xd5, 0x4c, 0x2b, 0xe5, 0x34, 0xb0, 0x85, 0x4f, + 0x48, 0x5a, 0x38, 0x1e, 0x1d, 0xe1, 0xb6, 0xf0, 0x53, 0x31, 0x22, 0xa2, 0x07, 0x70, 0xd8, 0xa7, + 0x75, 0x0d, 0xb7, 0xdd, 0x22, 0x62, 0xda, 0x97, 0x32, 0xb4, 0x6f, 0x04, 0x15, 0x76, 0xbf, 0xed, + 0xea, 0x4e, 0x7c, 0xb4, 0x0c, 0x5e, 0x89, 0xf5, 0xfb, 0x6d, 0x17, 0x7d, 0x1f, 0xf2, 0xcd, 0x56, + 0x83, 0xda, 0x41, 0x92, 0x6d, 0xa7, 0x4a, 0x8a, 0x47, 0x99, 0xb4, 0xeb, 0x19, 0xd2, 0xee, 0x99, + 0xa6, 0x1d, 0x98, 0xab, 0x37, 0x98, 0x5c, 0xef, 0x91, 0x53, 0x25, 0x1d, 0x99, 0x93, 0xa1, 0xa4, + 0xe0, 0x54, 0xfe, 0xff, 0x30, 0x9c, 0xee, 0x47, 0x8e, 0x6a, 0x3c, 0xb9, 0xd8, 0x4b, 0x14, 0xa8, + 0xf4, 0xcd, 0x0b, 0x74, 0x8a, 0x8b, 0x8d, 0x8a, 0xf4, 0x1d, 0xc8, 0x0b, 0x4d, 0x2f, 0x5a, 0xc4, + 0x6b, 0x35, 0xc5, 0xc4, 0x39, 0xc9, 0x0f, 0x9f, 0xb1, 0x33, 0xf4, 0x51, 0x0f, 0xf2, 0x03, 0x34, + 0x30, 0x30, 0x8f, 0x30, 0x30, 0x9f, 0xcb, 0x8a, 0x70, 0x88, 0x5e, 0xe6, 0xdd, 0xb1, 0x64, 0x7d, + 0xd8, 0x8e, 0xc5, 0x90, 0x4c, 0xe0, 0x6c, 0x24, 0x3d, 0x02, 0x5b, 0x52, 0xcf, 0xe8, 0x00, 0x7a, + 0xe6, 0x3b, 0xe2, 0x9e, 0x87, 0xd2, 0xe2, 0x0a, 0xe5, 0xff, 0x4a, 0x70, 0x3c, 0x3d, 0xf7, 0xe8, + 0x2e, 0x9c, 0x76, 0x3d, 0xbc, 0x65, 0x93, 0x96, 0xaf, 0x65, 0xbf, 0x69, 0x8a, 0x21, 0xcd, 0x46, + 0xd7, 0x1b, 0x07, 0xbd, 0x0b, 0x45, 0x42, 0x6b, 0xd8, 0xd3, 0xaa, 0x2d, 0x51, 0x32, 0xed, 0xa0, + 0x3c, 0x19, 0xef, 0x30, 0x6f, 0x12, 0xec, 0xfe, 0x01, 0xbf, 0xde, 0x6c, 0x3f, 0x6d, 0xd1, 0x80, + 0x4f, 0x87, 0xd9, 0x98, 0xde, 0xba, 0x96, 0xec, 0x18, 0x83, 0x04, 0xf9, 0x44, 0x64, 0x5b, 0x7d, + 0x35, 0xd6, 0x30, 0xe4, 0x27, 0x30, 0xbf, 0xd6, 0x13, 0x97, 0x04, 0xea, 0xae, 0x00, 0xf2, 0xdd, + 0xa0, 0xd9, 0xb2, 0xe4, 0x87, 0xbd, 0x90, 0xbb, 0x3c, 0xc5, 0x6e, 0x58, 0xd4, 0x58, 0x37, 0x94, + 0xff, 0x37, 0x02, 0x27, 0x32, 0xca, 0x17, 0x2d, 0xc2, 0x74, 0xac, 0x69, 0xc4, 0xc5, 0x44, 0xcd, + 0x84, 0xf7, 0x54, 0x03, 0x4e, 0x75, 0x5c, 0x8d, 0xa5, 0xde, 0xb6, 0x38, 0xe2, 0x87, 0x07, 0x70, + 0xbc, 0x18, 0x0a, 0x8a, 0x92, 0x6e, 0x5b, 0x0c, 0xe3, 0x29, 0x0d, 0x7e, 0x24, 0xad, 0xc1, 0xdf, + 0x86, 0xd9, 0x74, 0x98, 0x33, 0x96, 0x51, 0xc6, 0x72, 0x22, 0x0d, 0xc3, 0x01, 0x73, 0x15, 0x8e, + 0x47, 0x49, 0x4b, 0x40, 0xf7, 0xd0, 0x3e, 0xfb, 0xfd, 0x4c, 0xa7, 0xdf, 0xc7, 0xab, 0xe5, 0x67, + 0x52, 0x7a, 0xb9, 0x04, 0x1d, 0x2a, 0x6a, 0xbb, 0x63, 0xac, 0x55, 0xdd, 0xcc, 0xd0, 0xd9, 0x1f, + 0x07, 0x69, 0xf5, 0x13, 0xbf, 0x97, 0x0d, 0x58, 0xd8, 0x65, 0x4e, 0x44, 0xdf, 0x85, 0x51, 0x13, + 0x37, 0xf6, 0x37, 0xdb, 0x33, 0x4e, 0xf9, 0x93, 0x43, 0x50, 0xcc, 0xfc, 0xdc, 0xba, 0x0f, 0xb9, + 0xe0, 0x7d, 0xe5, 0xd9, 0x6e, 0x6c, 0x6e, 0x7b, 0x27, 0x1c, 0x37, 0x23, 0x0d, 0x7c, 0xd6, 0x5c, + 0x8b, 0x48, 0xd5, 0x38, 0x1f, 0x7a, 0x02, 0x60, 0x90, 0x66, 0xd3, 0xf6, 0x3b, 0xdf, 0xda, 0x13, + 0xe5, 0xa5, 0x2f, 0x5f, 0x2f, 0x9c, 0xe2, 0x82, 0x7c, 0xb3, 0x5e, 0xb2, 0x89, 0xd2, 0xd4, 0x69, + 0xad, 0xf4, 0x18, 0x5b, 0xba, 0xb1, 0xbd, 0x86, 0x8d, 0x57, 0x9f, 0x2d, 0x81, 0xd0, 0xb3, 0x86, + 0x0d, 0x35, 0x26, 0x00, 0x5d, 0x85, 0x51, 0x36, 0xda, 0x8d, 0xec, 0x32, 0xda, 0x31, 0xaa, 0xd8, + 0x50, 0x37, 0x7a, 0x60, 0x43, 0xdd, 0x1d, 0x18, 0x71, 0x89, 0xcb, 0xc6, 0xa8, 0xdc, 0xca, 0x95, + 0xac, 0x9d, 0x83, 0x47, 0x48, 0xf5, 0x69, 0x75, 0x9d, 0xf8, 0x3e, 0x66, 0x86, 0x97, 0x37, 0x57, + 0xd5, 0x80, 0x0f, 0xdd, 0x80, 0xe3, 0x0c, 0xba, 0xd8, 0xd4, 0x04, 0x6b, 0x7c, 0xee, 0x1a, 0x55, + 0x67, 0xc4, 0x6d, 0x99, 0x5f, 0x8a, 0x11, 0x2c, 0x98, 0x44, 0x42, 0xae, 0x68, 0x69, 0x72, 0x58, + 0x4c, 0x22, 0x82, 0x23, 0xdc, 0x9d, 0x04, 0x93, 0x88, 0xa0, 0x18, 0x67, 0x32, 0xc5, 0x53, 0x70, + 0xfe, 0x13, 0xdd, 0x6e, 0x60, 0x93, 0x0d, 0x5f, 0xe3, 0xaa, 0x78, 0x42, 0xd7, 0x60, 0xa6, 0x66, + 0x5b, 0x35, 0xec, 0x53, 0x6d, 0x8b, 0x50, 0xdc, 0x99, 0x04, 0x81, 0xc9, 0x47, 0xe2, 0xee, 0xc3, + 0xe0, 0x4a, 0x68, 0xf8, 0x1e, 0x4c, 0x45, 0x49, 0xe1, 0x6f, 0xee, 0x1c, 0x0b, 0xc8, 0xf9, 0xcc, + 0x12, 0x0c, 0xa9, 0x19, 0xcc, 0x0b, 0x46, 0xe2, 0x99, 0x0d, 0xa9, 0xa4, 0x4a, 0xd9, 0x87, 0x02, + 0xc5, 0xa6, 0x98, 0xa0, 0x72, 0xc1, 0xd9, 0x1a, 0x3f, 0x7a, 0x7f, 0x74, 0x7c, 0x72, 0x3a, 0x2f, + 0xcf, 0x89, 0x4f, 0xdf, 0xc7, 0xba, 0x67, 0x61, 0x9f, 0x96, 0xa9, 0xa1, 0xe2, 0xa7, 0x9e, 0x15, + 0xee, 0xd7, 0xbe, 0x92, 0xc4, 0x22, 0xa2, 0xe7, 0x5e, 0xa0, 0x7a, 0x0e, 0xa0, 0xd2, 0x20, 0x46, + 0x5d, 0x33, 0xed, 0x6a, 0xb5, 0xb3, 0x75, 0x0a, 0x4e, 0xd6, 0xec, 0x6a, 0x35, 0x18, 0x24, 0x3d, + 0xd2, 0x68, 0x54, 0x74, 0xa3, 0xae, 0x55, 0x3d, 0xd2, 0x14, 0x5f, 0x59, 0x89, 0xc6, 0x12, 0x5b, + 0xfa, 0x89, 0x02, 0x7b, 0x88, 0x75, 0xb3, 0x7b, 0x18, 0x09, 0xc5, 0x3c, 0xf0, 0x48, 0x13, 0x3d, + 0x83, 0x5c, 0x47, 0x2c, 0x25, 0x0c, 0xbc, 0xfb, 0x11, 0x0a, 0xa1, 0x90, 0x4d, 0x22, 0x3b, 0xe2, + 0xbb, 0x6b, 0x3d, 0x3e, 0x97, 0x1d, 0xf4, 0x8e, 0xea, 0xdb, 0xe3, 0xbf, 0xfc, 0x78, 0x61, 0xe8, + 0xdf, 0x1f, 0x2f, 0x0c, 0xc9, 0x9f, 0x4a, 0x89, 0x35, 0x63, 0xa4, 0x50, 0x04, 0xf6, 0x5e, 0x6c, + 0x1b, 0x37, 0xc2, 0x3a, 0x45, 0xd6, 0x40, 0x48, 0x3c, 0x6c, 0xa6, 0xee, 0xe4, 0x0e, 0xea, 0xfb, + 0x36, 0xb2, 0x7a, 0xe5, 0xaf, 0x05, 0x38, 0xc4, 0xac, 0x46, 0xbf, 0x90, 0x60, 0x8c, 0x6b, 0x45, + 0x97, 0x32, 0x4c, 0xeb, 0xdd, 0xd7, 0xce, 0x5e, 0xde, 0x0b, 0xa9, 0x68, 0xd9, 0xe7, 0x7f, 0xfe, + 0xf7, 0x7f, 0xfd, 0x7a, 0x78, 0x01, 0xcd, 0x29, 0xfd, 0xd6, 0xd1, 0xe8, 0x77, 0x12, 0x14, 0x92, + 0x31, 0x44, 0xcb, 0xbb, 0x6b, 0xe9, 0x4a, 0xf0, 0xec, 0xca, 0x20, 0x2c, 0xc2, 0xc0, 0x12, 0x33, + 0x70, 0x11, 0x5d, 0xe8, 0x6b, 0x60, 0x38, 0xf8, 0xfb, 0xe8, 0x0f, 0x12, 0x4c, 0x75, 0xed, 0x86, + 0xd1, 0x1e, 0xf4, 0x76, 0x6f, 0xa0, 0x67, 0xaf, 0x0f, 0xc4, 0x23, 0x8c, 0x55, 0x98, 0xb1, 0x97, + 0xd0, 0xc5, 0xbe, 0xc6, 0x2a, 0x2f, 0x85, 0xb5, 0x3b, 0xe8, 0x2f, 0x12, 0x1c, 0xe9, 0x59, 0x16, + 0xa3, 0x1b, 0x7b, 0xd1, 0xdd, 0xbd, 0x9b, 0x9e, 0xbd, 0x39, 0x20, 0x97, 0xb0, 0xf9, 0x0e, 0xb3, + 0xf9, 0x16, 0xba, 0xd9, 0xdf, 0xe6, 0xa8, 0x83, 0x2b, 0x2f, 0xa3, 0xff, 0x77, 0xd0, 0xa7, 0x12, + 0x1c, 0xe9, 0xd9, 0x05, 0xf7, 0xf7, 0x20, 0x6b, 0x49, 0xdd, 0xdf, 0x83, 0xcc, 0x85, 0xb3, 0xbc, + 0xcc, 0x3c, 0xb8, 0x82, 0x2e, 0x65, 0x78, 0xd0, 0xbb, 0x8d, 0x46, 0xaf, 0x24, 0x98, 0xee, 0x16, + 0x88, 0xae, 0x0f, 0xa2, 0x3e, 0xb4, 0xf9, 0xc6, 0x60, 0x4c, 0xc2, 0xe4, 0x0d, 0x66, 0xf2, 0x13, + 0xf4, 0xc1, 0x9e, 0x4d, 0x56, 0x5e, 0x26, 0x56, 0x8a, 0x3b, 0xbd, 0x24, 0xe8, 0x13, 0x09, 0x0a, + 0xc9, 0xed, 0x6a, 0xff, 0x22, 0x4d, 0x5d, 0x1a, 0xf7, 0x2f, 0xd2, 0xf4, 0xe5, 0xad, 0x7c, 0x8b, + 0xb9, 0xb3, 0x8c, 0x14, 0x25, 0xf3, 0x07, 0xab, 0xf8, 0xae, 0x51, 0x79, 0xc9, 0x17, 0x09, 0x3b, + 0xe8, 0x3f, 0x12, 0x9c, 0xea, 0xb3, 0xb9, 0x44, 0x77, 0x07, 0x89, 0x6e, 0x8a, 0x33, 0xdf, 0xd9, + 0x37, 0xbf, 0xf0, 0xec, 0x09, 0xf3, 0xec, 0x3d, 0x74, 0x7f, 0xff, 0x89, 0x8a, 0x39, 0x8e, 0xfe, + 0x24, 0x41, 0x3e, 0x11, 0x43, 0x74, 0x6d, 0xcf, 0xe1, 0x0e, 0x7d, 0x5a, 0x1e, 0x80, 0x43, 0x78, + 0xb1, 0xca, 0xbc, 0xb8, 0x83, 0x6e, 0xef, 0x29, 0x3f, 0x2c, 0x3d, 0xdd, 0x5f, 0xb8, 0x3b, 0xe8, + 0x8f, 0x12, 0x4c, 0x75, 0x4d, 0x28, 0xfd, 0x3b, 0x6b, 0xfa, 0xb8, 0xd3, 0xbf, 0xb3, 0x66, 0x8c, + 0x40, 0xf2, 0x35, 0xe6, 0xc1, 0x65, 0xb4, 0x98, 0xe1, 0x41, 0x83, 0xf3, 0xb1, 0xc0, 0x7b, 0x98, + 0x78, 0x56, 0xf9, 0xd9, 0xe7, 0x6f, 0xe6, 0xa5, 0x2f, 0xde, 0xcc, 0x4b, 0xff, 0x7c, 0x33, 0x2f, + 0xfd, 0xea, 0xed, 0xfc, 0xd0, 0x17, 0x6f, 0xe7, 0x87, 0xfe, 0xf1, 0x76, 0x7e, 0xe8, 0x07, 0x7b, + 0x1b, 0xa6, 0xdb, 0x71, 0x0d, 0x6c, 0xb2, 0xae, 0x8c, 0xb1, 0x5f, 0x49, 0xaf, 0x7f, 0x1d, 0x00, + 0x00, 0xff, 0xff, 0x01, 0x2b, 0x1a, 0x7f, 0x96, 0x1e, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2806,6 +2897,20 @@ func (m *BTCDelegationResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.MultisigInfo != nil { + { + size, err := m.MultisigInfo.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x9a + } if m.StkExp != nil { { size, err := m.StkExp.MarshalToSizedBuffer(dAtA[:i]) @@ -2959,6 +3064,76 @@ func (m *BTCDelegationResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *AdditionalStakerInfoResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *AdditionalStakerInfoResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *AdditionalStakerInfoResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.DelegatorUnbondingSlashingSigs) > 0 { + for iNdEx := len(m.DelegatorUnbondingSlashingSigs) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.DelegatorUnbondingSlashingSigs[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.DelegatorSlashingSigs) > 0 { + for iNdEx := len(m.DelegatorSlashingSigs) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.DelegatorSlashingSigs[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if m.StakerQuorum != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.StakerQuorum)) + i-- + dAtA[i] = 0x10 + } + if len(m.StakerBtcPkList) > 0 { + for iNdEx := len(m.StakerBtcPkList) - 1; iNdEx >= 0; iNdEx-- { + { + size := m.StakerBtcPkList[iNdEx].Size() + i -= size + if _, err := m.StakerBtcPkList[iNdEx].MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func (m *StakeExpansionResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -3759,6 +3934,40 @@ func (m *BTCDelegationResponse) Size() (n int) { l = m.StkExp.Size() n += 2 + l + sovQuery(uint64(l)) } + if m.MultisigInfo != nil { + l = m.MultisigInfo.Size() + n += 2 + l + sovQuery(uint64(l)) + } + return n +} + +func (m *AdditionalStakerInfoResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.StakerBtcPkList) > 0 { + for _, e := range m.StakerBtcPkList { + l = e.Size() + n += 1 + l + sovQuery(uint64(l)) + } + } + if m.StakerQuorum != 0 { + n += 1 + sovQuery(uint64(m.StakerQuorum)) + } + if len(m.DelegatorSlashingSigs) > 0 { + for _, e := range m.DelegatorSlashingSigs { + l = e.Size() + n += 1 + l + sovQuery(uint64(l)) + } + } + if len(m.DelegatorUnbondingSlashingSigs) > 0 { + for _, e := range m.DelegatorUnbondingSlashingSigs { + l = e.Size() + n += 1 + l + sovQuery(uint64(l)) + } + } return n } @@ -5947,6 +6156,214 @@ func (m *BTCDelegationResponse) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 19: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MultisigInfo", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MultisigInfo == nil { + m.MultisigInfo = &AdditionalStakerInfoResponse{} + } + if err := m.MultisigInfo.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *AdditionalStakerInfoResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: AdditionalStakerInfoResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: AdditionalStakerInfoResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StakerBtcPkList", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var v github_com_babylonlabs_io_babylon_v4_types.BIP340PubKey + m.StakerBtcPkList = append(m.StakerBtcPkList, v) + if err := m.StakerBtcPkList[len(m.StakerBtcPkList)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field StakerQuorum", wireType) + } + m.StakerQuorum = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.StakerQuorum |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DelegatorSlashingSigs", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DelegatorSlashingSigs = append(m.DelegatorSlashingSigs, &SignatureInfo{}) + if err := m.DelegatorSlashingSigs[len(m.DelegatorSlashingSigs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DelegatorUnbondingSlashingSigs", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DelegatorUnbondingSlashingSigs = append(m.DelegatorUnbondingSlashingSigs, &SignatureInfo{}) + if err := m.DelegatorUnbondingSlashingSigs[len(m.DelegatorUnbondingSlashingSigs)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQuery(dAtA[iNdEx:]) diff --git a/x/btcstaking/types/tx.pb.go b/x/btcstaking/types/tx.pb.go index 0dc025db3..0ddc486d3 100644 --- a/x/btcstaking/types/tx.pb.go +++ b/x/btcstaking/types/tx.pb.go @@ -352,6 +352,9 @@ type MsgCreateBTCDelegation struct { // delegator_unbonding_slashing_sig is the signature on the slashing tx by the // delegator (i.e., SK corresponding to btc_pk). DelegatorUnbondingSlashingSig *github_com_babylonlabs_io_babylon_v4_types.BIP340Signature `protobuf:"bytes,15,opt,name=delegator_unbonding_slashing_sig,json=delegatorUnbondingSlashingSig,proto3,customtype=github.com/babylonlabs-io/babylon/v4/types.BIP340Signature" json:"delegator_unbonding_slashing_sig,omitempty"` + // multisig_info is used when the given btc delegation is M-of-N multisig. + // If nil, it is not a M-of-N multisig. + MultisigInfo *AdditionalStakerInfo `protobuf:"bytes,16,opt,name=multisig_info,json=multisigInfo,proto3" json:"multisig_info,omitempty"` } func (m *MsgCreateBTCDelegation) Reset() { *m = MsgCreateBTCDelegation{} } @@ -450,6 +453,13 @@ func (m *MsgCreateBTCDelegation) GetUnbondingValue() int64 { return 0 } +func (m *MsgCreateBTCDelegation) GetMultisigInfo() *AdditionalStakerInfo { + if m != nil { + return m.MultisigInfo + } + return nil +} + // MsgCreateBTCDelegationResponse is the response for MsgCreateBTCDelegation type MsgCreateBTCDelegationResponse struct { } @@ -542,6 +552,9 @@ type MsgBtcStakeExpand struct { // to at least pay the fees for it. It can also be used to increase the total amount // of satoshi staked. This will be parsed into a *wire.MsgTx FundingTx []byte `protobuf:"bytes,16,opt,name=funding_tx,json=fundingTx,proto3" json:"funding_tx,omitempty"` + // multisig_info is used when the given btc delegation is M-of-N multisig. + // If nil, it is not a M-of-N multisig. + MultisigInfo *AdditionalStakerInfo `protobuf:"bytes,17,opt,name=multisig_info,json=multisigInfo,proto3" json:"multisig_info,omitempty"` } func (m *MsgBtcStakeExpand) Reset() { *m = MsgBtcStakeExpand{} } @@ -647,6 +660,13 @@ func (m *MsgBtcStakeExpand) GetFundingTx() []byte { return nil } +func (m *MsgBtcStakeExpand) GetMultisigInfo() *AdditionalStakerInfo { + if m != nil { + return m.MultisigInfo + } + return nil +} + // MsgBtcStakeExpandResponse is the response for MsgBtcStakeExpand type MsgBtcStakeExpandResponse struct { } @@ -1259,110 +1279,113 @@ func init() { func init() { proto.RegisterFile("babylon/btcstaking/v1/tx.proto", fileDescriptor_4baddb53e97f38f2) } var fileDescriptor_4baddb53e97f38f2 = []byte{ - // 1644 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x58, 0xcd, 0x6f, 0xdb, 0x46, - 0x16, 0x37, 0x25, 0xd9, 0x91, 0x9e, 0x3e, 0x2c, 0xd3, 0x5f, 0x32, 0x13, 0x4b, 0xb6, 0x92, 0x38, - 0x8e, 0xb3, 0x96, 0xe2, 0x24, 0x9b, 0xec, 0xda, 0xd8, 0xc5, 0x46, 0xb2, 0x83, 0xcd, 0x87, 0x51, - 0x87, 0x92, 0x7b, 0xe8, 0xa1, 0x2a, 0x45, 0x8d, 0x29, 0xc2, 0x12, 0x49, 0x70, 0x28, 0x41, 0x46, - 0x81, 0xa2, 0x08, 0x0a, 0x04, 0x3d, 0x14, 0x28, 0x50, 0xa0, 0xa7, 0x1e, 0x7a, 0xec, 0xad, 0x39, - 0xe4, 0x8f, 0xc8, 0x31, 0x08, 0x7a, 0x28, 0x7c, 0x30, 0x8a, 0xe4, 0x90, 0x5e, 0x0b, 0xf4, 0xde, - 0x82, 0x43, 0x72, 0x48, 0x29, 0xa2, 0x2d, 0x7f, 0x20, 0xe8, 0xa1, 0x17, 0xc3, 0x9a, 0xf9, 0xbd, - 0xdf, 0x7b, 0xf3, 0x9b, 0xf7, 0xde, 0xcc, 0x10, 0xd2, 0x55, 0xa1, 0xba, 0xd7, 0x50, 0x95, 0x7c, - 0xd5, 0x10, 0xb1, 0x21, 0xec, 0xca, 0x8a, 0x94, 0x6f, 0xaf, 0xe4, 0x8d, 0x4e, 0x4e, 0xd3, 0x55, - 0x43, 0x65, 0x27, 0xed, 0xf9, 0x9c, 0x3b, 0x9f, 0x6b, 0xaf, 0x70, 0x13, 0x92, 0x2a, 0xa9, 0x04, - 0x91, 0x37, 0xff, 0xb3, 0xc0, 0xdc, 0x8c, 0xa8, 0xe2, 0xa6, 0x8a, 0x2b, 0xd6, 0x84, 0xf5, 0xc3, - 0x9e, 0x9a, 0xb6, 0x7e, 0xe5, 0x9b, 0x98, 0xf0, 0x37, 0xb1, 0x64, 0x4f, 0x64, 0xfb, 0x07, 0xa0, - 0x09, 0xba, 0xd0, 0x74, 0x8c, 0x2f, 0xd9, 0xc6, 0xee, 0x7c, 0x15, 0x19, 0xc2, 0x8a, 0xf3, 0xdb, - 0x46, 0x65, 0x7c, 0x98, 0x54, 0xcd, 0x06, 0x2c, 0xf4, 0x07, 0x78, 0x56, 0x66, 0xe1, 0xc6, 0x84, - 0xa6, 0xac, 0xa8, 0x79, 0xf2, 0xd7, 0x1a, 0xca, 0x3e, 0x0d, 0xc2, 0xcc, 0x26, 0x96, 0x8a, 0x3a, - 0x12, 0x0c, 0x74, 0x4f, 0x56, 0x84, 0x86, 0x6c, 0xec, 0x6d, 0xe9, 0x6a, 0x5b, 0xae, 0x21, 0x9d, - 0xfd, 0x07, 0x84, 0x84, 0x5a, 0x4d, 0x4f, 0x31, 0x73, 0xcc, 0x62, 0xa4, 0x90, 0x7a, 0xf5, 0x7c, - 0x79, 0xc2, 0x5e, 0xfc, 0xdd, 0x5a, 0x4d, 0x47, 0x18, 0x97, 0x0c, 0x5d, 0x56, 0x24, 0x9e, 0xa0, - 0xd8, 0x0d, 0x88, 0xd6, 0x10, 0x16, 0x75, 0x59, 0x33, 0x64, 0x55, 0x49, 0x05, 0xe6, 0x98, 0xc5, - 0xe8, 0x8d, 0x8b, 0x39, 0xdb, 0xc2, 0x15, 0x99, 0xac, 0x31, 0xb7, 0xee, 0x42, 0x79, 0xaf, 0x1d, - 0xcb, 0xc3, 0x48, 0xd5, 0x10, 0x2b, 0xda, 0x6e, 0x2a, 0x34, 0xc7, 0x2c, 0xc6, 0x0a, 0x6b, 0xfb, - 0x07, 0x99, 0x3b, 0x92, 0x6c, 0xd4, 0x5b, 0xd5, 0x9c, 0xa8, 0x36, 0xf3, 0xf6, 0x62, 0x1b, 0x42, - 0x15, 0x2f, 0xcb, 0xaa, 0xf3, 0x33, 0xdf, 0xbe, 0x95, 0x37, 0xf6, 0x34, 0x84, 0x73, 0x85, 0xfb, - 0x5b, 0x37, 0x6f, 0x5d, 0xdf, 0x6a, 0x55, 0x1f, 0xa2, 0x3d, 0x7e, 0xb8, 0x6a, 0x88, 0x5b, 0xbb, - 0xec, 0x7f, 0x20, 0xa8, 0xa9, 0x5a, 0x6a, 0x98, 0x84, 0x74, 0x2d, 0xd7, 0x77, 0xef, 0x73, 0x5b, - 0xba, 0xaa, 0xee, 0x7c, 0xb0, 0xb3, 0xa5, 0x62, 0x8c, 0x30, 0x96, 0x55, 0xa5, 0x50, 0x2e, 0xf2, - 0xa6, 0x1d, 0xfb, 0x18, 0x40, 0x54, 0x9b, 0x4d, 0x99, 0x8c, 0xa6, 0xce, 0x11, 0x96, 0x05, 0x1f, - 0x96, 0x22, 0x05, 0xf2, 0x82, 0x81, 0x70, 0x21, 0xf2, 0xe2, 0x20, 0x33, 0xf4, 0xc3, 0xdb, 0x67, - 0x4b, 0x0c, 0xef, 0x21, 0x59, 0x8d, 0x3c, 0x79, 0xfb, 0x6c, 0x89, 0xe8, 0xf6, 0x20, 0x14, 0x0e, - 0x26, 0x43, 0xd9, 0x8b, 0x30, 0xef, 0xbb, 0x11, 0x3c, 0xc2, 0x9a, 0xaa, 0x60, 0x94, 0xfd, 0x36, - 0x00, 0xa3, 0x3d, 0x0e, 0xd8, 0x07, 0x10, 0xd2, 0x05, 0x03, 0xd9, 0x9b, 0x74, 0xdb, 0x74, 0xb7, - 0x7f, 0x90, 0x39, 0x6f, 0xc9, 0x8e, 0x6b, 0xbb, 0x39, 0x59, 0xcd, 0x37, 0x05, 0xa3, 0x9e, 0x7b, - 0x84, 0x24, 0x41, 0xdc, 0x5b, 0x47, 0xe2, 0xab, 0xe7, 0xcb, 0x60, 0xef, 0xca, 0x3a, 0x12, 0xad, - 0xd8, 0x08, 0x07, 0xfb, 0x18, 0xc2, 0x4d, 0xa1, 0x53, 0x21, 0x7c, 0x81, 0x53, 0xf1, 0x9d, 0x6b, - 0x0a, 0x1d, 0x33, 0x3e, 0xf6, 0x63, 0x18, 0x35, 0x29, 0xc5, 0xba, 0xa0, 0x48, 0xc8, 0x62, 0x0e, - 0x9e, 0x8a, 0x39, 0xde, 0x14, 0x3a, 0x45, 0xc2, 0x66, 0xf2, 0xaf, 0x86, 0x7e, 0xfd, 0x3e, 0xc3, - 0x64, 0xff, 0x60, 0x60, 0x7a, 0x13, 0x4b, 0x1b, 0x35, 0xd9, 0x38, 0x65, 0x16, 0x4f, 0xd2, 0xf4, - 0x33, 0x05, 0x88, 0x39, 0x19, 0xd4, 0x93, 0xdc, 0xc1, 0x13, 0x26, 0xf7, 0x66, 0x57, 0x26, 0x85, - 0x48, 0x44, 0xcb, 0xc7, 0x12, 0xc1, 0x27, 0x8b, 0xb2, 0xf3, 0x90, 0xf1, 0x11, 0x80, 0x66, 0xcf, - 0x37, 0x61, 0x98, 0xa2, 0x39, 0x56, 0x28, 0x17, 0xd7, 0x51, 0x03, 0x49, 0x02, 0x89, 0xeb, 0xdf, - 0x10, 0x35, 0xd7, 0x80, 0xf4, 0xca, 0x40, 0x52, 0x81, 0x05, 0x36, 0x07, 0x9d, 0xda, 0x0a, 0x9c, - 0xb0, 0xb6, 0xdc, 0x72, 0x0f, 0x9e, 0x59, 0xb9, 0x7f, 0x02, 0x89, 0x1d, 0xad, 0x62, 0xd1, 0x56, - 0x1a, 0x32, 0x36, 0x52, 0xa1, 0xb9, 0xe0, 0x69, 0xb9, 0xa3, 0x3b, 0x5a, 0xc1, 0x64, 0x7f, 0x24, - 0x63, 0x83, 0x9d, 0x87, 0x98, 0xbd, 0xba, 0x8a, 0x21, 0x37, 0x11, 0xe9, 0x2c, 0x71, 0x3e, 0x6a, - 0x8f, 0x95, 0xe5, 0x26, 0x62, 0x2f, 0x42, 0xdc, 0x81, 0xb4, 0x85, 0x46, 0x0b, 0xa5, 0x46, 0xe6, - 0x98, 0xc5, 0x20, 0xef, 0xd8, 0x7d, 0x68, 0x8e, 0xb1, 0xb3, 0x00, 0x94, 0xa7, 0x43, 0x3a, 0x4b, - 0x8c, 0x8f, 0x38, 0x2c, 0x1d, 0xb6, 0x0a, 0x9c, 0x3b, 0x5d, 0x91, 0x15, 0xb1, 0xd1, 0x32, 0xc5, - 0x33, 0x0f, 0x22, 0x75, 0x27, 0x15, 0x26, 0x92, 0x5f, 0xf6, 0x91, 0xfc, 0xbe, 0x83, 0x26, 0xda, - 0xf3, 0xd3, 0x94, 0xb5, 0x7b, 0x82, 0xbd, 0x01, 0x51, 0xdc, 0x10, 0x70, 0xdd, 0x8e, 0x21, 0x42, - 0x76, 0x61, 0x6c, 0xff, 0x20, 0x13, 0x2f, 0x94, 0x8b, 0x25, 0x7b, 0xa6, 0xdc, 0xe1, 0x01, 0xd3, - 0xff, 0x59, 0x03, 0xa6, 0x6a, 0x56, 0xf2, 0xa8, 0x7a, 0x85, 0x5a, 0x63, 0x59, 0x4a, 0x01, 0x31, - 0xff, 0xef, 0xfe, 0x41, 0x66, 0xf5, 0xd8, 0x42, 0x97, 0x64, 0x49, 0x11, 0x8c, 0x96, 0x8e, 0xf8, - 0x09, 0xca, 0xee, 0x04, 0x50, 0x92, 0x25, 0xf6, 0x32, 0x24, 0x5a, 0x4a, 0x55, 0x55, 0x6a, 0x54, - 0xf6, 0x28, 0x91, 0x3d, 0x4e, 0x47, 0x89, 0xf0, 0xf3, 0x10, 0xf3, 0xc0, 0x3a, 0xa9, 0x18, 0x51, - 0x35, 0xea, 0x82, 0x3a, 0xec, 0x15, 0x18, 0x75, 0x21, 0xd6, 0xee, 0xc4, 0xc9, 0xee, 0xb8, 0x0e, - 0xac, 0xfd, 0xd9, 0x80, 0x49, 0x17, 0xe8, 0x95, 0x29, 0xe1, 0x27, 0xd3, 0x38, 0xc5, 0xbb, 0x83, - 0xec, 0x53, 0x06, 0xe6, 0x5c, 0xc1, 0xfa, 0x30, 0x9a, 0xd2, 0x8d, 0x9e, 0x89, 0x74, 0xb3, 0xd4, - 0xcf, 0x76, 0x6f, 0x20, 0x25, 0x59, 0x5a, 0x4d, 0x9a, 0x1d, 0xc3, 0x5b, 0xeb, 0xd9, 0x39, 0x48, - 0xf7, 0x6f, 0x0a, 0xb4, 0x6f, 0x3c, 0x09, 0xc3, 0xd8, 0x26, 0x96, 0x0a, 0x86, 0x58, 0x32, 0xed, - 0x36, 0x3a, 0x9a, 0xa0, 0xd4, 0xfe, 0x6e, 0x19, 0x7f, 0xcd, 0x96, 0xd1, 0x53, 0xce, 0xe1, 0xd3, - 0x95, 0x73, 0xe4, 0xbd, 0x96, 0x33, 0x0c, 0x52, 0xce, 0xd1, 0x81, 0xca, 0x39, 0x76, 0xbc, 0x72, - 0x8e, 0x9f, 0x7d, 0x39, 0x27, 0xde, 0x43, 0x39, 0xb3, 0x77, 0x20, 0xa5, 0xe9, 0xa8, 0x2d, 0xab, - 0x2d, 0x5c, 0xf1, 0x9c, 0x14, 0x75, 0x01, 0xd7, 0x49, 0x3f, 0x89, 0xf0, 0x93, 0xce, 0x7c, 0xc9, - 0x49, 0x91, 0xff, 0x0b, 0xb8, 0x6e, 0x66, 0xd1, 0x4e, 0x8b, 0x6a, 0x9a, 0xb4, 0xb2, 0xc8, 0x1e, - 0x29, 0x77, 0xfa, 0xb4, 0x89, 0xf3, 0xe4, 0xa1, 0xd0, 0xdd, 0x03, 0x68, 0x87, 0xf8, 0x89, 0x21, - 0xb7, 0xd7, 0xbb, 0xb5, 0x5a, 0x57, 0x07, 0xe9, 0x39, 0x69, 0xa6, 0x60, 0x04, 0xcb, 0x92, 0x82, - 0xec, 0x66, 0xc1, 0xdb, 0xbf, 0xd8, 0x05, 0x18, 0xed, 0x8d, 0x9d, 0x5c, 0x3e, 0xf9, 0x38, 0xee, - 0x8a, 0xf9, 0xf0, 0xd3, 0x30, 0x78, 0x16, 0xa7, 0xe1, 0x6a, 0xd4, 0x5c, 0xb8, 0x1d, 0x58, 0xf6, - 0x1a, 0x5c, 0x3d, 0x72, 0x55, 0x54, 0x83, 0xdf, 0x83, 0xc0, 0x5a, 0xe8, 0xa2, 0xda, 0x46, 0x8a, - 0xa0, 0x18, 0x25, 0x59, 0xc2, 0xbe, 0x8b, 0x7e, 0x08, 0x01, 0xe7, 0x8e, 0x79, 0xba, 0x26, 0x13, - 0xd0, 0x76, 0xfb, 0x29, 0x18, 0xec, 0xa7, 0xe0, 0x22, 0x24, 0x3d, 0x59, 0x6f, 0xa6, 0x29, 0xb6, - 0xfa, 0x1c, 0x9f, 0x70, 0xdb, 0x01, 0x09, 0xbb, 0x0e, 0x49, 0x6f, 0xd5, 0x91, 0x8c, 0x1e, 0x3e, - 0x93, 0x8c, 0x4e, 0x78, 0x2a, 0xd7, 0x4c, 0xe1, 0x35, 0xe0, 0x68, 0x4c, 0xbd, 0x2e, 0x71, 0x6a, - 0x84, 0x44, 0x37, 0xed, 0x20, 0xb6, 0xbb, 0x6c, 0x31, 0x8b, 0x61, 0x8a, 0x24, 0x69, 0x05, 0x99, - 0x09, 0x49, 0xb2, 0xc1, 0x0e, 0xf6, 0xdc, 0x99, 0x04, 0x3b, 0x8e, 0x69, 0xb6, 0x9b, 0xe4, 0xc4, - 0x6b, 0x77, 0x8e, 0x5c, 0x00, 0xee, 0xdd, 0x5d, 0xa7, 0x49, 0xf1, 0x63, 0x00, 0x92, 0x66, 0xd9, - 0x94, 0x8b, 0xdb, 0x8a, 0x5d, 0xc9, 0xe8, 0xd4, 0x75, 0xb0, 0x04, 0x63, 0xd6, 0xa2, 0xb1, 0x86, - 0x68, 0x09, 0x93, 0xa3, 0x90, 0x27, 0x04, 0xa8, 0x64, 0x8f, 0x97, 0x3b, 0xac, 0x0a, 0xf3, 0xef, - 0x60, 0xdf, 0x29, 0x9d, 0xd0, 0x71, 0x4a, 0x67, 0xb6, 0xc7, 0x45, 0x4f, 0x91, 0xaf, 0xc0, 0x04, - 0x6d, 0x2c, 0xba, 0xa0, 0x60, 0x41, 0x34, 0x6b, 0x06, 0xa7, 0x86, 0xc9, 0x46, 0x8e, 0x3b, 0x2d, - 0xc6, 0x33, 0xd5, 0xad, 0x27, 0x07, 0xa9, 0x5e, 0xc1, 0xa8, 0x9a, 0x5f, 0x32, 0x70, 0x61, 0x13, - 0x4b, 0x25, 0xd4, 0x40, 0xa2, 0x21, 0xb7, 0x91, 0xd3, 0x09, 0x37, 0xcc, 0x77, 0x8e, 0x22, 0xfa, - 0x2b, 0xbb, 0x0c, 0xe3, 0x3a, 0x12, 0xd5, 0x36, 0xd2, 0x51, 0xad, 0x62, 0x9f, 0xf3, 0xd8, 0xbe, - 0x3e, 0xf0, 0x49, 0x3a, 0x75, 0xcf, 0x3c, 0xae, 0x4b, 0xbb, 0x5d, 0x01, 0x3d, 0x08, 0x85, 0x03, - 0xc9, 0x20, 0xdf, 0xbb, 0x33, 0xd9, 0x05, 0xb8, 0x74, 0x58, 0x28, 0xee, 0x93, 0x9d, 0x81, 0xd1, - 0x4d, 0x2c, 0x6d, 0x6b, 0x35, 0xc1, 0x40, 0x5b, 0xe4, 0xeb, 0x0f, 0x7b, 0x1b, 0x22, 0x42, 0xcb, - 0xa8, 0xab, 0xba, 0x6c, 0xec, 0x1d, 0x79, 0x71, 0x72, 0xa1, 0xec, 0x1a, 0x8c, 0x58, 0xdf, 0x8f, - 0xec, 0xab, 0xd3, 0xac, 0xdf, 0xd5, 0x89, 0x80, 0x0a, 0x21, 0xf3, 0x85, 0xcd, 0xdb, 0x26, 0xab, - 0x09, 0x73, 0x51, 0x2e, 0x59, 0x76, 0x86, 0xbc, 0x98, 0xbd, 0x71, 0x39, 0x31, 0xdf, 0xf8, 0x2d, - 0x0c, 0xc1, 0x4d, 0x2c, 0xb1, 0x5f, 0x30, 0x30, 0xe5, 0xf3, 0x69, 0xe8, 0xba, 0x8f, 0x6b, 0xdf, - 0x6f, 0x18, 0xdc, 0xbf, 0x8e, 0x6b, 0xe1, 0x84, 0xc3, 0x7e, 0x06, 0x13, 0x7d, 0x1f, 0xf6, 0x39, - 0x7f, 0xc6, 0x7e, 0x78, 0xee, 0xf6, 0xf1, 0xf0, 0xd4, 0xff, 0xa7, 0x30, 0xde, 0xef, 0xcd, 0xbc, - 0x7c, 0xd4, 0x82, 0xba, 0xe0, 0xdc, 0x3f, 0x8f, 0x05, 0xa7, 0xce, 0xbf, 0x63, 0x20, 0x7d, 0xc4, - 0xb9, 0x7a, 0x88, 0xb2, 0x87, 0x5b, 0x72, 0xff, 0x3b, 0xa9, 0x25, 0x0d, 0x4f, 0x85, 0xd1, 0xde, - 0x13, 0xef, 0xea, 0xa1, 0xa4, 0x5e, 0x28, 0xb7, 0x32, 0x30, 0x94, 0x3a, 0x94, 0x21, 0xde, 0xdd, - 0x4d, 0xaf, 0xf8, 0x73, 0x74, 0x01, 0xb9, 0xfc, 0x80, 0x40, 0xea, 0xea, 0x2b, 0x06, 0x66, 0xfc, - 0x7b, 0xcd, 0x4d, 0x7f, 0x3a, 0x5f, 0x23, 0x6e, 0xed, 0x04, 0x46, 0x34, 0x9e, 0x1d, 0x88, 0x75, - 0xb5, 0x91, 0x05, 0x7f, 0x32, 0x2f, 0x8e, 0xcb, 0x0d, 0x86, 0xa3, 0x7e, 0x1a, 0x90, 0xe8, 0x79, - 0xeb, 0x2d, 0x1e, 0x22, 0x5d, 0x17, 0x92, 0xbb, 0x3e, 0x28, 0xd2, 0xf1, 0xc6, 0x0d, 0x7f, 0xfe, - 0xf6, 0xd9, 0x12, 0x53, 0x78, 0xfc, 0xe2, 0x75, 0x9a, 0x79, 0xf9, 0x3a, 0xcd, 0xfc, 0xf2, 0x3a, - 0xcd, 0x7c, 0xfd, 0x26, 0x3d, 0xf4, 0xf2, 0x4d, 0x7a, 0xe8, 0xe7, 0x37, 0xe9, 0xa1, 0x8f, 0x06, - 0xbb, 0x19, 0x75, 0xbc, 0x5f, 0xbf, 0xc9, 0x61, 0x5e, 0x1d, 0x21, 0xdf, 0xb8, 0x6f, 0xfe, 0x19, - 0x00, 0x00, 0xff, 0xff, 0xa8, 0x1b, 0xbe, 0x4e, 0x0c, 0x18, 0x00, 0x00, + // 1684 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x58, 0xcf, 0x6f, 0x23, 0x49, + 0x15, 0x4e, 0xdb, 0x4e, 0xc6, 0x7e, 0xfe, 0x11, 0xa7, 0x93, 0x49, 0x3a, 0xbd, 0x1b, 0x3b, 0xf1, + 0xec, 0x66, 0xb3, 0x59, 0x62, 0x4f, 0x66, 0x96, 0x59, 0x48, 0x04, 0x62, 0xec, 0x64, 0xc5, 0xcc, + 0x6e, 0x84, 0xa7, 0xed, 0x70, 0xe0, 0x80, 0x69, 0x77, 0x97, 0xdb, 0xa5, 0xd8, 0x5d, 0xad, 0xae, + 0xb6, 0xe5, 0x08, 0x09, 0x21, 0x84, 0xb4, 0xe2, 0x80, 0xc4, 0x89, 0x13, 0x07, 0x8e, 0xdc, 0x98, + 0xc3, 0xfe, 0x11, 0x73, 0x5c, 0xad, 0x38, 0xa0, 0x1c, 0x22, 0x34, 0x23, 0x34, 0x5c, 0x91, 0xb8, + 0x83, 0xba, 0xfa, 0x97, 0xed, 0xb8, 0x13, 0xe7, 0x87, 0x56, 0x1c, 0xf6, 0x12, 0xa5, 0xab, 0xbe, + 0xfa, 0xea, 0xd5, 0x57, 0xef, 0x7d, 0x55, 0x2e, 0xc8, 0x35, 0xe5, 0xe6, 0x69, 0x87, 0xe8, 0xa5, + 0xa6, 0xa5, 0x50, 0x4b, 0x3e, 0xc1, 0xba, 0x56, 0xea, 0xef, 0x96, 0xac, 0x41, 0xd1, 0x30, 0x89, + 0x45, 0xf8, 0xfb, 0x6e, 0x7f, 0x31, 0xe8, 0x2f, 0xf6, 0x77, 0xc5, 0x25, 0x8d, 0x68, 0x84, 0x21, + 0x4a, 0xf6, 0x7f, 0x0e, 0x58, 0x5c, 0x55, 0x08, 0xed, 0x12, 0xda, 0x70, 0x3a, 0x9c, 0x0f, 0xb7, + 0x6b, 0xc5, 0xf9, 0x2a, 0x75, 0x29, 0xe3, 0xef, 0x52, 0xcd, 0xed, 0x28, 0x4c, 0x0e, 0xc0, 0x90, + 0x4d, 0xb9, 0xeb, 0x0d, 0x7e, 0xcf, 0x1d, 0x1c, 0xf4, 0x37, 0x91, 0x25, 0xef, 0x7a, 0xdf, 0x2e, + 0x2a, 0x1f, 0xc2, 0x44, 0x0c, 0x17, 0xb0, 0x39, 0x19, 0x30, 0xb4, 0x32, 0x07, 0xb7, 0x20, 0x77, + 0xb1, 0x4e, 0x4a, 0xec, 0xaf, 0xd3, 0x54, 0xf8, 0x22, 0x0a, 0xab, 0x47, 0x54, 0xab, 0x98, 0x48, + 0xb6, 0xd0, 0xa7, 0x58, 0x97, 0x3b, 0xd8, 0x3a, 0xad, 0x9a, 0xa4, 0x8f, 0x55, 0x64, 0xf2, 0xdf, + 0x81, 0x98, 0xac, 0xaa, 0xa6, 0xc0, 0xad, 0x73, 0x5b, 0x89, 0xb2, 0xf0, 0xf5, 0x97, 0x3b, 0x4b, + 0xee, 0xe2, 0x9f, 0xaa, 0xaa, 0x89, 0x28, 0xad, 0x59, 0x26, 0xd6, 0x35, 0x89, 0xa1, 0xf8, 0x43, + 0x48, 0xaa, 0x88, 0x2a, 0x26, 0x36, 0x2c, 0x4c, 0x74, 0x21, 0xb2, 0xce, 0x6d, 0x25, 0x1f, 0x3d, + 0x28, 0xba, 0x23, 0x02, 0x91, 0xd9, 0x1a, 0x8b, 0x07, 0x01, 0x54, 0x1a, 0x1e, 0xc7, 0x4b, 0x30, + 0xd7, 0xb4, 0x94, 0x86, 0x71, 0x22, 0xc4, 0xd6, 0xb9, 0xad, 0x54, 0x79, 0xff, 0xec, 0x3c, 0xff, + 0x89, 0x86, 0xad, 0x76, 0xaf, 0x59, 0x54, 0x48, 0xb7, 0xe4, 0x2e, 0xb6, 0x23, 0x37, 0xe9, 0x0e, + 0x26, 0xde, 0x67, 0xa9, 0xff, 0x71, 0xc9, 0x3a, 0x35, 0x10, 0x2d, 0x96, 0x9f, 0x55, 0x1f, 0x7f, + 0xfc, 0xb0, 0xda, 0x6b, 0x7e, 0x86, 0x4e, 0xa5, 0xd9, 0xa6, 0xa5, 0x54, 0x4f, 0xf8, 0x1f, 0x40, + 0xd4, 0x20, 0x86, 0x30, 0xcb, 0x42, 0xfa, 0xa8, 0x38, 0x71, 0xef, 0x8b, 0x55, 0x93, 0x90, 0xd6, + 0x4f, 0x5a, 0x55, 0x42, 0x29, 0xa2, 0x14, 0x13, 0xbd, 0x5c, 0xaf, 0x48, 0xf6, 0x38, 0xfe, 0x05, + 0x80, 0x42, 0xba, 0x5d, 0xcc, 0x5a, 0x85, 0x7b, 0x8c, 0x65, 0x33, 0x84, 0xa5, 0xe2, 0x03, 0x25, + 0xd9, 0x42, 0xb4, 0x9c, 0x78, 0x75, 0x9e, 0x9f, 0xf9, 0xcb, 0xdb, 0x97, 0xdb, 0x9c, 0x34, 0x44, + 0xb2, 0x97, 0xf8, 0xcd, 0xdb, 0x97, 0xdb, 0x4c, 0xb7, 0xe7, 0xb1, 0x78, 0x34, 0x1b, 0x2b, 0x3c, + 0x80, 0x8d, 0xd0, 0x8d, 0x90, 0x10, 0x35, 0x88, 0x4e, 0x51, 0xe1, 0x8f, 0x11, 0x98, 0x1f, 0x9b, + 0x80, 0x7f, 0x0e, 0x31, 0x53, 0xb6, 0x90, 0xbb, 0x49, 0x4f, 0xec, 0xe9, 0xce, 0xce, 0xf3, 0xef, + 0x38, 0xb2, 0x53, 0xf5, 0xa4, 0x88, 0x49, 0xa9, 0x2b, 0x5b, 0xed, 0xe2, 0xe7, 0x48, 0x93, 0x95, + 0xd3, 0x03, 0xa4, 0x7c, 0xfd, 0xe5, 0x0e, 0xb8, 0xbb, 0x72, 0x80, 0x14, 0x27, 0x36, 0xc6, 0xc1, + 0xbf, 0x80, 0x78, 0x57, 0x1e, 0x34, 0x18, 0x5f, 0xe4, 0x56, 0x7c, 0xf7, 0xba, 0xf2, 0xc0, 0x8e, + 0x8f, 0xff, 0x39, 0xcc, 0xdb, 0x94, 0x4a, 0x5b, 0xd6, 0x35, 0xe4, 0x30, 0x47, 0x6f, 0xc5, 0x9c, + 0xee, 0xca, 0x83, 0x0a, 0x63, 0xb3, 0xf9, 0xf7, 0x62, 0xff, 0xfa, 0x73, 0x9e, 0x2b, 0xfc, 0x97, + 0x83, 0x95, 0x23, 0xaa, 0x1d, 0xaa, 0xd8, 0xba, 0x65, 0x16, 0xdf, 0xf7, 0xd3, 0xcf, 0x16, 0x20, + 0xe5, 0x65, 0xd0, 0x58, 0x72, 0x47, 0x6f, 0x98, 0xdc, 0x47, 0x23, 0x99, 0x14, 0x63, 0x11, 0xed, + 0x5c, 0x4b, 0x84, 0x90, 0x2c, 0x2a, 0x6c, 0x40, 0x3e, 0x44, 0x00, 0x3f, 0x7b, 0xfe, 0x19, 0x87, + 0x65, 0x3f, 0xc7, 0xca, 0xf5, 0xca, 0x01, 0xea, 0x20, 0x4d, 0x66, 0x71, 0x7d, 0x1f, 0x92, 0xf6, + 0x1a, 0x90, 0xd9, 0x98, 0x4a, 0x2a, 0x70, 0xc0, 0x76, 0xa3, 0x57, 0x5b, 0x91, 0x1b, 0xd6, 0x56, + 0x50, 0xee, 0xd1, 0x3b, 0x2b, 0xf7, 0x5f, 0x40, 0xa6, 0x65, 0x34, 0x1c, 0xda, 0x46, 0x07, 0x53, + 0x4b, 0x88, 0xad, 0x47, 0x6f, 0xcb, 0x9d, 0x6c, 0x19, 0x65, 0x9b, 0xfd, 0x73, 0x4c, 0x2d, 0x7e, + 0x03, 0x52, 0xee, 0xea, 0x1a, 0x16, 0xee, 0x22, 0xe6, 0x2c, 0x69, 0x29, 0xe9, 0xb6, 0xd5, 0x71, + 0x17, 0xf1, 0x0f, 0x20, 0xed, 0x41, 0xfa, 0x72, 0xa7, 0x87, 0x84, 0xb9, 0x75, 0x6e, 0x2b, 0x2a, + 0x79, 0xe3, 0x7e, 0x6a, 0xb7, 0xf1, 0x6b, 0x00, 0x3e, 0xcf, 0x80, 0x39, 0x4b, 0x4a, 0x4a, 0x78, + 0x2c, 0x03, 0xbe, 0x09, 0x62, 0xd0, 0xdd, 0xc0, 0xba, 0xd2, 0xe9, 0xd9, 0xe2, 0xd9, 0x07, 0x11, + 0x69, 0x09, 0x71, 0x26, 0xf9, 0xfb, 0x21, 0x92, 0x3f, 0xf3, 0xd0, 0x4c, 0x7b, 0x69, 0xc5, 0x67, + 0x1d, 0xed, 0xe0, 0x1f, 0x41, 0x92, 0x76, 0x64, 0xda, 0x76, 0x63, 0x48, 0xb0, 0x5d, 0x58, 0x38, + 0x3b, 0xcf, 0xa7, 0xcb, 0xf5, 0x4a, 0xcd, 0xed, 0xa9, 0x0f, 0x24, 0xa0, 0xfe, 0xff, 0xbc, 0x05, + 0xcb, 0xaa, 0x93, 0x3c, 0xc4, 0x6c, 0xf8, 0xa3, 0x29, 0xd6, 0x04, 0x60, 0xc3, 0x7f, 0x78, 0x76, + 0x9e, 0xdf, 0xbb, 0xb6, 0xd0, 0x35, 0xac, 0xe9, 0xb2, 0xd5, 0x33, 0x91, 0xb4, 0xe4, 0xb3, 0x7b, + 0x01, 0xd4, 0xb0, 0xc6, 0xbf, 0x0f, 0x99, 0x9e, 0xde, 0x24, 0xba, 0xea, 0xcb, 0x9e, 0x64, 0xb2, + 0xa7, 0xfd, 0x56, 0x26, 0xfc, 0x06, 0xa4, 0x86, 0x60, 0x03, 0x21, 0xc5, 0x54, 0x4d, 0x06, 0xa0, + 0x01, 0xff, 0x01, 0xcc, 0x07, 0x10, 0x67, 0x77, 0xd2, 0x6c, 0x77, 0x82, 0x09, 0x9c, 0xfd, 0x39, + 0x84, 0xfb, 0x01, 0x70, 0x58, 0xa6, 0x4c, 0x98, 0x4c, 0x8b, 0x3e, 0x3e, 0x68, 0xe4, 0xbf, 0xe0, + 0x60, 0x3d, 0x10, 0x6c, 0x02, 0xa3, 0x2d, 0xdd, 0xfc, 0x9d, 0x48, 0xb7, 0xe6, 0xcf, 0x73, 0x3c, + 0x1e, 0x88, 0xad, 0x61, 0x15, 0xd2, 0xdd, 0x5e, 0xc7, 0xc2, 0x14, 0x6b, 0x0d, 0xac, 0xb7, 0x88, + 0x90, 0xbd, 0xb4, 0x6e, 0x9f, 0xaa, 0x2a, 0xb6, 0x0d, 0x42, 0xee, 0xd4, 0x58, 0xc5, 0x3f, 0xd3, + 0x5b, 0x44, 0x4a, 0x79, 0x0c, 0xf6, 0xd7, 0x5e, 0xd6, 0xf6, 0xa0, 0x61, 0xf7, 0x28, 0xac, 0x43, + 0x6e, 0xb2, 0xcd, 0xf8, 0x4e, 0xf4, 0xb7, 0x38, 0x2c, 0x1c, 0x51, 0xad, 0x6c, 0x29, 0x8c, 0xf6, + 0x70, 0x60, 0xc8, 0xba, 0xfa, 0xad, 0x09, 0xfd, 0x7f, 0x9a, 0xd0, 0x98, 0x41, 0xc4, 0x6f, 0x67, + 0x10, 0x89, 0x6f, 0xd4, 0x20, 0x60, 0x1a, 0x83, 0x48, 0x4e, 0x65, 0x10, 0xa9, 0xeb, 0x19, 0x44, + 0xfa, 0xee, 0x0d, 0x22, 0xf3, 0x4d, 0x18, 0xc4, 0x27, 0x20, 0x18, 0x26, 0xea, 0x63, 0xd2, 0xa3, + 0x8d, 0xa1, 0xb3, 0xa7, 0x2d, 0xd3, 0x36, 0x73, 0xa8, 0x84, 0x74, 0xdf, 0xeb, 0xaf, 0x79, 0x29, + 0xf2, 0x63, 0x99, 0xb6, 0xed, 0x2c, 0x6a, 0xf5, 0x7c, 0x4d, 0xb3, 0x4e, 0x16, 0xb9, 0x2d, 0xf5, + 0xc1, 0x45, 0xe3, 0x59, 0xb8, 0x7b, 0xe3, 0x79, 0x87, 0xfd, 0x98, 0x19, 0x75, 0x95, 0xc0, 0x73, + 0x38, 0x76, 0xc3, 0x7e, 0xaa, 0xaa, 0x23, 0x9e, 0x34, 0x76, 0x1a, 0x2e, 0xc3, 0x1c, 0xc5, 0x9a, + 0x8e, 0x5c, 0xfb, 0x91, 0xdc, 0x2f, 0x7e, 0x13, 0xe6, 0xc7, 0xd5, 0x60, 0x17, 0x64, 0x29, 0x4d, + 0x47, 0x54, 0xb8, 0xfc, 0xc4, 0x8e, 0xde, 0xc5, 0x89, 0xbd, 0x97, 0xb4, 0x17, 0xee, 0x06, 0x56, + 0xf8, 0x08, 0x3e, 0xbc, 0x72, 0x55, 0xbe, 0x06, 0xff, 0x89, 0x02, 0xef, 0xa0, 0x2b, 0xa4, 0x8f, + 0x74, 0x59, 0xb7, 0x6a, 0x58, 0xa3, 0xa1, 0x8b, 0xfe, 0x0c, 0x22, 0xde, 0x3d, 0xf8, 0x76, 0xb6, + 0x15, 0x31, 0x4e, 0x26, 0x29, 0x18, 0x9d, 0xa4, 0xe0, 0x16, 0x64, 0x87, 0xea, 0xc8, 0x4e, 0x7c, + 0xea, 0x38, 0xa7, 0x94, 0x09, 0x0c, 0x86, 0x85, 0xdd, 0x86, 0xec, 0x70, 0x1d, 0xb3, 0x1a, 0x99, + 0xbd, 0x93, 0x1a, 0xc9, 0x0c, 0x79, 0x81, 0x5d, 0x14, 0xfb, 0x20, 0xfa, 0x31, 0x8d, 0x4f, 0x49, + 0x85, 0x39, 0x16, 0xdd, 0x8a, 0x87, 0x38, 0x1e, 0x19, 0x4b, 0x79, 0x0a, 0xcb, 0x2c, 0x49, 0x1b, + 0xc8, 0x4e, 0x48, 0x96, 0x0d, 0x6e, 0xb0, 0xf7, 0xee, 0x24, 0xd8, 0x45, 0xea, 0x67, 0xbb, 0x4d, + 0xce, 0x66, 0x1d, 0xcd, 0x91, 0x77, 0x41, 0xbc, 0xb8, 0xeb, 0x7e, 0x52, 0xfc, 0x35, 0x02, 0x59, + 0xbb, 0x6c, 0xea, 0x95, 0x63, 0xdd, 0xf5, 0x06, 0x74, 0xeb, 0x3a, 0xd8, 0x86, 0x05, 0x67, 0xd1, + 0xd4, 0x40, 0xbe, 0x29, 0xb0, 0xc3, 0x55, 0x62, 0x04, 0xa8, 0xe6, 0xb6, 0xd7, 0x07, 0x3c, 0x81, + 0x8d, 0x0b, 0xd8, 0x0b, 0xa5, 0x13, 0xbb, 0x4e, 0xe9, 0xac, 0x8d, 0x4d, 0x31, 0x56, 0xe4, 0xbb, + 0xb0, 0xe4, 0x5b, 0x95, 0x29, 0xeb, 0x54, 0x56, 0xec, 0x9a, 0xa1, 0xc2, 0x2c, 0xdb, 0xc8, 0x45, + 0xcf, 0xb4, 0x86, 0xba, 0x46, 0xf5, 0x14, 0x41, 0x18, 0x17, 0xcc, 0x57, 0xf3, 0x77, 0x1c, 0xbc, + 0x7b, 0x44, 0xb5, 0x1a, 0xea, 0x20, 0xc5, 0xc2, 0x7d, 0xe4, 0x79, 0xeb, 0xa1, 0xfd, 0x5b, 0x4c, + 0x57, 0xc2, 0x95, 0xdd, 0x81, 0x45, 0x13, 0x29, 0xa4, 0x8f, 0x4c, 0xa4, 0x36, 0xdc, 0x9b, 0x03, + 0x75, 0x2f, 0x24, 0x52, 0xd6, 0xef, 0xfa, 0xd4, 0xbe, 0x00, 0xd4, 0x4e, 0x46, 0x02, 0x7a, 0x1e, + 0x8b, 0x47, 0xb2, 0x51, 0x69, 0x7c, 0x67, 0x0a, 0x9b, 0xf0, 0xde, 0x65, 0xa1, 0x04, 0xcf, 0x0a, + 0x1c, 0xcc, 0x1f, 0x51, 0xed, 0xd8, 0x50, 0x65, 0x0b, 0x55, 0xd9, 0x0b, 0x15, 0xff, 0x04, 0x12, + 0x72, 0xcf, 0x6a, 0x13, 0x13, 0x5b, 0xa7, 0x57, 0x5e, 0xc5, 0x02, 0x28, 0xbf, 0x0f, 0x73, 0xce, + 0x1b, 0x97, 0x7b, 0x19, 0x5b, 0x0b, 0xbb, 0x8c, 0x31, 0x50, 0x39, 0xf6, 0xea, 0x3c, 0x3f, 0x23, + 0xb9, 0x43, 0xf6, 0x32, 0xf6, 0xa2, 0x02, 0xb2, 0xc2, 0x2a, 0xfb, 0x55, 0x3f, 0x1c, 0x97, 0x17, + 0xf3, 0xa3, 0x7f, 0xc7, 0x21, 0x7a, 0x44, 0x35, 0xfe, 0xb7, 0x1c, 0x2c, 0x87, 0x3c, 0x5f, 0x3d, + 0x0c, 0x99, 0x3a, 0xf4, 0x9d, 0x45, 0xfc, 0xde, 0x75, 0x47, 0x78, 0xe1, 0xf0, 0xbf, 0x82, 0xa5, + 0x89, 0x8f, 0x0f, 0xc5, 0x70, 0xc6, 0x49, 0x78, 0xf1, 0xc9, 0xf5, 0xf0, 0xfe, 0xfc, 0xbf, 0x84, + 0xc5, 0x49, 0xbf, 0xeb, 0x77, 0xae, 0x5a, 0xd0, 0x08, 0x5c, 0xfc, 0xee, 0xb5, 0xe0, 0xfe, 0xe4, + 0x7f, 0xe2, 0x20, 0x77, 0xc5, 0xb9, 0x7a, 0x89, 0xb2, 0x97, 0x8f, 0x14, 0x7f, 0x74, 0xd3, 0x91, + 0x7e, 0x78, 0x04, 0xe6, 0xc7, 0x4f, 0xbc, 0x0f, 0x2f, 0x25, 0x1d, 0x86, 0x8a, 0xbb, 0x53, 0x43, + 0xfd, 0x09, 0x31, 0xa4, 0x47, 0xdd, 0xf4, 0x83, 0x70, 0x8e, 0x11, 0xa0, 0x58, 0x9a, 0x12, 0xe8, + 0x4f, 0xf5, 0x7b, 0x0e, 0x56, 0xc3, 0xbd, 0xe6, 0x71, 0x38, 0x5d, 0xe8, 0x20, 0x71, 0xff, 0x06, + 0x83, 0xfc, 0x78, 0x5a, 0x90, 0x1a, 0xb1, 0x91, 0xcd, 0x70, 0xb2, 0x61, 0x9c, 0x58, 0x9c, 0x0e, + 0xe7, 0xcf, 0xd3, 0x81, 0xcc, 0xd8, 0xaf, 0xc7, 0xad, 0x4b, 0xa4, 0x1b, 0x41, 0x8a, 0x0f, 0xa7, + 0x45, 0x7a, 0xb3, 0x89, 0xb3, 0xbf, 0x7e, 0xfb, 0x72, 0x9b, 0x2b, 0xbf, 0x78, 0xf5, 0x3a, 0xc7, + 0x7d, 0xf5, 0x3a, 0xc7, 0xfd, 0xe3, 0x75, 0x8e, 0xfb, 0xc3, 0x9b, 0xdc, 0xcc, 0x57, 0x6f, 0x72, + 0x33, 0x7f, 0x7f, 0x93, 0x9b, 0xf9, 0xd9, 0x74, 0x37, 0xa3, 0xc1, 0xf0, 0x0b, 0x3d, 0x3b, 0xcc, + 0x9b, 0x73, 0xec, 0x1d, 0xfe, 0xf1, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x9c, 0x2e, 0xd1, 0x42, + 0xb0, 0x18, 0x00, 0x00, } func (this *CommissionRates) Equal(that interface{}) bool { @@ -2042,6 +2065,20 @@ func (m *MsgCreateBTCDelegation) MarshalToSizedBuffer(dAtA []byte) (int, error) _ = i var l int _ = l + if m.MultisigInfo != nil { + { + size, err := m.MultisigInfo.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x82 + } if m.DelegatorUnbondingSlashingSig != nil { { size := m.DelegatorUnbondingSlashingSig.Size() @@ -2227,6 +2264,20 @@ func (m *MsgBtcStakeExpand) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.MultisigInfo != nil { + { + size, err := m.MultisigInfo.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x8a + } if len(m.FundingTx) > 0 { i -= len(m.FundingTx) copy(dAtA[i:], m.FundingTx) @@ -2953,6 +3004,10 @@ func (m *MsgCreateBTCDelegation) Size() (n int) { l = m.DelegatorUnbondingSlashingSig.Size() n += 1 + l + sovTx(uint64(l)) } + if m.MultisigInfo != nil { + l = m.MultisigInfo.Size() + n += 2 + l + sovTx(uint64(l)) + } return n } @@ -3033,6 +3088,10 @@ func (m *MsgBtcStakeExpand) Size() (n int) { if l > 0 { n += 2 + l + sovTx(uint64(l)) } + if m.MultisigInfo != nil { + l = m.MultisigInfo.Size() + n += 2 + l + sovTx(uint64(l)) + } return n } @@ -4370,6 +4429,42 @@ func (m *MsgCreateBTCDelegation) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 16: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MultisigInfo", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MultisigInfo == nil { + m.MultisigInfo = &AdditionalStakerInfo{} + } + if err := m.MultisigInfo.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTx(dAtA[iNdEx:]) @@ -4958,6 +5053,42 @@ func (m *MsgBtcStakeExpand) Unmarshal(dAtA []byte) error { m.FundingTx = []byte{} } iNdEx = postIndex + case 17: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MultisigInfo", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.MultisigInfo == nil { + m.MultisigInfo = &AdditionalStakerInfo{} + } + if err := m.MultisigInfo.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipTx(dAtA[iNdEx:]) diff --git a/x/btcstaking/types/validate_parsed_message.go b/x/btcstaking/types/validate_parsed_message.go index cd97e6016..a67f30ef5 100644 --- a/x/btcstaking/types/validate_parsed_message.go +++ b/x/btcstaking/types/validate_parsed_message.go @@ -3,7 +3,8 @@ package types import ( "bytes" "fmt" - + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/chaincfg" "github.com/babylonlabs-io/babylon/v4/btcstaking" @@ -21,6 +22,8 @@ func ValidateParsedMessageAgainstTheParams( parameters *Params, net *chaincfg.Params, ) (*ParamsValidationResult, error) { + var stakingOutputIdx uint32 + // 1. Validate unbonding time first as it will be used in other checks // Check unbonding time (staking time from unbonding tx) is not less than min unbonding time if uint32(pm.UnbondingTime) != parameters.UnbondingTimeBlocks { @@ -31,143 +34,275 @@ func ValidateParsedMessageAgainstTheParams( stakingTxHash := pm.StakingTx.Transaction.TxHash() covenantPks := parameters.MustGetCovenantPks() - // 2. Validate all data related to staking tx: - // - it has valid staking output - // - that staking time and value are correct - // - slashing tx is relevant to staking tx - // - slashing tx signature is valid - stakingInfo, err := btcstaking.BuildStakingInfo( - pm.StakerPK.PublicKey, - pm.FinalityProviderKeys.PublicKeys, - covenantPks, - parameters.CovenantQuorum, - pm.StakingTime, - pm.StakingValue, - net, - ) - if err != nil { - return nil, ErrInvalidStakingTx.Wrapf("failed to build staking info: %v", err) - } + // handle multi-sig btc delegation and single-sig btc delegation separately + if pm.MultisigInfo != nil { + var ( + stakerKeys []*btcec.PublicKey + slashingPubkey2Sig = make(map[*btcec.PublicKey][]byte) + unbondingSlashingPubkey2Sig = make(map[*btcec.PublicKey][]byte) + ) - stakingOutputIdx, err := bbn.GetOutputIdxInBTCTx(pm.StakingTx.Transaction, stakingInfo.StakingOutput) + if err := validateStakerQuorumAndPubKeyListAgainstParams(pm, parameters); err != nil { + return nil, err + } - if err != nil { - return nil, ErrInvalidStakingTx.Wrap("staking tx does not contain expected staking output") - } + stakerKeys, slashingPubkey2Sig, unbondingSlashingPubkey2Sig, err := buildAndVerifyStakerKeysAndPubKey2Sig(pm) + if err != nil { + return nil, err + } - if uint32(pm.StakingTime) < parameters.MinStakingTimeBlocks || - uint32(pm.StakingTime) > parameters.MaxStakingTimeBlocks { - return nil, ErrInvalidStakingTx.Wrapf( - "staking time %d is out of bounds. Min: %d, Max: %d", + stakerQuorum := pm.MultisigInfo.StakerQuorum + + // 2. Validate all data related to staking tx: + // - it has valid staking output + // - that staking time and value are correct + // - slashing tx is relevant to staking tx + // - slashing tx signature is valid + multisigStakingInfo, err := btcstaking.BuildMultisigStakingInfo( + stakerKeys, + stakerQuorum, + pm.FinalityProviderKeys.PublicKeys, + covenantPks, + parameters.CovenantQuorum, pm.StakingTime, - parameters.MinStakingTimeBlocks, - parameters.MaxStakingTimeBlocks, + pm.StakingValue, + net, ) - } + if err != nil { + return nil, ErrInvalidStakingTx.Wrapf("failed to build staking info: %v", err) + } - if pm.StakingTx.Transaction.TxOut[stakingOutputIdx].Value < parameters.MinStakingValueSat || - pm.StakingTx.Transaction.TxOut[stakingOutputIdx].Value > parameters.MaxStakingValueSat { - return nil, ErrInvalidStakingTx.Wrapf( - "staking value %d is out of bounds. Min: %d, Max: %d", - pm.StakingTx.Transaction.TxOut[stakingOutputIdx].Value, - parameters.MinStakingValueSat, - parameters.MaxStakingValueSat, + stakingOutputIdx, err = bbn.GetOutputIdxInBTCTx(pm.StakingTx.Transaction, multisigStakingInfo.StakingOutput) + + if err != nil { + return nil, ErrInvalidStakingTx.Wrap("staking tx does not contain expected staking output") + } + + if err := validateStakingTimeAndValueAgainstTheParams(pm, parameters, stakingOutputIdx); err != nil { + return nil, err + } + + if err := btcstaking.CheckSlashingTxMatchFundingTxMultisig( + pm.StakingSlashingTx.Transaction, + pm.StakingTx.Transaction, + stakingOutputIdx, + parameters.MinSlashingTxFeeSat, + parameters.SlashingRate, + parameters.SlashingPkScript, + stakerKeys, + stakerQuorum, + pm.UnbondingTime, + net, + ); err != nil { + return nil, ErrInvalidStakingTx.Wrap(err.Error()) + } + + slashingSpendInfo, err := multisigStakingInfo.SlashingPathSpendInfo() + if err != nil { + panic(fmt.Errorf("failed to construct slashing path from the staking tx: %w", err)) + } + + if err := btcstaking.VerifyTransactionMultiSigWithOutput( + pm.StakingSlashingTx.Transaction, + pm.StakingTx.Transaction.TxOut[stakingOutputIdx], + slashingSpendInfo.RevealedLeaf.Script, + slashingPubkey2Sig, + stakerQuorum, + ); err != nil { + return nil, ErrInvalidSlashingTx.Wrapf("invalid delegator signature: %v", err) + } + + // 3. Validate all data related to unbonding tx: + // - it is valid BTC pre-signed transaction + // - it has valid unbonding output + // - slashing tx is relevant to unbonding tx + // - slashing tx signature is valid + if err := btcstaking.CheckPreSignedUnbondingTxSanity( + pm.UnbondingTx.Transaction, + ); err != nil { + return nil, ErrInvalidUnbondingTx.Wrapf("unbonding tx is not a valid pre-signed transaction: %v", err) + } + + multisigUnbondingInfo, err := btcstaking.BuildMultisigUnbondingInfo( + stakerKeys, + stakerQuorum, + pm.FinalityProviderKeys.PublicKeys, + covenantPks, + parameters.CovenantQuorum, + pm.UnbondingTime, + pm.UnbondingValue, + net, ) - } + if err != nil { + return nil, ErrInvalidUnbondingTx.Wrapf("failed to build the unbonding info: %v", err) + } - if err := btcstaking.CheckSlashingTxMatchFundingTx( - pm.StakingSlashingTx.Transaction, - pm.StakingTx.Transaction, - stakingOutputIdx, - parameters.MinSlashingTxFeeSat, - parameters.SlashingRate, - parameters.SlashingPkScript, - pm.StakerPK.PublicKey, - pm.UnbondingTime, - net, - ); err != nil { - return nil, ErrInvalidStakingTx.Wrap(err.Error()) - } + unbondingTx := pm.UnbondingTx.Transaction + if !bytes.Equal(unbondingTx.TxOut[0].PkScript, multisigUnbondingInfo.UnbondingOutput.PkScript) { + return nil, ErrInvalidUnbondingTx. + Wrapf("the unbonding output script is not expected, expected: %x, got: %s", + multisigUnbondingInfo.UnbondingOutput.PkScript, unbondingTx.TxOut[0].PkScript) + } + if unbondingTx.TxOut[0].Value != multisigUnbondingInfo.UnbondingOutput.Value { + return nil, ErrInvalidUnbondingTx. + Wrapf("the unbonding output value is not expected, expected: %d, got: %d", + multisigUnbondingInfo.UnbondingOutput.Value, unbondingTx.TxOut[0].Value) + } - slashingSpendInfo, err := stakingInfo.SlashingPathSpendInfo() - if err != nil { - panic(fmt.Errorf("failed to construct slashing path from the staking tx: %w", err)) - } + err = btcstaking.CheckSlashingTxMatchFundingTxMultisig( + pm.UnbondingSlashingTx.Transaction, + pm.UnbondingTx.Transaction, + 0, // unbonding output always has only 1 output + parameters.MinSlashingTxFeeSat, + parameters.SlashingRate, + parameters.SlashingPkScript, + stakerKeys, + stakerQuorum, + pm.UnbondingTime, + net, + ) + if err != nil { + return nil, ErrInvalidUnbondingTx.Wrapf("err: %v", err) + } - if err := btcstaking.VerifyTransactionSigWithOutput( - pm.StakingSlashingTx.Transaction, - pm.StakingTx.Transaction.TxOut[stakingOutputIdx], - slashingSpendInfo.RevealedLeaf.Script, - pm.StakerPK.PublicKey, - pm.StakerStakingSlashingTxSig.BIP340Signature.MustMarshal(), - ); err != nil { - return nil, ErrInvalidSlashingTx.Wrapf("invalid delegator signature: %v", err) - } + unbondingSlashingSpendInfo, err := multisigUnbondingInfo.SlashingPathSpendInfo() + if err != nil { + panic(fmt.Errorf("failed to construct slashing path from the unbonding tx: %w", err)) + } - // 3. Validate all data related to unbonding tx: - // - it is valid BTC pre-signed transaction - // - it has valid unbonding output - // - slashing tx is relevant to unbonding tx - // - slashing tx signature is valid - if err := btcstaking.CheckPreSignedUnbondingTxSanity( - pm.UnbondingTx.Transaction, - ); err != nil { - return nil, ErrInvalidUnbondingTx.Wrapf("unbonding tx is not a valid pre-signed transaction: %v", err) - } + if err := btcstaking.VerifyTransactionMultiSigWithOutput( + pm.UnbondingSlashingTx.Transaction, + pm.UnbondingTx.Transaction.TxOut[0], // unbonding output always has only 1 output + unbondingSlashingSpendInfo.RevealedLeaf.Script, + unbondingSlashingPubkey2Sig, + stakerQuorum, + ); err != nil { + return nil, ErrInvalidSlashingTx.Wrapf("invalid delegator signature: %v", err) + } + } else { + // this is the original single-sig btc delegation + // 2. Validate all data related to staking tx: + // - it has valid staking output + // - that staking time and value are correct + // - slashing tx is relevant to staking tx + // - slashing tx signature is valid + stakingInfo, err := btcstaking.BuildStakingInfo( + pm.StakerPK.PublicKey, + pm.FinalityProviderKeys.PublicKeys, + covenantPks, + parameters.CovenantQuorum, + pm.StakingTime, + pm.StakingValue, + net, + ) + if err != nil { + return nil, ErrInvalidStakingTx.Wrapf("failed to build staking info: %v", err) + } - unbondingInfo, err := btcstaking.BuildUnbondingInfo( - pm.StakerPK.PublicKey, - pm.FinalityProviderKeys.PublicKeys, - covenantPks, - parameters.CovenantQuorum, - pm.UnbondingTime, - pm.UnbondingValue, - net, - ) - if err != nil { - return nil, ErrInvalidUnbondingTx.Wrapf("failed to build the unbonding info: %v", err) - } + stakingOutputIdx, err = bbn.GetOutputIdxInBTCTx(pm.StakingTx.Transaction, stakingInfo.StakingOutput) - unbondingTx := pm.UnbondingTx.Transaction - if !bytes.Equal(unbondingTx.TxOut[0].PkScript, unbondingInfo.UnbondingOutput.PkScript) { - return nil, ErrInvalidUnbondingTx. - Wrapf("the unbonding output script is not expected, expected: %x, got: %s", - unbondingInfo.UnbondingOutput.PkScript, unbondingTx.TxOut[0].PkScript) - } - if unbondingTx.TxOut[0].Value != unbondingInfo.UnbondingOutput.Value { - return nil, ErrInvalidUnbondingTx. - Wrapf("the unbonding output value is not expected, expected: %d, got: %d", - unbondingInfo.UnbondingOutput.Value, unbondingTx.TxOut[0].Value) - } + if err != nil { + return nil, ErrInvalidStakingTx.Wrap("staking tx does not contain expected staking output") + } - err = btcstaking.CheckSlashingTxMatchFundingTx( - pm.UnbondingSlashingTx.Transaction, - pm.UnbondingTx.Transaction, - 0, // unbonding output always has only 1 output - parameters.MinSlashingTxFeeSat, - parameters.SlashingRate, - parameters.SlashingPkScript, - pm.StakerPK.PublicKey, - pm.UnbondingTime, - net, - ) - if err != nil { - return nil, ErrInvalidUnbondingTx.Wrapf("err: %v", err) - } + if err := validateStakingTimeAndValueAgainstTheParams(pm, parameters, stakingOutputIdx); err != nil { + return nil, err + } - unbondingSlashingSpendInfo, err := unbondingInfo.SlashingPathSpendInfo() - if err != nil { - panic(fmt.Errorf("failed to construct slashing path from the unbonding tx: %w", err)) - } + if err := btcstaking.CheckSlashingTxMatchFundingTx( + pm.StakingSlashingTx.Transaction, + pm.StakingTx.Transaction, + stakingOutputIdx, + parameters.MinSlashingTxFeeSat, + parameters.SlashingRate, + parameters.SlashingPkScript, + pm.StakerPK.PublicKey, + pm.UnbondingTime, + net, + ); err != nil { + return nil, ErrInvalidStakingTx.Wrap(err.Error()) + } - if err := btcstaking.VerifyTransactionSigWithOutput( - pm.UnbondingSlashingTx.Transaction, - pm.UnbondingTx.Transaction.TxOut[0], // unbonding output always has only 1 output - unbondingSlashingSpendInfo.RevealedLeaf.Script, - pm.StakerPK.PublicKey, - pm.StakerUnbondingSlashingSig.BIP340Signature.MustMarshal(), - ); err != nil { - return nil, ErrInvalidSlashingTx.Wrapf("invalid delegator signature: %v", err) + slashingSpendInfo, err := stakingInfo.SlashingPathSpendInfo() + if err != nil { + panic(fmt.Errorf("failed to construct slashing path from the staking tx: %w", err)) + } + + if err := btcstaking.VerifyTransactionSigWithOutput( + pm.StakingSlashingTx.Transaction, + pm.StakingTx.Transaction.TxOut[stakingOutputIdx], + slashingSpendInfo.RevealedLeaf.Script, + pm.StakerPK.PublicKey, + pm.StakerStakingSlashingTxSig.BIP340Signature.MustMarshal(), + ); err != nil { + return nil, ErrInvalidSlashingTx.Wrapf("invalid delegator signature: %v", err) + } + + // 3. Validate all data related to unbonding tx: + // - it is valid BTC pre-signed transaction + // - it has valid unbonding output + // - slashing tx is relevant to unbonding tx + // - slashing tx signature is valid + if err := btcstaking.CheckPreSignedUnbondingTxSanity( + pm.UnbondingTx.Transaction, + ); err != nil { + return nil, ErrInvalidUnbondingTx.Wrapf("unbonding tx is not a valid pre-signed transaction: %v", err) + } + + unbondingInfo, err := btcstaking.BuildUnbondingInfo( + pm.StakerPK.PublicKey, + pm.FinalityProviderKeys.PublicKeys, + covenantPks, + parameters.CovenantQuorum, + pm.UnbondingTime, + pm.UnbondingValue, + net, + ) + if err != nil { + return nil, ErrInvalidUnbondingTx.Wrapf("failed to build the unbonding info: %v", err) + } + + unbondingTx := pm.UnbondingTx.Transaction + if !bytes.Equal(unbondingTx.TxOut[0].PkScript, unbondingInfo.UnbondingOutput.PkScript) { + return nil, ErrInvalidUnbondingTx. + Wrapf("the unbonding output script is not expected, expected: %x, got: %s", + unbondingInfo.UnbondingOutput.PkScript, unbondingTx.TxOut[0].PkScript) + } + if unbondingTx.TxOut[0].Value != unbondingInfo.UnbondingOutput.Value { + return nil, ErrInvalidUnbondingTx. + Wrapf("the unbonding output value is not expected, expected: %d, got: %d", + unbondingInfo.UnbondingOutput.Value, unbondingTx.TxOut[0].Value) + } + + err = btcstaking.CheckSlashingTxMatchFundingTx( + pm.UnbondingSlashingTx.Transaction, + pm.UnbondingTx.Transaction, + 0, // unbonding output always has only 1 output + parameters.MinSlashingTxFeeSat, + parameters.SlashingRate, + parameters.SlashingPkScript, + pm.StakerPK.PublicKey, + pm.UnbondingTime, + net, + ) + if err != nil { + return nil, ErrInvalidUnbondingTx.Wrapf("err: %v", err) + } + + unbondingSlashingSpendInfo, err := unbondingInfo.SlashingPathSpendInfo() + if err != nil { + panic(fmt.Errorf("failed to construct slashing path from the unbonding tx: %w", err)) + } + + if err := btcstaking.VerifyTransactionSigWithOutput( + pm.UnbondingSlashingTx.Transaction, + pm.UnbondingTx.Transaction.TxOut[0], // unbonding output always has only 1 output + unbondingSlashingSpendInfo.RevealedLeaf.Script, + pm.StakerPK.PublicKey, + pm.StakerUnbondingSlashingSig.BIP340Signature.MustMarshal(), + ); err != nil { + return nil, ErrInvalidSlashingTx.Wrapf("invalid delegator signature: %v", err) + } } // 4. Check that unbonding tx input is pointing to staking tx @@ -202,3 +337,129 @@ func ValidateParsedMessageAgainstTheParams( UnbondingOutputIdx: 0, // unbonding output always has only 1 output }, nil } + +func validateStakingTimeAndValueAgainstTheParams( + pm *ParsedCreateDelegationMessage, + parameters *Params, + stakingOutputIdx uint32, +) error { + if uint32(pm.StakingTime) < parameters.MinStakingTimeBlocks || + uint32(pm.StakingTime) > parameters.MaxStakingTimeBlocks { + return ErrInvalidStakingTx.Wrapf( + "staking time %d is out of bounds. Min: %d, Max: %d", + pm.StakingTime, + parameters.MinStakingTimeBlocks, + parameters.MaxStakingTimeBlocks, + ) + } + + if pm.StakingTx.Transaction.TxOut[stakingOutputIdx].Value < parameters.MinStakingValueSat || + pm.StakingTx.Transaction.TxOut[stakingOutputIdx].Value > parameters.MaxStakingValueSat { + return ErrInvalidStakingTx.Wrapf( + "staking value %d is out of bounds. Min: %d, Max: %d", + pm.StakingTx.Transaction.TxOut[stakingOutputIdx].Value, + parameters.MinStakingValueSat, + parameters.MaxStakingValueSat, + ) + } + + return nil +} + +func validateStakerQuorumAndPubKeyListAgainstParams( + pm *ParsedCreateDelegationMessage, + parameters *Params, +) error { + // this is the M-of-N multisig btc delegation + if len(pm.MultisigInfo.StakerBTCPkList.PublicKeys) < 1 || pm.MultisigInfo.StakerQuorum < 1 { + return ErrInvalidMultisigInfo.Wrapf("number of staker btc pk list and staker quorum must be greater than 0, got: %d, %d", + len(pm.MultisigInfo.StakerBTCPkList.PublicKeys), pm.MultisigInfo.StakerQuorum, + ) + } + + // validate staker quorum and length of staker btc pk list doesn't exceed max M-of-N + if int(parameters.MaxStakerNum) < len(pm.MultisigInfo.StakerBTCPkList.PublicKeys)+1 || + parameters.MaxStakerQuorum < pm.MultisigInfo.StakerQuorum { + return ErrInvalidMultisigInfo.Wrapf("invalid M-of-N parameters: staker quorum %d, staker num %d, max %d-of-%d", + pm.MultisigInfo.StakerQuorum, len(pm.MultisigInfo.StakerBTCPkList.PublicKeys)+1, parameters.MaxStakerQuorum, parameters.MaxStakerNum) + } + + return nil +} + +func buildAndVerifyStakerKeysAndPubKey2Sig( + pm *ParsedCreateDelegationMessage, +) ([]*btcec.PublicKey, map[*btcec.PublicKey][]byte, map[*btcec.PublicKey][]byte, error) { + // construct the complete list of staker pubkeys from `MultisigInfo` and `StakerPk` + stakerKeys := make([]*btcec.PublicKey, 0, len(pm.MultisigInfo.StakerBTCPkList.PublicKeys)+1) + stakerKeys = append(stakerKeys, pm.MultisigInfo.StakerBTCPkList.PublicKeys...) + // check if MultisigInfo contains duplicated `btc_pk` + for _, extraPk := range pm.MultisigInfo.StakerBTCPkList.PublicKeysBbnFormat { + if bytes.Equal(extraPk.MustMarshal(), pm.StakerPK.BIP340PubKey.MustMarshal()) { + return nil, nil, nil, ErrDuplicatedStakerKey.Wrapf("staker pk list contains the main staker pk") + } + } + stakerKeys = append(stakerKeys, pm.StakerPK.PublicKey) + + // used to detect duplicates + stakerKeyByBytes := make(map[string]*btcec.PublicKey, len(stakerKeys)) + for _, pk := range stakerKeys { + if pk == nil { + return nil, nil, nil, ErrInvalidMultisigInfo.Wrap("staker pk list contains nil key") + } + + keyBytes := schnorr.SerializePubKey(pk) + keyStr := string(keyBytes) + + if _, exists := stakerKeyByBytes[keyStr]; exists { + return nil, nil, nil, ErrDuplicatedStakerKey.Wrapf("staker pk list contains duplicate key %x", keyBytes) + } + + stakerKeyByBytes[keyStr] = pk + } + + // construct pubkey -> bip340 signature map for each slashing tx and unbonding slashing tx + slashingPubkey2Sig := make(map[*btcec.PublicKey][]byte) + slashingPubkey2Sig[pm.StakerPK.PublicKey] = pm.StakerStakingSlashingTxSig.BIP340Signature.MustMarshal() + for _, si := range pm.MultisigInfo.StakerStakingSlashingSigs { + keyBytes := si.PublicKey.BIP340PubKey.MustMarshal() + keyStr := string(keyBytes) + + canonicalPk, exists := stakerKeyByBytes[keyStr] + if !exists { + return nil, nil, nil, ErrInvalidMultisigInfo.Wrapf("signature key %s not found in staker set", si.PublicKey.BIP340PubKey.MarshalHex()) + } + + if _, dup := slashingPubkey2Sig[canonicalPk]; dup { + return nil, nil, nil, ErrDuplicatedStakerKey.Wrapf("duplicate slashing signature for key %s", si.PublicKey.BIP340PubKey.MarshalHex()) + } + + slashingPubkey2Sig[canonicalPk] = si.Sig.BIP340Signature.MustMarshal() + } + + unbondingSlashingPubkey2Sig := make(map[*btcec.PublicKey][]byte) + unbondingSlashingPubkey2Sig[pm.StakerPK.PublicKey] = pm.StakerUnbondingSlashingSig.BIP340Signature.MustMarshal() + for _, si := range pm.MultisigInfo.StakerUnbondingSlashingSigs { + keyBytes := si.PublicKey.BIP340PubKey.MustMarshal() + keyStr := string(keyBytes) + + canonicalPk, exists := stakerKeyByBytes[keyStr] + if !exists { + return nil, nil, nil, ErrInvalidMultisigInfo.Wrapf("unbonding signature key %s not found in staker set", si.PublicKey.BIP340PubKey.MarshalHex()) + } + + if _, dup := unbondingSlashingPubkey2Sig[canonicalPk]; dup { + return nil, nil, nil, ErrDuplicatedStakerKey.Wrapf("duplicate unbonding slashing signature for key %s", si.PublicKey.BIP340PubKey.MarshalHex()) + } + + unbondingSlashingPubkey2Sig[canonicalPk] = si.Sig.BIP340Signature.MustMarshal() + } + + // compare the length of pubkey -> sig map and the `StakerQuorum` + if len(slashingPubkey2Sig) < int(pm.MultisigInfo.StakerQuorum) || len(unbondingSlashingPubkey2Sig) < int(pm.MultisigInfo.StakerQuorum) { + return nil, nil, nil, ErrInvalidMultisigInfo.Wrapf("invalid %d-of-%d signatures: %d slashing signatures, %d unbonding slashing signatures", + pm.MultisigInfo.StakerQuorum, len(pm.MultisigInfo.StakerBTCPkList.PublicKeys)+1, len(slashingPubkey2Sig), len(unbondingSlashingPubkey2Sig)) + } + + return stakerKeys, slashingPubkey2Sig, unbondingSlashingPubkey2Sig, nil +} diff --git a/x/btcstaking/types/validate_parsed_message_test.go b/x/btcstaking/types/validate_parsed_message_test.go index 1c183c707..e3217d843 100644 --- a/x/btcstaking/types/validate_parsed_message_test.go +++ b/x/btcstaking/types/validate_parsed_message_test.go @@ -229,6 +229,219 @@ func createMsgDelegationForParams( return msgCreateBTCDel, delSK } +type unbondingInfoWithMultisig struct { + unbondingSlashingTx *types.BTCSlashingTx + unbondingSlashingSig *bbn.BIP340Signature + serializedUnbondingTx []byte + extraStakerUnbondingSigs []*types.SignatureInfo +} + +// generateMultisigUnbondingInfo generates unbonding info for multisig +func generateMultisigUnbondingInfo( + r *rand.Rand, + t *testing.T, + delSKs []*btcec.PrivateKey, + stakerQuorum uint32, + fpPk *btcec.PublicKey, + stkTxHash chainhash.Hash, + stkOutputIdx uint32, + unbondingTime uint16, + unbondingValue int64, + p *types.Params, +) *unbondingInfoWithMultisig { + covPKs, err := bbn.NewBTCPKsFromBIP340PKs(p.CovenantPks) + require.NoError(t, err) + + testUnbondingInfo := datagen.GenMultisigBTCUnbondingSlashingInfo( + r, + t, + &chaincfg.MainNetParams, + delSKs, + stakerQuorum, + []*btcec.PublicKey{fpPk}, + covPKs, + p.CovenantQuorum, + wire.NewOutPoint(&stkTxHash, stkOutputIdx), + unbondingTime, + unbondingValue, + p.SlashingPkScript, + p.SlashingRate, + unbondingTime, + ) + + // main staker signature + unbondingSlashingPathInfo, err := testUnbondingInfo.UnbondingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + mainStakerSig, err := testUnbondingInfo.SlashingTx.Sign( + testUnbondingInfo.UnbondingTx, + 0, + unbondingSlashingPathInfo.GetPkScriptPath(), + delSKs[0], + ) + require.NoError(t, err) + + // extra staker signatures + extraStakerSigs := make([]*types.SignatureInfo, len(delSKs)-1) + for i := 1; i < len(delSKs); i++ { + sig, err := testUnbondingInfo.SlashingTx.Sign( + testUnbondingInfo.UnbondingTx, + 0, + unbondingSlashingPathInfo.GetPkScriptPath(), + delSKs[i], + ) + require.NoError(t, err) + pk := bbn.NewBIP340PubKeyFromBTCPK(delSKs[i].PubKey()) + extraStakerSigs[i-1] = types.NewSignatureInfo(pk, sig) + } + + serializedUnbondingTx, err := bbn.SerializeBTCTx(testUnbondingInfo.UnbondingTx) + require.NoError(t, err) + + return &unbondingInfoWithMultisig{ + unbondingSlashingTx: testUnbondingInfo.SlashingTx, + unbondingSlashingSig: mainStakerSig, + serializedUnbondingTx: serializedUnbondingTx, + extraStakerUnbondingSigs: extraStakerSigs, + } +} + +// createMultisigMsgDelegationForParams creates a valid M-of-N multisig delegation message +func createMultisigMsgDelegationForParams( + r *rand.Rand, + t *testing.T, + p *types.Params, + stakerQuorum uint32, + stakerNum uint32, +) *types.MsgCreateBTCDelegation { + // generate N staker key pairs + delSKs, delPKs, err := datagen.GenRandomBTCKeyPairs(r, int(stakerNum)) + require.NoError(t, err) + + // first key is the main staker + mainStakerSK := delSKs[0] + mainStakerPK := bbn.NewBIP340PubKeyFromBTCPK(delPKs[0]) + + // rest are extra stakers + extraStakerPKs := make([]bbn.BIP340PubKey, stakerNum-1) + for i := 1; i < int(stakerNum); i++ { + extraStakerPKs[i-1] = *bbn.NewBIP340PubKeyFromBTCPK(delPKs[i]) + } + + // staker address + staker := sdk.MustAccAddressFromBech32(datagen.GenRandomAccount().Address) + pop, err := datagen.NewPoPBTC(staker, mainStakerSK) + require.NoError(t, err) + + // finality provider + _, fpPk, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + fpPkBBn := bbn.NewBIP340PubKeyFromBTCPK(fpPk) + + // covenants + covPKs, err := bbn.NewBTCPKsFromBIP340PKs(p.CovenantPks) + require.NoError(t, err) + + stakingTimeBlocks := uint16(randRange(r, int(p.MinStakingTimeBlocks), int(p.MaxStakingTimeBlocks))) + stakingValue := int64(randRange(r, int(p.MinStakingValueSat), int(p.MaxStakingValueSat))) + unbondingTime := p.UnbondingTimeBlocks + + // create multisig staking info + testStakingInfo := datagen.GenMultisigBTCStakingSlashingInfo( + r, + t, + &chaincfg.MainNetParams, + delSKs, + stakerQuorum, + []*btcec.PublicKey{fpPk}, + covPKs, + p.CovenantQuorum, + stakingTimeBlocks, + stakingValue, + p.SlashingPkScript, + p.SlashingRate, + uint16(unbondingTime), + ) + + slashingSpendInfo, err := testStakingInfo.StakingInfo.SlashingPathSpendInfo() + require.NoError(t, err) + + // generate main staker signature + mainStakerSig, err := testStakingInfo.SlashingTx.Sign( + testStakingInfo.StakingTx, + 0, + slashingSpendInfo.GetPkScriptPath(), + mainStakerSK, + ) + require.NoError(t, err) + + // generate extra staker signatures + extraStakerSlashingSigs := make([]*types.SignatureInfo, stakerNum-1) + for i := 1; i < int(stakerNum); i++ { + sig, err := testStakingInfo.SlashingTx.Sign( + testStakingInfo.StakingTx, + 0, + slashingSpendInfo.GetPkScriptPath(), + delSKs[i], + ) + require.NoError(t, err) + extraStakerSlashingSigs[i-1] = types.NewSignatureInfo(&extraStakerPKs[i-1], sig) + } + + // transaction inclusion proof + prevBlock, _ := datagen.GenRandomBtcdBlock(r, 0, nil) + btcHeaderWithProof := datagen.CreateBlockWithTransaction(r, &prevBlock.Header, testStakingInfo.StakingTx) + btcHeader := btcHeaderWithProof.HeaderBytes + serializedStakingTx, err := bbn.SerializeBTCTx(testStakingInfo.StakingTx) + require.NoError(t, err) + + txInclusionProof := types.NewInclusionProof( + &btcckpttypes.TransactionKey{Index: 1, Hash: btcHeader.Hash()}, + btcHeaderWithProof.SpvProof.MerkleNodes, + ) + + // unbonding info + stkTxHash := testStakingInfo.StakingTx.TxHash() + stkOutputIdx := uint32(0) + unbondingValue := stakingValue - p.UnbondingFeeSat + + unbondingInfo := generateMultisigUnbondingInfo( + r, + t, + delSKs, + stakerQuorum, + fpPk, + stkTxHash, + stkOutputIdx, + uint16(unbondingTime), + unbondingValue, + p, + ) + + return &types.MsgCreateBTCDelegation{ + StakerAddr: staker.String(), + BtcPk: mainStakerPK, + FpBtcPkList: []bbn.BIP340PubKey{*fpPkBBn}, + Pop: pop, + StakingTime: uint32(stakingTimeBlocks), + StakingValue: stakingValue, + StakingTx: serializedStakingTx, + StakingTxInclusionProof: txInclusionProof, + SlashingTx: testStakingInfo.SlashingTx, + DelegatorSlashingSig: mainStakerSig, + UnbondingTx: unbondingInfo.serializedUnbondingTx, + UnbondingTime: unbondingTime, + UnbondingValue: unbondingValue, + UnbondingSlashingTx: unbondingInfo.unbondingSlashingTx, + DelegatorUnbondingSlashingSig: unbondingInfo.unbondingSlashingSig, + MultisigInfo: &types.AdditionalStakerInfo{ + StakerBtcPkList: extraStakerPKs, + StakerQuorum: stakerQuorum, + DelegatorSlashingSigs: extraStakerSlashingSigs, + DelegatorUnbondingSlashingSigs: unbondingInfo.extraStakerUnbondingSigs, + }, + } +} + func TestValidateParsedMessageAgainstTheParams(t *testing.T) { tests := []struct { name string @@ -898,6 +1111,231 @@ func TestValidateParsedMessageAgainstTheParams(t *testing.T) { errParsing: nil, errValidation: types.ErrInvalidUnbondingTx.Wrapf("unbonding tx fee must be larger that 0"), }, + { + name: "multisig: duplicate staker pk in MultisigInfo.StakerBtcPkList", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + // set max multisig params to allow 3-of-5 + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + // create 3-of-3 multisig delegation + msg := createMultisigMsgDelegationForParams(r, t, params, 2, 3) + + // duplicate the first extra staker pk + if msg.MultisigInfo != nil { + msg.MultisigInfo.StakerBtcPkList = append( + msg.MultisigInfo.StakerBtcPkList, + msg.MultisigInfo.StakerBtcPkList[0], + ) + } + + return msg, params, checkpointParams + }, + errParsing: types.ErrDuplicatedStakerKey, + errValidation: nil, + }, + { + name: "multisig: duplicate between BtcPk and MultisigInfo.StakerBtcPkList", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + msg := createMultisigMsgDelegationForParams(r, t, params, 2, 3) + + // add the main staker BtcPk to the MultisigInfo list (duplicate) + if msg.MultisigInfo != nil { + msg.MultisigInfo.StakerBtcPkList = append( + msg.MultisigInfo.StakerBtcPkList, + *msg.BtcPk, + ) + } + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: types.ErrDuplicatedStakerKey, + }, + { + name: "multisig: N valid signatures - success", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + // create 2-of-3 multisig with all 3 valid signatures + msg := createMultisigMsgDelegationForParams(r, t, params, 2, 3) + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: nil, + }, + { + name: "multisig: M valid signatures - success", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + // create 2-of-3 multisig with all 3 valid signatures + msg := createMultisigMsgDelegationForParams(r, t, params, 2, 3) + + if msg.MultisigInfo != nil { + // keep only 2 signatures + msg.MultisigInfo.DelegatorSlashingSigs = msg.MultisigInfo.DelegatorSlashingSigs[:1] + msg.MultisigInfo.DelegatorUnbondingSlashingSigs = msg.MultisigInfo.DelegatorUnbondingSlashingSigs[:1] + } + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: nil, + }, + { + name: "multisig: M valid signatures + (N-M) invalid signatures - success", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + msg := createMultisigMsgDelegationForParams(r, t, params, 2, 3) + + // replace last signature of msg.MultisigInfo with invalid ones (keep first valid) + if msg.MultisigInfo != nil { + validSig := msg.MultisigInfo.DelegatorSlashingSigs[1].Sig.MustMarshal() + invalidSig := make([]byte, len(validSig)) + copy(invalidSig, validSig) + invalidSig[len(validSig)-1] ^= 0x01 + + dummyPk := msg.MultisigInfo.StakerBtcPkList[1] + dummySig, err := bbn.NewBIP340Signature(invalidSig) + require.NoError(t, err) + msg.MultisigInfo.DelegatorSlashingSigs[1] = types.NewSignatureInfo(&dummyPk, dummySig) + msg.MultisigInfo.DelegatorUnbondingSlashingSigs[1] = types.NewSignatureInfo(&dummyPk, dummySig) + } + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: nil, + }, + { + name: "multisig: M-1 valid signatures - fail", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + // create 3-of-5 multisig but only provide 2 valid signatures (M-1) + msg := createMultisigMsgDelegationForParams(r, t, params, 3, 5) + + // make all but M-1 signatures invalid + if msg.MultisigInfo != nil && len(msg.MultisigInfo.DelegatorSlashingSigs) >= 2 { + validSig := msg.MultisigInfo.DelegatorSlashingSigs[1].Sig.MustMarshal() + invalidSig := make([]byte, len(validSig)) + copy(invalidSig, validSig) + invalidSig[len(validSig)-1] ^= 0x01 + + // keep first signature valid (main staker) + // keep second signature valid (first extra staker) + // make rest invalid (need 3, only have 2 valid) + for i := 1; i < len(msg.MultisigInfo.DelegatorSlashingSigs); i++ { + dummyPk := msg.MultisigInfo.StakerBtcPkList[i] + dummySig, _ := bbn.NewBIP340Signature(invalidSig) + msg.MultisigInfo.DelegatorSlashingSigs[i] = types.NewSignatureInfo(&dummyPk, dummySig) + msg.MultisigInfo.DelegatorUnbondingSlashingSigs[i] = types.NewSignatureInfo(&dummyPk, dummySig) + } + } + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: types.ErrInvalidSlashingTx, + }, + { + name: "multisig: quorum exceeds number of stakers - fail", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + msg := createMultisigMsgDelegationForParams(r, t, params, 2, 3) + + if msg.MultisigInfo != nil { + msg.MultisigInfo.StakerQuorum = 4 // Quorum > N (3) + } + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: types.ErrInvalidMultisigInfo, + }, + { + name: "multisig: zero quorum - fail", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + msg := createMultisigMsgDelegationForParams(r, t, params, 2, 3) + + if msg.MultisigInfo != nil { + msg.MultisigInfo.StakerQuorum = 0 + } + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: types.ErrInvalidMultisigInfo, + }, + { + name: "multisig: fewer signatures than quorum - fail", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 3 + params.MaxStakerNum = 5 + checkpointParams := testCheckpointParams() + + msg := createMultisigMsgDelegationForParams(r, t, params, 3, 5) + + // remove signatures to have fewer than quorum + if msg.MultisigInfo != nil && len(msg.MultisigInfo.DelegatorSlashingSigs) > 0 { + // keep only 2 signatures when quorum is 3 + msg.MultisigInfo.DelegatorSlashingSigs = msg.MultisigInfo.DelegatorSlashingSigs[:1] + msg.MultisigInfo.DelegatorUnbondingSlashingSigs = msg.MultisigInfo.DelegatorUnbondingSlashingSigs[:1] + } + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: types.ErrInvalidMultisigInfo, + }, + { + name: "multisig: exceeds max params - fail", + fn: func(r *rand.Rand, t *testing.T) (*types.MsgCreateBTCDelegation, *types.Params, *btcckpttypes.Params) { + params := testStakingParams(r, t) + params.MaxStakerQuorum = 2 + params.MaxStakerNum = 3 + checkpointParams := testCheckpointParams() + + // try to create 3-of-5 when max is 2-of-3 + msg := createMultisigMsgDelegationForParams(r, t, params, 3, 5) + + return msg, params, checkpointParams + }, + errParsing: nil, + errValidation: types.ErrInvalidMultisigInfo, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/x/checkpointing/README.md b/x/checkpointing/README.md index 767dddd78..4778d0c30 100644 --- a/x/checkpointing/README.md +++ b/x/checkpointing/README.md @@ -57,7 +57,7 @@ that is included in the next block proposal. Once a valid checkpoint is generated, it is checkpointed into the Bitcoin ledger through an off-chain program -[Vigilante Submitter](https://docs.babylonlabs.io/docs/developer-guides/modules/submitter). +[Vigilante Submitter](https://docs.babylonlabs.io/guides/architecture/vigilantes/submitter/). It is responsible for constructing Bitcoin transactions that contain outputs utilizing the [`OP_RETURN`](https://en.bitcoin.it/wiki/OP_RETURN) script code @@ -83,7 +83,7 @@ The Checkpointing module maintains the following KV stores. ### Checkpoint -The [checkpoint state](./keeper/ckpt_state.go) maintains all the checkpoints. +The [checkpoint state](./keeper/ckpt_state.go) maintains all the checkpoints. The key is the epoch number and the value is a `RawCheckpointWithMeta` [object](../../proto/babylon/checkpointing/v1/checkpoint.proto) representing a raw checkpoint along with some metadata. @@ -131,7 +131,7 @@ The [registration state](./keeper/registration_state.go) maintains a two-way mapping between the validator address and its BLS public key. The Checkpoint module also stores the [validator set](../../proto/babylon/checkpointing/v1/bls_key.proto) -of every epoch with their public BLS keys. The key of the storage is the epoch +of every epoch with their public BLS keys. The key of the storage is the epoch number. ```protobuf @@ -152,7 +152,7 @@ message ValidatorWithBlsKey { ### Genesis -The [genesis state](./keeper/genesis_bls.go) maintains the BLS keys of the +The [genesis state](./keeper/genesis_bls.go) maintains the BLS keys of the genesis validators for the Checkpointing module. ```protobuf @@ -183,7 +183,7 @@ The message handler is defined at ### MsgWrappedCreateValidator -The `MsgWrappedCreateValidator` message wraps the [`MsgCreateValidator`](https://github.com/cosmos/cosmos-sdk/blob/9814f684b9dd7e384064ca86876688c05e685e54/proto/cosmos/staking/v1beta1/tx.proto#L51) +The `MsgWrappedCreateValidator` message wraps the [`MsgCreateValidator`](https://github.com/cosmos/cosmos-sdk/blob/main/proto/cosmos/staking/v1beta1/tx.proto#L52) defined in the staking module of the Cosmos SDK in order to also include the BLS public key. The message is used for registering a new validator and storing its BLS public @@ -213,7 +213,7 @@ Upon `MsgWrappedCreateValidator`, a Babylon node will execute as follows: ## Checkpointing via ABCI++ [ABCI++](https://docs.cometbft.com/v0.38/spec/abci/) or ABCI 2.0 is the middle -layer that controls the communication between the underlying consensus and the +layer that controls the communication between the underlying consensus and the application. We use ABCI++ interfaces to generate checkpoints a part of the CometBFT consensus. Particularly, validators are responsible for submitting a `VoteExtension` that includes their BLS signature at the end diff --git a/x/checkpointing/prepare/proposal.go b/x/checkpointing/prepare/proposal.go index 2c34ab0bf..3ce4252f0 100644 --- a/x/checkpointing/prepare/proposal.go +++ b/x/checkpointing/prepare/proposal.go @@ -224,6 +224,10 @@ func (h *ProposalHandler) verifyVoteExtension( return nil, fmt.Errorf("failed to unmarshal vote extension: %w", err) } + if err := ve.Validate(); err != nil { + return nil, fmt.Errorf("invalid vote extension: %w", err) + } + _, err := sdk.ValAddressFromBech32(ve.Signer) if err != nil { return nil, fmt.Errorf("invalid signer address in vote extension: %w", err) @@ -240,7 +244,6 @@ func (h *ProposalHandler) verifyVoteExtension( } sig := ve.ToBLSSig() - if err := h.ckptKeeper.VerifyBLSSig(ctx, sig); err != nil { return nil, fmt.Errorf("invalid BLS signature: %w", err) } diff --git a/x/checkpointing/prepare/proposal_test.go b/x/checkpointing/prepare/proposal_test.go index e443eafa6..c5806590f 100644 --- a/x/checkpointing/prepare/proposal_test.go +++ b/x/checkpointing/prepare/proposal_test.go @@ -516,8 +516,10 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) { var signedVoteExtensions []cbftt.ExtendedVoteInfo for i, val := range validatorAndExtensions.Vals { validator := val + require.NoError(t, validatorAndExtensions.Extensions[i].Validate()) + blsSig := validatorAndExtensions.Extensions[i].ToBLSSig() ek.EXPECT().GetPubKeyByConsAddr(gomock.Any(), sdk.ConsAddress(validator.ValidatorAddress(t).Bytes())).Return(validator.ProtoPubkey(), nil).AnyTimes() - ek.EXPECT().VerifyBLSSig(gomock.Any(), validatorAndExtensions.Extensions[i].ToBLSSig()).Return(nil).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(nil).AnyTimes() ek.EXPECT().GetBlsPubKey(gomock.Any(), validator.ValidatorAddress(t)).Return(validator.BlsPubKey(), nil).AnyTimes() // empty vote extension signedExtension := validator.SignVoteExtension(t, []byte{}, ec.Ctx.HeaderInfo().Height-1, ec.Ctx.ChainID()) @@ -545,12 +547,14 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) { var signedVoteExtensions []cbftt.ExtendedVoteInfo for i, val := range validatorAndExtensions.Vals { validator := val + require.NoError(t, validatorAndExtensions.Extensions[i].Validate()) + blsSig := validatorAndExtensions.Extensions[i].ToBLSSig() ek.EXPECT().GetPubKeyByConsAddr(gomock.Any(), sdk.ConsAddress(validator.ValidatorAddress(t).Bytes())).Return(validator.ProtoPubkey(), nil).AnyTimes() if i < invalidValidBlsSig { - ek.EXPECT().VerifyBLSSig(gomock.Any(), validatorAndExtensions.Extensions[i].ToBLSSig()).Return(checkpointingtypes.ErrInvalidBlsSignature).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(checkpointingtypes.ErrInvalidBlsSignature).AnyTimes() } else { - ek.EXPECT().VerifyBLSSig(gomock.Any(), validatorAndExtensions.Extensions[i].ToBLSSig()).Return(nil).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(nil).AnyTimes() } ek.EXPECT().GetBlsPubKey(gomock.Any(), validator.ValidatorAddress(t)).Return(validator.BlsPubKey(), nil).AnyTimes() marshaledExtension, err := validatorAndExtensions.Extensions[i].Marshal() @@ -580,12 +584,14 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) { var signedVoteExtensions []cbftt.ExtendedVoteInfo for i, val := range validatorAndExtensions.Vals { validator := val + require.NoError(t, validatorAndExtensions.Extensions[i].Validate()) + blsSig := validatorAndExtensions.Extensions[i].ToBLSSig() ek.EXPECT().GetPubKeyByConsAddr(gomock.Any(), sdk.ConsAddress(validator.ValidatorAddress(t).Bytes())).Return(validator.ProtoPubkey(), nil).AnyTimes() if i < invalidBlsSig { - ek.EXPECT().VerifyBLSSig(gomock.Any(), validatorAndExtensions.Extensions[i].ToBLSSig()).Return(checkpointingtypes.ErrInvalidBlsSignature).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(checkpointingtypes.ErrInvalidBlsSignature).AnyTimes() } else { - ek.EXPECT().VerifyBLSSig(gomock.Any(), validatorAndExtensions.Extensions[i].ToBLSSig()).Return(nil).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(nil).AnyTimes() } ek.EXPECT().GetBlsPubKey(gomock.Any(), validator.ValidatorAddress(t)).Return(validator.BlsPubKey(), nil).AnyTimes() marshaledExtension, err := validatorAndExtensions.Extensions[i].Marshal() @@ -624,8 +630,10 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) { var signedVoteExtensions []cbftt.ExtendedVoteInfo for i, val := range allvalidators { validator := val + require.NoError(t, allExtensions[i].Validate()) + blsSig := allExtensions[i].ToBLSSig() ek.EXPECT().GetPubKeyByConsAddr(gomock.Any(), sdk.ConsAddress(validator.ValidatorAddress(t).Bytes())).Return(validator.ProtoPubkey(), nil).AnyTimes() - ek.EXPECT().VerifyBLSSig(gomock.Any(), allExtensions[i].ToBLSSig()).Return(nil).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(nil).AnyTimes() ek.EXPECT().GetBlsPubKey(gomock.Any(), validator.ValidatorAddress(t)).Return(validator.BlsPubKey(), nil).AnyTimes() marshaledExtension, err := allExtensions[i].Marshal() require.NoError(t, err) @@ -652,8 +660,10 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) { var signedVoteExtensions []cbftt.ExtendedVoteInfo for i, val := range validatorAndExtensions.Vals { validator := val + require.NoError(t, validatorAndExtensions.Extensions[i].Validate()) + blsSig := validatorAndExtensions.Extensions[i].ToBLSSig() ek.EXPECT().GetPubKeyByConsAddr(gomock.Any(), sdk.ConsAddress(validator.ValidatorAddress(t).Bytes())).Return(validator.ProtoPubkey(), nil).AnyTimes() - ek.EXPECT().VerifyBLSSig(gomock.Any(), validatorAndExtensions.Extensions[i].ToBLSSig()).Return(nil).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(nil).AnyTimes() ek.EXPECT().GetBlsPubKey(gomock.Any(), validator.ValidatorAddress(t)).Return(validator.BlsPubKey(), nil).AnyTimes() marshaledExtension, err := validatorAndExtensions.Extensions[i].Marshal() require.NoError(t, err) @@ -678,8 +688,10 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) { var signedVoteExtensions []cbftt.ExtendedVoteInfo for i, val := range validatorAndExtensions.Vals { validator := val + require.NoError(t, validatorAndExtensions.Extensions[i].Validate()) + blsSig := validatorAndExtensions.Extensions[i].ToBLSSig() ek.EXPECT().GetPubKeyByConsAddr(gomock.Any(), sdk.ConsAddress(validator.ValidatorAddress(t).Bytes())).Return(validator.ProtoPubkey(), nil).AnyTimes() - ek.EXPECT().VerifyBLSSig(gomock.Any(), validatorAndExtensions.Extensions[i].ToBLSSig()).Return(nil).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(nil).AnyTimes() ek.EXPECT().GetBlsPubKey(gomock.Any(), validator.ValidatorAddress(t)).Return(validator.BlsPubKey(), nil).AnyTimes() marshaledExtension, err := validatorAndExtensions.Extensions[i].Marshal() require.NoError(t, err) @@ -706,8 +718,10 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) { var signedVoteExtensions []cbftt.ExtendedVoteInfo for i, val := range validatorAndExtensions.Vals { validator := val + require.NoError(t, validatorAndExtensions.Extensions[i].Validate()) + blsSig := validatorAndExtensions.Extensions[i].ToBLSSig() ek.EXPECT().GetPubKeyByConsAddr(gomock.Any(), sdk.ConsAddress(validator.ValidatorAddress(t).Bytes())).Return(validator.ProtoPubkey(), nil).AnyTimes() - ek.EXPECT().VerifyBLSSig(gomock.Any(), validatorAndExtensions.Extensions[i].ToBLSSig()).Return(nil).AnyTimes() + ek.EXPECT().VerifyBLSSig(gomock.Any(), blsSig).Return(nil).AnyTimes() ek.EXPECT().GetBlsPubKey(gomock.Any(), validator.ValidatorAddress(t)).Return(validator.BlsPubKey(), nil).AnyTimes() marshaledExtension, err := validatorAndExtensions.Extensions[i].Marshal() require.NoError(t, err) @@ -722,10 +736,10 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) { } return &Scenario{ - TotalPower: totalPower, - ValidatorSet: validatorAndExtensions.Vals, - Extensions: signedVoteExtensions, - TxVerifier: newTxVerifier(encCfg.TxConfig), + TotalPower: totalPower, + ValidatorSet: validatorAndExtensions.Vals, + Extensions: signedVoteExtensions, + TxVerifier: newTxVerifier(encCfg.TxConfig), ExpectedAbsentVotes: 1, // malicious validator's vote extension should be considered absent } }, diff --git a/x/checkpointing/types/types.go b/x/checkpointing/types/types.go index e76f8548e..0f58e0f55 100644 --- a/x/checkpointing/types/types.go +++ b/x/checkpointing/types/types.go @@ -215,6 +215,22 @@ func BytesToCkptWithMeta(cdc codec.BinaryCodec, bz []byte) (*RawCheckpointWithMe return ckptWithMeta, err } +func (ve *VoteExtension) Validate() error { + if ve.Signer == "" { + return fmt.Errorf("empty signer address") + } + if ve.ValidatorAddress == "" { + return fmt.Errorf("empty validator address") + } + if ve.BlockHash == nil { + return fmt.Errorf("empty block hash") + } + if ve.BlsSig == nil { + return fmt.Errorf("empty BLS signature") + } + return nil +} + func (ve *VoteExtension) ToBLSSig() *BlsSig { return &BlsSig{ EpochNum: ve.EpochNum, diff --git a/x/checkpointing/vote_extensions/vote_ext.go b/x/checkpointing/vote_extensions/vote_ext.go index b19942b68..cae31272e 100644 --- a/x/checkpointing/vote_extensions/vote_ext.go +++ b/x/checkpointing/vote_extensions/vote_ext.go @@ -1,6 +1,7 @@ package vote_extensions import ( + "bytes" "fmt" "cosmossdk.io/log" @@ -130,6 +131,12 @@ func (h *VoteExtensionHandler) VerifyVoteExtension() sdk.VerifyVoteExtensionHand return resReject, nil } + if err := ve.Validate(); err != nil { + h.logger.Info("invalid vote extension", + "error", err, "height", req.Height, "validator", extensionSigner) + return resReject, nil + } + // 1. verify epoch number if epoch.EpochNumber != ve.EpochNum { h.logger.Info("invalid epoch number in the vote extension", @@ -147,9 +154,9 @@ func (h *VoteExtensionHandler) VerifyVoteExtension() sdk.VerifyVoteExtensionHand } // 3. verify signing hash - if !blsSig.BlockHash.Equal(req.Hash) { + if !bytes.Equal(*blsSig.BlockHash, req.Hash) { // processed BlsSig message is for invalid last commit hash - h.logger.Info("in valid block ID in BLS sig", + h.logger.Info("invalid BlockHash BLS sig", "want", req.Hash, "got", blsSig.BlockHash, "validator", extensionSigner, "height", req.Height) return resReject, nil } diff --git a/x/checkpointing/vote_extensions/vote_ext_test.go b/x/checkpointing/vote_extensions/vote_ext_test.go index 1e4752f42..44a7fd34a 100644 --- a/x/checkpointing/vote_extensions/vote_ext_test.go +++ b/x/checkpointing/vote_extensions/vote_ext_test.go @@ -175,3 +175,71 @@ func FuzzExtendVote_InvalidBlockHash(f *testing.F) { require.NoError(t, err) }) } + +// TestVerifyVoteExtension_MalformedVoteExtension tests that malformed vote extensions +// are properly rejected +func TestVerifyVoteExtension_MalformedVoteExtension(t *testing.T) { + r := rand.New(rand.NewSource(42)) + // generate the validator set with 10 validators as genesis + genesisValSet, privSigner, err := datagen.GenesisValidatorSetWithPrivSigner(10) + require.NoError(t, err) + helper := testhelper.NewHelperWithValSet(t, genesisValSet, privSigner) + ek := helper.App.EpochingKeeper + + epoch := ek.GetEpoch(helper.Ctx) + require.Equal(t, uint64(1), epoch.EpochNumber) + + // go to block 10, reaching epoch boundary + interval := ek.GetParams(helper.Ctx).EpochInterval + for i := uint64(0); i < interval-2; i++ { + _, err := helper.ApplyEmptyBlockWithVoteExtension(r) + require.NoError(t, err) + } + + // case 1: create a vote extension with nil block hash + genesisKeys := genesisValSet.GetGenesisKeys() + sig := datagen.GenRandomBlsMultiSig(r) + ve1 := &types.VoteExtension{ + Signer: genesisKeys[0].ValidatorAddress, + ValidatorAddress: genesisKeys[0].ValidatorAddress, + BlockHash: nil, + EpochNum: epoch.EpochNumber, + Height: uint64(helper.App.LastBlockHeight()), + BlsSig: &sig, + } + veBytes1, err := ve1.Marshal() + require.NoError(t, err) + + expectedBlockHash := datagen.GenRandomBlockHash(r) + + res, err := helper.App.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{ + Hash: expectedBlockHash, + Height: helper.App.LastBlockHeight(), + VoteExtension: veBytes1, + ValidatorAddress: genesisValSet.Keys[0].ValPubkey.Address(), + }) + require.NoError(t, err) + require.Equal(t, abci.ResponseVerifyVoteExtension_REJECT, res.Status) + + // case 2: create a vote extension with mismatched block hash + wrongBlockHash := datagen.GenRandomBlockHash(r) + ve2 := &types.VoteExtension{ + Signer: genesisKeys[0].ValidatorAddress, + ValidatorAddress: genesisKeys[0].ValidatorAddress, + BlockHash: &wrongBlockHash, + EpochNum: epoch.EpochNumber, + Height: uint64(helper.App.LastBlockHeight()), + BlsSig: &sig, + } + veBytes2, err := ve2.Marshal() + require.NoError(t, err) + + res, err = helper.App.VerifyVoteExtension(&abci.RequestVerifyVoteExtension{ + Hash: expectedBlockHash, + Height: helper.App.LastBlockHeight(), + VoteExtension: veBytes2, + ValidatorAddress: genesisValSet.Keys[0].ValPubkey.Address(), + }) + require.NoError(t, err) + require.Equal(t, abci.ResponseVerifyVoteExtension_REJECT, res.Status) +} diff --git a/x/costaking/README.md b/x/costaking/README.md index 85b959d24..5a76b95ce 100644 --- a/x/costaking/README.md +++ b/x/costaking/README.md @@ -83,7 +83,7 @@ graph TD - `AfterBtcDelegationActivated`: Adds satoshis to costaker if the chosen fp was in the active set. - `AfterBtcDelegationUnbonded`: Removes satoshis from costaker if the chosen fp was active -in the previous and current babylon block. +in the previous babylon block. - `AfterBbnFpEntersActiveSet`: Iterates over all the BTC delegations made for this fp and add satoshi to the costaker structure. - `AfterBbnFpRemovedFromActiveSet`: Iterates over all the BTC delegations made for this fp and diff --git a/x/costaking/keeper/hooks_finality.go b/x/costaking/keeper/hooks_finality.go index c8e6db4af..2243b3ef4 100644 --- a/x/costaking/keeper/hooks_finality.go +++ b/x/costaking/keeper/hooks_finality.go @@ -20,18 +20,17 @@ type HookFinality struct { // AfterBtcDelegationUnbonded handles BTC delegation unbonding events. // This hook is triggered when a BTC delegation is unbonded/removed from the system. // -// State Changes: -// - If FP was active in both previous and current sets: ActiveSatoshis -= sats -// - Otherwise: No change (to prevent double subtraction) +// Possible State Changes (previous -> current): +// - inactive -> active: no-op +// - active -> active: subtract ActiveSatoshis +// - active -> inactive: subtract ActiveSatoshis (there's no risk of double counting +// because the AfterBbnFpRemovedFromActiveSet hook gets the updated active sats amount) +// - inactive -> inactive: no-op +// More concisely if the FP was active in the previous set, we subtract the sats. func (h HookFinality) AfterBtcDelegationUnbonded(ctx context.Context, fpAddr sdk.AccAddress, btcDelAddr sdk.AccAddress, isFpActiveInPrevSet, isFpActiveInCurrSet bool, sats uint64) error { - if !isFpActiveInPrevSet || !isFpActiveInCurrSet { - // It needs to check the fp was active in the previous set and in it is currently active in the current set for the case where: - // 1. the fp was active in the block X - // 2. block x+1 btc delegation was unbonded (removes sats) - // 3. fp becomes inactive (removes sats twice) + if !isFpActiveInPrevSet { return nil } - return h.k.costakerModified(ctx, btcDelAddr, func(rwdTracker *types.CostakerRewardsTracker) { rwdTracker.ActiveSatoshis = rwdTracker.ActiveSatoshis.SubRaw(int64(sats)) }) diff --git a/x/epoching/types/validator.go b/x/epoching/types/validator.go index 3e0c70bbe..cba5a7999 100644 --- a/x/epoching/types/validator.go +++ b/x/epoching/types/validator.go @@ -25,7 +25,7 @@ type ValidatorSet []Validator // NewSortedValidatorSet returns a sorted ValidatorSet by validator's address in the ascending order func NewSortedValidatorSet(vals []Validator) ValidatorSet { sort.Slice(vals, func(i, j int) bool { - return sdk.BigEndianToUint64(vals[i].Addr) < sdk.BigEndianToUint64(vals[j].Addr) + return bytes.Compare(vals[i].Addr, vals[j].Addr) < 0 }) return vals } @@ -73,10 +73,12 @@ func (vs ValidatorSet) binarySearch(targetAddr sdk.ValAddress) int { var mid = lo + (hi-lo)/2 midAddr := vs[mid].Addr + diff := bytes.Compare(midAddr, targetAddr) + switch { - case bytes.Equal(midAddr, targetAddr): + case diff == 0: return mid - case sdk.BigEndianToUint64(midAddr) > sdk.BigEndianToUint64(targetAddr): + case diff > 0: hi = mid - 1 default: lo = mid + 1 diff --git a/x/epoching/types/validator_test.go b/x/epoching/types/validator_test.go index c9fcd1feb..3d80aab7e 100644 --- a/x/epoching/types/validator_test.go +++ b/x/epoching/types/validator_test.go @@ -3,7 +3,10 @@ package types_test import ( "testing" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/babylonlabs-io/babylon/v4/testutil/datagen" + epochingtypes "github.com/babylonlabs-io/babylon/v4/x/epoching/types" "github.com/stretchr/testify/require" ) @@ -21,3 +24,93 @@ func TestValidatorSet_FindValidatorWithIndex(t *testing.T) { require.Nil(t, val) require.Equal(t, 0, index) } + +func TestNewSortedValidatorSetOrdersAscending(t *testing.T) { + raw := []epochingtypes.Validator{ + {Addr: sdk.ValAddress("c")}, + {Addr: sdk.ValAddress("a")}, + {Addr: sdk.ValAddress("b")}, + } + + sorted := epochingtypes.NewSortedValidatorSet(raw) + + got := make([]sdk.ValAddress, len(sorted)) + for i := range sorted { + got[i] = sorted[i].Addr + } + + require.Equal(t, []sdk.ValAddress{ + sdk.ValAddress("a"), + sdk.ValAddress("b"), + sdk.ValAddress("c"), + }, got) +} + +func TestValidatorSetBinarySearchUsesFullAddress(t *testing.T) { + raw := []epochingtypes.Validator{ + {Addr: sdk.ValAddress("a")}, + {Addr: sdk.ValAddress("b")}, + {Addr: sdk.ValAddress("c")}, + } + + valSet := epochingtypes.NewSortedValidatorSet(raw) + + target := sdk.ValAddress("b") + val, idx, err := valSet.FindValidatorWithIndex(target) + require.NoError(t, err) + require.Equal(t, target, sdk.ValAddress(val.Addr)) + require.Equal(t, 1, idx) + + missing := sdk.ValAddress("d") + val, idx, err = valSet.FindValidatorWithIndex(missing) + require.Error(t, err) + require.Nil(t, val) + require.Zero(t, idx) +} + +func TestValidatorSetBinarySearchHandlesBigEndianPrefix(t *testing.T) { + addr1 := makeAddr(0x10) + addr2 := makeAddr(0x20) + addr3 := makeAddr(0x30) + + raw := []epochingtypes.Validator{ + {Addr: addr3}, + {Addr: addr1}, + {Addr: addr2}, + } + + valSet := epochingtypes.NewSortedValidatorSet(raw) + got := make([]sdk.ValAddress, len(valSet)) + for i := range valSet { + got[i] = valSet[i].Addr + } + + require.Equal(t, []sdk.ValAddress{ + addr1, + addr2, + addr3, + }, got) + + val, idx, err := valSet.FindValidatorWithIndex(addr2) + require.NoError(t, err) + require.Equal(t, addr2, sdk.ValAddress(val.Addr)) + require.Equal(t, 1, idx) + + missing := makeAddr(0x40) + val, idx, err = valSet.FindValidatorWithIndex(missing) + require.Error(t, err) + require.Nil(t, val) + require.Zero(t, idx) +} + +func makeAddr(marker byte) sdk.ValAddress { + basePrefix := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22} + + addr := make([]byte, 20) + copy(addr, basePrefix) // first 8 bytes identical + addr[8] = marker // first differing byte outside BigEndianToUint64 range + for i := 9; i < len(addr); i++ { + addr[i] = 0x50 + marker + } + return sdk.ValAddress(addr) +} diff --git a/x/finality/keeper/power_dist_change_test.go b/x/finality/keeper/power_dist_change_test.go index 12c8e38a5..8bb5b6e1d 100644 --- a/x/finality/keeper/power_dist_change_test.go +++ b/x/finality/keeper/power_dist_change_test.go @@ -1627,6 +1627,8 @@ func TestHandleLivenessPanic(t *testing.T) { UnbondingFeeSat: 1000, AllowListExpirationHeight: 0, BtcActivationHeight: 1, + MaxStakerQuorum: 2, + MaxStakerNum: 3, }) require.NoError(t, err)