From 6ba8d4fff5e58c4b2a77bf84c320cab98c788f53 Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Thu, 25 Jul 2024 08:52:32 +0200 Subject: [PATCH] Encapsulate signing into separate function (#187) --- itest/e2e_test.go | 4 +- staker/babylontypes.go | 49 +++++++++++++--- staker/stakerapp.go | 106 ++++++++++++++++++++++++---------- staker/types.go | 90 +++++++++-------------------- walletcontroller/client.go | 44 ++++++++++---- walletcontroller/interface.go | 22 ++++++- 6 files changed, 204 insertions(+), 111 deletions(-) diff --git a/itest/e2e_test.go b/itest/e2e_test.go index d17afb8..5aea7c9 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -294,7 +294,7 @@ func StartManager( err = walletClient.UnlockWallet(20) require.NoError(t, err) - walletPrivKey, err := walletClient.DumpPrivateKey(minerAddressDecoded) + walletPrivKey, err := c.DumpPrivKey(minerAddressDecoded) require.NoError(t, err) interceptor, err := signal.Intercept() @@ -334,7 +334,7 @@ func StartManager( Db: dbbackend, Sa: stakerApp, BabylonClient: bl, - WalletPrivKey: walletPrivKey, + WalletPrivKey: walletPrivKey.PrivKey, MinerAddr: minerAddressDecoded, serverStopper: &interceptor, wg: &wg, diff --git a/staker/babylontypes.go b/staker/babylontypes.go index 5713bec..2003787 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -39,7 +39,12 @@ func (app *StakerApp) buildOwnedDelegation( slashingFee := app.getSlashingFee(externalData.babylonParams.MinSlashingTxFeeSat) - slashingTx, slashingTxSig, err := buildSlashingTxAndSig(slashingFee, externalData, storedTx, app.network) + stakingSlashingTx, stakingSlashingSpendInfo, err := slashingTxForStakingTx( + slashingFee, + externalData, + storedTx, + app.network, + ) if err != nil { // This is truly unexpected, most probably programming error we have // valid and btc confirmed staking transacion, but for some reason we cannot @@ -55,9 +60,9 @@ func (app *StakerApp) buildOwnedDelegation( // in case of estimation failure (25 sat/byte) unbondingTxFeeRatePerKb := btcutil.Amount(app.feeEstimator.EstimateFeePerKb()) - undelegationData, err := createUndelegationData( + undelegationDesc, err := createUndelegationData( storedTx, - externalData.stakerPrivKey, + externalData.stakerPublicKey, externalData.babylonParams.CovenantPks, externalData.babylonParams.CovenantQuruomThreshold, externalData.babylonParams.SlashingAddress, @@ -74,16 +79,46 @@ func (app *StakerApp) buildOwnedDelegation( return nil, fmt.Errorf("error creating undelegation data: %w", err) } + stakingSlashingSig, err := app.signTaprootScriptSpendUsingWallet( + stakingSlashingTx, + storedTx.StakingTx.TxOut[storedTx.StakingOutputIndex], + stakerAddress, + &stakingSlashingSpendInfo.RevealedLeaf, + &stakingSlashingSpendInfo.ControlBlock, + ) + + if err != nil { + return nil, fmt.Errorf("error signing slashing transaction for staking transaction: %w", err) + } + + unbondingSlashingSig, err := app.signTaprootScriptSpendUsingWallet( + undelegationDesc.SlashUnbondingTransaction, + undelegationDesc.UnbondingTransaction.TxOut[0], + stakerAddress, + &undelegationDesc.SlashUnbondingTransactionSpendInfo.RevealedLeaf, + &undelegationDesc.SlashUnbondingTransactionSpendInfo.ControlBlock, + ) + + if err != nil { + return nil, fmt.Errorf("error signing slashing transaction for unbonding transaction: %w", err) + } + dg := createDelegationData( - externalData.stakerPrivKey.PubKey(), + externalData.stakerPublicKey, req.inclusionBlock, req.txIndex, storedTx, - slashingTx, - slashingTxSig, + stakingSlashingTx, + stakingSlashingSig, externalData.babylonStakerAddr, stakingTxInclusionProof, - undelegationData, + &cl.UndelegationData{ + UnbondingTransaction: undelegationDesc.UnbondingTransaction, + UnbondingTxValue: undelegationDesc.UnbondingTxValue, + UnbondingTxUnbondingTime: undelegationDesc.UnbondingTxUnbondingTime, + SlashUnbondingTransaction: undelegationDesc.SlashUnbondingTransaction, + SlashUnbondingTransactionSig: unbondingSlashingSig, + }, ) return dg, nil diff --git a/staker/stakerapp.go b/staker/stakerapp.go index fc35cbf..2fa244e 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -39,10 +39,10 @@ import ( ) type externalDelegationData struct { - // stakerPrivKey needs to be retrieved from btc wallet - stakerPrivKey *btcec.PrivateKey // babylonStakerAddr the bech32 bbn address to receive staking rewards. babylonStakerAddr sdk.AccAddress + // stakerPublicKey the public key of the staker. + stakerPublicKey *btcec.PublicKey // params retrieved from babylon babylonParams *cl.StakingParams } @@ -768,36 +768,21 @@ func (app *StakerApp) mustBuildInclusionProof(req *sendDelegationRequest) []byte return proof } -func (app *StakerApp) stakerPrivateKey(stakerAddress btcutil.Address) (*btcec.PrivateKey, error) { - err := app.wc.UnlockWallet(defaultWalletUnlockTimeout) - - if err != nil { - return nil, err - } - - privkey, err := app.wc.DumpPrivateKey(stakerAddress) - - if err != nil { - return nil, err - } - - return privkey, nil -} - func (app *StakerApp) retrieveExternalDelegationData(stakerAddress btcutil.Address) (*externalDelegationData, error) { params, err := app.babylonClient.Params() if err != nil { return nil, err } - stakerPrivKey, err := app.stakerPrivateKey(stakerAddress) + stakerPublicKey, err := app.wc.AddressPublicKey(stakerAddress) + if err != nil { return nil, err } return &externalDelegationData{ - stakerPrivKey: stakerPrivKey, babylonStakerAddr: app.babylonClient.GetKeyAddress(), + stakerPublicKey: stakerPublicKey, babylonParams: params, }, nil } @@ -808,7 +793,8 @@ func (app *StakerApp) sendUnbondingTxToBtcWithWitness( storedTx *stakerdb.StoredTransaction, unbondingData *stakerdb.UnbondingStoreData, ) error { - privkey, err := app.stakerPrivateKey(stakerAddress) + + stakerPubKey, err := app.wc.AddressPublicKey(stakerAddress) if err != nil { app.logger.WithFields(logrus.Fields{ @@ -825,8 +811,8 @@ func (app *StakerApp) sendUnbondingTxToBtcWithWitness( return err } - witness, err := createWitnessToSendUnbondingTx( - privkey, + unbondingSpendInfo, err := buildUnbondingSpendInfo( + stakerPubKey, storedTx, unbondingData, params, @@ -838,7 +824,37 @@ func (app *StakerApp) sendUnbondingTxToBtcWithWitness( app.logger.WithFields(logrus.Fields{ "stakingTxHash": stakingTxHash, "err": err, - }).Fatalf("Failed to create witness to send unbonding tx to btc") + }).Fatalf("failed to create necessary spend info to send unbonding tx") + } + + stakerUnbondingSig, err := app.signTaprootScriptSpendUsingWallet( + unbondingData.UnbondingTx, + storedTx.StakingTx.TxOut[storedTx.StakingOutputIndex], + stakerAddress, + &unbondingSpendInfo.RevealedLeaf, + &unbondingSpendInfo.ControlBlock, + ) + + if err != nil { + return fmt.Errorf("failed to send unbondingtx. wallet signing error: %w", err) + } + + covenantSigantures := createWitnessSignaturesForPubKeys( + params.CovenantPks, + unbondingData.CovenantSignatures, + ) + + witness, err := unbondingSpendInfo.CreateUnbondingPathWitness( + covenantSigantures, + stakerUnbondingSig, + ) + + if err != nil { + // we panic here, as our data should be correct at this point + app.logger.WithFields(logrus.Fields{ + "stakingTxHash": stakingTxHash, + "err": err, + }).Fatalf("failed to build witness from correct data") } unbondingTx := unbondingData.UnbondingTx @@ -1590,6 +1606,37 @@ func (app *StakerApp) waitForSpendConfirmation(stakingTxHash chainhash.Hash, ev } } +func (app *StakerApp) signTaprootScriptSpendUsingWallet( + txToSign *wire.MsgTx, + fundingOutput *wire.TxOut, + signerAddress btcutil.Address, + leaf *txscript.TapLeaf, + controlBlock *txscript.ControlBlock, +) (*schnorr.Signature, error) { + + if err := app.wc.UnlockWallet(defaultWalletUnlockTimeout); err != nil { + return nil, fmt.Errorf("failed to unlock wallet before signing: %w", err) + } + + req := &walletcontroller.TaprootSigningRequest{ + FundingOutput: fundingOutput, + TxToSign: txToSign, + SignerAddress: signerAddress, + SpendDescription: &walletcontroller.SpendPathDescription{ + ScriptLeaf: leaf, + ControlBlock: controlBlock, + }, + } + + resp, err := app.wc.SignOneInputTaprootSpendingTransaction(req) + + if err != nil { + return nil, err + } + + return resp.Signature, nil +} + // SpendStake spends stake identified by stakingTxHash. Stake can be currently locked in // two types of outputs: // 1. Staking output - this is output which is created by staking transaction @@ -1643,7 +1690,7 @@ func (app *StakerApp) SpendStake(stakingTxHash *chainhash.Hash) (*chainhash.Hash return nil, nil, fmt.Errorf("cannot spend staking output. Error getting params: %w", err) } - privKey, err := app.stakerPrivateKey(destAddress) + pubKey, err := app.wc.AddressPublicKey(destAddress) if err != nil { return nil, nil, fmt.Errorf("cannot spend staking output. Error getting private key: %w", err) @@ -1652,7 +1699,7 @@ func (app *StakerApp) SpendStake(stakingTxHash *chainhash.Hash) (*chainhash.Hash currentFeeRate := app.feeEstimator.EstimateFeePerKb() spendStakeTxInfo, err := createSpendStakeTxFromStoredTx( - privKey.PubKey(), + pubKey, params.CovenantPks, params.CovenantQuruomThreshold, tx, @@ -1665,11 +1712,12 @@ func (app *StakerApp) SpendStake(stakingTxHash *chainhash.Hash) (*chainhash.Hash return nil, nil, err } - stakerSig, err := staking.SignTxWithOneScriptSpendInputFromTapLeaf( + stakerSig, err := app.signTaprootScriptSpendUsingWallet( spendStakeTxInfo.spendStakeTx, spendStakeTxInfo.fundingOutput, - privKey, - spendStakeTxInfo.fundingOutputSpendInfo.RevealedLeaf, + destAddress, + &spendStakeTxInfo.fundingOutputSpendInfo.RevealedLeaf, + &spendStakeTxInfo.fundingOutputSpendInfo.ControlBlock, ) if err != nil { diff --git a/staker/types.go b/staker/types.go index 99aa3d0..24de4ec 100644 --- a/staker/types.go +++ b/staker/types.go @@ -99,13 +99,13 @@ func createWitnessSignaturesForPubKeys( return signatures } -func buildSlashingTxAndSig( +func slashingTxForStakingTx( slashingFee btcutil.Amount, delegationData *externalDelegationData, storedTx *stakerdb.StoredTransaction, net *chaincfg.Params, -) (*wire.MsgTx, *schnorr.Signature, error) { - stakerPubKey := delegationData.stakerPrivKey.PubKey() +) (*wire.MsgTx, *staking.SpendInfo, error) { + stakerPubKey := delegationData.stakerPublicKey lockSlashTxLockTime := delegationData.babylonParams.MinUnbondingTime + 1 slashingTx, err := staking.BuildSlashingTxFromStakingTxStrict( @@ -124,7 +124,7 @@ func buildSlashingTxAndSig( } stakingInfo, err := staking.BuildStakingInfo( - delegationData.stakerPrivKey.PubKey(), + stakerPubKey, storedTx.FinalityProvidersBtcPks, delegationData.babylonParams.CovenantPks, delegationData.babylonParams.CovenantQuruomThreshold, @@ -143,18 +143,7 @@ func buildSlashingTxAndSig( return nil, nil, fmt.Errorf("building slashing path info failed: %w", err) } - slashingTxSignature, err := staking.SignTxWithOneScriptSpendInputFromScript( - slashingTx, - storedTx.StakingTx.TxOut[storedTx.StakingOutputIndex], - delegationData.stakerPrivKey, - slashingPathInfo.RevealedLeaf.Script, - ) - - if err != nil { - return nil, nil, fmt.Errorf("signing slashing transaction failed: %w", err) - } - - return slashingTx, slashingTxSignature, nil + return slashingTx, slashingPathInfo, nil } func createDelegationData( @@ -326,9 +315,17 @@ func createSpendStakeTxFromStoredTx( } } +type UnbondingSlashingDesc struct { + UnbondingTransaction *wire.MsgTx + UnbondingTxValue btcutil.Amount + UnbondingTxUnbondingTime uint16 + SlashUnbondingTransaction *wire.MsgTx + SlashUnbondingTransactionSpendInfo *staking.SpendInfo +} + func createUndelegationData( storedTx *stakerdb.StoredTransaction, - stakerPrivKey *btcec.PrivateKey, + stakerPubKey *btcec.PublicKey, covenantPubKeys []*btcec.PublicKey, covenantThreshold uint32, slashingAddress btcutil.Address, @@ -337,7 +334,7 @@ func createUndelegationData( slashingFee btcutil.Amount, slashingRate sdkmath.LegacyDec, btcNetwork *chaincfg.Params, -) (*cl.UndelegationData, error) { +) (*UnbondingSlashingDesc, error) { stakingTxHash := storedTx.StakingTx.TxHash() stakingOutpout := storedTx.StakingTx.TxOut[storedTx.StakingOutputIndex] @@ -346,8 +343,6 @@ func createUndelegationData( unbondingOutputValue := stakingOutpout.Value - int64(unbondingTxFee) - stakerPubKey := stakerPrivKey.PubKey() - if unbondingOutputValue <= 0 { return nil, fmt.Errorf( "too large fee rate %d sats/kb. Staking output value:%d sats. Unbonding tx fee:%d sats", int64(feeRatePerKb), stakingOutpout.Value, int64(unbondingTxFee), @@ -399,33 +394,23 @@ func createUndelegationData( return nil, fmt.Errorf("failed to build slashing path info: %w", err) } - slashUnbondingTxSignature, err := staking.SignTxWithOneScriptSpendInputFromScript( - slashUnbondingTx, - unbondingInfo.UnbondingOutput, - stakerPrivKey, - slashingPathInfo.RevealedLeaf.Script, - ) - - if err != nil { - return nil, fmt.Errorf("failed to build unbonding data: failed to sign slashing tx: %w", err) - } - - return &cl.UndelegationData{ - UnbondingTransaction: unbondingTx, - UnbondingTxValue: btcutil.Amount(unbondingOutputValue), - UnbondingTxUnbondingTime: unbondingTime, - SlashUnbondingTransaction: slashUnbondingTx, - SlashUnbondingTransactionSig: slashUnbondingTxSignature, + return &UnbondingSlashingDesc{ + UnbondingTransaction: unbondingTx, + UnbondingTxValue: btcutil.Amount(unbondingOutputValue), + UnbondingTxUnbondingTime: unbondingTime, + SlashUnbondingTransaction: slashUnbondingTx, + SlashUnbondingTransactionSpendInfo: slashingPathInfo, }, nil } -func createWitnessToSendUnbondingTx( - stakerPrivKey *btcec.PrivateKey, +// buildUnbondingSpendInfo +func buildUnbondingSpendInfo( + stakerPubKey *btcec.PublicKey, storedTx *stakerdb.StoredTransaction, unbondingData *stakerdb.UnbondingStoreData, params *cl.StakingParams, net *chaincfg.Params, -) (wire.TxWitness, error) { +) (*staking.SpendInfo, error) { if storedTx.State < proto.TransactionState_DELEGATION_ACTIVE { return nil, fmt.Errorf("cannot create witness for sending unbonding tx. Staking transaction is in invalid state: %s", storedTx.State) } @@ -434,12 +419,12 @@ func createWitnessToSendUnbondingTx( return nil, fmt.Errorf("cannot create witness for sending unbonding tx. Unbonding data does not contain unbonding transaction") } - if len(unbondingData.CovenantSignatures) < int(params.CovenantQuruomThreshold) { + if len(unbondingData.CovenantSignatures) != int(params.CovenantQuruomThreshold) { return nil, fmt.Errorf("cannot create witness for sending unbonding tx. Unbonding data does not contain all necessary signatures. Required: %d, received: %d", params.CovenantQuruomThreshold, len(unbondingData.CovenantSignatures)) } stakingInfo, err := staking.BuildStakingInfo( - stakerPrivKey.PubKey(), + stakerPubKey, storedTx.FinalityProvidersBtcPks, params.CovenantPks, params.CovenantQuruomThreshold, @@ -458,26 +443,7 @@ func createWitnessToSendUnbondingTx( return nil, fmt.Errorf("failed to build unbonding path info: %w", err) } - stakerUnbondingSig, err := staking.SignTxWithOneScriptSpendInputFromScript( - unbondingData.UnbondingTx, - storedTx.StakingTx.TxOut[storedTx.StakingOutputIndex], - stakerPrivKey, - unbondingPathInfo.RevealedLeaf.Script, - ) - - if err != nil { - return nil, err - } - - covenantSigantures := createWitnessSignaturesForPubKeys( - params.CovenantPks, - unbondingData.CovenantSignatures, - ) - - return unbondingPathInfo.CreateUnbondingPathWitness( - covenantSigantures, - stakerUnbondingSig, - ) + return unbondingPathInfo, nil } func parseWatchStakingRequest( diff --git a/walletcontroller/client.go b/walletcontroller/client.go index 1fa0229..54f3c40 100644 --- a/walletcontroller/client.go +++ b/walletcontroller/client.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" + staking "github.com/babylonchain/babylon/btcstaking" "github.com/babylonchain/babylon/crypto/bip322" "github.com/babylonchain/btc-staker/stakercfg" scfg "github.com/babylonchain/btc-staker/stakercfg" @@ -120,16 +121,6 @@ func (w *RpcWalletController) AddressPublicKey(address btcutil.Address) (*btcec. return btcec.ParsePubKey(decodedHex) } -func (w *RpcWalletController) DumpPrivateKey(address btcutil.Address) (*btcec.PrivateKey, error) { - privKey, err := w.DumpPrivKey(address) - - if err != nil { - return nil, err - } - - return privKey.PrivKey, nil -} - func (w *RpcWalletController) NetworkName() string { return w.network } @@ -312,3 +303,36 @@ func (w *RpcWalletController) SignBip322NativeSegwit(msg []byte, address btcutil return signed.TxIn[0].Witness, nil } + +// TODO: Temporary implementation to encapsulate signing of taproot spending transaction, it will be replaced with PSBT +// signing in the future +func (w *RpcWalletController) SignOneInputTaprootSpendingTransaction(req *TaprootSigningRequest) (*TaprootSigningResult, error) { + if len(req.TxToSign.TxIn) != 1 { + return nil, fmt.Errorf("cannot sign transaction with more than one input") + } + + if !txscript.IsPayToTaproot(req.FundingOutput.PkScript) { + return nil, fmt.Errorf("cannot sign transaction spending non-taproot output") + } + + privKey, err := w.DumpPrivKey(req.SignerAddress) + + if err != nil { + return nil, err + } + + sig, err := staking.SignTxWithOneScriptSpendInputFromTapLeaf( + req.TxToSign, + req.FundingOutput, + privKey.PrivKey, + *req.SpendDescription.ScriptLeaf, + ) + + if err != nil { + return nil, err + } + + return &TaprootSigningResult{ + Signature: sig, + }, nil +} diff --git a/walletcontroller/interface.go b/walletcontroller/interface.go index 95838ca..2019f29 100644 --- a/walletcontroller/interface.go +++ b/walletcontroller/interface.go @@ -2,8 +2,10 @@ package walletcontroller import ( "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" notifier "github.com/lightningnetwork/lnd/chainntnfs" ) @@ -16,10 +18,25 @@ const ( TxInChain ) +type SpendPathDescription struct { + ControlBlock *txscript.ControlBlock + ScriptLeaf *txscript.TapLeaf +} + +type TaprootSigningRequest struct { + FundingOutput *wire.TxOut + TxToSign *wire.MsgTx + SignerAddress btcutil.Address + SpendDescription *SpendPathDescription +} + +type TaprootSigningResult struct { + Signature *schnorr.Signature +} + type WalletController interface { UnlockWallet(timeoutSecs int64) error AddressPublicKey(address btcutil.Address) (*btcec.PublicKey, error) - DumpPrivateKey(address btcutil.Address) (*btcec.PrivateKey, error) ImportPrivKey(privKeyWIF *btcutil.WIF) error NetworkName() string CreateTransaction( @@ -37,4 +54,7 @@ type WalletController interface { ListOutputs(onlySpendable bool) ([]Utxo, error) TxDetails(txHash *chainhash.Hash, pkScript []byte) (*notifier.TxConfirmation, TxStatus, error) SignBip322NativeSegwit(msg []byte, address btcutil.Address) (wire.TxWitness, error) + // SignOneInputTaprootSpendingTransaction signs transactions with one taproot input that + // uses script spending path. + SignOneInputTaprootSpendingTransaction(req *TaprootSigningRequest) (*TaprootSigningResult, error) }