Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
345b514
feat: multisig btcstaking lib (#1822)
canu0205 Oct 22, 2025
0634c1c
merge main
canu0205 Oct 22, 2025
aa371c9
feat: babylon multisig support for btc delegation (#1824)
canu0205 Oct 27, 2025
e04bfcc
merge main
canu0205 Oct 27, 2025
71979d8
feat: e2e multisig test (#1834)
canu0205 Oct 28, 2025
54247ad
feat: upgrade handler to support multisig (#1827)
canu0205 Oct 29, 2025
a2da9a9
feat: e2ev2 upgrade test support (#1850)
canu0205 Nov 4, 2025
3640b55
merge main
canu0205 Nov 4, 2025
0b37bf4
chore: add changelog entry
canu0205 Nov 4, 2025
487ddc7
feat: add max staker quorum and num flag to testnet cmd
canu0205 Nov 5, 2025
e39a875
fix: addCovenantSigs to deal with multisig
canu0205 Nov 5, 2025
3368aef
chore: fix typo
canu0205 Nov 5, 2025
dcca98a
fix: detect duplicated pubkey to sig map
canu0205 Nov 6, 2025
9c98b68
chore: fix lint
canu0205 Nov 6, 2025
6be2876
feat: build multisig slashing tx with witness for vigilante (#1861)
canu0205 Nov 11, 2025
b06e47e
Merge branch 'main' of github.com:babylonlabs-io/babylon into feat/st…
canu0205 Nov 11, 2025
bcb6c58
fix: validate stake expansion sig with multisig (#1863)
canu0205 Nov 13, 2025
0b779d7
chore: add CreateMultisigUnbondingPathWitness for vigilante
canu0205 Nov 13, 2025
c8ef72f
fix: multisig `BTCUndelegate` (#1866)
canu0205 Nov 17, 2025
b60c34c
feat: babylond btcstaking multisig cli support (#1871)
canu0205 Nov 27, 2025
9a0da30
merge main
canu0205 Nov 27, 2025
2b5c01a
chore: fix x/btcstaking duplicated error code (#1881)
canu0205 Nov 27, 2025
a47c512
docs: btc multisig stake (#1868)
canu0205 Nov 27, 2025
92bcea0
fix: only allow stake extension when old and new delegation has the s…
canu0205 Nov 28, 2025
7989574
test: improve e2ev2 multisig coverage (#1877)
canu0205 Dec 1, 2025
70536d2
chore: pull main into feat/staker-multi-sig (#1909)
canu0205 Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Improvements

- [#1764](https://github.com/babylonlabs-io/babylon/pull/1764) Add mergify yaml file for automatic backporting
- [#1826](https://github.com/babylonlabs-io/babylon/pull/1826) Support M-of-N multisig btc staker

## v4.0.0-rc.3

Expand Down
2 changes: 2 additions & 0 deletions app/include_upgrade_mainnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
v22 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v2_2"
v23 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v2_3"
v4 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4"
v5 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v5"
)

var WhitelistedChannelsID = map[string]struct{}{
Expand All @@ -24,6 +25,7 @@ var WhitelistedChannelsID = map[string]struct{}{
// init is used to include v2.2 upgrade for mainnet data
func init() {
Upgrades = []upgrades.Upgrade{
v5.Upgrade,
v4.Upgrade,
v23.Upgrade, // same as v3rc3 testnet
v22.Upgrade,
Expand Down
2 changes: 2 additions & 0 deletions app/include_upgrade_testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import (
v2rc4 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v2rc4/testnet"
v4 "github.com/babylonlabs-io/babylon/v4/app/upgrades/v4"
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,
v4rc3.Upgrade,
v4.Upgrade,
v23.Upgrade,
Expand Down
51 changes: 51 additions & 0 deletions app/upgrades/v5/upgrades.go
Original file line number Diff line number Diff line change
@@ -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
}
}
159 changes: 159 additions & 0 deletions app/upgrades/v5/upgrades_test.go
Original file line number Diff line number Diff line change
@@ -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() {}
84 changes: 84 additions & 0 deletions btcstaking/btcstaking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down
Loading
Loading