Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ jobs:
name: Criterion benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run benchmarks
run: cargo bench --package dpp-benches
- name: Upload Criterion reports
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: criterion-reports
path: target/criterion/
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
name: Format check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
Expand All @@ -26,7 +26,7 @@ jobs:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
Expand All @@ -38,7 +38,7 @@ jobs:
name: Tests (unit + integration)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install cargo-nextest
Expand All @@ -57,7 +57,7 @@ jobs:
name: Security audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install cargo-audit
Expand Down
11 changes: 3 additions & 8 deletions .github/workflows/wasm-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ on:
paths:
- "crates/dpp-registry/**"
- "crates/dpp-domain/**"
- "crates/dpp-evidence/**"
- "plugins/**"

env:
Expand All @@ -17,10 +16,10 @@ env:

jobs:
wasm-build:
name: WASM build — dpp-registry + dpp-domain + dpp-evidence
name: WASM build — dpp-registry + dpp-domain
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Install Rust toolchain with wasm32 target
uses: dtolnay/rust-toolchain@stable
Expand All @@ -39,15 +38,11 @@ jobs:
working-directory: crates/dpp-domain
run: cargo build --target wasm32-unknown-unknown --release

- name: Build dpp-evidence (wasm32)
working-directory: crates/dpp-evidence
run: cargo build --target wasm32-unknown-unknown --release

sector-plugins:
name: WASM build — sector plugins
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Install Rust toolchain with wasm32-wasip1 target
uses: dtolnay/rust-toolchain@stable
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ This file was started retroactively on 2026-07-03 at v0.4.0; entries for

## [Unreleased]

New **`dpp-rules::lint`** module: a non-binding plausibility lint pack —
arithmetic and physical-plausibility checks distinct from the crate's binding
regulatory rules (e.g. `ratedEnergyWh` vs. `nominalVoltageV × nominalCapacityAh`,
material-composition sums, implausible date/range values). First pack covers
battery, textile, and unsold-goods (15 lints total); each finding carries a
`LintSeverity::{Warning,Notice}` and is phrased as a question, never a
verdict. `no_std`, wasm32-safe. `dpp-domain::Passport` gains
`lint_result: Option<LintResult>` plus a `lint_sector_data()` dispatcher and
`LintResult::compute()` adapter mapping `SectorData` onto the pack.

### Removed

- **`dpp-evidence`** removed from the workspace and dissolved into
`dpp-engine` — the evidence dossier format and its verification engine are
a DB-backed engine feature, not a core standard. `AuditEntry` and its hash-chain algorithm
(`chain_hash`/`verify_audit_chain`) return to `dpp-engine`'s `dpp-types`
crate. No other published core crate depended on `dpp-evidence`.

## [0.7.0] - 2026-07-07

New crate **`dpp-evidence`** — the evidence dossier wire format and offline
Expand Down
18 changes: 8 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ members = [
"crates/dpp-plugin-sdk",
"crates/dpp-registry",
"crates/dpp-calc",
"crates/dpp-evidence",
"crates/dpp-tests",
"benches",
]
Expand Down Expand Up @@ -50,14 +49,13 @@ dpp-domain = { path = "crates/dpp-domain", version = "0.7.0" }
dpp-plugin-traits = { path = "crates/dpp-plugin-traits", version = "0.7.0" }
dpp-crypto = { path = "crates/dpp-crypto", version = "0.7.0" }
dpp-digital-link = { path = "crates/dpp-digital-link", version = "0.7.0" }
dpp-evidence = { path = "crates/dpp-evidence", version = "0.7.0" }

# Serialisation
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_with = "3"
# RFC 8785 JSON Canonicalization Scheme (JCS) — the signing/content-binding contract
serde_jcs = "0.1"
serde_jcs = "0.2"
# GZIP — decoding W3C Bitstring Status List `encodedList` (pure, no infra)
flate2 = "1"

Expand All @@ -70,14 +68,14 @@ async-trait = "0.1"
# Identity & cryptography
base64 = "0.22"
hex = "0.4"
sha2 = "0.10"
ed25519-dalek = { version = "2", features = ["rand_core"] }
sha2 = "0.11"
ed25519-dalek = { version = "3", features = ["rand_core"] }
zeroize = { version = "1", features = ["derive"] }
aes-gcm = "0.10"
rand = "0.8"
aes-gcm = "0.11"
rand = "0.10"

# Schema validation
jsonschema = "0.17"
jsonschema = "0.46"
semver = "1"

# Telemetry
Expand All @@ -86,13 +84,13 @@ tracing = "0.1"
# Utilities
uuid = { version = "1", features = ["v7", "serde"] }
anyhow = "1"
thiserror = "1"
thiserror = "2"

# KDF
argon2 = "0.5"

# MAC
hmac = "0.12"
hmac = "0.13"

# Benchmarking
criterion = { version = "0.5", features = ["html_reports"] }
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ dpp-core/
dpp-rules ........... Pure no_std cross-field regulatory rules, shared by dpp-domain and plugins
dpp-registry ........ EU Central Registry interface types (wasm32-safe)
dpp-calc ............ EU-methodology calculators (CO2e, repairability), pure functions
dpp-evidence ........ Evidence dossier wire format (DossierV1) + offline verification engine —
deliberately free of BSL-licensed and wasm-unsafe dependencies (spec/)
dpp-tests ........... Cross-crate integration tests (domain + crypto + gs1)
plugins/ .............. 10 Wasm sector plugins (wasm32-wasip1, excluded from workspace)
```
Expand Down Expand Up @@ -95,9 +93,9 @@ When a product undergoes remanufacturing, repurposing, or preparation for reuse,
- Dual-signature transfer records (JWS from both parties)
- Rejection of invalid transfers (wrong operator, duplicate pending)

### Evidence Dossiers & Offline Verification
### Evidence Dossiers

`dpp-evidence` defines a self-contained, signed **evidence dossier** (`DossierV1`) — passport, both JWS proofs, the issuer's DID document, the hash-chained audit trail, and the transfer chain in one canonical JSON file — plus the verification engine that checks all of it **fully offline**: no resolver, no network, no trust in Odal. An auditor, customs officer, or skeptical buyer runs the independent checks (manifest signature, content integrity, both JWS, audit-chain linkage, transfer signatures) and gets a named verdict per check. The crate is deliberately free of BSL-licensed and wasm-unsafe dependencies. Wire format specification: [`crates/dpp-evidence/spec/dossier-v1.md`](crates/dpp-evidence/spec/dossier-v1.md).
A self-contained, signed **evidence dossier** — passport, both JWS proofs, the issuer's DID document, the hash-chained audit trail, and the transfer chain in one canonical document — and its verification engine (independent checks: manifest signature, content integrity, both JWS, audit-chain linkage, transfer signatures) are a `dpp-engine` feature: dossiers are generated and persisted by the node, and checked via its API or the `odal verify` CLI command. Core contributes the primitives this depends on — `dpp-crypto`'s Ed25519/JWS and the domain types the dossier snapshots.

### Schema Validation

Expand Down Expand Up @@ -190,7 +188,6 @@ cargo run -p dpp-digital-link --example gs1_and_aas # Parse GS1 lin
| [PLUGIN-HOST.md](docs/architecture/PLUGIN-HOST.md) | Wasm plugin sandbox design and ABI contract |
| [DESIGN-PATTERNS.md](docs/architecture/DESIGN-PATTERNS.md) | Hexagonal architecture, open-core boundary patterns |
| [CONFORMITY.md](docs/regulatory/CONFORMITY.md) | Regulatory alignment statement for assessment bodies |
| [dossier-v1.md](crates/dpp-evidence/spec/dossier-v1.md) | Evidence dossier wire-format specification (offline verification) |
| [CONTRIBUTING.md](CONTRIBUTING.md) | Contributor guide: setup, conventions, PR workflow |
| [SECURITY.md](SECURITY.md) | Vulnerability disclosure policy |
| [GOVERNANCE.md](GOVERNANCE.md) | Decision-making structure and maintainer authority |
Expand Down
1 change: 1 addition & 0 deletions benches/src/aas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn battery_passport() -> Passport {
co2e_per_unit: Some(CarbonFootprint::from_kg(73.0)),
repairability_score: None,
compliance_result: None,
lint_result: None,
sector_data: Some(SectorData::Battery(BatteryData {
gtin: Gtin::parse(GTIN).unwrap(),
battery_chemistry: BatteryChemistry::Lfp,
Expand Down
1 change: 1 addition & 0 deletions benches/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ fn valid_battery() -> SectorData {

fn valid_textile() -> SectorData {
SectorData::Textile(TextileData {
gtin: "09506000134352".into(),
fibre_composition: vec![
FibreEntry {
fibre: "cotton".into(),
Expand Down
82 changes: 82 additions & 0 deletions crates/dpp-calc/src/co2e/calculator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
//! country-specific grid factors, allocation rules) refines those factors and is
//! gated on a signed data license (`real-factors` feature).

use chrono::Utc;
use serde::{Deserialize, Serialize};

use super::parameters::Co2eInputs;
Expand Down Expand Up @@ -80,6 +81,12 @@ pub struct Co2eResult {
pub fn calculate(inputs: &Co2eInputs, ruleset: &dyn Co2eRuleset) -> Result<Co2eResult, CalcError> {
validate_inputs(inputs)?;

// A signed, dated receipt must never be computed from a ruleset that is not
// legally in force today (crate-wide invariant; see repairability::calculate).
ruleset
.effective_dates()
.ensure_active_on(ruleset.id(), Utc::now().date_naive())?;

let material_breakdown: Vec<MaterialLineResult> = inputs
.materials
.iter()
Expand All @@ -94,6 +101,16 @@ pub fn calculate(inputs: &Co2eInputs, ruleset: &dyn Co2eRuleset) -> Result<Co2eR
let energy_co2e_kg = inputs.energy_kwh * inputs.grid_factor_kg_co2e_per_kwh;
let total_co2e_kg = material_co2e_kg + energy_co2e_kg;

// Individually-valid inputs can still multiply/sum past f64's range (e.g.
// 1e200 × 1e200 = Infinity). Silent overflow into a legally cited figure is
// worse than silent clamping — reject it.
if !material_co2e_kg.is_finite() || !energy_co2e_kg.is_finite() || !total_co2e_kg.is_finite() {
return Err(CalcError::Overflow(format!(
"CO2e overflowed to a non-finite value \
(material={material_co2e_kg}, energy={energy_co2e_kg}, total={total_co2e_kg})"
)));
}

// Hash outputs before building the result (avoids chicken-and-egg with receipt).
let output_hash = jcs_hash(&(total_co2e_kg, material_co2e_kg, energy_co2e_kg))?;

Expand Down Expand Up @@ -149,6 +166,9 @@ mod tests {
use super::*;
use crate::co2e::parameters::MaterialFootprint;
use crate::co2e::thresholds::CradleToGateRuleset;
use crate::ruleset::{EffectiveDateBound, RegulatoryBasis, Ruleset, RulesetId, RulesetVersion};
use chrono::NaiveDate;
use std::sync::OnceLock;

fn material(mass: f64, factor: f64) -> MaterialFootprint {
MaterialFootprint {
Expand All @@ -157,6 +177,68 @@ mod tests {
}
}

/// A CO₂e ruleset whose effective period starts in 2100 — not yet in force.
struct FutureCo2eRuleset;
static FUT_ID: OnceLock<RulesetId> = OnceLock::new();
static FUT_VER: OnceLock<RulesetVersion> = OnceLock::new();
static FUT_DATES: OnceLock<EffectiveDateBound> = OnceLock::new();
static FUT_BASIS: RegulatoryBasis = RegulatoryBasis {
regulation: "test",
article: "test",
standard: None,
technical_study: None,
source_url: None,
superseded_by: None,
};
impl Ruleset for FutureCo2eRuleset {
fn id(&self) -> &RulesetId {
FUT_ID.get_or_init(|| RulesetId("co2e-future".into()))
}
fn version(&self) -> &RulesetVersion {
FUT_VER.get_or_init(|| RulesetVersion("1.0.0".into()))
}
fn effective_dates(&self) -> &EffectiveDateBound {
FUT_DATES.get_or_init(|| {
EffectiveDateBound::open(NaiveDate::from_ymd_opt(2100, 1, 1).unwrap())
})
}
fn regulatory_basis(&self) -> &RegulatoryBasis {
&FUT_BASIS
}
}
impl Co2eRuleset for FutureCo2eRuleset {
fn declared_stages(&self) -> &[LifecycleStage] {
&[]
}
}

#[test]
fn rejects_ruleset_not_yet_in_force() {
let inputs = Co2eInputs {
materials: vec![material(1.0, 2.0)],
energy_kwh: 1.0,
grid_factor_kg_co2e_per_kwh: 0.4,
};
assert!(matches!(
calculate(&inputs, &FutureCo2eRuleset),
Err(CalcError::RulesetNotYetEffective { .. })
));
}

#[test]
fn rejects_overflow_to_non_finite() {
// Each input is finite and in range, but the product overflows f64.
let inputs = Co2eInputs {
materials: vec![material(1e200, 1e200)],
energy_kwh: 0.0,
grid_factor_kg_co2e_per_kwh: 0.0,
};
assert!(matches!(
calculate(&inputs, &CradleToGateRuleset),
Err(CalcError::Overflow(_))
));
}

#[test]
fn rejects_negative_energy() {
let inputs = Co2eInputs {
Expand Down
10 changes: 10 additions & 0 deletions crates/dpp-calc/src/kernel/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ pub enum CalcError {
#[error("ruleset '{id}' expired on {until}")]
RulesetExpired { id: String, until: String },

/// The ruleset's effective period has not started yet (e.g. a pending
/// delegated act using the `2100-01-01` sentinel).
#[error("ruleset '{id}' is not yet effective (in force from {from})")]
RulesetNotYetEffective { id: String, from: String },

/// A computation overflowed to a non-finite value despite finite, in-range
/// inputs — a legally cited figure must never silently become Infinity.
#[error("calculation overflow: {0}")]
Overflow(String),

/// The requested activity UUID is not present in the injected factor dataset.
#[error("emission factor not found for activity '{0}'")]
FactorNotFound(String),
Expand Down
26 changes: 26 additions & 0 deletions crates/dpp-calc/src/kernel/ruleset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,32 @@ impl EffectiveDateBound {
pub fn is_active_on(&self, date: NaiveDate) -> bool {
date >= self.from && self.until.is_none_or(|u| date <= u)
}

/// Error if `date` falls outside the effective period, distinguishing a
/// ruleset that is **not yet effective** (before `from`) from one that has
/// **expired** (after `until`) — so a pending `2100` stub is never reported
/// as "expired".
pub fn ensure_active_on(
&self,
id: &RulesetId,
date: NaiveDate,
) -> Result<(), crate::error::CalcError> {
if date < self.from {
return Err(crate::error::CalcError::RulesetNotYetEffective {
id: id.0.clone(),
from: self.from.to_string(),
});
}
if let Some(until) = self.until
&& date > until
{
return Err(crate::error::CalcError::RulesetExpired {
id: id.0.clone(),
until: until.to_string(),
});
}
Ok(())
}
}

/// Structured legal citation for a regulatory ruleset.
Expand Down
Loading