`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.
32 lines
1.1 KiB
TOML
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"] }
|