skills/ark-musig2-signing/SKILL.md
MuSig2 distributed signing protocol for Ark transaction trees - nonce generation, aggregation, partial signatures
npx skillsauth add arklabshq/arkadian ark-musig2-signingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when:
MuSig2 is a multi-signature scheme for Schnorr signatures. In Ark:
Round 1 - Nonce Exchange:
Round 2 - Signing:
In Ark, entire transaction trees are signed at once:
TreeNonces: map of txid → public nonceTreePartialSigs: map of txid → partial signaturetype Musig2Nonce struct {
PubNonce [66]byte // Two compressed points (33 bytes each)
}
// TreeNonces maps txid to public nonce for each tree transaction
type TreeNonces map[string]*Musig2Nonce
// TreePartialSigs maps txid to partial signature
type TreePartialSigs map[string]*musig2.PartialSignature
Source: arkd/pkg/ark-lib/tree/musig2.go:26-98
type SignerSession interface {
// Initialize with batch output info and vtxo tree
Init(batchOutSweepClosure []byte, batchOutAmount int64, vtxoTree *TxTree) error
// Get this signer's public key
GetPublicKey() string
// Generate and return public nonces for all tree transactions
GetNonces() (TreeNonces, error)
// Set the aggregated nonces from coordinator
SetAggregatedNonces(TreeNonces)
// Alternative: aggregate nonces incrementally per-txid
AggregateNonces(txid string, pubkeyNonces map[string]*Musig2Nonce) (hasAllNonces bool, err error)
// Generate partial signatures for all transactions
Sign() (TreePartialSigs, error)
}
Source: arkd/pkg/ark-lib/tree/musig2.go:167-175
type CoordinatorSession interface {
// Add a signer's public nonces
AddNonce(*btcec.PublicKey, TreeNonces)
// Add and validate a signer's partial signatures
// Returns shouldBan=true if signature is invalid (malicious signer)
AddSignatures(*btcec.PublicKey, TreePartialSigs) (shouldBan bool, err error)
// Aggregate all collected nonces
AggregateNonces() (TreeNonces, error)
// Get all public nonces by pubkey
GetPublicNonces() map[string]TreeNonces
// Combine all partial signatures into final tree
SignTree() (*TxTree, error)
}
Source: arkd/pkg/ark-lib/tree/musig2.go:177-184
// In client code (go-sdk):
func (w *bitcoinWallet) NewVtxoTreeSigner(
ctx context.Context, derivationPath string,
) (tree.SignerSession, error) {
// Derive signing key from wallet
derivedPrivKey, _ := btcec.PrivKeyFromBytes(currentKey.Key)
return tree.NewTreeSignerSession(derivedPrivKey), nil
}
// Initialize the session with tree data
session := tree.NewTreeSignerSession(privateKey)
err := session.Init(batchOutSweepClosure, batchOutAmount, vtxoTree)
// Generate nonces
nonces, err := session.GetNonces()
// After receiving aggregated nonces from coordinator
session.SetAggregatedNonces(aggregatedNonces)
// Sign
partialSigs, err := session.Sign()
Source: go-sdk/wallet/singlekey/bitcoin_wallet.go:385-430
coordinator, err := tree.NewTreeCoordinatorSession(
batchOutSweepClosure, batchOutAmount, vtxoTree,
)
// Collect nonces from all signers
for pubkey, nonces := range signerNonces {
coordinator.AddNonce(pubkey, nonces)
}
// Aggregate nonces
aggregatedNonces, err := coordinator.AggregateNonces()
// Collect signatures from all signers
for pubkey, sigs := range signerSigs {
shouldBan, err := coordinator.AddSignatures(pubkey, sigs)
if shouldBan {
// Malicious signer detected
}
}
// Combine into final signed tree
signedTree, err := coordinator.SignTree()
Source: arkd/pkg/ark-lib/tree/musig2.go:484-615
// AggregateKeys combines multiple pubkeys into one aggregate key
// The tweak is applied for Taproot key-path spending
func AggregateKeys(pubkeys []*btcec.PublicKey, tweak []byte) (*musig2.AggregateKey, error) {
if len(pubkeys) == 0 {
return nil, errors.New("no pubkeys")
}
// Single key: just apply tweak
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
}
// Multiple keys: use MuSig2 aggregation
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
func generateNonces(signerPubKey *btcec.PublicKey) func(*psbt.Packet) (*musig2.Nonces, error) {
serializedSignerPubKey := schnorr.SerializePubKey(signerPubKey)
return func(ptx *psbt.Packet) (*musig2.Nonces, error) {
// Check if this signer needs to sign this transaction
mustGenerateNonce, _, err := getCosignersPublicKeys(serializedSignerPubKey, ptx)
if err != nil {
return nil, err
}
if !mustGenerateNonce {
return nil, nil // Skip - not a cosigner for this tx
}
// Generate MuSig2 nonces
nonce, err := musig2.GenNonces(
musig2.WithPublicKey(signerPubKey),
)
return nonce, err
}
}
Source: arkd/pkg/ark-lib/tree/musig2.go:846-871
func sign(
signer *btcec.PrivateKey, batchOutSweepClosure []byte,
) func(musigParams) (*musig2.PartialSignature, error) {
return func(params musigParams) (*musig2.PartialSignature, error) {
// Calculate sighash
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
}
// Create partial signature with Taproot tweak
return musig2.Sign(
params.secretNonce, // Secret nonce from round 1
signer, // Signing private key
params.combinedNonce, // Aggregated public nonce
params.cosigners, // All cosigner public keys
[32]byte(message), // Message to sign
musig2.WithSortedKeys(), // Deterministic key ordering
musig2.WithTaprootSignTweak(batchOutSweepClosure), // Taproot tweak
musig2.WithFastSign(), // Performance optimization
)
}
}
Source: arkd/pkg/ark-lib/tree/musig2.go:882-903
func combineSigs(
batchOutSweepClosure []byte, allSigs map[string]TreePartialSigs,
) func(combineSigsParams) (*schnorr.Signature, error) {
return func(params combineSigsParams) (*schnorr.Signature, error) {
// Get cosigner keys for this transaction
keys, err := txutils.ParseCosignerKeysFromArkPsbt(params.tx, 0)
// Collect partial signatures from all signers
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)
}
// Calculate message for verification
message, _ := txscript.CalcTaprootSignatureHash(...)
// Combine all partial signatures
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
func ValidateTreeSigs(
batchOutSweepClosure []byte, batchOutAmount int64, vtxoTree *TxTree,
) error {
// For each transaction in the tree
for _, ptx := range treeToIndexedTxs(vtxoTree) {
sig := ptx.Inputs[0].TaprootKeySpendSig
schnorrSig, _ := schnorr.ParseSignature(sig)
// Get aggregated key from cosigners
cosignerPubkeys, _ := txutils.ParseCosignerKeysFromArkPsbt(ptx, 0)
aggregateKey, _ := AggregateKeys(cosignerPubkeys, batchOutSweepClosure)
// Calculate message
message, _ := txscript.CalcTaprootSignatureHash(...)
// Verify signature
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
| 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 |
tree.NewTreeSignerSession(privKey)session.Init(batchOutSweepClosure, batchOutAmount, vtxoTree)nonces, _ := session.GetNonces()session.SetAggregatedNonces(aggNonces)partialSigs, _ := session.Sign()NewTreeCoordinatorSession(closure, amount, tree)coordinator.AddNonce(pubkey, nonces)aggNonces, _ := coordinator.AggregateNonces()coordinator.AddSignatures(pubkey, sigs)signedTree, _ := coordinator.SignTree()ValidateTreeSigs(closure, amount, signedTree)For streaming/real-time scenarios, use AggregateNonces per-txid:
for txid, pubkeyNonces := range receivedNonces {
complete, err := session.AggregateNonces(txid, pubkeyNonces)
if complete {
// All nonces received, ready to sign
}
}
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
documentation
Update project documentation based on new commits and changes in the repository. Use when: user wants to sync docs after project changes.
testing
Remove a project from the Arkadian documentation registry and delete all associated documentation files. Use when: user wants to deregister a project.
tools
RESTRICTED to ark-project-manager. Generate actionable, dependency-ordered task lists organized by user story.
testing
RESTRICTED to ark-project-manager. Create or update feature specifications from natural language descriptions.