open-standards/eoc/eoc-rs/crates/eoc-carbon/Cargo.toml
David Charlot bd5c960f13 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

32 lines
1.1 KiB
TOML

[package]
name = "eoc-carbon"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
description = "EOC carbon accounting: grid intensity ingest, PUE, region-aware scheduling, joules → gCO2e + GSF SCI bridge."
[features]
default = []
# Pulls in reqwest for live Electricity Maps / WattTime / CO2 Signal HTTP.
http = ["dep:reqwest"]
# Pulls in eoc-eval to expose SCI hooks for the eval harness.
eval-bridge = ["dep:eoc-eval"]
[dependencies]
eoc-core.workspace = true
blake3.workspace = true
eoc-meter.workspace = true
serde = { workspace = true }
serde_json = "1"
thiserror.workspace = true
async-trait.workspace = true
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
reqwest = { version = "0.13", default-features = false, features = ["rustls", "json"], optional = true }
eoc-eval = { path = "../eoc-eval", optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "test-util", "time"] }