Skip to content

Run the derivation-consent rule: a second-life claim is currently unilateral #314

Description

@LKSNDRTMLKV

dpp-core 0.20.0 ships two lineage rules. The engine now runs one of them (check_life_status_consistency, wired in #313). The other is still uncalled:

dpp_rules::lineage::check_derivation_consent(
    passport_operator_did: &str,
    edges: &[DerivationEdge],
) -> Vec<ConsentFinding>

Why it matters

A derivation edge pins its target with a hash. That proves the target has not been modified; it says nothing about whether the target's operator agreed to the relationship. Anyone can publish a passport claiming to derive from anyone else's product.

For a bill of materials that is tolerable — over-claiming a supplier is a commercial problem, and a component reference is openly a claim by the assembler. For second life it is not: Reg. (EU) 2023/1542 Art. 77(7) moves regulatory responsibility to the operator placing the second-life unit on the market, and responsibility must not be assignable by unilateral assertion.

So today, this node will accept, store, sign and publish a passport asserting that it took over responsibility for someone else's battery, with nothing anywhere checking that the other operator agreed.

Why it is not just wiring

The rule is explicit that the caller correlates and the rule only checks. It needs, per edge, the transfer of responsibility that consents to it — and the engine cannot currently produce that:

  • A derivedFrom edge identifies its target by URI.
  • TransferStore::get_chain is keyed by local PassportId.
  • The predecessor belongs to another operator, on another node.

So correlation is a cross-operator fetch of that node's transfer chain. That opens real questions rather than an afternoon's plumbing:

  1. Resolution path. Is the transfer chain fetchable at all from a passport URI? There is no public route serving it today — GET /dpp/{dppId}/history and the transfer routes are all Bearer-authenticated, and the predecessor's operator has no reason to hold our API key. Exposing a chain publicly is a disclosure decision, not a plumbing one.
  2. When it runs. At create (refuse), at publish (refuse), or as a lint finding (report)? Publish is the moment the claim becomes signed and permanent, which argues for there; but publish currently performs no outbound fetch, and making it depend on another operator's availability is a real change to its failure modes.
  3. Fail-open vs fail-closed. The rule reports an edge whose caller found no transfer as unconsented — "no evidence was found" rather than "none exists". If the fetch times out, those are indistinguishable from here, and treating a network failure as a consent defect would be wrong.
  4. SSRF and caching. Same outbound guard as verify_tree's fetch, and the answer is stable once a transfer completes, so it is cacheable — but a pending transfer is not.

Note on what the evidence actually is

Worth stating because it is easy to get backwards: exactly one signature in a transfer record is a counterparty's own — the outgoing operator's authorisation. The acceptance half is the hosting node's attestation that the acceptance step ran, not a signature by the incoming operator.

That asymmetry is harmless here. The consent this rule needs is the predecessor's, and in a second-life transfer the predecessor's operator is the outgoing party — so the half that is a genuine party signature is exactly the half that matters. TransferEvidence::accepted is still carried and checked, because an abandoned or rejected handover should not support a live claim, but it is not a second party's signature and the design must not lean on it as one.

Suggested first step

Decide (1) and (2) before writing code. If the chain is not publicly resolvable, this rule cannot run cross-operator at all, and the honest interim is a lint finding on the local case only — a derivation edge pointing at a passport this node itself hosts — with the cross-operator case explicitly reported as unverifiable rather than silently passing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions