Skip to content

Commit

Permalink
feat: Implement ADR-29 - generalized unbonding (#215)
Browse files Browse the repository at this point in the history
Highlights:
- Now `MsgBTCUndelegate` contains stake spending transaction as well as
inclusion proof
- `BTCDelegation` contains news field `DelegatorUnbondingInfo` instead
of old staker signature field
- new field `DelegatorUnbondingInfo` contains information about in which
header unbonding happened. Also, if `StakeSpendingTx` in the message was
different that registered `UnbondingTx`, `DelegatorUnbondingInfo`
contains whole stake spending tx.
- New event is emitted if unbonding happens through different
transaction the the pre-registered one.
  • Loading branch information
KonradStaniec authored Oct 22, 2024
1 parent b1a4b48 commit 6a0ccac
Show file tree
Hide file tree
Showing 24 changed files with 1,526 additions and 588 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

* [#207](https://github.com/babylonlabs-io/babylon/pull/207) Rename total voting power
to total bonded sat
* [#215](https://github.com/babylonlabs-io/babylon/pull/215) Implement ADR-29
generalized unbonding handler

### Improvements

Expand Down
28 changes: 19 additions & 9 deletions proto/babylon/btcstaking/v1/btcstaking.proto
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ message BTCDelegation {
uint32 params_version = 16;
}

// DelegatorUnbondingInfo contains the information about transaction which spent
// the staking output. It contains:
// - spend_stake_tx: the transaction which spent the staking output
// - spend_stake_tx_inclusion_block_hash: the block hash of the block in which
// spend_stake_tx was included
// - spend_stake_tx_sig_inclusion_index: the index of spend_stake_tx in the block
message DelegatorUnbondingInfo {
// spend_stake_tx is the transaction which spent the staking output. It is
// filled only if spend_stake_tx is different than unbonding_tx registered
// on the Babylon chain.
bytes spend_stake_tx = 1;
}

// BTCUndelegation contains the information about the early unbonding path of the BTC delegation
message BTCUndelegation {
// unbonding_tx is the transaction which will transfer the funds from staking
Expand All @@ -119,24 +132,21 @@ message BTCUndelegation {
// It is partially signed by SK corresponding to btc_pk, but not signed by
// finality provider or covenant yet.
bytes slashing_tx = 2 [ (gogoproto.customtype) = "BTCSlashingTx" ];
// delegator_unbonding_sig is the signature on the unbonding tx
// by the delegator (i.e., SK corresponding to btc_pk).
// It effectively proves that the delegator wants to unbond and thus
// Babylon will consider this BTC delegation unbonded. Delegator's BTC
// on Bitcoin will be unbonded after timelock
bytes delegator_unbonding_sig = 3 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340Signature" ];
// delegator_slashing_sig is the signature on the slashing tx
// by the delegator (i.e., SK corresponding to btc_pk).
// It will be a part of the witness for the unbonding tx output.
bytes delegator_slashing_sig = 4 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340Signature" ];
bytes delegator_slashing_sig = 3 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340Signature" ];
// covenant_slashing_sigs is a list of adaptor signatures on the slashing tx
// by each covenant member
// It will be a part of the witness for the staking tx output.
repeated CovenantAdaptorSignatures covenant_slashing_sigs = 5;
repeated CovenantAdaptorSignatures covenant_slashing_sigs = 4;
// covenant_unbonding_sig_list is the list of signatures on the unbonding tx
// by covenant members
// It must be provided after processing undelegate message by Babylon
repeated SignatureInfo covenant_unbonding_sig_list = 6;
repeated SignatureInfo covenant_unbonding_sig_list = 5;
// delegator_unbonding_info is the information about transaction which spent
// the staking output
DelegatorUnbondingInfo delegator_unbonding_info = 6;
}

// BTCDelegatorDelegations is a collection of BTC delegations from the same delegator.
Expand Down
14 changes: 14 additions & 0 deletions proto/babylon/btcstaking/v1/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,17 @@ message EventBTCDelegationExpired {
// new_state of the BTC delegation
string new_state = 2 [(amino.dont_omitempty) = true];
}

// EventUnexpectedUnbondingTx is the event emitted when an unbonding tx is
// is different that the one registered in the BTC delegation.
message EventUnexpectedUnbondingTx {
// staking_tx_hash uniquely identifies a BTC delegation being unbonded
string staking_tx_hash = 1 [(amino.dont_omitempty) = true];
// spend_stake_tx_hash has of the transactin spending staking output
string spend_stake_tx_hash = 2 [(amino.dont_omitempty) = true];
// spend_stake_tx_header_hash is the hash of the header of the block that
// includes the spend_stake_tx
string spend_stake_tx_header_hash = 3 [(amino.dont_omitempty) = true];
// spend_stake_tx_block_index is the spend_stake_tx index in the block
uint32 spend_stake_tx_block_index = 4 [(amino.dont_omitempty) = true];
}
25 changes: 15 additions & 10 deletions proto/babylon/btcstaking/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -310,32 +310,37 @@ message BTCDelegationResponse {
uint32 params_version = 17;
}

// DelegatorUnbondingInfoResponse provides all necessary info about transaction
// which spent the staking output
message DelegatorUnbondingInfoResponse {
// spend_stake_tx_hex is the transaction which spent the staking output. It is
// filled only if the spend_stake_tx_hex is different than the unbonding_tx_hex
string spend_stake_tx_hex = 1;
}

// BTCUndelegationResponse provides all necessary info about the undeleagation
message BTCUndelegationResponse {
// unbonding_tx is the transaction which will transfer the funds from staking
// output to unbonding output. Unbonding output will usually have lower timelock
// than staking output. The unbonding tx as string hex.
string unbonding_tx_hex = 1;
// delegator_unbonding_sig is the signature on the unbonding tx
// by the delegator (i.e., SK corresponding to btc_pk).
// It effectively proves that the delegator wants to unbond and thus
// Babylon will consider this BTC delegation unbonded. Delegator's BTC
// on Bitcoin will be unbonded after timelock. The unbonding delegator sig as string hex.
string delegator_unbonding_sig_hex = 2;
// covenant_unbonding_sig_list is the list of signatures on the unbonding tx
// by covenant members
repeated SignatureInfo covenant_unbonding_sig_list = 3;
repeated SignatureInfo covenant_unbonding_sig_list = 2;
// slashingTxHex is the hex string of slashing tx
string slashing_tx_hex = 4;
string slashing_tx_hex = 3;
// delegator_slashing_sig is the signature on the slashing tx
// by the delegator (i.e., SK corresponding to btc_pk).
// It will be a part of the witness for the unbonding tx output.
// The delegator slashing sig as string hex.
string delegator_slashing_sig_hex = 5;
string delegator_slashing_sig_hex = 4;
// covenant_slashing_sigs is a list of adaptor signatures on the
// unbonding slashing tx by each covenant member
// It will be a part of the witness for the staking tx output.
repeated CovenantAdaptorSignatures covenant_slashing_sigs = 6;
repeated CovenantAdaptorSignatures covenant_slashing_sigs = 5;
// btc_undelegation_info contains all necessary info about the transaction
// which spent the staking output
DelegatorUnbondingInfoResponse delegator_unbonding_info_response = 6;
}

// BTCDelegatorDelegationsResponse is a collection of BTC delegations responses from the same delegator.
Expand Down
9 changes: 6 additions & 3 deletions proto/babylon/btcstaking/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,12 @@ message MsgBTCUndelegate {
// staking_tx_hash is the hash of the staking tx.
// It uniquely identifies a BTC delegation
string staking_tx_hash = 2;
// unbonding_tx_sig is the signature of the staker on the unbonding tx submitted to babylon
// the signature follows encoding in BIP-340 spec
bytes unbonding_tx_sig = 3 [ (gogoproto.customtype) = "github.com/babylonlabs-io/babylon/types.BIP340Signature" ];
// stake_spending_tx is a bitcoin transaction that spends the staking transaction
// i.e it has staking output as an input
bytes stake_spending_tx = 3;
// spend_spending_tx_inclusion_proof is the proof of inclusion of the
// stake_spending_tx in the BTC chain
InclusionProof stake_spending_tx_inclusion_proof = 4;
}
// MsgBTCUndelegateResponse is the response for MsgBTCUndelegate
message MsgBTCUndelegateResponse {}
Expand Down
36 changes: 27 additions & 9 deletions test/e2e/btc_staking_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,14 +457,26 @@ func (s *BTCStakingTestSuite) Test5SubmitStakerUnbonding() {
s.NoError(err)
stakingTxHash := stakingMsgTx.TxHash()

// delegator signs unbonding tx
params := nonValidatorNode.QueryBTCStakingParams()
delUnbondingSig, err := activeDel.SignUnbondingTx(params, s.net, s.delBTCSK)
currentBtcTipResp, err := nonValidatorNode.QueryTip()
s.NoError(err)
currentBtcTip, err := chain.ParseBTCHeaderInfoResponseToInfo(currentBtcTipResp)
s.NoError(err)

unbondingTx := activeDel.BtcUndelegation.UnbondingTx
unbondingTxMsg, err := bbn.NewBTCTxFromBytes(unbondingTx)
s.NoError(err)

blockWithUnbondingTx := datagen.CreateBlockWithTransaction(s.r, currentBtcTip.Header.ToBlockHeader(), unbondingTxMsg)
nonValidatorNode.InsertHeader(&blockWithUnbondingTx.HeaderBytes)
inclusionProof := bstypes.NewInclusionProofFromSpvProof(blockWithUnbondingTx.SpvProof)

nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
// submit the message for creating BTC undelegation
nonValidatorNode.BTCUndelegate(&stakingTxHash, delUnbondingSig)
nonValidatorNode.BTCUndelegate(
&stakingTxHash,
unbondingTxMsg,
inclusionProof,
)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
}, true)
Expand Down Expand Up @@ -848,12 +860,18 @@ func ParseRespBTCDelToBTCDel(resp *bstypes.BTCDelegationResponse) (btcDel *bstyp
DelegatorSlashingSig: delSlashingSig,
}

if len(ud.DelegatorUnbondingSigHex) > 0 {
delUnbondingSig, err := bbn.NewBIP340SignatureFromHex(ud.DelegatorUnbondingSigHex)
if err != nil {
return nil, err
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 = &bstypes.DelegatorUnbondingInfo{
SpendStakeTx: spendStakeTx,
}
btcDel.BtcUndelegation.DelegatorUnbondingSig = delUnbondingSig
}
}

Expand Down
20 changes: 16 additions & 4 deletions test/e2e/btc_staking_pre_approval_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,14 +492,26 @@ func (s *BTCStakingPreApprovalTestSuite) Test5SubmitStakerUnbonding() {
s.NoError(err)
stakingTxHash := stakingMsgTx.TxHash()

// delegator signs unbonding tx
params := nonValidatorNode.QueryBTCStakingParams()
delUnbondingSig, err := activeDel.SignUnbondingTx(params, s.net, s.delBTCSK)
currentBtcTipResp, err := nonValidatorNode.QueryTip()
s.NoError(err)
currentBtcTip, err := chain.ParseBTCHeaderInfoResponseToInfo(currentBtcTipResp)
s.NoError(err)

unbondingTx := activeDel.BtcUndelegation.UnbondingTx
unbondingTxMsg, err := bbn.NewBTCTxFromBytes(unbondingTx)
s.NoError(err)

blockWithUnbondingTx := datagen.CreateBlockWithTransaction(s.r, currentBtcTip.Header.ToBlockHeader(), unbondingTxMsg)
nonValidatorNode.InsertHeader(&blockWithUnbondingTx.HeaderBytes)
inclusionProof := bstypes.NewInclusionProofFromSpvProof(blockWithUnbondingTx.SpvProof)

nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
// submit the message for creating BTC undelegation
nonValidatorNode.BTCUndelegate(&stakingTxHash, delUnbondingSig)
nonValidatorNode.BTCUndelegate(
&stakingTxHash,
unbondingTxMsg,
inclusionProof,
)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
}, true)
Expand Down
18 changes: 13 additions & 5 deletions test/e2e/configurer/chain/commands_btcstaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/stretchr/testify/require"

"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
Expand Down Expand Up @@ -231,13 +230,22 @@ func (n *NodeConfig) AddCovenantUnbondingSigs(
n.LogActionF("successfully added covenant unbonding sigs")
}

func (n *NodeConfig) BTCUndelegate(stakingTxHash *chainhash.Hash, delUnbondingSig *schnorr.Signature) {
func (n *NodeConfig) BTCUndelegate(
stakingTxHash *chainhash.Hash,
spendStakeTx *wire.MsgTx,
spendStakeTxInclusionProof *bstypes.InclusionProof,
) {
n.LogActionF("undelegate by using signature on unbonding tx from delegator")

sigHex := bbn.NewBIP340SignatureFromBTCSig(delUnbondingSig).ToHexStr()
cmd := []string{"babylond", "tx", "btcstaking", "btc-undelegate", stakingTxHash.String(), sigHex, "--from=val"}
spendStakeTxBytes, err := bbn.SerializeBTCTx(spendStakeTx)
require.NoError(n.t, err)
spendStakeTxHex := hex.EncodeToString(spendStakeTxBytes)
inclusionProofHex, err := spendStakeTxInclusionProof.MarshalHex()
require.NoError(n.t, err)

cmd := []string{"babylond", "tx", "btcstaking", "btc-undelegate", stakingTxHash.String(), spendStakeTxHex, inclusionProofHex, "--from=val"}

_, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd)
_, _, err = n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd)
require.NoError(n.t, err)
n.LogActionF("successfully added signature on unbonding tx from delegator")
}
Expand Down
23 changes: 14 additions & 9 deletions x/btcstaking/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,11 +421,11 @@ func NewAddCovenantSigsCmd() *cobra.Command {

func NewBTCUndelegateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "btc-undelegate [staking_tx_hash] [unbonding_tx_sig]",
Args: cobra.ExactArgs(2),
Short: "Add a signature on the unbonding tx of a BTC delegation identified by a given staking tx hash. ",
Use: "btc-undelegate [staking_tx_hash] [spend_stake_tx] [spend_stake_tx_inclusion_proof]",
Args: cobra.ExactArgs(3),
Short: "Add unbonding information about a BTC delegation identified by a given staking tx hash.",
Long: strings.TrimSpace(
`Add a signature on the unbonding tx of a BTC delegation identified by a given staking tx hash signed by the delegator. The signature proves that delegator wants to unbond, and Babylon will consider the BTC delegation unbonded.`, // TODO: example
`Add unbonding information about a BTC delegation identified by a given staking tx hash. Proof of inclusion proves stake was spent on BTC chain`, // TODO: example
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
Expand All @@ -436,16 +436,21 @@ func NewBTCUndelegateCmd() *cobra.Command {
// get staking tx hash
stakingTxHash := args[0]

// get delegator signature for unbonding tx
unbondingTxSig, err := bbn.NewBIP340SignatureFromHex(args[1])
_, bytes, err := bbn.NewBTCTxFromHex(args[1])
if err != nil {
return err
}

inclusionProof, err := types.NewInclusionProofFromHex(args[2])
if err != nil {
return err
}

msg := types.MsgBTCUndelegate{
Signer: clientCtx.FromAddress.String(),
StakingTxHash: stakingTxHash,
UnbondingTxSig: unbondingTxSig,
Signer: clientCtx.FromAddress.String(),
StakingTxHash: stakingTxHash,
StakeSpendingTx: bytes,
StakeSpendingTxInclusionProof: inclusionProof,
}

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
Expand Down
2 changes: 1 addition & 1 deletion x/btcstaking/keeper/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func benchBeginBlock(b *testing.B, numFPs int, numDelsUnderFP int) {
stakingValue := int64(2 * 10e8)
delSK, _, err := datagen.GenRandomBTCKeyPair(r)
h.NoError(err)
stakingTxHash, msgCreateBTCDel, actualDel, btcHeaderInfo, inclusionProof, err := h.CreateDelegation(
stakingTxHash, msgCreateBTCDel, actualDel, btcHeaderInfo, inclusionProof, _, err := h.CreateDelegation(
r,
delSK,
fp.BtcPk.MustToBTCPK(),
Expand Down
4 changes: 2 additions & 2 deletions x/btcstaking/keeper/btc_delegations.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ func (k Keeper) addCovenantSigsToBTCDelegation(
func (k Keeper) btcUndelegate(
ctx sdk.Context,
btcDel *types.BTCDelegation,
unbondingTxSig *bbn.BIP340Signature,
u *types.DelegatorUnbondingInfo,
) {
btcDel.BtcUndelegation.DelegatorUnbondingSig = unbondingTxSig
btcDel.BtcUndelegation.DelegatorUnbondingInfo = u
k.setBTCDelegation(ctx, btcDel)

if !btcDel.HasInclusionProof() {
Expand Down
2 changes: 1 addition & 1 deletion x/btcstaking/keeper/incentive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func FuzzRecordVotingPowerDistCache(f *testing.F) {
for j := uint64(0); j < numBTCDels; j++ {
delSK, _, err := datagen.GenRandomBTCKeyPair(r)
h.NoError(err)
stakingTxHash, delMsg, del, btcHeaderInfo, inclusionProof, err := h.CreateDelegation(
stakingTxHash, delMsg, del, btcHeaderInfo, inclusionProof, _, err := h.CreateDelegation(
r,
delSK,
fp.BtcPk.MustToBTCPK(),
Expand Down
24 changes: 21 additions & 3 deletions x/btcstaking/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ type Helper struct {
Net *chaincfg.Params
}

type UnbondingTxInfo struct {
UnbondingTxInclusionProof *types.InclusionProof
UnbondingHeaderInfo *btclctypes.BTCHeaderInfo
}

func NewHelper(
t testing.TB,
btclcKeeper *types.MockBTCLightClientKeeper,
Expand Down Expand Up @@ -171,7 +176,7 @@ func (h *Helper) CreateDelegation(
unbondingValue int64,
unbondingTime uint16,
usePreApproval bool,
) (string, *types.MsgCreateBTCDelegation, *types.BTCDelegation, *btclctypes.BTCHeaderInfo, *types.InclusionProof, error) {
) (string, *types.MsgCreateBTCDelegation, *types.BTCDelegation, *btclctypes.BTCHeaderInfo, *types.InclusionProof, *UnbondingTxInfo, error) {
stakingTimeBlocks := stakingTime
bsParams := h.BTCStakingKeeper.GetParams(h.Ctx)
bcParams := h.BTCCheckpointKeeper.GetParams(h.Ctx)
Expand Down Expand Up @@ -269,6 +274,16 @@ func (h *Helper) CreateDelegation(
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).AnyTimes()

// all good, construct and send MsgCreateBTCDelegation message
fpBTCPK := bbn.NewBIP340PubKeyFromBTCPK(fpPK)
msgCreateBTCDel := &types.MsgCreateBTCDelegation{
Expand All @@ -294,7 +309,7 @@ func (h *Helper) CreateDelegation(

_, err = h.MsgServer.CreateBTCDelegation(h.Ctx, msgCreateBTCDel)
if err != nil {
return "", nil, nil, nil, nil, err
return "", nil, nil, nil, nil, nil, err
}

stakingMsgTx, err := bbn.NewBTCTxFromBytes(msgCreateBTCDel.StakingTx)
Expand All @@ -313,7 +328,10 @@ func (h *Helper) CreateDelegation(
require.True(h.t, btcDel.HasInclusionProof())
}

return stakingTxHash, msgCreateBTCDel, btcDel, btcHeaderInfo, txInclusionProof, nil
return stakingTxHash, msgCreateBTCDel, btcDel, btcHeaderInfo, txInclusionProof, &UnbondingTxInfo{
UnbondingTxInclusionProof: unbondingTxInclusionProof,
UnbondingHeaderInfo: btcUnbondingHeaderInfo,
}, nil
}

func (h *Helper) GenerateCovenantSignaturesMessages(
Expand Down
Loading

0 comments on commit 6a0ccac

Please sign in to comment.