diff --git a/covenant/covenant.go b/covenant/covenant.go index 0b4a6de..ebe5dd8 100644 --- a/covenant/covenant.go +++ b/covenant/covenant.go @@ -3,6 +3,8 @@ package covenant import ( "encoding/hex" "fmt" + "sort" + "strings" "sync" "time" @@ -17,6 +19,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" "go.uber.org/zap" "github.com/babylonlabs-io/covenant-emulator/clientcontroller" @@ -86,7 +89,7 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( return nil, fmt.Errorf("no delegations") } - signingReq := make(map[chainhash.Hash]SigningTxsRequest) + signingReq := make(map[chainhash.Hash]SigningTxsRequest, len(btcDels)) for _, btcDel := range btcDels { // 0. nil checks if btcDel == nil { @@ -187,129 +190,107 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( continue } - // Generate signing request data. - - 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) + // 8. Generate Signing Request + // Finality providers encription keys + // pk script paths for Slash, unbond and unbonding slashing + fpsEncKeys, err := fpEncKeysFromDel(btcDel) if err != nil { - ce.logger.Error("failed to generate pk script path for slash and unbond", zap.Error(err)) + ce.logger.Error("failed to encript the finality provider keys of the btc delegation", zap.String("staker_pk", stakerPkHex), zap.Error(err)) continue } - // 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) + + slashingPkScriptPath, stakingTxUnbondingPkScriptPath, unbondingTxSlashingPkScriptPath, err := pkScriptPaths(btcDel, params, &ce.config.BTCNetParams, unbondingTx) if err != nil { - ce.logger.Error("failed to generate pk script path for slash unbonding", zap.Error(err)) + ce.logger.Error("failed to generate pk script path", 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, + StakingTx: stakingTx, + SlashingTx: slashingTx, + UnbondingTx: unbondingTx, + SlashUnbondingTx: slashUnbondingTx, + StakingOutputIdx: btcDel.StakingOutputIdx, + SlashingPkScriptPath: slashingPkScriptPath, + StakingTxUnbondingPkScriptPath: stakingTxUnbondingPkScriptPath, + UnbondingTxSlashingPkScriptPath: unbondingTxSlashingPkScriptPath, + FpEncKeys: fpsEncKeys, } - } - // Calls the signer to sign all - // builds the covenant signatures + respSigs, err := ce.SignTransactions(signingReq) + if err != nil { + return nil, err + } - // 8. sign covenant staking sigs - // record metrics - startSignTime := time.Now() - metricsTimeKeeper.SetPreviousSignStart(&startSignTime) + covenantSigs := BuildCovenantSigs(ce.pk, *respSigs) - // calls the signer - respSignatures, err := ce.signer.SignTransactions(SigningRequest{SigningTxsReqByStkTxHash: signingReq}) + // 9. submit covenant sigs + res, err := ce.cc.SubmitCovenantSigs(covenantSigs) if err != nil { - ce.recordMetricsFailedSignDelegations(len(btcDels)) + ce.recordMetricsFailedSignDelegations(len(covenantSigs)) return nil, err } - covenantSigs := make([]*types.CovenantSigs, 0, len(btcDels)) - for stkTxHash, signatures := range respSignatures.SignaturesByStkTxHash { - covenantSigs = append(covenantSigs, &types.CovenantSigs{ - PublicKey: ce.pk, + // record metrics + submittedTime := time.Now() + metricsTimeKeeper.SetPreviousSubmission(&submittedTime) + ce.recordMetricsTotalSignDelegationsSubmitted(len(covenantSigs)) + + return res, nil +} + +// BuildCovenantSigs creates the covenant signatures from the signature response +func BuildCovenantSigs(pk *secp.PublicKey, resp SigningResponse) []types.CovenantSigs { + covenantSigs := make([]types.CovenantSigs, 0, len(resp.SignaturesByStkTxHash)) + for stkTxHash, signatures := range resp.SignaturesByStkTxHash { + covenantSigs = append(covenantSigs, types.CovenantSigs{ + PublicKey: *pk, StakingTxHash: stkTxHash, SlashingSigs: signatures.SlashSigs, - UnbondingSig: signatures.UnbondingSig, + UnbondingSig: *signatures.UnbondingSig, SlashingUnbondingSigs: signatures.SlashUnbondingSigs, }) } + return SortCovenantSigs(covenantSigs) +} +// SignTransactions calls the signer and record metrics about signing +func (ce *CovenantEmulator) SignTransactions(signingReq map[chainhash.Hash]SigningTxsRequest) (*SigningResponse, error) { // 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, - // }) + startSignTime := time.Now() + metricsTimeKeeper.SetPreviousSignStart(&startSignTime) - // 9. submit covenant sigs - res, err := ce.cc.SubmitCovenantSigs(covenantSigs) + // 8. sign covenant transactions + respSignatures, err := ce.signer.SignTransactions(SigningRequest{SigningTxsReqByStkTxHash: signingReq}) if err != nil { - ce.recordMetricsFailedSignDelegations(len(covenantSigs)) + ce.recordMetricsFailedSignDelegations(len(signingReq)) return nil, err } // record metrics - submittedTime := time.Now() - metricsTimeKeeper.SetPreviousSubmission(&submittedTime) - ce.recordMetricsTotalSignDelegationsSubmitted(len(covenantSigs)) + finishSignTime := time.Now() + metricsTimeKeeper.SetPreviousSignFinish(&finishSignTime) + timedSignDelegationLag.Observe(time.Since(startSignTime).Seconds()) - return res, nil + return respSignatures, nil +} + +func fpEncKeysFromDel(btcDel *types.Delegation) ([]*asig.EncryptionKey, error) { + fpsEncKeys := make([]*asig.EncryptionKey, 0, len(btcDel.FpBtcPks)) + for _, fpPk := range btcDel.FpBtcPks { + encKey, err := asig.NewEncryptionKeyFromBTCPK(fpPk) + if err != nil { + fpPkHex := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex() + return nil, fmt.Errorf("failed to get encryption key from finality provider public key %s: %w", fpPkHex, err) + } + fpsEncKeys = append(fpsEncKeys, encKey) + } + + return fpsEncKeys, nil } -func pkScriptPathSlashUnbonding( +func pkScriptPathUnbondingSlash( del *types.Delegation, unbondingTx *wire.MsgTx, params *types.StakingParams, @@ -337,56 +318,26 @@ func pkScriptPathSlashUnbonding( return unbondingTxSlashingScriptPath, nil } -func signSlashUnbondingSignatures( +func pkScriptPaths( del *types.Delegation, - unbondingTx *wire.MsgTx, - slashUnbondingTx *wire.MsgTx, - signer Signer, params *types.StakingParams, btcNet *chaincfg.Params, -) ([][]byte, error) { - unbondingInfo, err := btcstaking.BuildUnbondingInfo( - del.BtcPk, - del.FpBtcPks, - params.CovenantPks, - params.CovenantQuorum, - del.UnbondingTime, - btcutil.Amount(unbondingTx.TxOut[0].Value), - btcNet, - ) + unbondingTx *wire.MsgTx, +) (slash, unbond, unbondSlash []byte, err error) { + slash, unbond, err = pkScriptPathSlashAndUnbond(del, params, btcNet) if err != nil { - return nil, err + return nil, nil, nil, err } - unbondingTxSlashingPath, err := unbondingInfo.SlashingPathSpendInfo() + unbondSlash, err = pkScriptPathUnbondingSlash(del, unbondingTx, params, btcNet) if err != nil { - return nil, err - } - unbondingTxSlashingPathInfo := unbondingTxSlashingPath.GetPkScriptPath() - - slashUnbondingSigs := make([][]byte, 0, len(del.FpBtcPks)) - for _, fpPk := range del.FpBtcPks { - encKey, err := asig.NewEncryptionKeyFromBTCPK(fpPk) - if err != nil { - return nil, err - } - slashUnbondingSig, err := signer.EncSignTxWithOneScriptSpendInputStrict( - slashUnbondingTx, - unbondingTx, - 0, // 0th output is always the unbonding script output - unbondingTxSlashingPathInfo, - encKey, - ) - if err != nil { - return nil, err - } - slashUnbondingSigs = append(slashUnbondingSigs, slashUnbondingSig.MustMarshal()) + return nil, nil, nil, err } - return slashUnbondingSigs, nil + return slash, unbond, unbondSlash, nil } -func generatePkScriptPathSlashAndUnbond( +func pkScriptPathSlashAndUnbond( del *types.Delegation, params *types.StakingParams, btcNet *chaincfg.Params, @@ -421,74 +372,6 @@ func generatePkScriptPathSlashAndUnbond( return slashingPkScriptPath, stakingTxUnbondingPkScriptPath, nil } -func signSlashAndUnbondSignatures( - del *types.Delegation, - stakingTx *wire.MsgTx, - slashingTx *wire.MsgTx, - unbondingTx *wire.MsgTx, - signer Signer, - params *types.StakingParams, - btcNet *chaincfg.Params, -) ([][]byte, *schnorr.Signature, 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) - } - - slashSigs := make([][]byte, 0, len(del.FpBtcPks)) - for _, fpPk := range del.FpBtcPks { - fpPkHex := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex() - encKey, err := asig.NewEncryptionKeyFromBTCPK(fpPk) - if err != nil { - return nil, nil, fmt.Errorf("failed to get encryption key from finality provider public key %s: %w", - fpPkHex, err) - } - slashSig, err := signer.EncSignTxWithOneScriptSpendInputStrict( - slashingTx, - stakingTx, - del.StakingOutputIdx, - slashingPathInfo.GetPkScriptPath(), - encKey, - ) - if err != nil { - return nil, nil, fmt.Errorf("failed to sign adaptor signature with finality provider public key %s: %w", - fpPkHex, err) - } - slashSigs = append(slashSigs, slashSig.MustMarshal()) - } - - // sign unbonding sig - stakingTxUnbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() - if err != nil { - return nil, nil, fmt.Errorf("failed to get unbonding path spend info") - } - unbondingSig, err := signer.SignTxWithOneScriptSpendInputStrict( - unbondingTx, - stakingTx, - del.StakingOutputIdx, - stakingTxUnbondingPathInfo.GetPkScriptPath(), - ) - if err != nil { - return nil, nil, fmt.Errorf("failed to sign unbonding tx: %w", err) - } - - return slashSigs, unbondingSig, nil -} - func decodeDelegationTransactions(del *types.Delegation, params *types.StakingParams, btcNet *chaincfg.Params) (*wire.MsgTx, *wire.MsgTx, error) { // 1. decode staking tx and slashing tx stakingMsgTx, _, err := bbntypes.NewBTCTxFromHex(del.StakingTxHex) @@ -723,3 +606,14 @@ func (ce *CovenantEmulator) Stop() error { }) return stopErr } + +// SortCovenantSigs helper function to sort all covenant signatures by the staking tx hash +func SortCovenantSigs(covSigs []types.CovenantSigs) []types.CovenantSigs { + sorted := make([]types.CovenantSigs, len(covSigs)) + copy(sorted, covSigs) + sort.SliceStable(sorted, func(i, j int) bool { + return strings.Compare(sorted[i].StakingTxHash.String(), sorted[j].StakingTxHash.String()) == 1 + }) + + return sorted +} diff --git a/covenant/expected_interfaces.go b/covenant/expected_interfaces.go index 4b8d07b..7701189 100644 --- a/covenant/expected_interfaces.go +++ b/covenant/expected_interfaces.go @@ -8,16 +8,25 @@ import ( secp "github.com/decred/dcrd/dcrec/secp256k1/v4" ) +// Signer wrapper interface to sign messages +type Signer interface { + // SignTransactions signs all the transactions from the request + // and returns all the signatures for Slash, Unbond and Unbonding Slash. + SignTransactions(req SigningRequest) (*SigningResponse, error) + // PubKey returns the current secp256k1 public key + PubKey() (*secp.PublicKey, error) +} + 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 + StakingTx *wire.MsgTx + SlashingTx *wire.MsgTx + UnbondingTx *wire.MsgTx + SlashUnbondingTx *wire.MsgTx + StakingOutputIdx uint32 + SlashingPkScriptPath []byte + StakingTxUnbondingPkScriptPath []byte + UnbondingTxSlashingPkScriptPath []byte + FpEncKeys []*asig.EncryptionKey } type SigningRequest struct { @@ -33,34 +42,3 @@ type SignaturesResponse struct { 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 - // SignTxWithOneScriptSpendInputStrict with the output to be encrypted - // by an encryption key (adaptor signature) - EncSignTxWithOneScriptSpendInputStrict( - txToSign *wire.MsgTx, - fundingTx *wire.MsgTx, - fundingOutputIdx uint32, - signedScriptPath []byte, - encKey *asig.EncryptionKey, - ) (*asig.AdaptorSignature, error) - // SignTxWithOneScriptSpendInputStrict signs transaction with one input coming - // from script spend output with provided script. - // It checks: - // - txToSign is not nil - // - txToSign has exactly one input - // - fundingTx is not nil - // - fundingTx has one output committing to the provided script - // - txToSign input is pointing to the correct output in fundingTx - SignTxWithOneScriptSpendInputStrict( - txToSign *wire.MsgTx, - fundingTx *wire.MsgTx, - fundingOutputIdx uint32, - signedScriptPath []byte, - ) (*schnorr.Signature, error) -} diff --git a/keyring/signer.go b/keyring/signer.go index 3f113bd..efe04ef 100644 --- a/keyring/signer.go +++ b/keyring/signer.go @@ -7,11 +7,8 @@ import ( "github.com/babylonlabs-io/babylon/btcstaking" "github.com/babylonlabs-io/covenant-emulator/covenant" - 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" ) @@ -67,59 +64,6 @@ func (kcs KeyringSigner) getPrivKey() (*btcec.PrivateKey, error) { return privKey, nil } -// EncSignTxWithOneScriptSpendInputStrict is encrypted version of -// SignTxWithOneScriptSpendInputStrict with the output to be encrypted -// by an encryption key (adaptor signature) -func (kcs KeyringSigner) EncSignTxWithOneScriptSpendInputStrict( - txToSign *wire.MsgTx, - fundingTx *wire.MsgTx, - fundingOutputIdx uint32, - signedScriptPath []byte, - encKey *asig.EncryptionKey, -) (*asig.AdaptorSignature, error) { - covenantPrivKey, err := kcs.getPrivKey() - if err != nil { - return nil, fmt.Errorf("failed to get Covenant private key: %w", err) - } - - return btcstaking.EncSignTxWithOneScriptSpendInputStrict( - txToSign, - fundingTx, - fundingOutputIdx, - signedScriptPath, - covenantPrivKey, - encKey, - ) -} - -// SignTxWithOneScriptSpendInputStrict signs transaction with one input coming -// from script spend output with provided script. -// It checks: -// - txToSign is not nil -// - txToSign has exactly one input -// - fundingTx is not nil -// - fundingTx has one output committing to the provided script -// - txToSign input is pointing to the correct output in fundingTx -func (kcs KeyringSigner) SignTxWithOneScriptSpendInputStrict( - txToSign *wire.MsgTx, - fundingTx *wire.MsgTx, - fundingOutputIdx uint32, - signedScriptPath []byte, -) (*schnorr.Signature, error) { - covenantPrivKey, err := kcs.getPrivKey() - if err != nil { - return nil, fmt.Errorf("failed to get Covenant private key: %w", err) - } - - return btcstaking.SignTxWithOneScriptSpendInputStrict( - txToSign, - fundingTx, - fundingOutputIdx, - signedScriptPath, - 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)) @@ -156,7 +100,7 @@ func (kcs KeyringSigner) SignTransactions(req covenant.SigningRequest) (*covenan signingTxReq.SlashUnbondingTx, signingTxReq.UnbondingTx, 0, // 0th output is always the unbonding script output - signingTxReq.UnbondingTxSlashingScriptPath, + signingTxReq.UnbondingTxSlashingPkScriptPath, covenantPrivKey, fpEncKey, )