Skip to content

Commit 9b556ba

Browse files
committed
bip322: add message signing
1 parent f98cff8 commit 9b556ba

1 file changed

Lines changed: 243 additions & 0 deletions

File tree

btcutil/bip322/signing.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package bip322
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcd/btcec/v2"
8+
"github.com/btcsuite/btcd/btcutil"
9+
"github.com/btcsuite/btcd/btcutil/psbt"
10+
"github.com/btcsuite/btcd/txscript"
11+
)
12+
13+
// PsbtPrevOutputFetcher returns a txscript.PrevOutFetcher built from the UTXO
14+
// information in a PSBT packet.
15+
func PsbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher {
16+
fetcher := txscript.NewMultiPrevOutFetcher(nil)
17+
for idx, txIn := range packet.UnsignedTx.TxIn {
18+
in := packet.Inputs[idx]
19+
20+
// Skip any input that has no UTXO.
21+
if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil {
22+
continue
23+
}
24+
25+
if in.NonWitnessUtxo != nil {
26+
prevIndex := txIn.PreviousOutPoint.Index
27+
fetcher.AddPrevOut(
28+
txIn.PreviousOutPoint,
29+
in.NonWitnessUtxo.TxOut[prevIndex],
30+
)
31+
32+
continue
33+
}
34+
35+
// Fall back to witness UTXO only for older wallets.
36+
if in.WitnessUtxo != nil {
37+
fetcher.AddPrevOut(
38+
txIn.PreviousOutPoint, in.WitnessUtxo,
39+
)
40+
}
41+
}
42+
43+
return fetcher
44+
}
45+
46+
// addPartialSignature adds a signature to the given input's PartialSigs and
47+
// checks for duplicate pubkeys.
48+
func addPartialSignature(in *psbt.PInput, sig []byte, pubKey []byte) error {
49+
for _, existingSig := range in.PartialSigs {
50+
if bytes.Equal(existingSig.PubKey, pubKey) {
51+
return fmt.Errorf("duplicate signature for pubkey %x",
52+
pubKey)
53+
}
54+
}
55+
56+
in.PartialSigs = append(in.PartialSigs, &psbt.PartialSig{
57+
Signature: sig,
58+
PubKey: pubKey,
59+
})
60+
61+
return nil
62+
}
63+
64+
// signInputTaprootKeySpend signs the P2TR input at the given index in the PSBT
65+
// packet using the given private key.
66+
func signInputTaprootKeySpend(packet *psbt.Packet, idx int,
67+
privateKey *btcec.PrivateKey) error {
68+
69+
if idx >= len(packet.Inputs) {
70+
return fmt.Errorf("invalid input index %d", idx)
71+
}
72+
73+
in := &packet.Inputs[idx]
74+
utxo := in.WitnessUtxo
75+
if utxo == nil {
76+
return fmt.Errorf("input %d has no witness UTXO", idx)
77+
}
78+
79+
prevOutFetcher := PsbtPrevOutputFetcher(packet)
80+
sigHashes := txscript.NewTxSigHashes(packet.UnsignedTx, prevOutFetcher)
81+
sig, err := txscript.RawTxInTaprootSignature(
82+
packet.UnsignedTx, sigHashes, idx, utxo.Value, utxo.PkScript,
83+
[]byte{}, txscript.SigHashDefault, privateKey,
84+
)
85+
if err != nil {
86+
return fmt.Errorf("error signing: %w", err)
87+
}
88+
89+
in.TaprootKeySpendSig = sig
90+
91+
return nil
92+
}
93+
94+
// signInputWitness signs a SegWit v0 input at the given index in the PSBT
95+
// packet using the given private key. The script must be the UTXO's pkScript
96+
// for P2WPKH, the redeem script for P2SH, or the witness program for P2WSH.
97+
func signInputWitness(packet *psbt.Packet, idx int, script []byte,
98+
privateKey *btcec.PrivateKey) error {
99+
100+
if idx >= len(packet.Inputs) {
101+
return fmt.Errorf("invalid input index %d", idx)
102+
}
103+
104+
in := &packet.Inputs[idx]
105+
txIn := packet.UnsignedTx.TxIn[idx]
106+
utxo := in.WitnessUtxo
107+
if utxo == nil {
108+
prevTx := in.NonWitnessUtxo
109+
if prevTx == nil {
110+
return fmt.Errorf("input %d has no UTXO", idx)
111+
}
112+
113+
if txIn.PreviousOutPoint.Index >= uint32(len(prevTx.TxOut)) {
114+
return fmt.Errorf("input %d has no UTXO", idx)
115+
}
116+
return fmt.Errorf("input %d has no witness UTXO", idx)
117+
}
118+
119+
prevOutFetcher := PsbtPrevOutputFetcher(packet)
120+
sigHashes := txscript.NewTxSigHashes(packet.UnsignedTx, prevOutFetcher)
121+
sig, err := txscript.RawTxInWitnessSignature(
122+
packet.UnsignedTx, sigHashes, idx, utxo.Value, script,
123+
txscript.SigHashAll, privateKey,
124+
)
125+
if err != nil {
126+
return fmt.Errorf("error signing: %w", err)
127+
}
128+
129+
return addPartialSignature(
130+
in, sig, privateKey.PubKey().SerializeCompressed(),
131+
)
132+
}
133+
134+
// signInputLegacy signs a legacy input at the given index in the PSBT packet
135+
// using the given private key. The script must be the UTXO's pkScript for
136+
// P2PKH or the redeem script for P2SH.
137+
func signInputLegacy(packet *psbt.Packet, idx int, script []byte,
138+
privateKey *btcec.PrivateKey) error {
139+
140+
if idx >= len(packet.Inputs) {
141+
return fmt.Errorf("invalid input index %d", idx)
142+
}
143+
144+
in := &packet.Inputs[idx]
145+
146+
sig, err := txscript.RawTxInSignature(
147+
packet.UnsignedTx, idx, script, txscript.SigHashAll, privateKey,
148+
)
149+
if err != nil {
150+
return fmt.Errorf("error signing: %w", err)
151+
}
152+
153+
return addPartialSignature(
154+
in, sig, privateKey.PubKey().SerializeCompressed(),
155+
)
156+
}
157+
158+
// payToTaprootScript creates a new script to pay to a version 1 Taproot key
159+
// spend address.
160+
func payToTaprootScript(privateKey *btcec.PrivateKey) ([]byte, error) {
161+
trKey := txscript.ComputeTaprootKeyNoScript(privateKey.PubKey())
162+
return txscript.PayToTaprootScript(trKey)
163+
}
164+
165+
// SignP2TR signs a message using the given private key using the P2TR address
166+
// that corresponds to the given key.
167+
func SignP2TR(message string, privateKey *btcec.PrivateKey) (string, error) {
168+
pkScript, err := payToTaprootScript(privateKey)
169+
if err != nil {
170+
return "", fmt.Errorf("error creating pkScript: %w", err)
171+
}
172+
173+
toSign, err := BuildToSignPacketSimple([]byte(message), pkScript)
174+
if err != nil {
175+
return "", fmt.Errorf("error creating toSign packet: %w", err)
176+
}
177+
178+
err = signInputTaprootKeySpend(toSign, 0, privateKey)
179+
if err != nil {
180+
return "", fmt.Errorf("error signing: %w", err)
181+
}
182+
183+
return SerializeSignature(toSign)
184+
}
185+
186+
// payToWitnessPubKeyHashScript creates a new script to pay to a version 0
187+
// pubkey hash witness program. The passed hash is expected to be valid.
188+
func payToWitnessPubKeyHashScript(
189+
privateKey *btcec.PrivateKey) ([]byte, error) {
190+
191+
pubKeyHash := btcutil.Hash160(privateKey.PubKey().SerializeCompressed())
192+
return txscript.NewScriptBuilder().AddOp(txscript.OP_0).
193+
AddData(pubKeyHash).Script()
194+
}
195+
196+
// SignP2WPKH signs a message using the given private key using the P2WPKH
197+
// address that corresponds to the given key.
198+
func SignP2WPKH(message string, privateKey *btcec.PrivateKey) (string, error) {
199+
pkScript, err := payToWitnessPubKeyHashScript(privateKey)
200+
if err != nil {
201+
return "", fmt.Errorf("error creating pkScript: %w", err)
202+
}
203+
204+
toSign, err := BuildToSignPacketSimple([]byte(message), pkScript)
205+
if err != nil {
206+
return "", fmt.Errorf("error creating toSign packet: %w", err)
207+
}
208+
209+
err = signInputWitness(toSign, 0, pkScript, privateKey)
210+
if err != nil {
211+
return "", fmt.Errorf("error signing: %w", err)
212+
}
213+
214+
return SerializeSignature(toSign)
215+
}
216+
217+
// payToPubKeyHashScript creates a new script to pay a transaction
218+
// output to a 20-byte pubkey hash. It is expected that the input is a valid
219+
// hash.
220+
func payToPubKeyHashScript(privateKey *btcec.PrivateKey) ([]byte, error) {
221+
pubKeyHash := btcutil.Hash160(privateKey.PubKey().SerializeCompressed())
222+
return txscript.NewScriptBuilder().AddOp(txscript.OP_DUP).
223+
AddOp(txscript.OP_HASH160). AddData(pubKeyHash).
224+
AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG).
225+
Script()
226+
}
227+
228+
// SignP2PKH signs a message using the given private key using the P2PKH
229+
// address that corresponds to the given key.
230+
func SignP2PKH(message string, privateKey *btcec.PrivateKey) (string, error) {
231+
pkScript, err := payToPubKeyHashScript(privateKey)
232+
if err != nil {
233+
return "", fmt.Errorf("error creating pkScript: %w", err)
234+
}
235+
236+
toSign := BuildToSignPacketFull([]byte(message), pkScript, 0, 0, 0)
237+
err = signInputLegacy(toSign, 0, pkScript, privateKey)
238+
if err != nil {
239+
return "", fmt.Errorf("error signing: %w", err)
240+
}
241+
242+
return SerializeSignature(toSign)
243+
}

0 commit comments

Comments
 (0)