# 0005 — Pooling is an optimisation, never a gate **Status:** Accepted **Resolves:** the publisher-pooling question left open by phase2-findings.md — *"shared roots across publishers would fix the cost but need a party to assemble them, which is an availability and trust question rather than an arithmetic one"* — and the roadmap's requirement that pooling be settled before the staking contracts assume one root per publisher. ## Decision Publishers may anchor through a **pool**: one more level of the existing tree, whose leaves are batch digests and whose root is anchored once for all members (specification §10.10). Four commitments define the arrangement: 1. **A pool leaf is a batch digest, and nothing else.** The batch digest already binds publisher, epoch, leaf count, and root, so attribution is sealed one level below anything the pool operator touches. There is no membership list to forge, no assignment field to edit. 2. **Members witness for themselves.** The pool anchors and serves proofs; each member collects cosignatures over the resulting checkpoint from witnesses *it* chose. The property that the witnessed checkpoint in a receipt is one the reader's or publisher's own witnesses signed — decision 0003, one level up — survives pooling untouched. 3. **A member sealed out anchors directly.** The refusal grammar distinguishes "not sealed yet" (retry) from "sealed without this batch" (fall back), and a member that receives the second anchors its batch itself and publishes a v1 response, with no operator intervention. Exclusion from a pool costs the victim one log write. 4. **A malfunctioning pool is surfaced, not routed around.** A bundle that fails verification — a proof that does not reach the root, a receipt for something else — is a loud fault at the member, not a silent fallback. Falling back would still publish, and it would hide exactly the evidence members need to replace the operator. The pooled response is a **new record version** (`read.response.v2`), never a reinterpretation of v1; a verifier that does not know it refuses it. Implemented in `pool.py`, `service/poold.py`, and the pooled path of `service/publisherd.py`. Pinned by `tests/test_pool.py`, the conformance vectors, and the Rust verifier. ## What prompted this The Phase 2 cost measurement. Batching commitments under one publisher's root reduced anchoring from 49.4 million writes a year to 8,760 — and then the publisher count undid it: writes scale linearly with publishers, and at a thousand independent publishers anchoring hourly the cost returns to the range that made per-fragment anchoring untenable. Either publishers pool into shared roots, or the epoch lengthens, or the cost returns. The epoch length belongs to settlement counterparties, not to this design; that leaves pooling, and pooling introduces a new party — the assembler — into a system whose entire argument is that no party needs trusting. ## The reasoning **Why the leaf is a batch digest.** The alternative was a leaf that names the publisher beside its root — `hash(publisher_id ‖ batch_root)` or a small record. Every variant gives the pool operator a field it writes and a verifier must trust it wrote honestly, and the attack that field invites is exactly the one Phase 2's adversarial work kept finding: an aggregator that attributes one party's work to another. Using the batch digest bare means the attribution was fixed by the publisher's own record before the pool ever saw it. The lie has no field to live in. **Why members witness for themselves.** The tempting design is that the pool collects cosignatures once and shares them — it is, after all, one checkpoint. But then a member's receipt carries signatures from witnesses the *pool* chose, and decision 0003's argument applies with full force: a quorum alone is satisfiable by any parties an operator can produce. Witnessing is cheap (the checkpoint is already there); trust migration is not. **Why the fallback is direct anchoring rather than a protocol obligation to seal.** A pool that must seal everyone is a pool that one unresponsive member can stall, and a sealing deadline re-introduces a clock into a node that otherwise needs none. Letting the operator seal early — and letting the excluded member route around it at the cost of one log write — converts censorship from an availability attack into a pricing question. A pool that habitually seals its members out is a pool whose members leave; nothing about that requires protocol enforcement, because the member was never *dependent* on the pool. **Why a hostile pool is not routed around.** Falling back on a bad bundle would keep the member publishing, which looks like robustness. But the two failure modes deserve opposite treatments: refusal is a legible act the operator can be held to; garbage is either a serious defect or an attack, and either way the member's next action should be taken by its operator with the evidence in hand, not smoothed over by a retry loop. The adversarial suite pins both behaviours. **Why the epoch label makes no claim about member epochs.** The pool record's `epoch` labels the sealing round. A verifier could be made to check it against each batch's own epoch, but the check protects nothing — the batch's epoch is already bound in the digest the publisher signed against — and it would forbid a useful recovery: a straggler's batch joining the next round's sealing. A field that invites cross-checks without protecting anything is a field verifiers will disagree about; the specification says explicitly that no cross-check is made. ## What this does not settle **Who runs a pool, and what it charges.** The pool operator provides a real service (availability at sealing time) with a measurable value (the anchor-write saving). Pricing it is a business question; the design constrains only what the operator *cannot* sell — ordering preference inside a sealing is meaningless because leaves are sorted, and attribution is not the pool's to alter. **Pools of pools.** The construction composes — a pool digest could be a leaf one level up — but no measured need exists, and each level adds a proof to every receipt. Revisit if publisher counts reach the point where pools themselves are the write bottleneck. **Reversal conditions.** If a deployment shows members routinely falling back (the pool failing at its one job), or the two-version read path proving costlier than the writes it saves, collapse back to direct anchoring — v1 never stopped working, which is the point of having made pooling an optimisation.