Skip to content

Commit c1efa39

Browse files
committed
bip322: add message signing
1 parent 7fa1072 commit c1efa39

2 files changed

Lines changed: 293 additions & 1 deletion

File tree

bip322/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.25.0
44

55
require (
66
github.com/btcsuite/btcd/address/v2 v2.0.0
7+
github.com/btcsuite/btcd/btcec/v2 v2.5.0
78
github.com/btcsuite/btcd/chaincfg/v2 v2.0.0
89
github.com/btcsuite/btcd/chainhash/v2 v2.0.0
910
github.com/btcsuite/btcd/psbt/v2 v2.0.0
@@ -13,7 +14,6 @@ require (
1314
)
1415

1516
require (
16-
github.com/btcsuite/btcd/btcec/v2 v2.5.0 // indirect
1717
github.com/btcsuite/btcd/btcutil/v2 v2.0.0 // indirect
1818
github.com/btcsuite/btclog v1.0.0 // indirect
1919
github.com/davecgh/go-spew v1.1.1 // indirect

bip322/signing.go

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

0 commit comments

Comments
 (0)