diff --git a/itest/bitcoind_node_setup.go b/itest/bitcoind_node_setup.go index a8d7eea..f757a77 100644 --- a/itest/bitcoind_node_setup.go +++ b/itest/bitcoind_node_setup.go @@ -88,7 +88,7 @@ func (h *BitcoindTestHandler) CreateWallet(walletName string, passphrase string) // last false on the list will create legacy wallet. This is needed, as currently // we are signing all taproot transactions by dumping the private key and signing it // on app level. Descriptor wallets do not allow dumping private keys. - buff, _, err := h.m.ExecBitcoindCliCmd(h.t, []string{"createwallet", walletName, "false", "false", passphrase, "false", "false"}) + buff, _, err := h.m.ExecBitcoindCliCmd(h.t, []string{"createwallet", walletName, "false", "false", passphrase}) require.NoError(h.t, err) var response CreateWalletResponse diff --git a/itest/containers/config.go b/itest/containers/config.go index 2521e78..c93dbd0 100644 --- a/itest/containers/config.go +++ b/itest/containers/config.go @@ -10,7 +10,7 @@ type ImageConfig struct { //nolint:deadcode const ( dockerBitcoindRepository = "lncm/bitcoind" - dockerBitcoindVersionTag = "v24.0.1" + dockerBitcoindVersionTag = "v26.0" ) // NewImageConfig returns ImageConfig needed for running e2e test. diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 1d95096..9be4f5d 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -20,6 +20,7 @@ import ( "github.com/babylonlabs-io/babylon/crypto/bip322" btcctypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" + "github.com/cometbft/cometbft/crypto/tmhash" staking "github.com/babylonlabs-io/babylon/btcstaking" txformat "github.com/babylonlabs-io/babylon/btctxformatter" @@ -139,7 +140,7 @@ type TestManager struct { Db kvdb.Backend Sa *staker.StakerApp BabylonClient *babylonclient.BabylonController - WalletPrivKey *btcec.PrivateKey + WalletPubKey *btcec.PublicKey MinerAddr btcutil.Address serverStopper *signal.Interceptor wg *sync.WaitGroup @@ -294,7 +295,13 @@ func StartManager( err = walletClient.UnlockWallet(20) require.NoError(t, err) - walletPrivKey, err := c.DumpPrivKey(minerAddressDecoded) + info, err := c.GetAddressInfo(br.Address) + require.NoError(t, err) + + pubKeyHex := *info.PubKey + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + require.NoError(t, err) + walletPubKey, err := btcec.ParsePubKey(pubKeyBytes) require.NoError(t, err) interceptor, err := signal.Intercept() @@ -334,7 +341,7 @@ func StartManager( Db: dbbackend, Sa: stakerApp, BabylonClient: bl, - WalletPrivKey: walletPrivKey.PrivKey, + WalletPubKey: walletPubKey, MinerAddr: minerAddressDecoded, serverStopper: &interceptor, wg: &wg, @@ -809,13 +816,20 @@ func (tm *TestManager) sendWatchedStakingTx( stakingTxSlashingPathInfo, err := stakingInfo.SlashingPathSpendInfo() require.NoError(t, err) - slashSig, err := staking.SignTxWithOneScriptSpendInputFromScript( - slashingTx, - tx.TxOut[stakingOutputIdx], - tm.WalletPrivKey, - stakingTxSlashingPathInfo.RevealedLeaf.Script, + slashingSigResult, err := tm.Sa.Wallet().SignOneInputTaprootSpendingTransaction( + &walletcontroller.TaprootSigningRequest{ + FundingOutput: stakingInfo.StakingOutput, + TxToSign: slashingTx, + SignerAddress: tm.MinerAddr, + SpendDescription: &walletcontroller.SpendPathDescription{ + ControlBlock: &stakingTxSlashingPathInfo.ControlBlock, + ScriptLeaf: &stakingTxSlashingPathInfo.RevealedLeaf, + }, + }, ) + require.NoError(t, err) + require.NotNil(t, slashingSigResult.Signature) serializedStakingTx, err := utils.SerializeBtcTransaction(tx) require.NoError(t, err) @@ -855,23 +869,35 @@ func (tm *TestManager) sendWatchedStakingTx( ) require.NoError(t, err) - slashUnbondingSig, err := staking.SignTxWithOneScriptSpendInputFromScript( - slashUnbondingTx, - unbondingTx.TxOut[0], - tm.WalletPrivKey, - unbondingSlashingPathInfo.RevealedLeaf.Script, + slashingUnbondingSigResult, err := tm.Sa.Wallet().SignOneInputTaprootSpendingTransaction( + &walletcontroller.TaprootSigningRequest{ + FundingOutput: unbondingTx.TxOut[0], + TxToSign: slashUnbondingTx, + SignerAddress: tm.MinerAddr, + SpendDescription: &walletcontroller.SpendPathDescription{ + ControlBlock: &unbondingSlashingPathInfo.ControlBlock, + ScriptLeaf: &unbondingSlashingPathInfo.RevealedLeaf, + }, + }, ) + require.NoError(t, err) + require.NotNil(t, slashingUnbondingSigResult.Signature) + serializedUnbondingTx, err := utils.SerializeBtcTransaction(unbondingTx) require.NoError(t, err) serializedSlashUnbondingTx, err := utils.SerializeBtcTransaction(slashUnbondingTx) require.NoError(t, err) - // TODO: Update pop when new version will be ready, for now using schnorr as we don't have - // easy way to generate bip322 sig on backend side - pop, err := btcstypes.NewPoPBTC( - testStakingData.StakerBabylonAddr, - tm.WalletPrivKey, + babylonAddrHash := tmhash.Sum(testStakingData.StakerBabylonAddr.Bytes()) + + sig, err := tm.Sa.Wallet().SignBip322NativeSegwit(babylonAddrHash, tm.MinerAddr) + require.NoError(t, err) + + pop, err := babylonclient.NewBabylonBip322Pop( + babylonAddrHash, + sig, + tm.MinerAddr, ) require.NoError(t, err) @@ -888,16 +914,16 @@ func (tm *TestManager) sendWatchedStakingTx( hex.EncodeToString(schnorr.SerializePubKey(testStakingData.StakerKey)), fpBTCPKs, hex.EncodeToString(serializedSlashingTx), - hex.EncodeToString(slashSig.Serialize()), + hex.EncodeToString(slashingSigResult.Signature.Serialize()), testStakingData.StakerBabylonAddr.String(), tm.MinerAddr.String(), hex.EncodeToString(pop.BtcSig), hex.EncodeToString(serializedUnbondingTx), hex.EncodeToString(serializedSlashUnbondingTx), - hex.EncodeToString(slashUnbondingSig.Serialize()), + hex.EncodeToString(slashingUnbondingSigResult.Signature.Serialize()), int(unbondingTme), // Use schnor verification - int(btcstypes.BTCSigType_BIP340), + int(btcstypes.BTCSigType_BIP322), ) require.NoError(t, err) @@ -1077,7 +1103,7 @@ func TestStakingFailures(t *testing.T) { require.NoError(t, err) stakingTime := uint16(staker.GetMinStakingTime(params)) - testStakingData := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 10000, 1) + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 10000, 1) fpKey := hex.EncodeToString(schnorr.SerializePubKey(testStakingData.FinalityProviderBtcKeys[0])) tm.createAndRegisterFinalityProviders(t, testStakingData) @@ -1117,7 +1143,7 @@ func TestSendingStakingTransaction(t *testing.T) { require.NoError(t, err) stakingTime := uint16(staker.GetMinStakingTime(params)) - testStakingData := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 10000, 1) + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 10000, 1) hashed, err := chainhash.NewHash(datagen.GenRandomByteArray(r, 32)) require.NoError(t, err) @@ -1197,7 +1223,7 @@ func TestMultipleWithdrawableStakingTransactions(t *testing.T) { stakingTime4 := minStakingTime + 2 stakingTime5 := minStakingTime + 3 - testStakingData1 := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime1, 10000, 1) + testStakingData1 := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime1, 10000, 1) testStakingData2 := testStakingData1.withStakingTime(stakingTime2) testStakingData3 := testStakingData1.withStakingTime(stakingTime3) testStakingData4 := testStakingData1.withStakingTime(stakingTime4) @@ -1257,7 +1283,7 @@ func TestSendingWatchedStakingTransaction(t *testing.T) { params, err := cl.Params() require.NoError(t, err) stakingTime := uint16(staker.GetMinStakingTime(params)) - testStakingData := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 10000, 1) + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 10000, 1) tm.createAndRegisterFinalityProviders(t, testStakingData) @@ -1279,7 +1305,7 @@ func TestRestartingTxNotDeepEnough(t *testing.T) { params, err := cl.Params() require.NoError(t, err) stakingTime := uint16(staker.GetMinStakingTime(params)) - testStakingData := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 10000, 1) + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 10000, 1) tm.createAndRegisterFinalityProviders(t, testStakingData) txHash := tm.sendStakingTxBTC(t, testStakingData) @@ -1305,7 +1331,7 @@ func TestRestartingTxNotOnBabylon(t *testing.T) { require.NoError(t, err) stakingTime := uint16(staker.GetMinStakingTime(params)) - testStakingData1 := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 10000, 1) + testStakingData1 := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 10000, 1) testStakingData2 := testStakingData1.withStakingAmout(11000) tm.createAndRegisterFinalityProviders(t, testStakingData1) @@ -1347,7 +1373,7 @@ func TestStakingUnbonding(t *testing.T) { require.NoError(t, err) // large staking time stakingTime := uint16(1000) - testStakingData := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 50000, 1) + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 50000, 1) tm.createAndRegisterFinalityProviders(t, testStakingData) @@ -1418,7 +1444,7 @@ func TestUnbondingRestartWaitingForSignatures(t *testing.T) { require.NoError(t, err) // large staking time stakingTime := uint16(1000) - testStakingData := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 50000, 1) + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 50000, 1) tm.createAndRegisterFinalityProviders(t, testStakingData) @@ -1588,7 +1614,7 @@ func TestSendingStakingTransaction_Restaking(t *testing.T) { stakingTime := uint16(staker.GetMinStakingTime(params)) // restaked to 5 finality providers - testStakingData := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 10000, 5) + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 10000, 5) hashed, err := chainhash.NewHash(datagen.GenRandomByteArray(r, 32)) require.NoError(t, err) @@ -1628,7 +1654,7 @@ func TestRecoverAfterRestartDuringWithdrawal(t *testing.T) { require.NoError(t, err) stakingTime := uint16(staker.GetMinStakingTime(params)) - testStakingData := tm.getTestStakingData(t, tm.WalletPrivKey.PubKey(), stakingTime, 10000, 1) + testStakingData := tm.getTestStakingData(t, tm.WalletPubKey, stakingTime, 10000, 1) hashed, err := chainhash.NewHash(datagen.GenRandomByteArray(r, 32)) require.NoError(t, err) diff --git a/staker/babylontypes.go b/staker/babylontypes.go index 5f7b120..4178050 100644 --- a/staker/babylontypes.go +++ b/staker/babylontypes.go @@ -91,6 +91,10 @@ func (app *StakerApp) buildOwnedDelegation( return nil, fmt.Errorf("error signing slashing transaction for staking transaction: %w", err) } + if stakingSlashingSig.Signature == nil { + return nil, fmt.Errorf("failed to receive stakingSlashingSig.Signature ") + } + unbondingSlashingSig, err := app.signTaprootScriptSpendUsingWallet( undelegationDesc.SlashUnbondingTransaction, undelegationDesc.UnbondingTransaction.TxOut[0], @@ -103,13 +107,17 @@ func (app *StakerApp) buildOwnedDelegation( return nil, fmt.Errorf("error signing slashing transaction for unbonding transaction: %w", err) } + if unbondingSlashingSig.Signature == nil { + return nil, fmt.Errorf("failed to receive unbondingSlashingSig.Signature ") + } + dg := createDelegationData( externalData.stakerPublicKey, req.inclusionBlock, req.txIndex, storedTx, stakingSlashingTx, - stakingSlashingSig, + stakingSlashingSig.Signature, externalData.babylonStakerAddr, stakingTxInclusionProof, &cl.UndelegationData{ @@ -117,7 +125,7 @@ func (app *StakerApp) buildOwnedDelegation( UnbondingTxValue: undelegationDesc.UnbondingTxValue, UnbondingTxUnbondingTime: undelegationDesc.UnbondingTxUnbondingTime, SlashUnbondingTransaction: undelegationDesc.SlashUnbondingTransaction, - SlashUnbondingTransactionSig: unbondingSlashingSig, + SlashUnbondingTransactionSig: unbondingSlashingSig.Signature, }, ) diff --git a/staker/stakerapp.go b/staker/stakerapp.go index 57dfb7a..b36880e 100644 --- a/staker/stakerapp.go +++ b/staker/stakerapp.go @@ -944,6 +944,10 @@ func (app *StakerApp) sendUnbondingTxToBtcWithWitness( return fmt.Errorf("failed to send unbondingtx. wallet signing error: %w", err) } + if stakerUnbondingSig.Signature == nil { + return fmt.Errorf("failed to receive stakerUnbondingSig.Signature") + } + covenantSigantures := createWitnessSignaturesForPubKeys( params.CovenantPks, unbondingData.CovenantSignatures, @@ -951,7 +955,7 @@ func (app *StakerApp) sendUnbondingTxToBtcWithWitness( witness, err := unbondingSpendInfo.CreateUnbondingPathWitness( covenantSigantures, - stakerUnbondingSig, + stakerUnbondingSig.Signature, ) if err != nil { @@ -1720,7 +1724,7 @@ func (app *StakerApp) signTaprootScriptSpendUsingWallet( signerAddress btcutil.Address, leaf *txscript.TapLeaf, controlBlock *txscript.ControlBlock, -) (*schnorr.Signature, error) { +) (*walletcontroller.TaprootSigningResult, error) { if err := app.wc.UnlockWallet(defaultWalletUnlockTimeout); err != nil { return nil, fmt.Errorf("failed to unlock wallet before signing: %w", err) @@ -1742,7 +1746,7 @@ func (app *StakerApp) signTaprootScriptSpendUsingWallet( return nil, err } - return resp.Signature, nil + return resp, nil } // SpendStake spends stake identified by stakingTxHash. Stake can be currently locked in @@ -1832,15 +1836,15 @@ func (app *StakerApp) SpendStake(stakingTxHash *chainhash.Hash) (*chainhash.Hash return nil, nil, fmt.Errorf("cannot spend staking output. Error building signature: %w", err) } - witness, err := spendStakeTxInfo.fundingOutputSpendInfo.CreateTimeLockPathWitness( - stakerSig, - ) + if stakerSig.FullInputWitness == nil { + return nil, nil, fmt.Errorf("failed to recevie full witness to spend staking transactions") + } if err != nil { return nil, nil, fmt.Errorf("cannot spend staking output. Error building witness: %w", err) } - spendStakeTxInfo.spendStakeTx.TxIn[0].Witness = witness + spendStakeTxInfo.spendStakeTx.TxIn[0].Witness = stakerSig.FullInputWitness // We do not check if transaction is spendable i.e the staking time has passed // as this is validated in mempool so in of not meeting this time requirement diff --git a/walletcontroller/client.go b/walletcontroller/client.go index 46b9285..2b38122 100644 --- a/walletcontroller/client.go +++ b/walletcontroller/client.go @@ -1,18 +1,21 @@ package walletcontroller import ( + "bytes" + "encoding/base64" "encoding/hex" "fmt" "sort" - staking "github.com/babylonlabs-io/babylon/btcstaking" "github.com/babylonlabs-io/babylon/crypto/bip322" "github.com/babylonlabs-io/btc-staker/stakercfg" scfg "github.com/babylonlabs-io/btc-staker/stakercfg" "github.com/babylonlabs-io/btc-staker/types" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" @@ -319,35 +322,117 @@ func (w *RpcWalletController) OutputSpent( return res == nil, 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 { +func (w *RpcWalletController) SignOneInputTaprootSpendingTransaction(request *TaprootSigningRequest) (*TaprootSigningResult, error) { + if len(request.TxToSign.TxIn) != 1 { return nil, fmt.Errorf("cannot sign transaction with more than one input") } - if !txscript.IsPayToTaproot(req.FundingOutput.PkScript) { + if !txscript.IsPayToTaproot(request.FundingOutput.PkScript) { return nil, fmt.Errorf("cannot sign transaction spending non-taproot output") } - privKey, err := w.DumpPrivKey(req.SignerAddress) + key, err := w.AddressPublicKey(request.SignerAddress) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get public key for address: %w", err) } - sig, err := staking.SignTxWithOneScriptSpendInputFromTapLeaf( - req.TxToSign, - req.FundingOutput, - privKey.PrivKey, - *req.SpendDescription.ScriptLeaf, + psbtPacket, err := psbt.New( + []*wire.OutPoint{&request.TxToSign.TxIn[0].PreviousOutPoint}, + request.TxToSign.TxOut, + request.TxToSign.Version, + request.TxToSign.LockTime, + []uint32{request.TxToSign.TxIn[0].Sequence}, ) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create PSBT packet with transaction to sign: %w", err) } - return &TaprootSigningResult{ - Signature: sig, - }, nil + psbtPacket.Inputs[0].SighashType = txscript.SigHashDefault + psbtPacket.Inputs[0].WitnessUtxo = request.FundingOutput + psbtPacket.Inputs[0].Bip32Derivation = []*psbt.Bip32Derivation{ + { + PubKey: key.SerializeCompressed(), + }, + } + + ctrlBlockBytes, err := request.SpendDescription.ControlBlock.ToBytes() + + if err != nil { + return nil, fmt.Errorf("failed to serialize control block: %w", err) + } + + psbtPacket.Inputs[0].TaprootLeafScript = []*psbt.TaprootTapLeafScript{ + { + ControlBlock: ctrlBlockBytes, + Script: request.SpendDescription.ScriptLeaf.Script, + LeafVersion: request.SpendDescription.ScriptLeaf.LeafVersion, + }, + } + + psbtEncoded, err := psbtPacket.B64Encode() + + if err != nil { + return nil, fmt.Errorf("failed to encode PSBT packet: %w", err) + } + + sign := true + signResult, err := w.Client.WalletProcessPsbt( + psbtEncoded, + &sign, + "DEFAULT", + nil, + ) + + if err != nil { + return nil, fmt.Errorf("failed to sign PSBT packet: %w", err) + } + + decodedBytes, err := base64.StdEncoding.DecodeString(signResult.Psbt) + + if err != nil { + return nil, fmt.Errorf("failed to decode signed PSBT packet from b64: %w", err) + } + + decodedPsbt, err := psbt.NewFromRawBytes(bytes.NewReader(decodedBytes), false) + + if err != nil { + return nil, fmt.Errorf("failed to decode signed PSBT packet from bytes: %w", err) + } + + // In our signing request we only handle transaction with one input, and request + // signature for one public key, thus we can receive at most one signature from btc + if len(decodedPsbt.Inputs[0].TaprootScriptSpendSig) == 1 { + schnorSignature := decodedPsbt.Inputs[0].TaprootScriptSpendSig[0].Signature + + parsedSignature, err := schnorr.ParseSignature(schnorSignature) + + if err != nil { + return nil, fmt.Errorf("failed to parse schnorr signature in psbt packet: %w", err) + } + + return &TaprootSigningResult{ + Signature: parsedSignature, + }, nil + } + + // decodedPsbt.Inputs[0].TaprootScriptSpendSig was 0, it is possible that script + // required only one signature to build whole witness + if len(decodedPsbt.Inputs[0].FinalScriptWitness) > 0 { + // we go whole witness, return it to the caller + witness, err := bip322.SimpleSigToWitness(decodedPsbt.Inputs[0].FinalScriptWitness) + + if err != nil { + return nil, fmt.Errorf("failed to parse witness in psbt packet: %w", err) + } + + return &TaprootSigningResult{ + FullInputWitness: witness, + }, nil + } + + // neither witness, nor signature is filled. + return nil, fmt.Errorf("no signature found in PSBT packet. Wallet can't sign given tx") + } diff --git a/walletcontroller/interface.go b/walletcontroller/interface.go index 112fdad..8dd4f4c 100644 --- a/walletcontroller/interface.go +++ b/walletcontroller/interface.go @@ -30,8 +30,11 @@ type TaprootSigningRequest struct { SpendDescription *SpendPathDescription } +// TaprootSigningResult contains result of signing taproot spend through bitcoind +// wallet. It will contain either Signature or FullInputWitness, never both. type TaprootSigningResult struct { - Signature *schnorr.Signature + Signature *schnorr.Signature + FullInputWitness wire.TxWitness } type WalletController interface {