@@ -10,6 +10,9 @@ package psbt
1010// is in the correct state.
1111
1212import (
13+ "bytes"
14+ "fmt"
15+
1316 "github.com/btcsuite/btcd/txscript/v2"
1417)
1518
@@ -136,6 +139,143 @@ 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+ _ , err := FindLeafScript (pInput , partialSig .TapLeafHash )
229+ if err != nil {
230+ return SignInvalid , fmt .Errorf ("%w: tap leaf hash %x " +
231+ "on partial signature does not match any leaf " +
232+ "script on input: %v" ,
233+ ErrInvalidSignatureForInput ,
234+ partialSig .TapLeafHash , err )
235+ }
236+ }
237+
238+ // Shape validation, duplicate-key detection and the actual append are
239+ // done by the existing low-level updater helper.
240+ if err := u .AddInMuSig2PartialSig (inIndex , partialSig ); err != nil {
241+ return SignInvalid , err
242+ }
243+
244+ return SignSuccesful , nil
245+ }
246+
247+ // musig2ParticipantRegistered reports whether the partial sig's participant
248+ // pubkey appears in any PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS record on the
249+ // input. The aggregate key on the partial sig is intentionally not compared
250+ // against the record's aggregate; in BIP-373 case 4 the partial sig records
251+ // the BIP-32 derived aggregate while the record carries the bare aggregate.
252+ func musig2ParticipantRegistered (pInput * PInput ,
253+ partialSig * MuSig2PartialSig ) bool {
254+
255+ for _ , participants := range pInput .MuSig2Participants {
256+ for _ , key := range participants .Keys {
257+ if key .IsEqual (partialSig .PubKey ) {
258+ return true
259+ }
260+ }
261+ }
262+
263+ return false
264+ }
265+
266+ // musig2HasMatchingPubNonce reports whether the input carries a
267+ // PSBT_IN_MUSIG2_PUB_NONCE whose key data matches the partial signature's
268+ // key data (participant pubkey || aggregate key || optional tap leaf hash).
269+ func musig2HasMatchingPubNonce (pInput * PInput , partialSigKeyData []byte ) bool {
270+ for _ , n := range pInput .MuSig2PubNonces {
271+ if bytes .Equal (n .KeyData (), partialSigKeyData ) {
272+ return true
273+ }
274+ }
275+
276+ return false
277+ }
278+
139279// nonWitnessToWitness extracts the TxOut from the existing NonWitnessUtxo
140280// field in the given PSBT input and sets it as type witness by replacing the
141281// NonWitnessUtxo field with a WitnessUtxo field. See
0 commit comments