Status: Accepted
Date: 2026-07-07
Branch / PR: feat/cas-first-broadcast
References: ADR 037, ADR 056, ADR 069
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:
buildSignAndBroadcast returns the txid of the signal transaction; every beacon dropped it. Callers could not record which on-chain transaction anchors their update without re-querying the chain.casPublish callback (the sidecar-only flow the spec explicitly permits) had no way to capture the very object they are required to retain and distribute for resolution.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.
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.
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.
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.
CasPublishFn documentation is made executor-neutral (it referenced a specific IPFS implementation) and now states the pre-spend invocation contract.
sidecar.smtProofs). A regression test broadcasts against a mocked Bitcoin connection and round-trips the returned proof through SMTBeacon.processSignals.Promise<SignedBTCR2Update> to Promise<BroadcastResult> is breaking for callers that used the return value directly; at 0.x this ships as a minor version bump. Callers that ignored the return value are unaffected.casPublish users: the callback now runs earlier, and its failure now prevents the spend instead of following it. This is called out in the changelog as the intended new contract.Updater state machine itself is unchanged: NeedBroadcast fulfillment and UpdaterResult keep their shapes. The caller driving the machine performs the broadcast and holds the BroadcastResult; threading it through provide() into the machine’s result would grow the state machine’s surface for data the caller already has.BroadcastResult field. The serialized proof already embeds the nonce in the format processSignals verifies; a second copy would be redundant state that could drift from the proof.casPublish seam and its own pre-broadcast publish step.