Skip to content

feat: multisig btc delegation support #480

@canu0205

Description

@canu0205

Description

As babylon genesis support multisig btc delegation, Vigilante also needs corresponding changes in btcstaking-tracker:

1. stakingeventwatcher

tryParseStakerSignatureFromSpentTx should properly parse staker's signatures even in case of M-of-N multisig signatures.

// tryParseStakerSignatureFromSpentTx tries to parse staker signature from unbonding tx.
// If provided tx is not unbonding tx it returns error.
func tryParseStakerSignatureFromSpentTx(tx *wire.MsgTx, td *TrackedDelegation) (*schnorr.Signature, error) {
if len(tx.TxOut) != 1 {
return nil, fmt.Errorf("unbonding tx must have exactly one output. Priovided tx has %d outputs", len(tx.TxOut))
}
if tx.TxOut[0].Value != td.UnbondingOutput.Value || !bytes.Equal(tx.TxOut[0].PkScript, td.UnbondingOutput.PkScript) {
return nil, fmt.Errorf("unbonding tx must have output which matches unbonding output of retrieved from Babylon")
}
stakingTxInputIdx, err := getStakingTxInputIdx(tx, td)
if err != nil {
return nil, fmt.Errorf("unbonding tx does not spend staking output: %w", err)
}
stakingTxInput := tx.TxIn[stakingTxInputIdx]
witnessLen := len(stakingTxInput.Witness)
// minimal witness size for staking tx input is 4:
// covenant_signature, staker_signature, script, control_block
// If that is not the case, something weird is going on and we should investigate.
if witnessLen < 4 {
panic(fmt.Errorf("staking tx input witness has less than 4 elements for unbonding tx %s", tx.TxHash()))
}
// staker signature is 3rd element from the end
stakerSignature := stakingTxInput.Witness[witnessLen-3]
return schnorr.ParseSignature(stakerSignature)
}

func tryParseStakerSignatureFromSpentTx(tx *wire.MsgTx, td *TrackedDelegation) (*schnorr.Signature, error) {
  // ...
  stakingTxInput := tx.TxIn[stakingTxInputIdx]
  witnessLen := len(stakingTxInput.Witness)
  // minimal witness size for staking tx input is at least 4:
  // covenant_signatures, staker_signatures, script, control_block
  // If that is not the case, something weird is going on and we should investigate.
  if witnessLen < 4 {
    panic(fmt.Errorf("staking tx input witness has less than 4 elements for unbonding tx %s", tx.TxHash()))
  }

  // staker signatures started from 3rd element from the end
+ // TODO: there are more than one stakerSignature if it's multisig btc delegation, so we need to handle this case properly
  stakerSignature := stakingTxInput.Witness[witnessLen-3]

  return schnorr.ParseSignature(stakerSignature)
}

and also we need to add IsStakerMultisig field to TrackedDelegation to parse depending on M-of-N multisig.

type TrackedDelegation struct {
StakingTx *wire.MsgTx
StakingOutputIdx uint32
UnbondingOutput *wire.TxOut
DelegationStartHeight uint32
InProgress bool
}

But there's one problem when parsing stakerSignature which is it's hard to calculate exact number of staker signatures since there are covenant signatures of greater or equal to covenant quorum and staker signatures of greater or equal to staker quorum.
e.g., let's say covenant committee is using 3-of-5 multisig and staker is using 2-of-3 multisig. there are multiple scenarios of the number of signatures (covenant_sigs, staker_sigs): (3, 2), (3, 3), (4, 2), (4, 3), (5, 2), (5, 3).

However, since M-of-N multisig with OP_CHECKSIGADD requires exact amount of signatures equal to the number of total size of multisig party (N), even though it's nil. So we can parse signature of N. (Can u confirm this approach is correct? @KonradStaniec )

2. btcslasher

BuildUnbondingSlashingTxWithWitness and BuildSlashingTxWithWitness should also handle multisig btc delegation.

// BuildUnbondingSlashingTxWithWitness returns the unbonding slashing tx.
func BuildUnbondingSlashingTxWithWitness(
d *bstypes.BTCDelegationResponse,
bsParams *bstypes.Params,
btcNet *chaincfg.Params,
fpSK *btcec.PrivateKey,
) (*wire.MsgTx, error) {
if d.UnbondingTime > uint32(^uint16(0)) {
panic(fmt.Errorf("unbondingTime (%d) exceeds maximum for uint16", d.UnbondingTime))
}
config := SlashingConfig{
TxHex: d.UndelegationResponse.UnbondingTxHex,
SlashingTxHex: d.UndelegationResponse.SlashingTxHex,
SlashingSigHex: d.UndelegationResponse.DelegatorSlashingSigHex,
CovenantSigs: d.UndelegationResponse.CovenantSlashingSigs,
OutputIdx: 0,
InfoBuilder: func() (StakingInfoProvider, error) {
unbondingMsgTx, _, err := bbn.NewBTCTxFromHex(d.UndelegationResponse.UnbondingTxHex)
if err != nil {
return nil, err
}
fpBtcPkList, err := bbn.NewBTCPKsFromBIP340PKs(d.FpBtcPkList)
if err != nil {
return nil, err
}
covenantBtcPkList, err := bbn.NewBTCPKsFromBIP340PKs(bsParams.CovenantPks)
if err != nil {
return nil, err
}
// #nosec G115 -- performed the conversion check above
return btcstaking.BuildUnbondingInfo(
d.BtcPk.MustToBTCPK(),
fpBtcPkList,
covenantBtcPkList,
bsParams.CovenantQuorum,
uint16(d.UnbondingTime),
btcutil.Amount(unbondingMsgTx.TxOut[0].Value),
btcNet,
)
},
}
return buildSlashingTxWithWitness(d, bsParams, fpSK, config)
}

// BuildSlashingTxWithWitness constructs a Bitcoin slashing transaction with the required witness data
// using the provided finality provider's private key. It handles the conversion and validation of
// various parameters needed for slashing a Bitcoin delegation, including the staking transaction,
// finality provider public keys, and covenant public keys.
// Note: this function is UNSAFE for concurrent accesses as slashTx.BuildSlashingTxWithWitness is not safe for
// concurrent access inside it's calling asig.NewDecyptionKeyFromBTCSK
func BuildSlashingTxWithWitness(
d *bstypes.BTCDelegationResponse,
bsParams *bstypes.Params,
btcNet *chaincfg.Params,
fpSK *btcec.PrivateKey,
) (*wire.MsgTx, error) {
if d.TotalSat > math.MaxInt64 {
panic(fmt.Errorf("TotalSat %d exceeds int64 range", d.TotalSat))
}
config := SlashingConfig{
TxHex: d.StakingTxHex,
SlashingTxHex: d.SlashingTxHex,
SlashingSigHex: d.DelegatorSlashSigHex,
CovenantSigs: d.CovenantSigs,
OutputIdx: d.StakingOutputIdx,
InfoBuilder: func() (StakingInfoProvider, error) {
fpBtcPkList, err := bbn.NewBTCPKsFromBIP340PKs(d.FpBtcPkList)
if err != nil {
return nil, err
}
covenantBtcPkList, err := bbn.NewBTCPKsFromBIP340PKs(bsParams.CovenantPks)
if err != nil {
return nil, err
}
// #nosec G115 -- performed the conversion check above
return btcstaking.BuildStakingInfo(
d.BtcPk.MustToBTCPK(),
fpBtcPkList,
covenantBtcPkList,
bsParams.CovenantQuorum,
uint16(d.EndHeight-d.StartHeight),
btcutil.Amount(d.TotalSat),
btcNet,
)
},
}
return buildSlashingTxWithWitness(d, bsParams, fpSK, config)
}

specifically, we need to implement buildSlashingTxWithWitness to handle multisig btc delegation. i.e., using BuildMultisigSlashingTxWithWitness(babylonlabs-io/babylon#1861) of babylon/x/btcstaking/types/btc_slashing_tx.go. this function would require list of delSlashingSig instead of a single sig.

To implement this, we will introduce a new field in SlashingConfig, that contains delegatorSlashingSigs and delegatorUnbondingSlashingSigs.

// SlashingConfig holds configuration for building slashing transactions
type SlashingConfig struct {
TxHex string
SlashingTxHex string
SlashingSigHex string
CovenantSigs []*bstypes.CovenantAdaptorSignatures
OutputIdx uint32
InfoBuilder func() (StakingInfoProvider, error)
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions