Battery Passport Architecture¶
Why not CIP-68 per battery¶
CIP-68 is a datum format and naming convention, not a scalable architecture. Each CIP-68 reference NFT requires its own UTxO with a min-ADA deposit (~1.5-2 ADA) locked for the battery's lifetime.
| Metric | CIP-68 per battery | Problem |
|---|---|---|
| EU batteries/year | ~4-5M | 4-5M new UTxOs per year |
| Locked ADA | ~1.5-2 ADA × 4-5M = 6-10M ADA/year | Capital locked indefinitely (batteries live 8-15 years) |
| Accumulated after 10 years | 60-100M ADA locked in deposits | ~$15-25M at current prices, growing every year |
| UTxO set bloat | Millions of reference NFTs | Node memory and chain sync burden |
This is not viable. It treats the blockchain as a database, minting one row per battery.
The model: one Merkle Patricia Trie per responsible operator¶
The Battery Regulation Art. 77(4) assigns responsibility to the economic operator who places the battery on the EU market — the manufacturer or importer. This operator is the single writer for the passport throughout its first life.
This maps directly to a Merkle Patricia Trie (MPT) per operator:
graph TD
subgraph "BMW's MPT"
R1[Root hash] --> N1[Branch]
N1 --> L1["Battery #DE-BMW-2027-00001<br/>SoH: 98%, cycles: 47"]
N1 --> L2["Battery #DE-BMW-2027-00002<br/>SoH: 95%, cycles: 230"]
N1 --> N2[Branch]
N2 --> L3["Battery #DE-BMW-2027-00003<br/>SoH: 88%, cycles: 1200"]
N2 --> L4["...millions more"]
end
subgraph "CATL's MPT"
R2[Root hash] --> N3[Branch]
N3 --> L5["Battery #CN-CATL-2027-00001"]
N3 --> L6["Battery #CN-CATL-2027-00002"]
end
subgraph "Cardano L1"
U1["UTxO: BMW operator<br/>datum = BMW MPT root hash"]
U2["UTxO: CATL operator<br/>datum = CATL MPT root hash"]
end
R1 -.->|anchored in| U1
R2 -.->|anchored in| U2
On-chain footprint¶
| Resource | Per-battery CIP-68 | MPT per operator |
|---|---|---|
| UTxOs on chain | 1 per battery (millions) | 1 per operator (hundreds) |
| Locked ADA | ~1.5-2 ADA per battery | ~1.5-2 ADA per operator |
| Update cost | ~0.2 ADA per battery update | ~0.2 ADA per root update (any number of batteries) |
| Proof of specific battery | Read UTxO directly | Merkle proof: leaf → root |
A single UTxO per operator, containing the MPT root hash. Updating one battery's SoH means recomputing the path from that leaf to root and submitting the new root on-chain. Cost: one transaction, regardless of whether the operator manages 100 or 10 million batteries.
Why MPT, not a plain Merkle tree¶
A plain Merkle tree is append-only — you can prove a leaf exists, but updating a leaf means rebuilding the tree. An MPT (also called Merkle Patricia Forestry in the Cardano ecosystem) supports:
| Operation | Plain Merkle Tree | Merkle Patricia Trie |
|---|---|---|
| Insert new battery | Rebuild tree | Insert at key, update path to root |
| Update battery SoH | Rebuild tree | Update leaf, recompute path to root |
| Delete (recycling) | Not supported | Remove leaf, recompute path |
| Prove battery exists | Merkle proof (log n) | Merkle proof (log n) |
| Prove battery does NOT exist | Not possible | Non-membership proof |
| Key-based lookup | Not supported | Lookup by battery ID (key) |
The non-membership proof is important: an authority can ask "does this battery ID exist in BMW's trie?" and get a cryptographic proof that it doesn't — without revealing any other batteries. This is useful for market surveillance and anti-counterfeiting.
How it maps to the regulation¶
Responsibility = trie ownership¶
| Regulatory concept | MPT mapping |
|---|---|
| Economic operator places battery on market | Creates an MPT, anchors root on-chain |
| Legal responsibility for passport accuracy (Art. 77(4)) | Operator holds the signing key for the UTxO containing their root |
| Battery manufacturing (passport creation) | Insert new leaf into MPT |
| SoH update (daily) | Update leaf data, recompute root, anchor on-chain |
| Service / maintenance | Update leaf with maintenance event |
| Delegation to service provider | Service provider submits leaf update, operator recomputes root |
Repurposing = new trie, new operator¶
When a battery is repurposed (Art. 77(6)(a)), a new economic operator takes responsibility:
sequenceDiagram
participant BMW as BMW (original operator)
participant RE as Repurposing Co (new operator)
participant C as Cardano L1
BMW->>BMW: Update leaf status → "end of first life"
BMW->>C: Anchor updated root (BMW's MPT)
RE->>RE: Create new leaf in own MPT
Note over RE: New leaf contains:<br/>- New battery ID<br/>- Link to original (BMW MPT root + proof path)<br/>- Initial SoH at repurposing
RE->>C: Anchor updated root (Repurposing Co's MPT)
Note over C: Two UTxOs, two operators<br/>BMW's root: original history preserved<br/>RE's root: second-life passport
The original battery's history is preserved in BMW's trie (immutable — the old root hashes remain on-chain in the transaction history). The new passport in the repurposer's trie links back to the original via a reference to BMW's root hash and the Merkle proof path.
Recycling = leaf removal¶
When a battery is recycled (Art. 77(6)(b)), the recycler (if they have a role authorization) updates the leaf status to Recycled. The passport "ceases to exist" in regulatory terms, but the on-chain history of root hashes preserves the full audit trail.
Verification flow¶
A consumer scans a battery's QR code. The resolver needs to prove the battery's passport data is authentic:
sequenceDiagram
participant U as Consumer
participant R as Resolver API
participant C as Cardano L1
participant S as Off-chain Storage
U->>R: Scan QR → battery ID
R->>C: Find operator's UTxO (by operator ID or policy)
C-->>R: Current MPT root hash
R->>S: Fetch battery leaf data + Merkle proof path
S-->>R: Leaf data + proof
R->>R: Verify: hash(leaf) → intermediate nodes → root
R->>R: Compare computed root with on-chain root
alt Root matches
R-->>U: Passport data (verified, untampered)
else Root mismatch
R-->>U: WARNING: data has been altered
end
The consumer doesn't need a Cardano wallet or any blockchain knowledge. The resolver does the verification and presents the result.
Operator identity¶
Each operator's UTxO is controlled by their signing key. The link between the on-chain UTxO and the real-world legal entity is established via:
- did:prism: The operator's DID Document references their Cardano public key
- EU DPP Registry: The operator registers their DPP endpoint (which resolves through the Cardano adapter)
- GS1 GLN: The operator's Global Location Number links to their MPT via the resolver
The Aiken validator on the UTxO enforces that only the operator's key (or delegated keys) can update the root.
Updates via MPFS¶
The MPFS infrastructure already solves the trie update problem. The architecture splits into an off-chain service and on-chain validators:
graph TD
subgraph "Operator's infrastructure"
A[BMS readings / service events] --> B[MPFS off-chain service]
B -->|insert / update leaves| C[Off-chain MPT store]
C -->|new root hash| D[Transaction builder]
end
subgraph "Cardano L1"
D -->|submit tx with new root + proof| E[MPFS on-chain validator]
E -->|verifies proof against old root| F[Updated UTxO with new root]
end
subgraph "Verification"
G[Consumer / Authority] -->|fetch leaf + proof| C
G -->|fetch current root| F
G -->|verify proof| H{Root matches?}
end
- Off-chain (
cardano-foundation/mpfs): Haskell HTTP service managing the trie. Handles inserts, updates, deletes, proof generation, transaction building. The operator runs this as part of their passport backend. - On-chain (
cardano-foundation/cardano-mpfs-onchain): Aiken validators that verify MPT transition proofs — given the old root, a proof, and the new root, the validator confirms the transition is valid. This is what locks the operator's UTxO. - Cage (
cardano-foundation/cardano-mpfs-cage): Language-agnostic specification of the validator logic with cross-language test vectors, ensuring off-chain and on-chain implementations agree.
Update flow¶
- Operator receives BMS readings / service events throughout the day
- MPFS off-chain service updates affected leaves in the trie
- At the chosen cadence (hourly, daily, per business cycle): the service computes the new root and builds a Cardano transaction
- The on-chain validator verifies the MPT transition proof and accepts the new root
- One transaction per batch, regardless of how many batteries were updated
At ~0.2 ADA per transaction, daily root updates for an operator with millions of batteries costs ~73 ADA/year (~$18).
Concurrent updates¶
MPFS handles multiple leaf modifications in a single batch natively — the off-chain service applies all mutations to the trie and produces one new root with one transition proof. No contention at the on-chain level because there is exactly one UTxO per operator.
Signed readings and user incentives¶
The challenge-response protocol for signed BMS readings integrates with MPFS in three phases. The key design: rewards are released at incorporation time, when the operator batches leaf updates into a new MPT root. The operator's transaction simultaneously updates the trie and pays out all pending rewards — one atomic operation.
sequenceDiagram
participant U as User (battery holder)
participant C as Cardano L1 (MPFS)
participant B as BMS (NFC)
participant O as Operator (MPT owner)
Note over U,C: Phase 1 — Commitment + Reading
U->>C: Mint commitment in MPFS (challenge token)
Note over C: Commitment UTxO created at slot N
U->>B: NFC tap with challenge = commitment_tx_hash
B-->>U: COSE_Sign1 { state_data, commitment_tx_hash, signature }
U->>C: Submit signed reading, consume commitment UTxO
Note over C: Reading UTxO sits on-chain, pending incorporation
Note over O,C: Phase 2 — Batch incorporation + reward
O->>O: Collect pending reading UTxOs for this batch
O->>O: Update affected leaves in off-chain MPT
O->>C: Single MPFS transaction:
Note over C: 1. Consume pending reading UTxOs
Note over C: 2. Anchor new MPT root (transition proof)
Note over C: 3. Release rewards to users in this batch
C-->>U: Reward released
Why incorporation and reward are atomic¶
The operator's batch update transaction does three things at once:
- Consumes pending reading UTxOs — the signed readings submitted by users
- Updates the MPT root — incorporating those readings into the trie leaves
- Releases rewards — paying each user whose reading was incorporated
This is a single transaction. If the operator doesn't incorporate, the readings sit on-chain unconsumed and the rewards stay locked. The operator is incentivized to incorporate because:
- Art. 77(4) legally requires them to keep the passport up-to-date
- Unconsumed readings are visible on-chain — market surveillance can see the operator is ignoring data
- The readings are signed by BMS hardware — the operator can't claim they're invalid
- Users provide data the operator needs but can't easily obtain (especially for non-connected batteries)
Acceptance vs reward — two levels of validation¶
The validator distinguishes between acceptance (is this reading valid?) and reward (should the user get paid?). All valid readings are incorporated. The reward is the only thing gated by the frequency cap.
Hard gate: certified timestamp (acceptance)¶
The BMS signs over the commitment transaction hash. This binds the reading to a specific on-chain moment. A reading without a valid certified timestamp is rejected entirely — not incorporated, not rewarded, discarded.
| Check | Gate level | Failure = |
|---|---|---|
| BMS signature valid (COSE_Sign1 over commitment_tx_hash) | Hard | Reading rejected |
| Commitment UTxO exists and is recent | Hard | Reading rejected |
| Plausibility (SoH ≤ previous, cycles ≥ previous) | Hard | Reading rejected |
Reader authorization (readerPkh matches submitter) |
Soft | Reading incorporated, no reward |
| Frequency cap (cooldown since last rewarded reading) | Soft | Reading incorporated, no reward |
The certified timestamp is the foundation — without it, nothing happens. With it, the reading is always useful data for the operator.
Soft gate: reward eligibility¶
For each pending reading in this batch:
-- Hard gates (reject if any fail)
1. Verify COSE_Sign1 signature against leaf.bmsPublicKey → authentic?
2. Verify commitment_tx_hash matches consumed commitment UTxO → timestamped?
3. Check: commitment slot + maxAge ≥ current slot → fresh?
4. Check: reading.soH ≤ leaf.lastSoH → plausible?
If any hard gate fails → discard reading, do not incorporate
-- Always incorporate
5. Update leaf: lastSoH, lastCycleCount, lastReadingSlot
-- Soft gates (reward only if all pass)
6. Check: reading.submitterPkh == leaf.readerPkh → authorized?
7. Check: current slot ≥ leaf.lastRewardedSlot + minInterval → cooldown passed?
If all soft gates pass:
- Release reward from pool to submitter
- Update leaf: lastRewardedSlot = current slot
Else:
- No reward (reading is still incorporated)
This means:
- Anyone can submit a valid signed reading — only the authorized reader gets rewarded
- Readings are always incorporated if they pass the hard gates — the operator needs the data regardless of who submitted it
- The reward is a bonus, not a prerequisite — the passport stays up-to-date even without rewarded submissions
- The certified timestamp is non-negotiable — it's what makes the reading trustworthy
Leaf value structure (updated)¶
BatteryLeaf {
batteryId : ByteString
status : Status -- Virgin | Active | Repurposed | Recycled
readerPkh : Maybe PubKeyHash -- authorized reader (reward-eligible)
bmsPublicKey : ByteString -- registered at manufacturing
lastSoH : Integer -- last known State of Health
lastCycleCount : Integer -- last known cycle count
lastReadingSlot : Integer -- slot of last accepted reading (any)
lastRewardedSlot : Integer -- slot of last rewarded reading
... -- other passport fields
}
Note the split: lastReadingSlot tracks the most recent incorporated reading (for data purposes). lastRewardedSlot tracks the most recent rewarded reading (for cooldown purposes). They diverge when readings are submitted more frequently than the reward interval.
Reward funding¶
The operator pre-funds a reward pool locked at the MPFS contract address. Rewards are released from this pool during the batch update transaction. The operator controls:
- Reward amount per reading (fixed or variable by battery category)
- Minimum interval between rewarded readings (e.g. 30 days = ~129,600 slots)
- Batch cadence (how often they run the incorporation transaction)
Reading rights as MPT leaf state¶
Ownership of the reading right — who is allowed to submit signed readings and claim rewards — is a field in the MPT leaf value (readerPkh), not a separate token. MPFS supports state transitions on leaf values, so transferring the reading right is just another transition on the battery's key.
Battery lifecycle through MPT transitions¶
stateDiagram-v2
[*] --> Virgin: Manufacturer inserts leaf (readerPkh = Nothing)
Virgin --> Active: First user scans NFC → readerPkh set to user's key
Active --> Active: User submits signed reading → SoH/cycles updated
Active --> Active: Reading right transferred → readerPkh changes
Active --> Repurposed: Operator marks end of first life
Repurposed --> [*]: New operator creates leaf in their own MPT
Active --> Recycled: Battery end of life
Recycled --> [*]: Passport ceases (leaf remains as historical record)
| Transition | Who initiates | What changes in the leaf |
|---|---|---|
| Manufacturing | Operator | Leaf inserted: status=Virgin, readerPkh=Nothing |
| First scan | User (NFC tap) | readerPkh set to user's public key hash |
| Reading submission | Current reader | lastSoH, lastCycleCount, lastReadingSlot updated |
| Transfer reading right | Current reader | readerPkh changes to new user's key |
| Repurposing | Operator | status → Repurposed; new operator creates new leaf in their MPT |
| Recycling | Operator | status → Recycled; readerPkh → Nothing |
Why no separate token¶
The reading right lives in the MPT leaf because:
- No locked ADA: an MPT field costs nothing to maintain (unlike a CIP-68 UTxO with min-ADA deposit)
- Atomic with passport data: the reading right and the battery state are in the same leaf, updated in the same transition — no risk of them getting out of sync
- Transfer is a state transition: MPFS already handles leaf value transitions; transferring the reading right is just changing one field
- Verification is a Merkle proof: to prove you hold the reading right, provide a proof from your leaf to the operator's MPT root
Transfer on resale¶
Rare for batteries (you typically don't resell an EV battery separately), but the mechanism exists for the general case. The current reader submits a transition that sets readerPkh to the new user's key. The MPFS on-chain validator verifies that the current reader signed the transaction.
Open design questions¶
- Proof size at scale: An MPT proof for a battery in a trie of 10M leaves needs benchmarking against MPFS's actual proof format. The theoretical size (~20 hash nodes, ~640 bytes) is well within limits but should be validated empirically.