Skip to content

Commit 0541026

Browse files
committed
psbt: add unit tests and test vectors
1 parent e1ae8d8 commit 0541026

7 files changed

Lines changed: 3024 additions & 1 deletion
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// Copyright (c) 2026 The btcsuite developers
2+
// Use of this source code is governed by an ISC
3+
// license that can be found in the LICENSE file.
4+
5+
package psbt
6+
7+
import (
8+
"bytes"
9+
"crypto/hmac"
10+
"crypto/sha512"
11+
"encoding/binary"
12+
"encoding/hex"
13+
"testing"
14+
15+
"github.com/btcsuite/btcd/btcec/v2"
16+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
17+
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
18+
"github.com/btcsuite/btcd/btcutil/v2/hdkeychain"
19+
"github.com/btcsuite/btcd/chaincfg/v2"
20+
"github.com/btcsuite/btcd/chainhash/v2"
21+
"github.com/btcsuite/btcd/txscript/v2"
22+
"github.com/btcsuite/btcd/wire/v2"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
// makeTestParticipants returns three deterministic (priv, pub) pairs to use
27+
// as the MuSig2 signing set. The test only needs a self-consistent 3-party
28+
// setup; it does not depend on the specific keys matching any external
29+
// vector.
30+
func makeTestParticipants(t *testing.T) (
31+
[]*btcec.PrivateKey, []*btcec.PublicKey) {
32+
33+
t.Helper()
34+
35+
scalars := []string{
36+
"f5dd1de7b85c0e8c1ada7c0c95eaa42d2bcb29ee71f5e0e63d8df1eb9e3a0e75",
37+
"7da2bf6e2e09f9da7e1f60af26b0e94649ada55da00bdc8a7d8b4eaf72fe69bb",
38+
"0000000000000000000000000000000000000000000000000000000000000003",
39+
}
40+
41+
privs := make([]*btcec.PrivateKey, len(scalars))
42+
pubs := make([]*btcec.PublicKey, len(scalars))
43+
for i, hexStr := range scalars {
44+
raw, err := hex.DecodeString(hexStr)
45+
require.NoError(t, err)
46+
47+
priv, pub := btcec.PrivKeyFromBytes(raw)
48+
privs[i] = priv
49+
pubs[i] = pub
50+
}
51+
52+
return privs, pubs
53+
}
54+
55+
// bip32ChildTweak computes the per-step BIP-32 tweak (the IL half of the
56+
// HMAC-SHA512) for an unhardened child derivation.
57+
func bip32ChildTweak(t *testing.T, parent *hdkeychain.ExtendedKey,
58+
idx uint32) [32]byte {
59+
60+
t.Helper()
61+
62+
pub, err := parent.ECPubKey()
63+
require.NoError(t, err)
64+
65+
var idxBytes [4]byte
66+
binary.BigEndian.PutUint32(idxBytes[:], idx)
67+
68+
h := hmac.New(sha512.New, parent.ChainCode())
69+
h.Write(pub.SerializeCompressed())
70+
h.Write(idxBytes[:])
71+
ilr := h.Sum(nil)
72+
73+
var tweak [32]byte
74+
copy(tweak[:], ilr[:32])
75+
return tweak
76+
}
77+
78+
// computeTaprootTweak computes BIP-86 (when merkleRoot is nil) or
79+
// taproot-with-merkle-root tap tweak for the given x-only key.
80+
func computeTaprootTweak(t *testing.T, xOnlyKey []byte,
81+
merkleRoot []byte) [32]byte {
82+
83+
t.Helper()
84+
85+
hashedTweak := chainhash.TaggedHash(
86+
chainhash.TagTapTweak, xOnlyKey, merkleRoot,
87+
)
88+
var out [32]byte
89+
copy(out[:], hashedTweak[:])
90+
return out
91+
}
92+
93+
// TestFinalize_MuSig2_BIP32Derived_WithGlobalXpub builds a synthetic PSBT
94+
// that exercises the BIP-373 case 4 finalize path: the taproot internal
95+
// key is an unhardened BIP-32 child of the bare MuSig2 aggregate, and the
96+
// PSBT carries the synthetic aggregate xpub via PSBT_GLOBAL_XPUB. The
97+
// test generates fresh nonces and partial signatures programmatically,
98+
// then runs Finalize and verifies the produced witness with the script
99+
// engine.
100+
func TestFinalize_MuSig2_BIP32Derived_WithGlobalXpub(t *testing.T) {
101+
privs, pubs := makeTestParticipants(t)
102+
103+
// Bare aggregate is the KeyAgg of the three participant keys.
104+
bareAgg, _, _, err := musig2.AggregateKeys(pubs, true)
105+
require.NoError(t, err)
106+
107+
// Synthetic aggregate xpub: pick a deterministic 32-byte chain code.
108+
chainCode := bytes.Repeat([]byte{0xa5}, 32)
109+
parentFP := []byte{0, 0, 0, 0}
110+
xpubAgg := hdkeychain.NewExtendedKey(
111+
chaincfg.MainNetParams.HDPublicKeyID[:],
112+
bareAgg.PreTweakedKey.SerializeCompressed(),
113+
chainCode, parentFP, 0, 0, false,
114+
)
115+
116+
// Derive the internal key at path 1/2.
117+
derivPath := []uint32{1, 2}
118+
119+
bip32T1 := bip32ChildTweak(t, xpubAgg, derivPath[0])
120+
xpubChild1, err := xpubAgg.Derive(derivPath[0])
121+
require.NoError(t, err)
122+
123+
bip32T2 := bip32ChildTweak(t, xpubChild1, derivPath[1])
124+
xpubChild2, err := xpubChild1.Derive(derivPath[1])
125+
require.NoError(t, err)
126+
127+
internalKey, err := xpubChild2.ECPubKey()
128+
require.NoError(t, err)
129+
130+
// BIP-86: output key = internal_key + tap_tweak * G, where
131+
// tap_tweak = TaggedHash("TapTweak", x_only(internal_key)).
132+
internalKeyXOnly := schnorr.SerializePubKey(internalKey)
133+
tapTweak := computeTaprootTweak(t, internalKeyXOnly, nil)
134+
135+
allTweaks := []musig2.KeyTweakDesc{
136+
{Tweak: bip32T1, IsXOnly: false},
137+
{Tweak: bip32T2, IsXOnly: false},
138+
{Tweak: tapTweak, IsXOnly: true},
139+
}
140+
141+
// FinalKey of the full tweak chain = the taproot output key.
142+
fullAgg, _, _, err := musig2.AggregateKeys(
143+
pubs, true, musig2.WithKeyTweaks(allTweaks...),
144+
)
145+
require.NoError(t, err)
146+
outputKey := fullAgg.FinalKey
147+
148+
// Build a P2TR pkScript for the output key and a dummy spending
149+
// transaction. The transaction sends a single input (referencing an
150+
// arbitrary outpoint) to a single dummy P2WPKH output.
151+
pkScript, err := txscript.PayToTaprootScript(outputKey)
152+
require.NoError(t, err)
153+
154+
const inputAmount = int64(100_000_000)
155+
prevHash := chainhash.Hash{0xde, 0xad, 0xbe, 0xef}
156+
157+
tx := wire.NewMsgTx(2)
158+
tx.AddTxIn(&wire.TxIn{
159+
PreviousOutPoint: wire.OutPoint{Hash: prevHash, Index: 0},
160+
Sequence: 0xfffffffd,
161+
})
162+
dummyOutScript := append(
163+
[]byte{0x00, 0x14}, bytes.Repeat([]byte{1}, 20)...,
164+
)
165+
tx.AddTxOut(wire.NewTxOut(inputAmount-1000, dummyOutScript))
166+
167+
// Compute the sighash that the participants will sign over.
168+
prevFetcher := txscript.NewCannedPrevOutputFetcher(
169+
pkScript, inputAmount,
170+
)
171+
sigHashes := txscript.NewTxSigHashes(tx, prevFetcher)
172+
sigHash, err := txscript.CalcTaprootSignatureHash(
173+
sigHashes, txscript.SigHashDefault, tx, 0, prevFetcher,
174+
)
175+
require.NoError(t, err)
176+
var sigHashMsg [32]byte
177+
copy(sigHashMsg[:], sigHash)
178+
179+
// Each participant generates a (sec, pub) nonce pair.
180+
type nonceEntry struct {
181+
sec [musig2.SecNonceSize]byte
182+
pub [musig2.PubNonceSize]byte
183+
}
184+
nonces := make([]nonceEntry, len(privs))
185+
for i, priv := range privs {
186+
n, err := musig2.GenNonces(
187+
musig2.WithPublicKey(priv.PubKey()),
188+
musig2.WithNonceCombinedKeyAux(outputKey),
189+
)
190+
require.NoError(t, err)
191+
nonces[i] = nonceEntry{sec: n.SecNonce, pub: n.PubNonce}
192+
}
193+
194+
pubNonces := make([][musig2.PubNonceSize]byte, len(nonces))
195+
for i, n := range nonces {
196+
pubNonces[i] = n.pub
197+
}
198+
combinedNonce, err := musig2.AggregateNonces(pubNonces)
199+
require.NoError(t, err)
200+
201+
// Each participant computes a partial signature using the full tweak
202+
// chain so the resulting sigs combine under the post-tweak output
203+
// key.
204+
partialSigs := make([]*musig2.PartialSignature, len(privs))
205+
for i, priv := range privs {
206+
ps, err := musig2.Sign(
207+
nonces[i].sec, priv, combinedNonce, pubs, sigHashMsg,
208+
musig2.WithSortedKeys(),
209+
musig2.WithTweaks(allTweaks...),
210+
)
211+
require.NoError(t, err)
212+
partialSigs[i] = ps
213+
}
214+
215+
// Construct the PSBT.
216+
p, err := NewFromUnsignedTx(tx)
217+
require.NoError(t, err)
218+
219+
updater, err := NewUpdater(p)
220+
require.NoError(t, err)
221+
222+
require.NoError(t, updater.AddInWitnessUtxo(
223+
&wire.TxOut{Value: inputAmount, PkScript: pkScript}, 0,
224+
))
225+
226+
// Add the synthetic aggregate xpub to PSBT_GLOBAL_XPUB. We use a
227+
// zero master fingerprint and an empty path because the xpub *is*
228+
// the master in this synthetic setup.
229+
p.XPubs = append(p.XPubs, XPub{
230+
ExtendedKey: EncodeExtendedKey(xpubAgg),
231+
MasterKeyFingerprint: 0,
232+
Bip32Path: nil,
233+
})
234+
235+
// Internal key + its derivation entry pinning the BIP-32 path.
236+
p.Inputs[0].TaprootInternalKey = internalKeyXOnly
237+
p.Inputs[0].TaprootBip32Derivation = append(
238+
p.Inputs[0].TaprootBip32Derivation,
239+
&TaprootBip32Derivation{
240+
XOnlyPubKey: internalKeyXOnly,
241+
MasterKeyFingerprint: 0,
242+
Bip32Path: derivPath,
243+
},
244+
)
245+
246+
// MuSig2 fields.
247+
require.NoError(t, updater.AddInMuSig2Participants(
248+
0, &MuSig2Participants{
249+
AggregateKey: bareAgg.PreTweakedKey,
250+
Keys: pubs,
251+
},
252+
))
253+
254+
for i, priv := range privs {
255+
require.NoError(t, updater.AddInMuSig2PubNonce(
256+
0, &MuSig2PubNonce{
257+
PubKey: priv.PubKey(),
258+
AggregateKey: outputKey,
259+
PubNonce: nonces[i].pub,
260+
},
261+
))
262+
require.NoError(t, updater.AddInMuSig2PartialSig(
263+
0, &MuSig2PartialSig{
264+
PubKey: priv.PubKey(),
265+
AggregateKey: outputKey,
266+
PartialSig: *partialSigs[i],
267+
},
268+
))
269+
}
270+
271+
// Round-trip through serialize/deserialize to make sure the wire
272+
// form survives, then finalize and verify.
273+
var buf bytes.Buffer
274+
require.NoError(t, p.Serialize(&buf))
275+
276+
parsed, err := NewFromRawBytes(bytes.NewReader(buf.Bytes()), false)
277+
require.NoError(t, err)
278+
279+
require.NoError(t, MaybeFinalizeAll(parsed))
280+
require.NotNil(t, parsed.Inputs[0].FinalScriptWitness)
281+
282+
verifyFinalized(t, parsed)
283+
}

0 commit comments

Comments
 (0)