diff --git a/covenant/covenant.go b/covenant/covenant.go index fa3a6f1..0b4a6de 100644 --- a/covenant/covenant.go +++ b/covenant/covenant.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "go.uber.org/zap" @@ -84,7 +85,8 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( if len(btcDels) == 0 { return nil, fmt.Errorf("no delegations") } - covenantSigs := make([]*types.CovenantSigs, 0, len(btcDels)) + + signingReq := make(map[chainhash.Hash]SigningTxsRequest) for _, btcDel := range btcDels { // 0. nil checks if btcDel == nil { @@ -177,7 +179,6 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( // 7. Check unbonding fee unbondingFee := stakingTx.TxOut[btcDel.StakingOutputIdx].Value - unbondingTx.TxOut[0].Value - if unbondingFee != int64(params.UnbondingFee) { ce.logger.Error("invalid unbonding fee", zap.Int64("expected_unbonding_fee", int64(params.UnbondingFee)), @@ -186,54 +187,113 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( continue } - // 8. sign covenant staking sigs - // record metrics - startSignTime := time.Now() - metricsTimeKeeper.SetPreviousSignStart(&startSignTime) + // Generate signing request data. - slashSigs, unbondingSig, err := signSlashAndUnbondSignatures( - btcDel, - stakingTx, - slashingTx, - unbondingTx, - ce.signer, - params, - &ce.config.BTCNetParams, - ) + fpsEncKeys := make([]*asig.EncryptionKey, 0, len(btcDel.FpBtcPks)) + for _, fpPk := range btcDel.FpBtcPks { + encKey, err := asig.NewEncryptionKeyFromBTCPK(fpPk) + if err != nil { + continue + // fpPkHex := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex() + // return nil, nil, fmt.Errorf("failed to get encryption key from finality provider public key %s: %w", fpPkHex, err) + } + + fpsEncKeys = append(fpsEncKeys, encKey) + } + + slashingPkScriptPath, stakingTxUnbondingPkScriptPath, err := generatePkScriptPathSlashAndUnbond(btcDel, params, &ce.config.BTCNetParams) if err != nil { - ce.logger.Error("failed to sign signatures or unbonding signature", zap.Error(err)) + ce.logger.Error("failed to generate pk script path for slash and unbond", zap.Error(err)) continue } - - // 7. sign covenant slash unbonding signatures - slashUnbondingSigs, err := signSlashUnbondingSignatures( - btcDel, - unbondingTx, - slashUnbondingTx, - ce.signer, - params, - &ce.config.BTCNetParams, - ) + // slashSigs, unbondingSig, err := signSlashAndUnbondSignatures( + // btcDel, + // stakingTx, + // slashingTx, + // unbondingTx, + // ce.signer, + // params, + // &ce.config.BTCNetParams, + // ) + // if err != nil { + // ce.logger.Error("failed to sign signatures or unbonding signature", zap.Error(err)) + // continue + // } + + unbondingTxSlashingScriptPath, err := pkScriptPathSlashUnbonding(btcDel, unbondingTx, params, &ce.config.BTCNetParams) if err != nil { - ce.logger.Error("failed to slash unbonding signature", zap.Error(err)) + ce.logger.Error("failed to generate pk script path for slash unbonding", zap.Error(err)) continue } + // 7. sign covenant slash unbonding signatures + // slashUnbondingSigs, err := signSlashUnbondingSignatures( + // btcDel, + // unbondingTx, + // slashUnbondingTx, + // ce.signer, + // params, + // &ce.config.BTCNetParams, + // ) + // if err != nil { + // ce.logger.Error("failed to slash unbonding signature", zap.Error(err)) + // continue + // } + + signingReq[stakingTx.TxHash()] = SigningTxsRequest{ + StakingTx: stakingTx, + SlashingTx: slashingTx, + UnbondingTx: unbondingTx, + SlashUnbondingTx: slashUnbondingTx, + StakingOutputIdx: btcDel.StakingOutputIdx, + SlashingPkScriptPath: slashingPkScriptPath, + StakingTxUnbondingPkScriptPath: stakingTxUnbondingPkScriptPath, + UnbondingTxSlashingScriptPath: unbondingTxSlashingScriptPath, + FpEncKeys: fpsEncKeys, + } + + } + + // Calls the signer to sign all + // builds the covenant signatures - // record metrics - finishSignTime := time.Now() - metricsTimeKeeper.SetPreviousSignFinish(&finishSignTime) - timedSignDelegationLag.Observe(time.Since(startSignTime).Seconds()) + // 8. sign covenant staking sigs + // record metrics + startSignTime := time.Now() + metricsTimeKeeper.SetPreviousSignStart(&startSignTime) - // 8. collect covenant sigs + // calls the signer + respSignatures, err := ce.signer.SignTransactions(SigningRequest{SigningTxsReqByStkTxHash: signingReq}) + if err != nil { + ce.recordMetricsFailedSignDelegations(len(btcDels)) + return nil, err + } + + covenantSigs := make([]*types.CovenantSigs, 0, len(btcDels)) + for stkTxHash, signatures := range respSignatures.SignaturesByStkTxHash { covenantSigs = append(covenantSigs, &types.CovenantSigs{ PublicKey: ce.pk, - StakingTxHash: stakingTx.TxHash(), - SlashingSigs: slashSigs, - UnbondingSig: unbondingSig, - SlashingUnbondingSigs: slashUnbondingSigs, + StakingTxHash: stkTxHash, + SlashingSigs: signatures.SlashSigs, + UnbondingSig: signatures.UnbondingSig, + SlashingUnbondingSigs: signatures.SlashUnbondingSigs, }) } + // record metrics + finishSignTime := time.Now() + metricsTimeKeeper.SetPreviousSignFinish(&finishSignTime) + timedSignDelegationLag.Observe(time.Since(startSignTime).Seconds()) + + // builds the covenant signs to submit + // 8. collect covenant sigs + // covenantSigs = append(covenantSigs, &types.CovenantSigs{ + // PublicKey: ce.pk, + // StakingTxHash: stakingTx.TxHash(), + // SlashingSigs: slashSigs, + // UnbondingSig: unbondingSig, + // SlashingUnbondingSigs: slashUnbondingSigs, + // }) + // 9. submit covenant sigs res, err := ce.cc.SubmitCovenantSigs(covenantSigs) if err != nil { @@ -249,6 +309,34 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( return res, nil } +func pkScriptPathSlashUnbonding( + del *types.Delegation, + unbondingTx *wire.MsgTx, + params *types.StakingParams, + btcNet *chaincfg.Params, +) (unbondingTxSlashingScriptPath []byte, err error) { + unbondingInfo, err := btcstaking.BuildUnbondingInfo( + del.BtcPk, + del.FpBtcPks, + params.CovenantPks, + params.CovenantQuorum, + del.UnbondingTime, + btcutil.Amount(unbondingTx.TxOut[0].Value), + btcNet, + ) + if err != nil { + return nil, err + } + + unbondingTxSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() + if err != nil { + return nil, err + } + unbondingTxSlashingScriptPath = unbondingTxSlashingPathInfo.GetPkScriptPath() + + return unbondingTxSlashingScriptPath, nil +} + func signSlashUnbondingSignatures( del *types.Delegation, unbondingTx *wire.MsgTx, @@ -274,6 +362,7 @@ func signSlashUnbondingSignatures( if err != nil { return nil, err } + unbondingTxSlashingPathInfo := unbondingTxSlashingPath.GetPkScriptPath() slashUnbondingSigs := make([][]byte, 0, len(del.FpBtcPks)) for _, fpPk := range del.FpBtcPks { @@ -285,7 +374,7 @@ func signSlashUnbondingSignatures( slashUnbondingTx, unbondingTx, 0, // 0th output is always the unbonding script output - unbondingTxSlashingPath.GetPkScriptPath(), + unbondingTxSlashingPathInfo, encKey, ) if err != nil { @@ -297,6 +386,41 @@ func signSlashUnbondingSignatures( return slashUnbondingSigs, nil } +func generatePkScriptPathSlashAndUnbond( + del *types.Delegation, + params *types.StakingParams, + btcNet *chaincfg.Params, +) (slashingPkScriptPath, stakingTxUnbondingPkScriptPath []byte, err error) { + // sign slash signatures with every finality providers + stakingInfo, err := btcstaking.BuildStakingInfo( + del.BtcPk, + del.FpBtcPks, + params.CovenantPks, + params.CovenantQuorum, + del.GetStakingTime(), + del.TotalSat, + btcNet, + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to build staking info: %w", err) + } + + slashingPathInfo, err := stakingInfo.SlashingPathSpendInfo() + if err != nil { + return nil, nil, fmt.Errorf("failed to get slashing path info: %w", err) + } + slashingPkScriptPath = slashingPathInfo.GetPkScriptPath() + + // sign unbonding sig + stakingTxUnbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() + if err != nil { + return nil, nil, fmt.Errorf("failed to get unbonding path spend info") + } + stakingTxUnbondingPkScriptPath = stakingTxUnbondingPathInfo.GetPkScriptPath() + + return slashingPkScriptPath, stakingTxUnbondingPkScriptPath, nil +} + func signSlashAndUnbondSignatures( del *types.Delegation, stakingTx *wire.MsgTx, @@ -341,7 +465,7 @@ func signSlashAndUnbondSignatures( encKey, ) if err != nil { - return nil, nil, fmt.Errorf("failed to sign adaptor signature with finaliyt provider public key %s: %w", + return nil, nil, fmt.Errorf("failed to sign adaptor signature with finality provider public key %s: %w", fpPkHex, err) } slashSigs = append(slashSigs, slashSig.MustMarshal()) diff --git a/covenant/expected_interfaces.go b/covenant/expected_interfaces.go index 1040a0e..4b8d07b 100644 --- a/covenant/expected_interfaces.go +++ b/covenant/expected_interfaces.go @@ -3,12 +3,40 @@ package covenant import ( asig "github.com/babylonlabs-io/babylon/crypto/schnorr-adaptor-signature" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" ) +type SigningTxsRequest struct { + StakingTx *wire.MsgTx + SlashingTx *wire.MsgTx + UnbondingTx *wire.MsgTx + SlashUnbondingTx *wire.MsgTx + StakingOutputIdx uint32 + SlashingPkScriptPath []byte + StakingTxUnbondingPkScriptPath []byte + UnbondingTxSlashingScriptPath []byte + FpEncKeys []*asig.EncryptionKey +} + +type SigningRequest struct { + SigningTxsReqByStkTxHash map[chainhash.Hash]SigningTxsRequest +} + +type SignaturesResponse struct { + SlashSigs [][]byte + UnbondingSig *schnorr.Signature + SlashUnbondingSigs [][]byte +} + +type SigningResponse struct { + SignaturesByStkTxHash map[chainhash.Hash]SignaturesResponse +} + // Signer wrapper interface to sign messages type Signer interface { + SignTransactions(req SigningRequest) (*SigningResponse, error) // PubKey returns the current secp256k1 public key PubKey() (*secp.PublicKey, error) // EncSignTxWithOneScriptSpendInputStrict is encrypted version of diff --git a/keyring/signer.go b/keyring/signer.go index 0b13a03..3f113bd 100644 --- a/keyring/signer.go +++ b/keyring/signer.go @@ -10,6 +10,7 @@ import ( asig "github.com/babylonlabs-io/babylon/crypto/schnorr-adaptor-signature" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" secp "github.com/decred/dcrd/dcrec/secp256k1/v4" ) @@ -118,3 +119,72 @@ func (kcs KeyringSigner) SignTxWithOneScriptSpendInputStrict( covenantPrivKey, ) } + +// SignTransactions receives a batch of transactions to sign and returns all the signatures if nothing fails. +func (kcs KeyringSigner) SignTransactions(req covenant.SigningRequest) (*covenant.SigningResponse, error) { + resp := make(map[chainhash.Hash]covenant.SignaturesResponse, len(req.SigningTxsReqByStkTxHash)) + + covenantPrivKey, err := kcs.getPrivKey() + if err != nil { + return nil, fmt.Errorf("failed to get Covenant private key: %w", err) + } + + for stakingTxHash, signingTxReq := range req.SigningTxsReqByStkTxHash { + // for each signing tx request + + slashSigs := make([][]byte, 0, len(signingTxReq.FpEncKeys)) + slashUnbondingSigs := make([][]byte, 0, len(signingTxReq.FpEncKeys)) + for _, fpEncKey := range signingTxReq.FpEncKeys { + // creates slash sigs + // TODO: split to diff func + slashSig, err := btcstaking.EncSignTxWithOneScriptSpendInputStrict( + signingTxReq.SlashingTx, + signingTxReq.StakingTx, + signingTxReq.StakingOutputIdx, + signingTxReq.SlashingPkScriptPath, + covenantPrivKey, + fpEncKey, + ) + if err != nil { + return nil, fmt.Errorf("failed to sign adaptor slash signature with finality provider public key %s: %w", fpEncKey.ToBytes(), err) + } + slashSigs = append(slashSigs, slashSig.MustMarshal()) + + // TODO: split to diff func + // creates slash unbonding sig + slashUnbondingSig, err := btcstaking.EncSignTxWithOneScriptSpendInputStrict( + signingTxReq.SlashUnbondingTx, + signingTxReq.UnbondingTx, + 0, // 0th output is always the unbonding script output + signingTxReq.UnbondingTxSlashingScriptPath, + covenantPrivKey, + fpEncKey, + ) + if err != nil { + return nil, fmt.Errorf("failed to sign adaptor slash unbonding signature with finality provider public key %s: %w", fpEncKey.ToBytes(), err) + } + slashUnbondingSigs = append(slashUnbondingSigs, slashUnbondingSig.MustMarshal()) + } + + unbondingSig, err := btcstaking.SignTxWithOneScriptSpendInputStrict( + signingTxReq.UnbondingTx, + signingTxReq.StakingTx, + signingTxReq.StakingOutputIdx, + signingTxReq.StakingTxUnbondingPkScriptPath, + covenantPrivKey, + ) + if err != nil { + return nil, fmt.Errorf("failed to sign unbonding tx: %w", err) + } + + resp[stakingTxHash] = covenant.SignaturesResponse{ + SlashSigs: slashSigs, + UnbondingSig: unbondingSig, + SlashUnbondingSigs: slashUnbondingSigs, + } + } + + return &covenant.SigningResponse{ + SignaturesByStkTxHash: resp, + }, nil +}