Where the threshold lives

A bitcoin spend can require more than one party in two ways, and btclib and btclib_wallet implement both between them. They are separate code paths, and the numbers k, t and n do not tell them apart. What tells them apart is where the threshold is enforced: in consensus, by every node, or in cryptography, settled off-chain before the transaction exists.

The discriminant

In script multisig the threshold is script policy, enforced by consensus. The script holds n public keys, the input supplies k signatures, and every node runs OP_CHECKMULTISIG — OP_CHECKSIGADD in tapscript — and counts. Those k signatures are ordinary independent signatures, each under its own key: the signers never talk to one another, each signs alone, in any order, months apart.

In MuSig2 and in FROST the threshold is cryptography, off-chain. Consensus knows nothing of it: it sees one key and one 64-byte BIP340 signature, exactly what a single-key spend presents. The group structure is gone before the transaction exists.

Where MuSig2 and FROST separate

They are two mechanisms, not two settings of one.

MuSig2 aggregates independent keys. Each signer holds its own secret key and never shares it; the n public keys combine into one aggregate key. All n have to take part, because there is nothing to interpolate: the keys are not points of a polynomial, and a missing partial signature is a sum that does not close.

FROST shares one key. There is a single threshold secret key, Shamir-split into n shares, and nobody holds it whole. Any t of the shares reconstruct, by Lagrange interpolation, not the key but the signature.

So MuSig2 with a t smaller than n is not a configuration. It is a different algorithm.

What changes in practice

script multisig

MuSig2

FROST

threshold enforced by

consensus

cryptography

cryptography

on chain

n keys, k sigs

one key, one sig

one key, one sig

signer interaction

none

two rounds

two rounds

secret keys

n independent

n independent

one, in n shares

who signed

in the input

not derivable

not derivable

membership change

new address

new aggregate key

same public key

bound on n

consensus

serialization

security

Cost and privacy

A 3-of-5 script spend reveals five public keys and three signatures, and tells anyone watching that the output was a 3-of-5. MuSig2 and FROST produce a spend indistinguishable from a single-key one, of constant size, whatever n is.

No consensus ceiling

OP_CHECKMULTISIG stops at MAX_PUBKEYS_PER_MULTISIG keys, which src/btclib/script/engine/script.py enforces. A multi_a() leaf is bounded by the stack instead, one element per key, which is what _MAX_PUBKEYS_PER_MULTI_A in btclib_wallet.descriptors.miniscript states; and a tapscript spend charges its sigops budget 50 for every non-empty signature, whether or not that signature verifies.

MuSig2 and FROST have no ceiling the chain imposes, the chain seeing one key. What bounds them is the specification, and the two bounds are not the same kind of thing. BIP327’s is serialization: the number of individual public keys is an unsigned 32-bit count. BIP445’s is security: it refuses an n past 128, btclib_ecc.ecc.frost refuses it too, and the module’s own docstring carries the reason — the unforgeability proof assumes the compromised set is fixed before key generation, and an adaptive adversary’s advantage rests on a search problem BIP445 cites as provably hard for n ≤ 131.

The price is state and simultaneity

Script multisig is stateless and has no shared nonce: a signer that is compromised or offline does not touch the others. MuSig2 and FROST need a nonce round before the partial signatures — so, signers available at the same time — and above all a secret nonce that is never reused. Reusing one does not degrade security, it hands out the key. musig2.sign and frost.sign zero the two secret scalars of the bytearray they are handed the moment they read them, before either is used for anything, so a second call on the same buffer reads two zeros and raises rather than signing.

Attribution

A script multisig input says which k of the n signed: it is an audit trail. A MuSig2 or a FROST signature does not — privacy for some uses, a gap for others, custody and internal delegation among them.

Membership change

In script multisig, adding or removing a signer means a new script, a new address, and moving the funds. MuSig2 sits in between: changing the set changes the aggregate key. In FROST the group public key is a property of the secret and not of how it is shared, so the same secret can be shared again over a different set and the funds stay where they are. Producing those shares is key generation, which is out of scope here as it is in BIP445.

Who has seen the secret

Script multisig has no phase in which anybody holds everything. FROST has one wherever a trusted dealer distributes the shares: that dealer transiently knows the whole key. Avoiding it takes a distributed key generation, and btclib_ecc.ecc.frost takes key material already split rather than splitting any. ISS 2200 tracks key generation: a dealer split first, then ChillDKG.

t = n FROST is not MuSig2

BIP445’s own vectors include a 3-of-3 group. That group is still not MuSig2: one shared key against n independent keys aggregated, with everything that follows from it about who has seen the secret and about what a departing member costs.

MuSig2 with t < n

Not natively, for the reason above. The two real routes are composition rather than configuration.

A taproot tree with one leaf per admitted subset. Each leaf carries a MuSig2 key over that subset alone, or a multi_a(). The cost is combinatorial, C(n, t) leaves: three for a 2-of-3, 126 for a 5-of-9. Only the leaf used is revealed, and the key path can stay the n-of-n “everybody agrees”, which is the cheap case. Workable for a small n, senseless past it.

Nesting with FROST. A MuSig2 aggregate key can be one participant of a FROST group, and a FROST group key can be one key inside a MuSig2 aggregation. That is how “either the board’s 2-of-3 or the chief executive” is expressed with one on-chain key.

A FROST spend’s psbt transport is btclib-wallet’s own

BIP373 assigns MuSig2 psbt field types of its own — participant public keys, a public nonce and a partial signature on an input, participant public keys on an output — which btclib_wallet.psbt.psbt_in and btclib_wallet.psbt.psbt_out carry and btclib_wallet.psbt.musig2 gives meaning to. They are MuSig2-specific and do not extend to a threshold: a participants field would have to map a group key to its t, its n, the identifiers and the public shares, rather than an aggregate key to its participants; nonces and partial signatures would be indexed by identifier rather than by a participant’s public key; and a session needs a field naming the subset that is signing it, for which BIP373 has no counterpart, MuSig2’s subset always being everybody. The taproot tweaks are the same and carry over.

BIP445 specifies no psbt transport of its own, and no type bytes are assigned for FROST: choosing bytes in the space BIP174 reserves for future assignment is how two implementations collide. So btclib_wallet.psbt.frost writes the session into the key space BIP174 reserves for proprietary use instead, under a btclib identifier. No other wallet reads those records, and when a psbt BIP does assign FROST type bytes they are dropped rather than aliased – that module’s docstring is where both are stated, and it is the whole of what a reader of a psbt carrying them is owed.

Where each of them lives

The two routes are both implemented, and they do not touch.

The script threshold is the op codes OP_CHECKMULTISIG and OP_CHECKSIGADD under src/btclib/script/, with the descriptor grammar that writes one in btclib_wallet.descriptors.descriptors — multi() and sortedmulti() (BIP383), multi_a() and sortedmulti_a() (BIP386, BIP387).

The cryptographic threshold is btclib_ecc.ecc.musig2 (BIP327) and btclib_ecc.ecc.frost (BIP445), in the btclib-ecc distribution that btclib depends on, with btclib_wallet.psbt.musig2 (BIP373) and btclib_wallet.psbt.frost carrying a session of each through a psbt – the first in the fields BIP373 assigns, the second in proprietary records of btclib’s own. MuSig2 also has a spelling in the descriptor grammar, BIP390’s musig() key expression, inside a tr() or a rawtr(); FROST has none.

What they produce is not one object for all three. musig2.partial_sig_agg and frost.partial_sig_agg each answer an btclib_ecc.ecc.ssa.Sig, the BIP340 signature btclib_ecc.ecc.ssa verifies and so does every other BIP340 verifier; a multi_a() leaf is checked against BIP340 signatures too, the tapscript engine calling btclib_ecc.ecc.ssa for them. OP_CHECKMULTISIG is the one that differs: it verifies ECDSA, through btclib_ecc.ecc.dsa.