did-btcr2-js

ADR 070: Beacon Broadcasts Return Structured Artifacts and CAS Publication Precedes the On-Chain Spend

Status: Accepted

Date: 2026-07-07

Branch / PR: feat/cas-first-broadcast

References: ADR 037, ADR 056, ADR 069

Context

Every single-party beacon broadcast (SinglePartyBeacon.broadcastSignal on the Singleton, CAS, and SMT beacons) returned only the SignedBTCR2Update the caller had passed in. Everything else the broadcast produced was discarded:

Separately, the CAS beacon invoked the optional casPublish callback after the transaction broadcast. A CAS publish failure therefore surfaced only after the beacon UTXO was irrevocably spent, leaving an on-chain signal pointing at an announcement that never reached the store. The spec’s data-retention requirement is that update data be available at resolution time; it mandates no publish-versus-broadcast ordering, so the ordering is an implementation-quality decision, and publishing after the spend is the strictly worse of the two orders.

Finally, Updater.announce (the static utility wrapping BeaconFactory.establish plus broadcastSignal) accepted no options parameter, so callers going through it could not supply a fee estimator, change address, or casPublish callback at all.

Decision

  1. broadcastSignal returns a structured BroadcastResult on all three beacons (and on the abstract base signature):

    interface BroadcastResult {
      signedUpdate: SignedBTCR2Update;
      txid: string;
      announcement?: CASAnnouncement; // CAS beacons
      proof?: SMTProof;               // SMT beacons
    }
    

    The Singleton beacon returns { signedUpdate, txid }. The CAS beacon adds the announcement. The SMT beacon serializes the inclusion proof from the tree it just built (BTCR2MerkleTree.proof(did), which embeds the leaf nonce and the update hash) and returns it, fixing the unresolvable-signal defect. The nonce is not returned as a separate field: the serialized proof already carries it in the wire format the resolver consumes.

  2. The CAS beacon publishes before it spends. casPublish(announcement) runs before buildSignAndBroadcast. A publish failure aborts the operation while the beacon UTXO is still unspent. Because the announcement is content-addressed, the ordering is retry-safe in both failure directions: a publish that succeeded before a failed broadcast re-publishes the same bytes to the same address on retry, and an orphaned announcement in the store (published, never anchored) is inert.

  3. Updater.announce gains an options parameter and returns the BroadcastResult. The parameter is typed as CASBroadcastOptions (the widest single-party options shape); non-CAS beacons ignore casPublish.

  4. CasPublishFn documentation is made executor-neutral (it referenced a specific IPFS implementation) and now states the pre-spend invocation contract.

Consequences

Rejected alternatives