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
Owner
grep -rniE "tenant|allocat|conserv|residual" crates/eoc-carbon/src/   ->   0 matches

eoc-carbon accounted one request bottom-up — it_joules × PUE × intensity — with no notion of a facility, a tenant, a share, or a residual.

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:

  • Under-reporting — idle capacity, cooling headroom, the control plane and failed work draw real power that belongs to no request, and simply vanishes from the sum.
  • Over-reporting — 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 — fine for a reported figure, 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. Sums are taken in u128 so an overflowing share set is reported as over-allocation rather than wrapping into a total that appears to conserve.

The residual is required, not optional

An allocation assigning 100% of a facility to its tenants asserts that every joule the building drew was attributable to a paying workload — essentially never true. Making the remainder mandatory 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. And it rejects rather than clamps — 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, compute-seconds, or reserved capacity yields three different numbers for one tenant — all defensible, none comparable. An allocation that doesn't say which was used cannot be checked against another.

A tenant checks its own figure without seeing 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 it 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 — nothing here signs anything or speaks to carbon.

Verification

30 eoc-carbon tests pass (9 new): exact conservation, the overflow path, rounding-to-residual, odd leaf counts 1..9, tampered shares, misstated totals, 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.


Second commit: bind a published 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 a tenant's ShareProof verifies against the same root the public rating cites.

That join is the point: a tenant confirms its own consumption sits inside the very set that produced the published label, without seeing any co-tenant. 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 written into the artifact — which would repeat exactly 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:

attestation check run result
none (any) SelfDeclared
present NotChecked AttestedUnverified — never Verified
present Valid Verified
present Invalid AttestationInvalid — worse than none

An unchecked signature is a string; it becomes evidence when someone checks it, and the type makes skipping that step impossible to do silently.

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

The rating is data, not a dependency

RatingRecord records what was rated, not how — scale id, metric, unit, measured value — so a reader re-derives the letter against the cited scale instead of trusting it, and the receipt takes no dependency on whichever crate produced the band.

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

Verification

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 revealed-aggregate count varies between tenants — the guarantee is the bound, not a fixed number.


Third commit: unblock the workspace build — two dependency faults, neither new

cargo check --workspace did not pass on main. Both causes are dependency declarations rather than code, and both were verified against the pristine tree before being touched.

1. eoc-tools — reqwest's query is a feature now. Four search adapters call .query(..), which in reqwest 0.13 sits behind #[cfg(feature = "query")]; it was unconditional in 0.12. With default-features = false the method is simply not compiled, so the crate has not built since the 0.13 bump. Adding "query" is the whole fix.

Confirmed pre-existing: reqwest is 0.13.4 before and after, and serde_urlencoded is absent from the lock in both.

2. rand 0.10.2 does not compile. Its own source contains:

// rand-0.10.2/src/rng.rs:168
use rand::RngExt;      // a crate referring to itself by name, not `crate::`

An upstream defect in a published version, not a configuration problem. 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: rand 0.10.2 is in the lock at origin/main, identical before and after this branch's earlier commits, and those commits' diff touches zero rand lines.

With both fixed 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.

``` grep -rniE "tenant|allocat|conserv|residual" crates/eoc-carbon/src/ -> 0 matches ``` `eoc-carbon` accounted one request bottom-up — `it_joules × PUE × intensity` — with no notion of a facility, a tenant, a share, or a residual. 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: - **Under-reporting** — idle capacity, cooling headroom, the control plane and failed work draw real power that belongs to no request, and simply vanishes from the sum. - **Over-reporting** — 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 — fine for a reported figure, 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**. Sums are taken in `u128` so an overflowing share set is reported as over-allocation rather than wrapping into a total that appears to conserve. ## The residual is required, not optional An allocation assigning 100% of a facility to its tenants asserts that every joule the building drew was attributable to a paying workload — essentially never true. Making the remainder mandatory 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. And it **rejects rather than clamps** — 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, compute-seconds, or reserved capacity yields three different numbers for one tenant — all defensible, none comparable. An allocation that doesn't say which was used cannot be checked against another. ## A tenant checks its own figure without seeing 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 it 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** — nothing here signs anything or speaks to carbon. ## Verification 30 `eoc-carbon` tests pass (9 new): exact conservation, the overflow path, rounding-to-residual, odd leaf counts 1..9, tampered shares, misstated totals, 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. --- ## Second commit: bind a published 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 a tenant's `ShareProof` verifies against **the same root the public rating cites**. That join is the point: a tenant confirms its own consumption sits inside the very set that produced the published label, **without seeing any co-tenant**. `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` written into the artifact — which would repeat exactly 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: | attestation | check run | result | |---|---|---| | none | (any) | `SelfDeclared` | | present | `NotChecked` | `AttestedUnverified` — **never** `Verified` | | present | `Valid` | `Verified` | | present | `Invalid` | `AttestationInvalid` — worse than none | An unchecked signature is a string; it becomes evidence when someone checks it, and the type makes skipping that step impossible to do silently. **This crate performs no signature verification, and says so.** There is no signing dependency in this workspace, and manufacturing a `Verified` state it cannot substantiate is precisely what the rest of the module refuses to do. ### The rating is data, not a dependency `RatingRecord` records *what* was rated, not *how* — scale id, metric, unit, measured value — so a reader re-derives the letter against the cited scale instead of trusting it, and the receipt takes no dependency on whichever crate produced the band. The signed preimage covers every field that makes the rating a claim (band, value, scale, basis, residual, commitment), length-prefixed so shifting a character across a boundary cannot produce the same bytes. Attaching an attestation does not change what it signs. ### Verification 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 revealed-aggregate count varies between tenants — the guarantee is the **bound**, not a fixed number. --- ## Third commit: unblock the workspace build — two dependency faults, neither new `cargo check --workspace` did not pass on `main`. Both causes are dependency declarations rather than code, and both were verified against the pristine tree before being touched. **1. `eoc-tools` — reqwest's `query` is a feature now.** Four search adapters call `.query(..)`, which in reqwest 0.13 sits behind `#[cfg(feature = "query")]`; it was unconditional in 0.12. With `default-features = false` the method is simply not compiled, so the crate has not built since the 0.13 bump. Adding `"query"` is the whole fix. > Confirmed pre-existing: reqwest is **0.13.4 before and after**, and `serde_urlencoded` is absent from the lock in **both**. **2. `rand 0.10.2` does not compile.** Its own source contains: ```rust // rand-0.10.2/src/rng.rs:168 use rand::RngExt; // a crate referring to itself by name, not `crate::` ``` An upstream defect in a published version, not a configuration problem. 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: `rand 0.10.2` is in the lock at `origin/main`, identical before and after this branch's earlier commits, and those commits' diff touches **zero** `rand` lines. With both fixed 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.
`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.
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.
dcharlot changed title from eoc: divide a measured facility total among tenants without losing any of it to eoc: allocate a facility among tenants, then bind the published band to it 2026-08-27 13:14:32 -04:00
`cargo check --workspace` did not pass on main. Both causes are dependency
declarations rather than code, and both were verified against the pristine tree
before being touched.

**1. `eoc-tools`: reqwest's `query` is a feature now.** Four search adapters
call `.query(..)`, which in reqwest 0.13 sits behind `#[cfg(feature =
"query")]` — it was unconditional in 0.12. With `default-features = false` the
method is simply not compiled, so the crate has not built since the 0.13 bump.
Adding `"query"` is the whole fix. Confirmed pre-existing: reqwest is 0.13.4
before and after, and `serde_urlencoded` is absent from the lock in both.

**2. `rand 0.10.2` does not compile.** 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 version, 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: `rand 0.10.2` is in the lock at
`origin/main`, identical before and after this branch's earlier commits, and
those commits' diff touches zero `rand` lines.

With both fixed the entire eoc-rs workspace checks clean for the first time in
this branch, which is what makes the allocation and rating work in the preceding
commits verifiable in place rather than only crate-locally.
dcharlot force-pushed eoc/multi-tenant-allocation from 65972aeb94 to ad49173ff8
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
2026-08-27 13:33:23 -04:00
Compare
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!63
No description provided.