dpp-core is changing both passport lineage edges. The work here is larger than
a version bump, and one part of it is a silent, untested regression on a
public scan-facing surface that would not be caught by either repo's CI.
Filed ahead of the release so the scope is known before the repin, not during it.
Blocked on odal-node/dpp-core PRs #214–#217 merging and 0.20.0 being cut.
All four are green and MERGEABLE, but nothing here should start until the
version exists — and the release is deliberately not imminent.
What changes in core
| Before |
After |
parent_passport_ref: Option<PassportRef> |
derived_from: Vec<DerivationRef> |
wire key parentPassportRef |
wire key derivedFrom |
component_refs: Vec<PassportRef> |
component_refs: Vec<ComponentRef> |
| — |
key componentRefs unchanged; elements are now objects |
DerivationRef is { reference, operation } where operation is one of the
four Reg. (EU) 2023/1542 Art. 77(7) second-life operations, required.
ComponentRef is { reference, quantity?, role? }.
Core ships a tolerant reader for the downward edge. ComponentRef
deserializes the bare PassportRef element shape as well, mapping it to an edge
with no qualifiers. That exists because componentRefs is in the signed public
view that other operators' nodes fetch — those passports are signed and belong
to someone else, so they can never be rewritten, and refusing them would report a
not-yet-upgraded node's bill of materials as an integrity violation.
That tolerance only reaches consumers that deserialize into ComponentRef.
Anything hand-navigating serde_json::Value gets nothing from it. See the
resolver item below.
Scope: 20 non-test source files
Counted with git grep -l 'parent_passport_ref\|component_refs\|parentPassportRef\|componentRefs' -- '*.rs' on main, excluding tests. Roughly 150
references across .rs, .yaml and .json.
Compiler-driven — the type change finds these for you
crates/dpp-integrator/src/domain/validate/{aluminium,battery,steel,textile,tyre}.rs
crates/dpp-integrator/src/infra/vault_client.rs
crates/dpp-vault/src/domain/service/{mod,create,publish}.rs
crates/dpp-vault/src/public_view.rs
- test fixtures across
dpp-vault, dpp-node, dpp-dal
Mostly parent_passport_ref: None → derived_from: Vec::new() and the
equivalent for component refs.
Needs judgement
crates/dpp-vault/src/domain/verify/tree.rs — verify_and_children parses
each fetched entry with serde_json::from_value::<PassportRef>. Switching that
to ComponentRef is what inherits core's tolerance; leaving it produces
RefUnverifiable::MalformedRef for every entry of a passport published by a node
at a different core version. Given verify/engine.rs's
const TAMPER: [&str; 3] = ["hashMismatch", "cycle", "malformedRef"], that
surfaces as component tree integrity violation — a tamper finding caused by a
version difference. This one is not optional.
crates/dpp-types/src/evidence.rs + crates/dpp-vault/src/domain/service/evidence.rs —
component_graph is Option<serde_json::Value> attested via content_hash, so
the type does not break but the hashed content shape changes. Worth deciding
explicitly what that means for dossiers assembled before and after.
crates/dpp-types/src/passport_request.rs + crates/dpp-vault/src/handlers/create.rs —
parentPassportRef is a create-API field with its own shape validation
(validate_passport_ref(parent, "parentPassportRef")). New field name, new
shape, and operation is required, so this is an API contract change with a
validation rule attached.
crates/dpp-vault/src/api/passport_response.rs — served shape.
crates/dpp-vault/src/domain/verify/reference.rs — second-life verification
path; check whether the plural edge changes anything for it.
crates/dpp-dal/src/pg/repo_passport.rs — good news: it derives its
protected list from core's via ADDED_HERE/REMOVED_HERE, and
REMOVED_HERE = ["componentRefs"] still matches since that key name is
unchanged. derivedFrom is picked up automatically. Only the doc comments
naming parentPassportRef go stale.
OpenAPI
api/components/schemas/{CreateRequest,PassportResponse}.yaml, plus
api/openapi.bundled.{json,yaml}. The contract gate fails until regenerated.
The item worth doing first, and separately
crates/dpp-resolver/src/handlers/resolve_by_gtin.rs builds the public GS1
linkset by hand-navigating raw JSON, and it has no tests.
passport.get("parentPassportRef").and_then(|r| r.get("uri")) // → Predecessor
passport.get("componentRefs") … .filter_map(|c| c.get("uri")) // → hasComponent
After the change, the first is None and the second drops every entry. So the
Predecessor and hasComponent relations simply stop being emitted — a QR scan
can no longer walk to a battery's predecessor or into its bill of materials.
Three things make this the priority:
- Core's tolerant reader does not help here. This code never deserializes
into ComponentRef; it reads entry["uri"] off a Value.
- It fails silently by construction —
and_then → None, filter_map →
drop. No error, no log, no degraded response. The linkset is emitted without
the relations.
- It is untested.
Gs1LinkType::Predecessor and HasComponent appear at
exactly two places in this repository, both in that source file. The file has
three inline tests and neither symbol appears in any of them.
Suggested order: write the characterization tests against main first, before
the core change lands. Tests asserting that a passport with lineage emits both
relations, written against today's behaviour, turn this from a silent regression
into a failing test the moment the shape changes. Writing them afterwards means
relying on someone remembering the relations existed.
Verification caveat for the repin
The committed Cargo.lock has no source or checksum for the dpp-*
packages, so it was generated with .cargo/config.toml's [patch.crates-io]
override active, pointing at a local dpp-core tree. That file's own header
notes CI and release builds intentionally have no such config.
So verifying the repin against the published crates requires switching the
override off first (just core-published) — otherwise the build is against a
working tree and proves nothing about the released version.
Not in scope
dpp-web does not reference these shapes. The CLI's client-side validation
heuristic names only productName and productGroupData, so it is unaffected.
dpp-coreis changing both passport lineage edges. The work here is larger thana version bump, and one part of it is a silent, untested regression on a
public scan-facing surface that would not be caught by either repo's CI.
Filed ahead of the release so the scope is known before the repin, not during it.
Blocked on
odal-node/dpp-corePRs #214–#217 merging and 0.20.0 being cut.All four are green and
MERGEABLE, but nothing here should start until theversion exists — and the release is deliberately not imminent.
What changes in core
parent_passport_ref: Option<PassportRef>derived_from: Vec<DerivationRef>parentPassportRefderivedFromcomponent_refs: Vec<PassportRef>component_refs: Vec<ComponentRef>componentRefsunchanged; elements are now objectsDerivationRefis{ reference, operation }whereoperationis one of thefour Reg. (EU) 2023/1542 Art. 77(7) second-life operations, required.
ComponentRefis{ reference, quantity?, role? }.Core ships a tolerant reader for the downward edge.
ComponentRefdeserializes the bare
PassportRefelement shape as well, mapping it to an edgewith no qualifiers. That exists because
componentRefsis in the signed publicview that other operators' nodes fetch — those passports are signed and belong
to someone else, so they can never be rewritten, and refusing them would report a
not-yet-upgraded node's bill of materials as an integrity violation.
That tolerance only reaches consumers that deserialize into
ComponentRef.Anything hand-navigating
serde_json::Valuegets nothing from it. See theresolver item below.
Scope: 20 non-test source files
Counted with
git grep -l 'parent_passport_ref\|component_refs\|parentPassportRef\|componentRefs' -- '*.rs'onmain, excluding tests. Roughly 150references across
.rs,.yamland.json.Compiler-driven — the type change finds these for you
crates/dpp-integrator/src/domain/validate/{aluminium,battery,steel,textile,tyre}.rscrates/dpp-integrator/src/infra/vault_client.rscrates/dpp-vault/src/domain/service/{mod,create,publish}.rscrates/dpp-vault/src/public_view.rsdpp-vault,dpp-node,dpp-dalMostly
parent_passport_ref: None→derived_from: Vec::new()and theequivalent for component refs.
Needs judgement
crates/dpp-vault/src/domain/verify/tree.rs—verify_and_childrenparseseach fetched entry with
serde_json::from_value::<PassportRef>. Switching thatto
ComponentRefis what inherits core's tolerance; leaving it producesRefUnverifiable::MalformedReffor every entry of a passport published by a nodeat a different core version. Given
verify/engine.rs'sconst TAMPER: [&str; 3] = ["hashMismatch", "cycle", "malformedRef"], thatsurfaces as
component tree integrity violation— a tamper finding caused by aversion difference. This one is not optional.
crates/dpp-types/src/evidence.rs+crates/dpp-vault/src/domain/service/evidence.rs—component_graphisOption<serde_json::Value>attested viacontent_hash, sothe type does not break but the hashed content shape changes. Worth deciding
explicitly what that means for dossiers assembled before and after.
crates/dpp-types/src/passport_request.rs+crates/dpp-vault/src/handlers/create.rs—parentPassportRefis a create-API field with its own shape validation(
validate_passport_ref(parent, "parentPassportRef")). New field name, newshape, and
operationis required, so this is an API contract change with avalidation rule attached.
crates/dpp-vault/src/api/passport_response.rs— served shape.crates/dpp-vault/src/domain/verify/reference.rs— second-life verificationpath; check whether the plural edge changes anything for it.
crates/dpp-dal/src/pg/repo_passport.rs— good news: it derives itsprotected list from core's via
ADDED_HERE/REMOVED_HERE, andREMOVED_HERE = ["componentRefs"]still matches since that key name isunchanged.
derivedFromis picked up automatically. Only the doc commentsnaming
parentPassportRefgo stale.OpenAPI
api/components/schemas/{CreateRequest,PassportResponse}.yaml, plusapi/openapi.bundled.{json,yaml}. The contract gate fails until regenerated.The item worth doing first, and separately
crates/dpp-resolver/src/handlers/resolve_by_gtin.rsbuilds the public GS1linkset by hand-navigating raw JSON, and it has no tests.
After the change, the first is
Noneand the second drops every entry. So thePredecessorandhasComponentrelations simply stop being emitted — a QR scancan no longer walk to a battery's predecessor or into its bill of materials.
Three things make this the priority:
into
ComponentRef; it readsentry["uri"]off aValue.and_then→None,filter_map→drop. No error, no log, no degraded response. The linkset is emitted without
the relations.
Gs1LinkType::PredecessorandHasComponentappear atexactly two places in this repository, both in that source file. The file has
three inline tests and neither symbol appears in any of them.
Suggested order: write the characterization tests against
mainfirst, beforethe core change lands. Tests asserting that a passport with lineage emits both
relations, written against today's behaviour, turn this from a silent regression
into a failing test the moment the shape changes. Writing them afterwards means
relying on someone remembering the relations existed.
Verification caveat for the repin
The committed
Cargo.lockhas nosourceorchecksumfor thedpp-*packages, so it was generated with
.cargo/config.toml's[patch.crates-io]override active, pointing at a local
dpp-coretree. That file's own headernotes CI and release builds intentionally have no such config.
So verifying the repin against the published crates requires switching the
override off first (
just core-published) — otherwise the build is against aworking tree and proves nothing about the released version.
Not in scope
dpp-webdoes not reference these shapes. The CLI's client-side validationheuristic names only
productNameandproductGroupData, so it is unaffected.