Skip to content

Commit 1907255

Browse files
committed
psbt: support adding partial MuSig2 signatures
1 parent 555165d commit 1907255

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

psbt/signer.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ package psbt
1010
// is in the correct state.
1111

1212
import (
13+
"bytes"
14+
"fmt"
15+
1316
"github.com/btcsuite/btcd/txscript/v2"
1417
)
1518

@@ -136,6 +139,145 @@ func (u *Updater) Sign(inIndex int, sig []byte, pubKey []byte,
136139
return SignSuccesful, nil
137140
}
138141

142+
// SignMuSig2 attaches a MuSig2 partial signature to the input at index
143+
// inIndex, following the BIP-174 Signer role for the MuSig2 fields defined by
144+
// BIP-373.
145+
//
146+
// Before appending, SignMuSig2 enforces the invariants a finalizer will later
147+
// rely on:
148+
//
149+
// - The input must not be finalized.
150+
// - The input must carry a witness UTXO (every BIP-373 MuSig2 spend is
151+
// segwit v1).
152+
// - The participant pubkey on the partial sig must appear in at least one
153+
// PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record on the input. The aggregate
154+
// key recorded on the partial sig itself does not need to equal the
155+
// bare aggregate in the participants record: BIP-373 case 4 deliberately
156+
// records the BIP-32-derived aggregate on the partial sigs while the
157+
// participants record carries the parent (bare) aggregate, and the
158+
// finalizer reconciles the two via PSBT_GLOBAL_XPUB. Enforcing equality
159+
// here would reject the legitimate derived-aggregate case.
160+
// - A PSBT_IN_MUSIG2_PUB_NONCE field with the same key data prefix
161+
// (participant pubkey || aggregate pubkey || optional tap leaf hash) must
162+
// already be present — partial sigs cannot be combined without their
163+
// matching nonces.
164+
// - If TapLeafHash is set, it must resolve to a leaf script recorded on
165+
// the input (i.e. the script the partial sig commits to is actually
166+
// part of this spend).
167+
//
168+
// On any of these checks failing the input is left untouched and SignInvalid
169+
// is returned together with the underlying error. Shape validation
170+
// (compressed pubkeys, 32-byte partial sig) and duplicate-key detection are
171+
// handled by AddInMuSig2PartialSig, which SignMuSig2 delegates to once the
172+
// Signer-role checks pass.
173+
//
174+
// SignMuSig2 does not itself compute the partial signature; callers are
175+
// expected to feed in the output of musig2.Sign (held in the
176+
// MuSig2PartialSig.PartialSig field). This mirrors the existing Sign()
177+
// helper, which accepts a pre-computed ECDSA signature rather than driving
178+
// the signing key directly.
179+
func (u *Updater) SignMuSig2(inIndex int,
180+
partialSig *MuSig2PartialSig) (SignOutcome, error) {
181+
182+
if inIndex < 0 || inIndex >= len(u.Upsbt.Inputs) {
183+
return SignInvalid, ErrInvalidPsbtFormat
184+
}
185+
186+
if isFinalized(u.Upsbt, inIndex) {
187+
return SignFinalized, nil
188+
}
189+
190+
if partialSig == nil || partialSig.PubKey == nil ||
191+
partialSig.AggregateKey == nil {
192+
193+
return SignInvalid, ErrInvalidPsbtFormat
194+
}
195+
196+
pInput := &u.Upsbt.Inputs[inIndex]
197+
198+
// BIP-373 inputs are taproot (segwit v1); a witness UTXO is required
199+
// for the finalizer to recompute the sighash.
200+
if pInput.WitnessUtxo == nil {
201+
return SignInvalid, ErrInvalidPsbtFormat
202+
}
203+
204+
// Make sure the participant is actually a member of a known aggregate
205+
// on this input. Without a PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record
206+
// the finalizer cannot reproduce the aggregate, so the partial sig is
207+
// not usable.
208+
if !musig2ParticipantRegistered(pInput, partialSig) {
209+
return SignInvalid, fmt.Errorf("%w: participant pubkey not "+
210+
"found in any MuSig2 participants record matching the "+
211+
"supplied aggregate key", ErrInvalidSignatureForInput)
212+
}
213+
214+
// A matching pub nonce (same participant pubkey || aggregate key ||
215+
// optional tap leaf hash) must exist; nonces are the precondition for
216+
// signing per BIP-373 §Signer.
217+
keyData := partialSig.KeyData()
218+
if !musig2HasMatchingPubNonce(pInput, keyData) {
219+
return SignInvalid, fmt.Errorf("%w: no matching "+
220+
"PSBT_IN_MUSIG2_PUB_NONCE found for partial signature",
221+
ErrInvalidSignatureForInput)
222+
}
223+
224+
// If the partial sig commits to a tap leaf, the leaf script must
225+
// actually be present on the input, otherwise the finalizer will not
226+
// be able to assemble the script-spend witness.
227+
if len(partialSig.TapLeafHash) > 0 {
228+
if _, err := FindLeafScript(
229+
pInput, partialSig.TapLeafHash,
230+
); err != nil {
231+
232+
return SignInvalid, fmt.Errorf("%w: tap leaf hash %x "+
233+
"on partial signature does not match any leaf "+
234+
"script on input: %v",
235+
ErrInvalidSignatureForInput,
236+
partialSig.TapLeafHash, err)
237+
}
238+
}
239+
240+
// Shape validation, duplicate-key detection and the actual append are
241+
// done by the existing low-level updater helper.
242+
if err := u.AddInMuSig2PartialSig(inIndex, partialSig); err != nil {
243+
return SignInvalid, err
244+
}
245+
246+
return SignSuccesful, nil
247+
}
248+
249+
// musig2ParticipantRegistered reports whether the partial sig's participant
250+
// pubkey appears in any PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record on the
251+
// input. The aggregate key on the partial sig is intentionally not compared
252+
// against the record's aggregate; in BIP-373 case 4 the partial sig records
253+
// the BIP-32 derived aggregate while the record carries the bare aggregate.
254+
func musig2ParticipantRegistered(pInput *PInput,
255+
partialSig *MuSig2PartialSig) bool {
256+
257+
for _, participants := range pInput.MuSig2Participants {
258+
for _, key := range participants.Keys {
259+
if key.IsEqual(partialSig.PubKey) {
260+
return true
261+
}
262+
}
263+
}
264+
265+
return false
266+
}
267+
268+
// musig2HasMatchingPubNonce reports whether the input carries a
269+
// PSBT_IN_MUSIG2_PUB_NONCE whose key data matches the partial signature's
270+
// key data (participant pubkey || aggregate key || optional tap leaf hash).
271+
func musig2HasMatchingPubNonce(pInput *PInput, partialSigKeyData []byte) bool {
272+
for _, n := range pInput.MuSig2PubNonces {
273+
if bytes.Equal(n.KeyData(), partialSigKeyData) {
274+
return true
275+
}
276+
}
277+
278+
return false
279+
}
280+
139281
// nonWitnessToWitness extracts the TxOut from the existing NonWitnessUtxo
140282
// field in the given PSBT input and sets it as type witness by replacing the
141283
// NonWitnessUtxo field with a WitnessUtxo field. See

0 commit comments

Comments
 (0)