Where the threshold lives¶
A bitcoin spend can require more than one party in two ways. They are separate code paths, in separate packages: script multisig is btclib’s, MuSig2 and FROST are this package’s, and the psbt transport of all of them is btclib-wallet’s. The numbers k, t and n do not tell the two ways 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 address |
same address |
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
btclib’s btclib.script.engine.script enforces. A multi_a() leaf is
bounded by the stack instead, one element per key, which is what
_MAX_PUBKEYS_PER_MULTI_A in bip32-descriptors’
bip32_descriptors.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, and so does ecc.ecc.frost,
whose module docstring gives the reason.
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 refuse a secret nonce they
have already consumed; their docstrings say how.
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 costs the same: changing the set changes the aggregate key, so the output key and the address change too. 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.
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 ecc.ecc.frost takes key material already split
rather than splitting any;
btclib’s ISS 2200
tracks key generation.
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. What works is 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.
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. That module’s docstring says what
becomes of them when a psbt BIP assigns FROST type bytes.
Where each of them lives¶
The two routes are both implemented, and they do not touch.
The script threshold is btclib’s: the op codes OP_CHECKMULTISIG and
OP_CHECKSIGADD under btclib.script. bip32-descriptors’
bip32_descriptors.descriptors.descriptors holds the descriptor grammar
that writes one: multi() and sortedmulti() (BIP383), multi_a() and
sortedmulti_a() (BIP386, BIP387).
The cryptographic threshold is this package’s:
ecc.ecc.musig2 (BIP327) and ecc.ecc.frost (BIP445).
btclib-wallet’s btclib_wallet.psbt.musig2 (BIP373) and
btclib_wallet.psbt.frost carry 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 ecc.ecc.ssa.Sig, the BIP340 signature
ecc.ecc.ssa verifies and so does every other BIP340 verifier.
A multi_a() leaf is checked against BIP340 signatures too, btclib’s
tapscript engine calling ecc.ecc.ssa for them.
OP_CHECKMULTISIG is the one that differs: it verifies ECDSA, through
ecc.ecc.dsa.