did-btcr2-js

ADR 042: Fault-Tolerant Aggregate Beacon Output (Hybrid Taproot)

Status: Accepted (supersedes ADR 041)

Date: 2026-06-23

Branch / PR: feat/aggregation-non-inclusion

References: ADR 008, ADR 017, ADR 037, ADR 038, ADR 039, ADR 040, ADR 041

Context

An aggregate beacon is a cohort of n participants who jointly control one Bitcoin Taproot address (the beacon address) and collaboratively sign a single transaction that spends a UTXO there and writes an OP_RETURN committing to a batch of their DID updates (a CAS Announcement Map, or a Sparse Merkle Tree root). Today the cohort signs with n-of-n MuSig2 (BIP-327) over a key-path-only Taproot output: p2tr(aggPubkey, undefined, network) with the key-path tweak taggedHash("TapTweak", aggPubkey) (cohort.ts:127, cohort.ts:131).

That output has one fatal property. A key-path-only Taproot UTXO can be spent in exactly one way: a single Schnorr signature under the tweaked aggregate key, which mathematically requires all n MuSig2 partial signatures. One missing or defecting signer makes that signature unobtainable, and with no script-path escape the funded UTXO is then permanently locked. Liveness and fund-safety both hinge on 100% signer availability for the life of every funded UTXO, which is untenable for a cohort whose members come and go.

The premise that forced the prior approach was wrong. ADR 041 treated n-of-n MuSig2 as mandated by the specification and therefore deferred the liveness fix as structurally impossible (you cannot skip a signer in n-of-n without changing the aggregate key and the funded address). But the did:btcr2 specification only RECOMMENDS the MuSig2 example: “A full protocol definition is out of scope for this specification, but a RECOMMENDED example is provided for illustration” (Aggregate Beacons). The signing and recovery scheme is ours to choose. ADR 041 is superseded; its data-layer non-inclusion design (a member with no update is absent from the CAS map, or carries a non-inclusion leaf in the SMT) survives and is carried forward under this decision.

A research spike compared four directions. The findings that drive this decision:

Option Liveness (announcement completes despite missing signers) Funds recoverable Common-case on-chain cost Privacy Custody change vs ADR 038 In current stack Implementation depth
Today: n-of-n MuSig2, key-path only none, one missing signer freezes it no, permanent lock best (~57 vB, flat in n) best - shipped -
A: MuSig2 + timelock recovery leaf none, still n-of-n to announce yes, after the timelock best best tweak change only yes shallow
B: tapscript k-of-n (OP_CHECKSIGADD) + recovery k-of-n yes worst, roughly 6 to 10 times the witness on every spend and it reveals membership worst none, independent keys yes (helper flagged experimental) moderate
C: FROST k-of-n key-path + recovery k-of-n yes best (~57 vB, like MuSig2) best major, a DKG plus secret shares and re-sharing to change membership partial, in @noble/curves v2 but unaudited and Bitcoin BIPs are draft deep, plus self-implemented crypto risk
D: hybrid (MuSig2 key-path + k-of-n script leaf + timelock recovery) k-of-n via fallback yes best in the common case; fallback cost paid only when used best in the common case none, independent keys, plus the tweak change yes moderate

Decisive facts from the spike: the hybrid is buildable today with @scure/btc-signer@1.8.1 (it already exports p2tr(internalKey, tree), p2tr_ms(k, pubkeys) for the k-of-n OP_CHECKSIGADD leaf, the CHECKSEQUENCEVERIFY/CHECKLOCKTIMEVERIFY opcodes, control-block encoding, and the script-path sighash and finalizer) with no new crypto dependency. A relative-timelock (CSV, BIP-68/112) recovery leaf is the canonical pattern behind Lightning, Ark, and Bitcoin vaults, it composes with either signing scheme, and it costs nothing in privacy or fees unless it is actually used. FROST is reachable (@noble/curves v2 ships a Taproot-compatible schnorr_FROST with DKG, and the keypair package already depends on v2) but the module is explicitly unaudited, the Bitcoin FROST BIPs (445 signing, ChillDKG) are drafts, and the tree carries two @noble/curves majors (@scure/btc-signer bundles v1.9.7).

Decision

Adopt the hybrid Taproot beacon output (option D) with three spend paths, delivered in two increments, with operator-funded recovery designed so participant-funded can be added later without restructuring.

  1. Beacon output becomes an internal key plus a script tree, replacing the key-path-only output. The internal key P is the n-of-n MuSig2 aggregate of the cohort’s independent BIP-340 keys (the optimistic cooperative key-path). The script tree carries a k-of-n fallback leaf and a timelock recovery leaf. The output key becomes Q = P + taggedHash("TapTweak", P || merkleRoot) * G, so the MuSig2 session tweak must now include the Merkle root. This is the one correctness-critical change to the existing signing path (cohort.ts:127/131, the session tweak in signing-session.ts): a wrong tweak silently produces an invalid key-path signature.

  2. Three spend paths, tried as an optimistic cascade:
    • Key-path MuSig2 (all n cooperate): the cheapest, most private path, roughly 57 vB and indistinguishable on-chain from a single-signer spend, flat regardless of n. This is the common case and is unchanged on the wire from today.
    • Script-path k-of-n fallback (leaf A, OP_CHECKSIGADD over the same independent keys): when the optimistic round stalls at any step, any k present members each contribute a standalone BIP-340 signature and the announcement still finalizes. Tolerates up to n-k absent or defecting members. The larger witness cost is paid only on this path.
    • Script-path timelock recovery (leaf B, relative CSV timelock): when even k cannot be reached, the funder recovers the UTXO after the delay. Funds are never permanently locked.
  3. Operator-funded for now, with an extensible funding model. The operator funds the beacon UTXO and holds the single CSV recovery key (leaf B is <delay> CHECKSEQUENCEVERIFY DROP <operatorKey> CHECKSIG). The <delay> is constrained at validation time to a BIP-68 block-based relative timelock in the range 1 to 0xffff: this keeps the nSequence disable bit (bit 31) and the type bit (bit 22) clear, so a misconfigured or hostile delay cannot silently disable the timelock (a value with bit 31 set turns CHECKSEQUENCEVERIFY into a no-op, which would let the recovery key spend with no wait). The bound is enforced in the recovery policy, in the cohort-condition validator, and in the advert wire guard, because a participant funds against the advert and never separately re-validates the conditions. This fits the advertise-only economics of ADR 039, where the operator fronts the on-chain cost and may charge enrollment or per-announcement fees. To keep the future open: add a fundingModel field to the cohort advert (operator-funded now, participant-funded reserved) and build the recovery leaf behind a small recovery-policy seam, so a participant-funded model (per-participant CSV refund leaves over separate per-participant UTXOs) can be added later as a new policy implementation rather than a re-architecture. Only operator-funded is implemented now.

  4. Fail-fast at any step. The cascade detects non-progress at any point (the nonce round, the partial-signature round, or earlier) and transitions optimistic to fallback to recovery rather than hanging, generalizing the per-cohort failure isolation already present in the multi-cohort runner (ADR 040). The cascade must avoid signing two paths for one UTXO (a wasteful or hazardous double-spend attempt); a single point-of-no-return per round governs the switch.

  5. Custody is unchanged, with a bonus. Independent participant keys throughout: the MuSig2 internal key and the k-of-n leaf both use each member’s own BIP-340 key. No FROST, no DKG, no secret shares, so ADR 038’s bounded-secret model and the liveness-only-coordinator invariant hold unchanged. Bonus: a fallback per-signer signature is a one-shot digest -> signature, so unlike MuSig2 it can be driven by an opaque or HSM-backed KeyManager (the case ADR 038 found mathematically impossible for MuSig2). The fallback path is custody-friendlier than the optimistic one.

  6. Phased delivery:
    • Increment 1: the internal-key plus CSV recovery leaf (leaf B), the tweak fix, the recovery spend builder, and fail-fast. This establishes the script-tree output and immediately removes permanent fund-locking. The announcement still needs all n on the optimistic path at this stage, but funds are always recoverable.
    • Increment 2: add the k-of-n p2tr_ms fallback leaf (leaf A) and the path-selection cascade, delivering graceful liveness. De-risk the library’s experimental p2tr_ms first with a regtest round-trip and our own test vectors before relying on it for value.
  7. FROST is held in reserve. FROST is the only option that gives k-of-n at MuSig2’s on-chain cost (a single Schnorr key-path signature), so it is the upgrade path if the tapscript fallback’s witness bloat proves painful in practice. It is not adopted now: it requires a DKG and secret-share custody (a major departure from the independent-key model of ADR 038), the available implementation is unaudited, the Bitcoin BIPs are drafts, and the @noble/curves v1.9.7-versus-v2.0.1 split in the dependency tree must be resolved first. Revisit gated on an audit and the BIPs settling.

  8. Non-inclusion becomes a pure data-commitment concern. Because the hybrid handles liveness directly, “a member with no update this round” is no longer entangled with signing: such a member is simply absent from the CAS Announcement Map, or carries a non-inclusion leaf (SHA-256(SHA-256(nonce)), see ADR 036 and ADR 041) in the SMT. That data-layer work (the SUBMIT_NONINCLUDED message, the response gate, the slotted SMT tree) carries forward from the superseded ADR 041, proceeds underneath this output, and no longer needs any “defer because n-of-n” rationale.

Rejected alternatives

Consequences

Positive

Negative

Accepted

References