@@ -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,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