| name | ark-musig2-signing |
| description | MuSig2 distributed signing protocol for Ark transaction trees - nonce generation, aggregation, partial signatures |
MuSig2 Signing for Ark
When to Use
Use this skill when:
- Implementing distributed signing for VTXO trees
- Working with nonce generation and aggregation
- Creating partial signatures for tree transactions
- Coordinating multi-party signing sessions
- Validating tree signatures
- Understanding the signer/coordinator session pattern
Key Concepts
1. MuSig2 Protocol Overview
MuSig2 is a multi-signature scheme for Schnorr signatures. In Ark:
- Multiple parties (users + ASP) collaboratively sign transaction trees
- Each party generates nonces, aggregates them, then produces partial signatures
- Partial signatures are combined into a single valid Schnorr signature
2. Two-Round Protocol
Round 1 - Nonce Exchange:
- Each signer generates secret nonces (never shared)
- Each signer shares public nonces
- Coordinator aggregates all public nonces
Round 2 - Signing:
- Each signer uses aggregated nonce + secret nonce to create partial signature
- Coordinator collects and combines partial signatures
- Result: single valid Schnorr signature
3. Session Types
- SignerSession: Used by individual signers (users)
- CoordinatorSession: Used by the aggregator (ASP)
4. Tree-Wide Signing
In Ark, entire transaction trees are signed at once:
- Each transaction in the tree gets its own set of nonces
TreeNonces: map of txid → public nonce
TreePartialSigs: map of txid → partial signature
Code Patterns
Pattern 1: Nonce Structure
type Musig2Nonce struct {
PubNonce [66]byte
}
type TreeNonces map[string]*Musig2Nonce
type TreePartialSigs map[string]*musig2.PartialSignature
Source: arkd/pkg/ark-lib/tree/musig2.go:26-98
Pattern 2: SignerSession Interface
type SignerSession interface {
Init(batchOutSweepClosure []byte, batchOutAmount int64, vtxoTree *TxTree) error
GetPublicKey() string
GetNonces() (TreeNonces, error)
SetAggregatedNonces(TreeNonces)
AggregateNonces(txid string, pubkeyNonces map[string]*Musig2Nonce) (hasAllNonces bool, err error)
Sign() (TreePartialSigs, error)
}
Source: arkd/pkg/ark-lib/tree/musig2.go:167-175
Pattern 3: CoordinatorSession Interface
type CoordinatorSession interface {
AddNonce(*btcec.PublicKey, TreeNonces)
AddSignatures(*btcec.PublicKey, TreePartialSigs) (shouldBan bool, err error)
AggregateNonces() (TreeNonces, error)
GetPublicNonces() map[string]TreeNonces
SignTree() (*TxTree, error)
}
Source: arkd/pkg/ark-lib/tree/musig2.go:177-184
Pattern 4: Creating a Signer Session
func (w *bitcoinWallet) NewVtxoTreeSigner(
ctx context.Context, derivationPath string,
) (tree.SignerSession, error) {
derivedPrivKey, _ := btcec.PrivKeyFromBytes(currentKey.Key)
return tree.NewTreeSignerSession(derivedPrivKey), nil
}
session := tree.NewTreeSignerSession(privateKey)
err := session.Init(batchOutSweepClosure, batchOutAmount, vtxoTree)
nonces, err := session.GetNonces()
session.SetAggregatedNonces(aggregatedNonces)
partialSigs, err := session.Sign()
Source: go-sdk/wallet/singlekey/bitcoin_wallet.go:385-430
Pattern 5: Creating a Coordinator Session
coordinator, err := tree.NewTreeCoordinatorSession(
batchOutSweepClosure, batchOutAmount, vtxoTree,
)
for pubkey, nonces := range signerNonces {
coordinator.AddNonce(pubkey, nonces)
}
aggregatedNonces, err := coordinator.AggregateNonces()
for pubkey, sigs := range signerSigs {
shouldBan, err := coordinator.AddSignatures(pubkey, sigs)
if shouldBan {
}
}
signedTree, err := coordinator.SignTree()
Source: arkd/pkg/ark-lib/tree/musig2.go:484-615
Pattern 6: Key Aggregation with Taproot Tweak
func AggregateKeys(pubkeys []*btcec.PublicKey, tweak []byte) (*musig2.AggregateKey, error) {
if len(pubkeys) == 0 {
return nil, errors.New("no pubkeys")
}
if len(pubkeys) == 1 {
res := &musig2.AggregateKey{PreTweakedKey: pubkeys[0]}
if len(tweak) > 0 {
res.FinalKey = txscript.ComputeTaprootOutputKey(pubkeys[0], tweak)
} else {
res.FinalKey = pubkeys[0]
}
return res, nil
}
opts := make([]musig2.KeyAggOption, 0)
if len(tweak) > 0 {
opts = append(opts, musig2.WithTaprootKeyTweak(tweak))
}
key, _, _, err := musig2.AggregateKeys(pubkeys, true, opts...)
return key, err
}
Source: arkd/pkg/ark-lib/tree/musig2.go:186-225
Pattern 7: Generating Nonces
func generateNonces(signerPubKey *btcec.PublicKey) func(*psbt.Packet) (*musig2.Nonces, error) {
serializedSignerPubKey := schnorr.SerializePubKey(signerPubKey)
return func(ptx *psbt.Packet) (*musig2.Nonces, error) {
mustGenerateNonce, _, err := getCosignersPublicKeys(serializedSignerPubKey, ptx)
if err != nil {
return nil, err
}
if !mustGenerateNonce {
return nil, nil
}
nonce, err := musig2.GenNonces(
musig2.WithPublicKey(signerPubKey),
)
return nonce, err
}
}
Source: arkd/pkg/ark-lib/tree/musig2.go:846-871
Pattern 8: Signing with Taproot Tweak
func sign(
signer *btcec.PrivateKey, batchOutSweepClosure []byte,
) func(musigParams) (*musig2.PartialSignature, error) {
return func(params musigParams) (*musig2.PartialSignature, error) {
message, err := txscript.CalcTaprootSignatureHash(
txscript.NewTxSigHashes(params.tx.UnsignedTx, params.prevoutFetcher),
txscript.SigHashDefault, params.tx.UnsignedTx, 0, params.prevoutFetcher,
)
if err != nil {
return nil, err
}
return musig2.Sign(
params.secretNonce,
signer,
params.combinedNonce,
params.cosigners,
[32]byte(message),
musig2.WithSortedKeys(),
musig2.WithTaprootSignTweak(batchOutSweepClosure),
musig2.WithFastSign(),
)
}
}
Source: arkd/pkg/ark-lib/tree/musig2.go:882-903
Pattern 9: Combining Partial Signatures
func combineSigs(
batchOutSweepClosure []byte, allSigs map[string]TreePartialSigs,
) func(combineSigsParams) (*schnorr.Signature, error) {
return func(params combineSigsParams) (*schnorr.Signature, error) {
keys, err := txutils.ParseCosignerKeysFromArkPsbt(params.tx, 0)
var combinedNonce *btcec.PublicKey
sigs := make([]*musig2.PartialSignature, 0, len(keys))
for _, key := range keys {
keySigs := allSigs[hex.EncodeToString(schnorr.SerializePubKey(key))]
s := keySigs[params.tx.UnsignedTx.TxID()]
if s.R != nil {
combinedNonce = s.R
}
sigs = append(sigs, s)
}
message, _ := txscript.CalcTaprootSignatureHash(...)
combineOpts := []musig2.CombineOption{
musig2.WithTaprootTweakedCombine(
[32]byte(message), keys, batchOutSweepClosure, true,
),
}
combinedSig := musig2.CombineSigs(combinedNonce, sigs, combineOpts...)
return combinedSig, nil
}
}
Source: arkd/pkg/ark-lib/tree/musig2.go:926-1010
Pattern 10: Validating Tree Signatures
func ValidateTreeSigs(
batchOutSweepClosure []byte, batchOutAmount int64, vtxoTree *TxTree,
) error {
for _, ptx := range treeToIndexedTxs(vtxoTree) {
sig := ptx.Inputs[0].TaprootKeySpendSig
schnorrSig, _ := schnorr.ParseSignature(sig)
cosignerPubkeys, _ := txutils.ParseCosignerKeysFromArkPsbt(ptx, 0)
aggregateKey, _ := AggregateKeys(cosignerPubkeys, batchOutSweepClosure)
message, _ := txscript.CalcTaprootSignatureHash(...)
if !schnorrSig.Verify(message, aggregateKey.FinalKey) {
return fmt.Errorf("invalid signature for txid %s", ptx.UnsignedTx.TxID())
}
}
return nil
}
Source: arkd/pkg/ark-lib/tree/musig2.go:227-292
File References
| Purpose | File | Key Functions/Types |
|---|
| MuSig2 session management | arkd/pkg/ark-lib/tree/musig2.go | SignerSession, CoordinatorSession, TreeNonces, TreePartialSigs |
| Key aggregation | arkd/pkg/ark-lib/tree/musig2.go | AggregateKeys, ValidateTreeSigs |
| Signer session impl | arkd/pkg/ark-lib/tree/musig2.go | treeSignerSession, NewTreeSignerSession |
| Coordinator session impl | arkd/pkg/ark-lib/tree/musig2.go | treeCoordinatorSession, NewTreeCoordinatorSession |
| Client wallet signing | go-sdk/wallet/singlekey/bitcoin_wallet.go | NewVtxoTreeSigner |
| PSBT cosigner utils | arkd/pkg/ark-lib/txutils/psbt.go | ParseCosignerKeysFromArkPsbt, GetArkPsbtFields |
Common Operations
Operation 1: Full Signing Flow (Client Side)
- Receive vtxo tree from ASP
- Create signer session:
tree.NewTreeSignerSession(privKey)
- Initialize:
session.Init(batchOutSweepClosure, batchOutAmount, vtxoTree)
- Generate nonces:
nonces, _ := session.GetNonces()
- Send nonces to ASP
- Receive aggregated nonces from ASP
- Set aggregated nonces:
session.SetAggregatedNonces(aggNonces)
- Sign:
partialSigs, _ := session.Sign()
- Send partial signatures to ASP
Operation 2: Full Signing Flow (ASP/Coordinator Side)
- Build vtxo tree
- Create coordinator:
NewTreeCoordinatorSession(closure, amount, tree)
- Collect nonces from all signers:
coordinator.AddNonce(pubkey, nonces)
- Aggregate nonces:
aggNonces, _ := coordinator.AggregateNonces()
- Send aggregated nonces to all signers
- Collect signatures:
coordinator.AddSignatures(pubkey, sigs)
- Combine into final tree:
signedTree, _ := coordinator.SignTree()
- Validate:
ValidateTreeSigs(closure, amount, signedTree)
Operation 3: Incremental Nonce Aggregation
For streaming/real-time scenarios, use AggregateNonces per-txid:
for txid, pubkeyNonces := range receivedNonces {
complete, err := session.AggregateNonces(txid, pubkeyNonces)
if complete {
}
}
Gotchas & Edge Cases
-
Secret Nonce Security: Secret nonces (SecNonce) must NEVER be reused or shared. Reusing a nonce leaks the private key.
-
Nonce Size: Public nonces are 66 bytes (two 33-byte compressed points). Always validate length.
-
Cosigner Verification: Before signing, verify your pubkey is in the cosigners list for each transaction. The session handles this via getCosignersPublicKeys.
-
Signature Order: When combining signatures, the order must match the cosigner pubkey order. Use musig2.WithSortedKeys() for deterministic ordering.
-
Taproot Tweak: Always apply the taproot tweak (batchOutSweepClosure) when signing and combining. Without it, signatures won't verify.
-
Malicious Signer Detection: AddSignatures returns shouldBan=true if a signer provides invalid signatures. Ban these signers immediately.
-
Parallel Processing: The implementation uses workPoolMap for parallel signature generation. This is important for large trees.
-
Single Key Fallback: AggregateKeys handles the single-key case specially - no MuSig2 aggregation needed, just apply tweak.
-
Derivation Path: When creating tree signers, use the correct derivation path. Wrong path = wrong key = invalid signatures.
-
Session State: Signer sessions are stateful. Don't reuse a session across different trees. Create a new session for each round.
Skill Owner: ark-developer
Repos: arkd, go-sdk