Skip to content

Commit e1ae8d8

Browse files
committed
psbt: support adding partial MuSig2 signatures
1 parent 0b5b208 commit e1ae8d8

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

psbt/signer.go

Lines changed: 140 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,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

Comments
 (0)