diff --git a/btcutil/psbt/bip322.go b/btcutil/psbt/bip322.go new file mode 100644 index 0000000000..58b7991d3a --- /dev/null +++ b/btcutil/psbt/bip322.go @@ -0,0 +1,187 @@ +package psbt + +import ( + "encoding/base64" + "errors" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +// BuildToSpendTx constructs a transaction to spend an output using the +// specified message and output script. It computes the message hash, +// constructs the scriptSig, and creates the to_spend transaction according to +// BIP-322. For more details on BIP-322, see: +// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +// +// Parameters: +// - message: The message to include in the transaction. +// - outPkScript: The output script to spend. +// +// Returns: +// - *wire.MsgTx: The constructed transaction. +// - error: An error if occurred during the construction. +func BuildToSpendTx(message string, outPkScript []byte) (*wire.MsgTx, error) { + // Compute the message tagged hash: + // SHA256(SHA256(tag) || SHA256(tag) || message). + messageHash := *chainhash.TaggedHash( + chainhash.TagBIP0322SignedMsg, []byte(message), + ) + + // Construct the scriptSig - OP_0 PUSH32[ message_hash ]. + scriptSigPartOne := []byte{0x00, 0x20} + + // Convert messageHash to a byte slice. + messageHashBytes := messageHash[:] + + // Create scriptSig with the length of scriptSigPartOne + messageHash. + scriptSig := make([]byte, len(scriptSigPartOne)+len(messageHashBytes)) + + // Copy scriptSigPartOne into scriptSig. + copy(scriptSig, scriptSigPartOne) + + // Copy messageHash into scriptSig starting from + // the end of scriptSigPartOne. + copy(scriptSig[len(scriptSigPartOne):], messageHashBytes) + + // Create to_spend transaction in accordance to BIP-322: + // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full + toSpendTx := &wire.MsgTx{ + Version: 0, + LockTime: 0, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Index: 0xFFFFFFFF, + }, + Sequence: 0, + }}, + TxOut: []*wire.TxOut{{ + Value: 0, + PkScript: outPkScript, + }}, + } + + pInputs := []PInput{{ + FinalScriptSig: scriptSig, + WitnessScript: []byte{}, + }} + + pOutputs := []POutput{{}} + + psbt := &Packet{ + UnsignedTx: toSpendTx, + Inputs: pInputs, + Outputs: pOutputs, + } + + tx, err := Extract(psbt) + if err != nil { + return nil, err + } + + return tx, nil +} + +// BuildToSignTx constructs a transaction to prepare for signing using the +// specified transaction ID, witness script, and additional parameters. It +// creates the to_sign transaction according to BIP-322. For more details on +// BIP-322, see: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +// +// Parameters: +// - toSpendTxId: The transaction ID of the output to spend. +// - witnessScript: The witness script corresponding to the output being +// spent. +// - isRedeemScript: Whether the witness script should be used as the redeem +// script. +// - tapInternalKey: The internal key for Taproot if applicable, or nil if +// not used. +// +// Returns: +// - *Packet: The constructed Partially Signed Bitcoin Transaction (PSBT). +// - error: An error if occurred during the construction. +func BuildToSignTx(toSpendTxId string, witnessScript []byte, + isRedeemScript bool, tapInternalKey []byte) (*Packet, error) { + + toSpendTxIdHash, err := chainhash.NewHashFromStr(toSpendTxId) + if err != nil { + return nil, err + } + + // Create to_sign transaction in accordance to BIP-322: + // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full + toSignTx := &wire.MsgTx{ + Version: 0, + LockTime: 0, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *toSpendTxIdHash, + Index: 0, + }, + Sequence: 0, + }}, + TxOut: []*wire.TxOut{{ + Value: 0, + PkScript: []byte{txscript.OP_RETURN}, + }}, + } + + pInputs := []PInput{{ + WitnessUtxo: &wire.TxOut{ + Value: 0, + PkScript: witnessScript, + }, + }} + + pOutputs := []POutput{{}} + + psbt := &Packet{ + UnsignedTx: toSignTx, + Inputs: pInputs, + Outputs: pOutputs, + } + + // Create an updater for the psbt. This also performs a sanity check. + updater, err := NewUpdater(psbt) + if err != nil { + return nil, err + } + + // Set redeemScript as witnessScript if isRedeemScript. + if isRedeemScript { + err = updater.AddInRedeemScript(witnessScript, 0) + if err != nil { + return nil, err + } + } + + // Set tapInternalKey if provided. + if tapInternalKey != nil { + err = updater.AddInTaprootInternalKey(tapInternalKey, 0) + if err != nil { + return nil, err + } + } + + return psbt, nil +} + +// EncodeWitness encodes witness stack in a signed BIP-322 PSBT into +// its base-64 encoded format. For more details on +// BIP-322, see: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +// +// Parameters: +// - signedPsbt: The signed Partially Signed Bitcoin Transaction (PSBT). +// +// Returns: +// - string: The base-64 encoded witness stack. +// - error: An error if the witness data is empty. +func EncodeWitness(signedPsbt *Packet) (string, error) { + witness := signedPsbt.Inputs[0].FinalScriptWitness + if len(witness) > 0 { + return base64.StdEncoding.EncodeToString(witness), nil + } else { + return "", errors.New("witness data is empty") + } + +} diff --git a/btcutil/psbt/bip322_test.go b/btcutil/psbt/bip322_test.go new file mode 100644 index 0000000000..cd031201a3 --- /dev/null +++ b/btcutil/psbt/bip322_test.go @@ -0,0 +1,218 @@ +package psbt + +import ( + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/stretchr/testify/require" +) + +// TestBuildToSpendTx tests that the BuildToSpendTx function works as +// expected on the passed test vector(s) as mentioned in BIP-322: +// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +func TestBuildToSpendTx(t *testing.T) { + SegWitAddress := "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l" + + addr, err := btcutil.DecodeAddress( + SegWitAddress, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + scriptPubKey, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + emptyStringToSpendTx, err := BuildToSpendTx("", scriptPubKey) + require.NoError(t, err) + + // Create to_spend transaction for the empty message. + EmptyStringToSpendTxExpected := "c5680aa69bb8d860bf82d4e9cd3504b55dd" + + "e018de765a91bb566283c545a99a7" + require.Equal(t, EmptyStringToSpendTxExpected, + emptyStringToSpendTx.TxHash().String(), + ) + + // Create to_spend transaction for the "Hello World" message. + helloWorldToSpendTx, err := BuildToSpendTx( + "Hello World", scriptPubKey, + ) + require.NoError(t, err) + + HelloWorldToSpendTxExpected := "b79d196740ad5217771c1098fc4a4b51e053" + + "5c32236c71f1ea4d61a2d603352b" + require.Equal(t, HelloWorldToSpendTxExpected, + helloWorldToSpendTx.TxHash().String(), + ) +} + +// TestBuildToSignTx tests that the BuildToSignTx function works as +// expected on the passed test vector(s) as mentioned in BIP-322: +// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +func TestBuildToSignTx(t *testing.T) { + SegWitAddress := "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l" + + addr, err := btcutil.DecodeAddress( + SegWitAddress, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + scriptPubKey, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + TestPrivateKey := "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k" + + wif, err := btcutil.DecodeWIF(TestPrivateKey) + require.NoError(t, err) + + // Create to_sign transaction for the empty message. + EmptyStringToSpendTxId := "c5680aa69bb8d860bf82d4e9cd3504b55dd" + + "e018de765a91bb566283c545a99a7" + emptyStringToSignTx, err := BuildToSignTx( + EmptyStringToSpendTxId, scriptPubKey, false, nil, + ) + require.NoError(t, err) + + sig1, err := txscript.RawTxInSignature( + emptyStringToSignTx.UnsignedTx, 0, scriptPubKey, txscript.SigHashAll, wif.PrivKey, + ) + require.NoError(t, err) + + emptyStringToSignTxUpdater, err := NewUpdater(emptyStringToSignTx) + if err != nil { + t.Fatalf("Failed to create updater: %v", err) + } + + _, err = emptyStringToSignTxUpdater.Sign( + 0, sig1, wif.SerializePubKey(), nil, nil, + ) + require.NoError(t, err) + + err = Finalize(emptyStringToSignTx, 0) + require.NoError(t, err) + + emptyStringToSignTxSigned, err := Extract(emptyStringToSignTx) + require.NoError(t, err) + + EmptyStringToSignTxExpected := "1e9654e951a5ba44c8604c4de6c67fd78" + + "a27e81dcadcfe1edf638ba3aaebaed6" + require.Equal(t, EmptyStringToSignTxExpected, + emptyStringToSignTxSigned.TxHash().String(), + ) + + // Create to_sign transaction for the "Hello World" message. + HelloWorldToSpendTxId := "b79d196740ad5217771c1098fc4a4b51e053" + + "5c32236c71f1ea4d61a2d603352b" + helloWorldToSignTx, err := BuildToSignTx( + HelloWorldToSpendTxId, scriptPubKey, false, nil, + ) + require.NoError(t, err) + + sig2, err := txscript.RawTxInSignature( + helloWorldToSignTx.UnsignedTx, 0, scriptPubKey, + txscript.SigHashAll, wif.PrivKey, + ) + require.NoError(t, err) + + helloWorldToSignTxUpdater, err := NewUpdater(helloWorldToSignTx) + if err != nil { + t.Fatalf("Failed to create updater: %v", err) + } + + _, err = helloWorldToSignTxUpdater.Sign( + 0, sig2, wif.SerializePubKey(), nil, nil, + ) + require.NoError(t, err) + + err = Finalize(helloWorldToSignTx, 0) + require.NoError(t, err) + + helloWorldToSignTxSigned, err := Extract(helloWorldToSignTx) + require.NoError(t, err) + + HelloWorldToSignTxExpected := "88737ae86f2077145f93cc4b153ae9a1cb8d5" + + "6afa511988c149c5c8c9d93bddf" + require.Equal(t, HelloWorldToSignTxExpected, + helloWorldToSignTxSigned.TxHash().String(), + ) +} + +// TestEncodeWitness tests that the EncodeWitness function works as +// expected on the passed test vector(s) as mentioned in BIP-322: +// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +func TestEncodeWitness(t *testing.T) { + SegWitAddress := "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l" + + addr, err := btcutil.DecodeAddress( + SegWitAddress, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + scriptPubKey, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + TestPrivateKey := "L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k" + + wif, err := btcutil.DecodeWIF(TestPrivateKey) + require.NoError(t, err) + + // Create to_sign transaction with "Hello World" message. + toSpendTxId := "b79d196740ad5217771c1098fc4a4b51e053" + + "5c32236c71f1ea4d61a2d603352b" + toSignTx, err := BuildToSignTx( + toSpendTxId, scriptPubKey, false, nil, + ) + require.NoError(t, err) + + sigInput, err := txscript.RawTxInSignature( + toSignTx.UnsignedTx, 0, scriptPubKey, + txscript.SigHashAll, wif.PrivKey, + ) + require.NoError(t, err) + + toSignTxUpdater, err := NewUpdater(toSignTx) + if err != nil { + t.Fatalf("Failed to create updater: %v", err) + } + + _, err = toSignTxUpdater.Sign( + 0, sigInput, wif.SerializePubKey(), nil, nil, + ) + require.NoError(t, err) + + err = Finalize(toSignTx, 0) + require.NoError(t, err) + + signature, err := EncodeWitness(toSignTx) + require.NoError(t, err) + + signatureExpected := "AkgwRQIhALd8FRASRGU0ZayijetagasQv/2Vv0hEadfFC/" + + "m5PtdKAiAevL10hn93NSSag0nYg1zZPIVKrL+e9MoStYkc" + + "+JKvGgEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy" + require.Equal(t, signatureExpected, signature) +} + +// TestEncodeWitnessFromUnsignedPSBT tests that the EncodeWitness function +// throws an error if unsigned psbt. +func TestEncodeWitnessFromUnsignedPSBT(t *testing.T) { + SegWitAddress := "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l" + + addr, err := btcutil.DecodeAddress( + SegWitAddress, &chaincfg.MainNetParams, + ) + require.NoError(t, err) + + scriptPubKey, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + // Create to_sign transaction with "Hello World" message. + toSpendTxId := "b79d196740ad5217771c1098fc4a4b51e053" + + "5c32236c71f1ea4d61a2d603352b" + toSignTx, err := BuildToSignTx( + toSpendTxId, scriptPubKey, false, nil, + ) + require.NoError(t, err) + + _, err = EncodeWitness(toSignTx) + require.NotNil(t, err) +} diff --git a/btcutil/psbt/updater.go b/btcutil/psbt/updater.go index 66c8d1d83c..b3fda83cfb 100644 --- a/btcutil/psbt/updater.go +++ b/btcutil/psbt/updater.go @@ -256,6 +256,22 @@ func (u *Updater) AddInRedeemScript(redeemScript []byte, return nil } +// AddInTaprootInternalKey adds the taproot internal key information for an +// input. The taproot internal key is passed serialized, as a byte slice, along +// with the index of the input. An error is returned if addition of this +// key-value pair to the Psbt fails. +func (u *Updater) AddInTaprootInternalKey(tapRootInternalKey []byte, + inIndex int) error { + + u.Upsbt.Inputs[inIndex].TaprootInternalKey = tapRootInternalKey + + if err := u.Upsbt.SanityCheck(); err != nil { + return ErrInvalidPsbtFormat + } + + return nil +} + // AddInWitnessScript adds the witness script information for an input. The // witness script is passed serialized, as a byte slice, along with the index // of the input. An error is returned if addition of this key-value pair to the