Skip to content

Commit f98cff8

Browse files
committed
bip322: add new bip322 submodule
This commit adds a new Golang submodule that implements BIP-0322 generic message signing. This first commit adds helper methods for producing a PSBT packet that, when signed, can be turned into a BIP-0322 valid "signature" (which, depending on the variant "simple" vs. "full" is either just the serialized witness stack or the full serialized to_sign transaction). Co-Authored-By: mohamedmohey2352@gmail.com
1 parent 51e9b53 commit f98cff8

5 files changed

Lines changed: 600 additions & 0 deletions

File tree

btcutil/bip322/bip322.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Package bip322 implements generic message signing. For more details on
2+
// BIP-322 see: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
3+
package bip322
4+
5+
import (
6+
"bytes"
7+
"encoding/base64"
8+
"errors"
9+
"fmt"
10+
11+
"github.com/btcsuite/btcd/btcutil/psbt"
12+
"github.com/btcsuite/btcd/chaincfg/chainhash"
13+
"github.com/btcsuite/btcd/txscript"
14+
"github.com/btcsuite/btcd/wire"
15+
)
16+
17+
const (
18+
// TagBIP0322SignedMsg is the BIP-0322 tag for a signed message. It is
19+
// declared as an untyped string constant so it cannot be mutated at
20+
// runtime (which would silently break all sign/verify operations in
21+
// the same process).
22+
TagBIP0322SignedMsg = "BIP0322-signed-message"
23+
24+
// PrefixSimple is the signature prefix for the "simple" variant of
25+
// BIP-322.
26+
PrefixSimple = "smp"
27+
28+
// PrefixFull is the signature prefix for the "full" variant of BIP-322.
29+
PrefixFull = "ful"
30+
31+
// PrefixProofOfFunds is the signature prefix for the "proof of funds"
32+
// variant of BIP-322.
33+
PrefixProofOfFunds = "pof"
34+
)
35+
36+
var (
37+
// errSimpleSegWitOnly is returned when a non-native SegWit output is
38+
// attempted to be signed with the "simple" variant of BIP-322.
39+
errSimpleSegWitOnly = errors.New(
40+
"only native SegWit outputs (P2WPKH, P2WSH, P2TR) are " +
41+
"supported for the simple variant",
42+
)
43+
44+
// b64Encode is a shortcut for the standard base64 encoding function.
45+
b64Encode = base64.StdEncoding.EncodeToString
46+
47+
// b64Decode is a shortcut for the standard base64 decoding function.
48+
b64Decode = base64.StdEncoding.DecodeString
49+
)
50+
51+
// buildToSpendTx constructs a transaction to spend an output using the
52+
// specified message and output script. It computes the message hash,
53+
// constructs the scriptSig, and creates the to_spend transaction, according to
54+
// BIP-322.
55+
func buildToSpendTx(message, outPkScript []byte) *wire.MsgTx {
56+
// Compute the message tagged hash:
57+
// SHA256(SHA256(tag) || SHA256(tag) || message).
58+
messageHash := *chainhash.TaggedHash(
59+
[]byte(TagBIP0322SignedMsg), message,
60+
)
61+
62+
// Construct the scriptSig - OP_0 PUSH32[ message_hash ].
63+
scriptSig := append([]byte{0x00, 0x20}, messageHash[:]...)
64+
65+
// Create to_spend transaction in accordance to BIP-322:
66+
// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full
67+
return &wire.MsgTx{
68+
Version: 0,
69+
LockTime: 0,
70+
TxIn: []*wire.TxIn{{
71+
PreviousOutPoint: wire.OutPoint{
72+
Index: 0xFFFFFFFF,
73+
},
74+
Sequence: 0,
75+
SignatureScript: scriptSig,
76+
Witness: wire.TxWitness{},
77+
}},
78+
TxOut: []*wire.TxOut{{
79+
Value: 0,
80+
PkScript: outPkScript,
81+
}},
82+
}
83+
}
84+
85+
// isNativeSegWitPkScript returns true iff pkScript is one of the native SegWit
86+
// script types (P2WPKH, P2WSH, P2TR) supported by the BIP-322 "simple" variant.
87+
func isNativeSegWitPkScript(pkScript []byte) bool {
88+
return txscript.IsPayToWitnessPubKeyHash(pkScript) ||
89+
txscript.IsPayToWitnessScriptHash(pkScript) ||
90+
txscript.IsPayToTaproot(pkScript)
91+
}
92+
93+
// BuildToSignPacketSimple constructs a transaction template PSBT packet to
94+
// prepare for signing, using the message and spend pkScript for the "simple"
95+
// variant of the BIP-322 specification. It creates the to_sign transaction
96+
// template, according to BIP-322. This can only be used for native SegWit
97+
// outputs (P2WPKH, P2WSH, P2TR), and the spend pkScript is validated
98+
// accordingly.
99+
func BuildToSignPacketSimple(message, pkScript []byte) (*psbt.Packet, error) {
100+
// Enforce an inclusion list: only native SegWit outputs are valid for
101+
// the simple variant. Any other script type (legacy P2PKH/P2SH, bare
102+
// multisig, OP_RETURN, unknown future witness versions, etc.) must use
103+
// the full variant.
104+
if !isNativeSegWitPkScript(pkScript) {
105+
return nil, errSimpleSegWitOnly
106+
}
107+
108+
return BuildToSignPacketFull(message, pkScript, 0, 0, 0), nil
109+
}
110+
111+
// BuildToSignPacketFull constructs a transaction template PSBT packet to
112+
// prepare for signing, using the message, spend pkScript, and tx parameters for
113+
// the "full" variant of the BIP-322 specification. It creates the to_sign
114+
// transaction template, according to BIP-322.
115+
func BuildToSignPacketFull(message, spendPkScript []byte,
116+
txVersion int32, lockTime, sequence uint32) *psbt.Packet {
117+
118+
spendTx := buildToSpendTx(message, spendPkScript)
119+
120+
// Create to_sign transaction in accordance to BIP-322:
121+
// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full
122+
packet := &psbt.Packet{
123+
UnsignedTx: &wire.MsgTx{
124+
Version: txVersion,
125+
LockTime: lockTime,
126+
TxIn: []*wire.TxIn{{
127+
PreviousOutPoint: wire.OutPoint{
128+
Hash: spendTx.TxHash(),
129+
Index: 0,
130+
},
131+
Sequence: sequence,
132+
}},
133+
TxOut: []*wire.TxOut{{
134+
Value: 0,
135+
PkScript: []byte{txscript.OP_RETURN},
136+
}},
137+
},
138+
Inputs: []psbt.PInput{{
139+
WitnessUtxo: &wire.TxOut{
140+
Value: 0,
141+
PkScript: spendPkScript,
142+
},
143+
NonWitnessUtxo: spendTx,
144+
}},
145+
Outputs: []psbt.POutput{{}},
146+
}
147+
148+
// Legacy scripts can't have a witness UTXO, otherwise the PSBT
149+
// extraction will fail.
150+
if txscript.IsPayToPubKey(spendPkScript) ||
151+
txscript.IsPayToPubKeyHash(spendPkScript) {
152+
153+
packet.Inputs[0].WitnessUtxo = nil
154+
}
155+
156+
return packet
157+
}
158+
159+
// SerializeTxWitness returns the wire witness stack as raw bytes.
160+
func SerializeTxWitness(txWitness wire.TxWitness) ([]byte, error) {
161+
var witnessBytes bytes.Buffer
162+
err := psbt.WriteTxWitness(&witnessBytes, txWitness)
163+
if err != nil {
164+
return nil, fmt.Errorf("error serializing witness: %w", err)
165+
}
166+
167+
return witnessBytes.Bytes(), nil
168+
}
169+
170+
// SerializeSignature serializes the signature of a finalized PSBT packet of a
171+
// BIP-322 to_sign transaction. According to the rules described in the BIP,
172+
// this writes the signature as one of the three formats:
173+
// 1. Simple (smp): Version and LockTime are 0, only one input with Sequence 0.
174+
// 2. Full (ful): Version or LockTime or Sequence are non-zero, single input.
175+
// 3. Proof of Funds (pof): Version or LockTime or Sequence are non-zero,
176+
// multiple inputs.
177+
func SerializeSignature(finalizedPacket *psbt.Packet) (string, error) {
178+
if finalizedPacket == nil {
179+
return "", errors.New("nil packet")
180+
}
181+
182+
// Prevent us from panicking on either if the length doesn't match.
183+
if len(finalizedPacket.Inputs) != len(finalizedPacket.UnsignedTx.TxIn) {
184+
return "", errors.New("input and txin length mismatch")
185+
}
186+
187+
// At this point at least one input must be provided.
188+
if len(finalizedPacket.Inputs) == 0 {
189+
return "", errors.New("missing inputs")
190+
}
191+
192+
// There is no exported IsFinalized function. But calling
193+
// MaybeFinalizeAll on an already finalized packet should not produce an
194+
// error if it's already finalized.
195+
if err := psbt.MaybeFinalizeAll(finalizedPacket); err != nil {
196+
return "", fmt.Errorf("packet must be finalizable: %w", err)
197+
}
198+
199+
tx := finalizedPacket.UnsignedTx
200+
utxo := finalizedPacket.Inputs[0].WitnessUtxo
201+
if utxo == nil {
202+
if finalizedPacket.Inputs[0].NonWitnessUtxo == nil {
203+
return "", errors.New("missing utxo")
204+
}
205+
206+
// The to_spend transaction must have exactly one output to be
207+
// a valid BIP-322 previous transaction to the to_sign's first
208+
// input.
209+
prevTx := finalizedPacket.Inputs[0].NonWitnessUtxo
210+
if len(prevTx.TxOut) != 1 {
211+
return "", errors.New("invalid non witness UTXO")
212+
}
213+
214+
utxo = prevTx.TxOut[0]
215+
}
216+
217+
// Detect the variant of the signature.
218+
switch {
219+
// Proof of Fund (pof) variant has multiple inputs.
220+
case len(finalizedPacket.Inputs) > 1:
221+
content, err := finalizedPacket.B64Encode()
222+
if err != nil {
223+
return "", fmt.Errorf("error encoding packet: %w", err)
224+
}
225+
226+
return PrefixProofOfFunds + content, nil
227+
228+
// Full (ful) variant has non-zero version, locktime, or sequence or a
229+
// non-native SegWit input.
230+
case tx.Version != 0 || tx.LockTime != 0 || tx.TxIn[0].Sequence != 0 ||
231+
!isNativeSegWitPkScript(utxo.PkScript):
232+
233+
signedTx, err := psbt.Extract(finalizedPacket)
234+
if err != nil {
235+
return "", fmt.Errorf("error extracting packet: %w",
236+
err)
237+
}
238+
239+
var signedTxBytes bytes.Buffer
240+
err = signedTx.Serialize(&signedTxBytes)
241+
if err != nil {
242+
return "", fmt.Errorf("error serializing signed tx: "+
243+
"%w", err)
244+
}
245+
246+
content := b64Encode(signedTxBytes.Bytes())
247+
return PrefixFull + content, nil
248+
249+
// The simple (smp) variant is used if the above cases don't match.
250+
default:
251+
signedTx, err := psbt.Extract(finalizedPacket)
252+
if err != nil {
253+
return "", fmt.Errorf("error extracting packet: %w",
254+
err)
255+
}
256+
257+
witnessBytes, err := SerializeTxWitness(
258+
signedTx.TxIn[0].Witness,
259+
)
260+
if err != nil {
261+
return "", fmt.Errorf("error serializing witness: %w",
262+
err)
263+
}
264+
265+
content := b64Encode(witnessBytes)
266+
return PrefixSimple + content, nil
267+
}
268+
}

btcutil/bip322/bip322_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package bip322
2+
3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/btcsuite/btcd/btcutil"
12+
"github.com/btcsuite/btcd/chaincfg"
13+
"github.com/btcsuite/btcd/txscript"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
var (
18+
hexEncode = hex.EncodeToString
19+
)
20+
21+
// testVectors is the top-level structure of the test data file.
22+
type testVectors struct {
23+
TxHashes []txHashVector `json:"tx_hashes,omitempty"`
24+
Simple []simpleSignatureVector `json:"simple"`
25+
}
26+
27+
// txHashVector contains expected transaction hashes for a given message.
28+
type txHashVector struct {
29+
Message string `json:"message"`
30+
Address string `json:"address"`
31+
MessageHash string `json:"message_hash"`
32+
ToSpendTxHash string `json:"to_spend_tx_hash"`
33+
ToSignTxHash string `json:"to_sign_tx_hash"`
34+
}
35+
36+
// simpleSignatureVector contains BIP-322 signature data for a given "simple"
37+
// variant test case.
38+
type simpleSignatureVector struct {
39+
Message string `json:"message"`
40+
PrivateKeys []string `json:"private_keys"`
41+
Address string `json:"address"`
42+
Type string `json:"type"`
43+
WitnessScript string `json:"witness_script"`
44+
Bip322Signatures []string `json:"bip322_signatures"`
45+
}
46+
47+
// fullSignatureVector contains BIP-322 signature data for a given "full"
48+
// variant test case.
49+
type fullSignatureVector struct {
50+
Message string `json:"message"`
51+
PrivateKeys []string `json:"private_keys"`
52+
Address string `json:"address"`
53+
Type string `json:"type"`
54+
WitnessScript string `json:"witness_script"`
55+
TxVersion int32 `json:"tx_version"`
56+
LockTime uint32 `json:"lock_time"`
57+
Sequence uint32 `json:"sequence"`
58+
Bip322Signatures []string `json:"bip322_signatures"`
59+
}
60+
61+
// loadTestVectors reads and parses a test data file.
62+
func loadTestVectors(t *testing.T, fileName string) *testVectors {
63+
t.Helper()
64+
65+
data, err := os.ReadFile(filepath.Join("testdata", fileName))
66+
require.NoError(t, err)
67+
68+
var vectors testVectors
69+
err = json.Unmarshal(data, &vectors)
70+
require.NoError(t, err)
71+
72+
return &vectors
73+
}
74+
75+
// testName returns a human-readable sub-test name for a given message.
76+
func testName(message string) string {
77+
if message == "" {
78+
return "msg=<empty>"
79+
}
80+
81+
return fmt.Sprintf("msg=%s", message)
82+
}
83+
84+
// TestTxHashes tests the tx_hashes test vectors as mentioned in BIP-322:
85+
// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
86+
func TestTxHashes(t *testing.T) {
87+
vectors := loadTestVectors(t, "basic-test-vectors.json")
88+
89+
for _, tc := range vectors.TxHashes {
90+
t.Run(testName(tc.Message), func(t *testing.T) {
91+
addr, err := btcutil.DecodeAddress(
92+
tc.Address, &chaincfg.MainNetParams,
93+
)
94+
require.NoError(t, err)
95+
96+
scriptPubKey, err := txscript.PayToAddrScript(addr)
97+
require.NoError(t, err)
98+
99+
toSpendTx := buildToSpendTx(
100+
[]byte(tc.Message), scriptPubKey,
101+
)
102+
103+
// The message hash must be set as the OP_PUSH of the
104+
// first input's scriptSig.
105+
msgHash := toSpendTx.TxIn[0].SignatureScript[2:]
106+
require.Equal(t, tc.MessageHash, hexEncode(msgHash))
107+
108+
require.Equal(
109+
t, tc.ToSpendTxHash,
110+
toSpendTx.TxHash().String(),
111+
)
112+
113+
toSignTx, err := BuildToSignPacketSimple(
114+
[]byte(tc.Message), scriptPubKey,
115+
)
116+
require.NoError(t, err)
117+
118+
require.Equal(
119+
t, tc.ToSignTxHash,
120+
toSignTx.UnsignedTx.TxHash().String(),
121+
)
122+
})
123+
}
124+
}

0 commit comments

Comments
 (0)