Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement ADR-29 - generalized unbonding #215

Merged
merged 7 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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

## v0.13.0

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
Comment on lines +115 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: remove outdated documentation.

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;
}
Comment on lines +118 to +123
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this struct given that it only contains 1 field?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, this signals whether unbonding actually happened i.e

if del.BtcUndelegation.DelegatorUnbondingInfo != nil

delegation is considered unbonded.

And if

len(del.BtcUndelegation.DelegatorUnbondingInfo.SpendStakeTx) > 0 

then unbonding happened through unexpected unbonding transaction i.e the transaction different that del.BtcUndelegation.UnbondingTx

Assuming most of unbondings will happen through unbonding transaction committed to Babylon, this modelling enables us to save space i.e we do not store stake spending transaction if it happens to be already known unbonding transaction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK sounds good. Probably we can wrap some functions for BTCDelegation to signal these in the future 👍


// 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
Loading