eoc: allocate a facility among tenants, then bind the published band to it #63

Merged
dcharlot merged 3 commits from eoc/multi-tenant-allocation into main 2026-08-29 03:11:42 -04:00

2026-08-27

David Charlot
eoc: pin rand off 0.10.2, which does not compile
All checks were successful
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 58s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 1m6s
WAI conformance / cargo-class corpora · windows (pull_request) Successful in 1m15s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 23s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 28s
WAI conformance / cross-arch byte-equality · windows (pull_request) Successful in 22s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 14s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 2m17s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 24s
`rand 0.10.2` fails to build: its own `src/rng.rs:168` contains `use
rand::RngExt;` — a crate referring to itself by name rather than `crate::`. That
is an upstream defect in a published release, not a configuration problem, and
it reaches this workspace through `eoc-route-learned`, `eoc-spec-decode` and
`rand_distr`. Pinned to 0.10.1, which builds.

The lock is pinned rather than the manifests: the constraint is "not this exact
release", not a new minimum, and a manifest pin would outlive the broken version
and quietly hold the workspace back once upstream fixes it.

Verified pre-existing rather than assumed: 0.10.2 is in the lock at
`origin/main` and this branch's other commits touch zero `rand` lines.

The `eoc-tools` reqwest `query` feature — the other half of what kept
`cargo check --workspace` from passing — landed on main independently while this
branch was in flight, with the same fix and the same reasoning. That commit is
taken as-is here rather than duplicated.

With both in place the entire eoc-rs workspace checks clean, which is what makes
the allocation and rating work in the preceding commits verifiable in place
rather than only crate-locally.
2026-08-27 13:32:56 -04:00
David Charlot
eoc: bind a published facility band to the allocation it came from
A band published on its own is unfalsifiable: a letter, a facility name, and
nothing a reader can check. `FacilityRating` carries the commitment to the share
set the rating aggregates, so the tenant proof from `Allocation::proof_for`
verifies against **the same root the public rating cites**.

That join is the point. A tenant can confirm its own consumption sits inside the
very set that produced the published label, without seeing any co-tenant — which
is the only thing that makes a facility label mean anything to the customers it
was derived from. `admits()` checks the proof against the receipt's root *and*
that the totals agree, so a proof that is internally valid against a different
period is rejected rather than quietly accepted.

**Assurance is derived, not stored.** The obvious design is a `verified: bool`
or an assurance level written into the artifact, and that would repeat the
failure the band itself had: a claim about evidence, asserted by the party under
audit. So there is no assurance field. `assurance()` takes a `SignatureStatus`
the caller must supply, and an attestation that nobody checked yields
`AttestedUnverified` — never `Verified`. An unchecked signature is a string; it
becomes evidence when someone checks it, and the type makes skipping that step
impossible to do silently. A failed check reports `AttestationInvalid`, which is
strictly worse than no attestation: someone tried to pass off a bad signature.

This crate does no signature verification at all, and says so. There is no
signing dependency in this workspace, and manufacturing a `Verified` state it
cannot substantiate is exactly what the rest of the module refuses to do.

`RatingRecord` is data, not a dependency on whichever crate produced the band: a
receipt records *what* was rated, not *how*. It carries the scale id, metric,
unit and measured value, so a reader re-derives the letter against the cited
scale instead of trusting it — and a band whose scale is unstated is not
comparable to any other band.

The signed preimage covers every field that makes the rating a claim (band,
value, scale, basis, residual, commitment), with length-prefixed fields so
shifting a character across a boundary cannot produce the same bytes. Attaching
an attestation does not change what it signs.

Verified: 36 eoc-carbon tests pass (7 new), no clippy warnings in either new
file. One test assertion was corrected rather than the code: with an odd leaf
count a promoted node has no sibling at that level, so the aggregate count
varies between tenants — the guarantee is the bound, not a fixed number.
2026-08-27 13:32:23 -04:00
David Charlot
eoc: divide a measured facility total among tenants without losing any of it
`eoc-carbon` accounted one request bottom-up — `it_joules × PUE × intensity` —
and had no notion of a facility, a tenant, a share, or a residual. `grep -rniE
"tenant|allocat|conserv|residual"` across the crate returned zero.

That shape is right for a single tenant and wrong for a facility, because
nothing forces the tenants' figures to add up to what the building actually
drew. Both failure modes are silent and opposite: idle capacity, cooling
headroom, the control plane and failed work draw real power that belongs to no
request and simply vanishes from the sum; while two tenants can each be charged
a PUE multiplier over the same shared overhead and nothing notices. An
allocation here is anchored top-down on a measured total and the arithmetic is
required to close.

**Integer microjoules, on purpose.** `CarbonAccount` is `f64` throughout, which
is fine for a reported figure and unusable for an invariant: a conservation law
that holds to within floating-point error is not a conservation law. Allocation
uses `u64` µJ, matching `eoc_core::JouleCost`, so `Σ shares + residual == total`
holds exactly and is checked rather than hoped for. Sums are taken in `u128` so
an overflowing share set is reported as over-allocation instead of wrapping into
a total that appears to conserve.

**The residual is required, not optional.** An allocation that assigns 100% of a
facility to its tenants asserts that every joule the building drew was
attributable to a paying workload, which is essentially never true. Making the
remainder a mandatory field forces an operator to state it. `proportional()`
sends its integer-division loss there too, rather than spreading it by a
rounding rule — the residual is the honest home for energy no tenant
demonstrably caused. Rejects rather than clamps throughout: an over-allocation
means the model is wrong, and trimming it silently would hide the only signal
that says so.

**The basis is a disclosed choice.** The same facility split by measured energy,
by compute-seconds or by reserved capacity yields three different numbers for
one tenant, all defensible and none comparable, so an allocation that does not
say which was used cannot be checked against another.

**A tenant can check its own figure without being shown its co-tenants.** Shares
are committed to a Merkle **sum** tree — every node carries a hash and its
subtree total. A tenant gets its own leaf plus the sibling `(hash, sum)` pairs
along its path, recomputes the root, and confirms the root's sum plus the
residual equals the signed facility total. Both halves are checked together on
purpose: a share can be genuinely included in a tree that does not add up, so
inclusion alone proves nothing.

What that leaks is stated rather than glossed: the tenant learns the aggregate
of each sibling subtree on its path — about `log2(n)` partial sums — and no
individual co-tenant's share or identity. `aggregates_revealed()` reports the
count so both sides know. Hiding the aggregates as well needs a zero-knowledge
range proof, which this module does not attempt and does not claim.

Scoped as multi-tenant plumbing, not "verifiable emissions reporting": nothing
here signs anything or speaks to carbon.

Verified: 30 eoc-carbon tests pass (9 new, covering exact conservation, the
overflow path, rounding-to-residual, odd leaf counts 1..9, tampered shares,
misstated totals and restated residuals), no clippy warnings in the new file.
The eoc-rs workspace has a pre-existing build failure in `eoc-tools` (reqwest
`query` behind `default-features = false`) that predates this change and is
untouched by it.
2026-08-27 13:32:23 -04:00