Signed Sensor Readings Protocol¶
The pattern¶
Any physical product with at least one sensor and a secure element can produce cryptographically signed readings that are verifiable on Cardano. The protocol is not specific to any product category — batteries are the first application because the Battery Regulation creates the regulatory demand.
The foundational assumption: the user is the transport layer. The item has no internet connection. The user has physical access to the item and an incentive (reward) to carry signed readings from the item to the chain.
Components¶
| Component | Role | Product-agnostic? |
|---|---|---|
| Sensor(s) | Measures physical state (voltage, temperature, pressure, depth, humidity, vibration...) | Sensor type varies by product |
| Secure element | Holds private key, signs readings (ECDSA/EdDSA) | Yes — same chip for any product |
| NFC interface | Delivers signed reading to user's phone, powered by NFC field | Yes — same chip for any product |
| COSE_Sign1 envelope | Signing format (RFC 9052) | Yes — standard envelope |
| CBOR payload | Structured reading data (RFC 8949) | Schema varies by product |
| Operator MPT | Merkle Patricia Trie holding item registry + reporter rewards | Yes — same MPFS infrastructure |
| Operator reward tokens | Native tokens redeemable with the operator | Yes — same minting pattern |
The only product-specific part is the CBOR payload schema — which fields, which units, which plausibility checks. Everything else is reusable.
Hardware: signing module¶
Two chips on the item's board, connected via I2C:
| Chip | Role | Cost (1M vol) |
|---|---|---|
| NXP NTAG 5 Link | NFC interface, I2C master, energy harvesting | $0.35 |
| NXP EdgeLock SE050 | Secure element, secp256k1 + Ed25519, CC EAL 6+ | ~$1.50* |
| NFC antenna + passives | $0.06 | |
| Total | ~$1.91 |
* Distributor pricing at 3k units is ~$1.71-2.49. Estimate assumes NXP direct volume negotiation at 1M+.
The module is powered entirely by the phone's NFC field. No battery, no internet, no wiring beyond I2C to the item's existing sensor bus.
See NFC Hardware for the detailed bill of materials, energy budget analysis, and alternative chip options.
Data model¶
The operator's MPT contains two types of leaves, distinguished by their key.
Item leaf¶
-- MPT key: hash(itemPubKey)
ItemLeaf {
schemaVersion : Integer
metadata : ByteString -- product-specific (battery model, tyre DOT, etc.)
reporter : Maybe ReporterAssignment
commitment : Maybe Commitment -- set by operator (Tx 1), cleared on reading (Tx 2)
}
Commitment {
validFrom : Integer -- slot: reading accepted from this slot
validUntil : Integer -- slot: reading rejected after this slot
}
ReporterAssignment {
reporterPubKey : ByteString
nextReward : Integer -- reward for the next reading (always > 0)
}
Reporter leaf¶
-- MPT key: hash(reporterPubKey)
ReporterLeaf {
rewardsAccumulated : Integer
}
Lifecycle¶
stateDiagram-v2
[*] --> Virgin: Operator registers item
Virgin --> Assigned: User claims (no reward)
Assigned --> Committed: Tx 1: operator sets commitment
Committed --> Assigned: Tx 2: user submits reading
Assigned --> Committed: Next window, operator sets commitment
Assigned --> Reassigned: Item changes hands
Reassigned --> Committed: New user, operator sets commitment
note right of Virgin
reporter = Nothing
commitment = Nothing
end note
note right of Committed
commitment = Just (validFrom, validUntil)
Single-use: cleared on Tx 2
end note
note right of Assigned
commitment = Nothing
Rewards accumulated
end note
| Step | What happens | Tx? | Who pays? |
|---|---|---|---|
| Registration | Item leaf created: reporter = Nothing, commitment = Nothing |
Yes (MPT insert) | Operator |
| Assignment | reporter = Just { pubKey, reward } |
Yes (MPT update) | Operator |
| Tx 1: Commitment | commitment = Just (validFrom, validUntil) |
Yes (MPT update) | Operator |
| Tx 2: Reading | commitment = Nothing, reporter leaf rewards updated |
Yes (MPT update) | Operator |
| Transfer | reporter changed to new user's key |
Yes (MPT update) | Operator |
Key properties:
- Virgin items have no reporter and no commitment.
- Assignment pays nothing — the user is registering as reporter, not earning.
- Every reading pays —
nextRewardis always > 0. - Rewards belong to the reporter, not the item — old reporter keeps accumulated rewards on transfer.
- Commitment is single-use — created in Tx 1, cleared in Tx 2. Cannot be reused.
- Commitment has a validity window —
validFrom ≤ currentSlot ≤ validUntil. Outside this range it's inert even if still in the leaf. - Rate limiting — the operator decides when to create the next commitment. A burned commitment (never consumed) just sits inert until the operator overwrites it with the next one.
Reward tokens¶
Rewards are operator-issued native tokens, not ADA.
| Property | Design |
|---|---|
| Issuer | The operator (manufacturer/brand) |
| Value | Defined by operator (e.g., "10 tokens = free service", "50 tokens = €5 discount on next purchase") |
| Accumulation | Tracked in ReporterLeaf.rewardsAccumulated — no on-chain token movement to user |
| Redemption | Off-chain, at point of sale — user proves ownership of reporter key, operator checks accumulated balance |
The user never holds on-chain tokens or ADA. Rewards are a counter in the MPT, redeemable by proving ownership of the reporter key.
User identity: no wallet needed¶
The user needs only a key pair — generated in the phone app, never touches the chain as a UTxO.
| What the user has | Where it lives |
|---|---|
| Key pair (public + private) | Phone app |
| Reporter public key registered in item leaf | On-chain (in operator's MPT) |
| Accumulated rewards | On-chain (in operator's MPT, reporter leaf) |
The user has no UTxOs, no ADA, no tokens. The operator pays all transaction fees.
Redemption is off-chain: the user signs a message with their reporter key, the operator verifies it and checks the on-chain rewardsAccumulated balance.
The protocol (cooperative path)¶
Two transactions per reading, both paid by the operator. The user pays nothing.
sequenceDiagram
participant U as User App
participant O as Operator API
participant C as Cardano L1
participant I as Item (NFC)
Note over U,O: Tx 1: Commitment
U->>O: Request reading (itemPubKeyHash, reporterPubKey, signature)
O->>O: Verify reporter matches item leaf
O->>O: Rate limit check
O->>C: MPT update: set commitment = Just (validFrom, validUntil)
C-->>O: Confirmed
O-->>U: {validFrom, validUntil}
Note over U,I: Tap (between validFrom and validUntil)
U->>I: NFC: validFrom, validUntil
I->>I: Read sensors
I->>I: Sign(state ‖ validFrom ‖ validUntil) → COSE_Sign1
I-->>U: Signed reading
Note over U,O: Tx 2: Reading inclusion
U->>O: Submit signed reading
O->>O: Verify COSE signature against itemPubKey
O->>O: Verify (validFrom, validUntil) matches item leaf commitment
O->>C: MPT update: commitment = Nothing, reporter rewards updated
C-->>O: Confirmed
O-->>U: {txHash, rewardsEarned}
What each transaction does¶
Tx 1 — Commitment (operator → chain):
- Consumes operator's MPT UTxO (old root)
- Produces operator's MPT UTxO (new root) with
commitment = Just (validFrom, validUntil)in the item leaf - Cost: ~0.2 ADA tx fee, 0 locked ADA
Tx 2 — Reading inclusion (operator → chain):
- Consumes operator's MPT UTxO (old root)
- Produces operator's MPT UTxO (new root) with:
- Item leaf:
commitment = Nothing - Reporter leaf:
rewardsAccumulated += nextReward(insert if first reading)
- Item leaf:
- Cost: ~0.2-0.3 ADA tx fee, 0 locked ADA
What the commitment prevents¶
The commitment binds the reading to a specific time window. Without it, a user could:
- Tap the item today (good state), hold the signed reading, submit it months later (item degraded)
- The signed reading would still be valid because the item key signed it
With the commitment:
- The item signs
(validFrom, validUntil, state)— binding the reading to a specific window - The validator checks: COSE payload
validFromandvalidUntilmatch the commitment in the leaf - The validator checks:
validFrom ≤ currentSlot ≤ validUntil - Tx 2 clears the commitment — the reading cannot be submitted twice
- A reading from a different window fails because the slots don't match the leaf
Single-use guarantee¶
The commitment is single-use by construction:
- Tx 1 creates it (
Nothing → Just (validFrom, validUntil)) - Tx 2 destroys it (
Just (validFrom, validUntil) → Nothing)
Between Tx 1 and Tx 2, exactly one reading can be submitted. After Tx 2, the commitment is gone. A second tap produces a signed reading that references a commitment no longer in the leaf — the validator rejects it.
Burned commitment¶
If the user requests a commitment (Tx 1) but never submits a reading, the commitment sits in the leaf past validUntil. It's inert — no reading can use it because currentSlot > validUntil. The operator simply sets a new commitment when the next reading is due. No cleanup needed — the old commitment is overwritten.
The operator's only cost for a burned commitment is the wasted Tx 1 fee (~0.2 ADA). No ADA is locked.
Cost per reading¶
| Per reading | Notes | |
|---|---|---|
| Tx fees | ~0.4-0.5 ADA (~$0.10-0.13) | 2 transactions, operator pays both |
| Locked ADA | 0 | No separate UTxOs, MPT UTxO is permanent |
| Reward cost | Operator-defined tokens | Not ADA — loyalty points redeemed at next purchase |
At scale¶
| Items | Reading frequency | Readings/year | Annual tx fees |
|---|---|---|---|
| 1,000 | Monthly | 12,000 | ~6,000 ADA (~$1,500) |
| 10,000 | Monthly | 120,000 | ~60,000 ADA (~$15,000) |
| 100,000 | Monthly | 1,200,000 | ~600,000 ADA (~$150,000) |
| 1,000,000 | Monthly | 12,000,000 | ~6,000,000 ADA (~$1,500,000) |
For a €50 item over a 5-year lifetime with monthly readings: 60 readings × ~$0.12 = ~$7 in tx fees. That's ~14% of the item price. Acceptable for high-value items (batteries, commercial tyres), challenging for low-value items.
Future optimization
CIP-118 (nested transactions, expected H1-H2 2026) would allow batching multiple readings into a single top-level transaction, significantly reducing per-reading fees. L2 solutions (Hydra) could reduce costs further but introduce operational complexity. These are future design options, not current protocol requirements.
On-chain validator¶
The reading validator checks the MPT transition:
ReadingValidator (Aiken):
Redeemer: SubmitReading {
coseSign1 : ByteString -- COSE_Sign1 from item
itemProof : MerkleProof -- item leaf exists
reporterProof : MerkleProof -- reporter leaf exists (or insert proof if first reading)
updatedItemLeaf : ItemLeaf -- new item leaf value
updatedReporter : ReporterLeaf -- new reporter leaf value
mptTransition : MerkleProof -- proves old root → new root with both updates
}
Validation:
-- Commitment
1. Item leaf has commitment = Just (validFrom, validUntil)
2. COSE payload fields validFrom and validUntil match the leaf commitment
3. validFrom ≤ currentSlot ≤ validUntil
-- Item signature (algorithm from COSE protected header)
4. itemProof verifies leaf exists under current MPT root
5. Protected header alg = -47 (ES256K) → verifyEcdsaSecp256k1Signature
Protected header alg = -8 (EdDSA) → verifyEd25519Signature
COSE_Sign1 signature valid against itemPubKey (the MPT key)
-- Reporter authorization
6. Item leaf has reporter = Just assignment
7. Transaction signed by assignment.reporterPubKey
-- Leaf transitions
8. updatedItemLeaf.commitment = Nothing (cleared — single use)
9. updatedItemLeaf.reporter.reporterPubKey unchanged
10. Reporter key = hash(assignment.reporterPubKey)
11. If first reading: reporter leaf is MPT insert (rewardsAccumulated = assignment.nextReward)
12. If subsequent: updatedReporter.rewardsAccumulated = old + assignment.nextReward
-- MPT
13. mptTransition proves old root → new root with both leaf updates
14. Operator output UTxO has new root hash
-- Product-specific
15. CBOR payload conforms to schemaVersion
16. Plausibility checks per product type
Signing format: COSE_Sign1¶
Every signed reading is a COSE_Sign1 structure — the same signing envelope used in the EU Digital COVID Certificate, mobile driving licences (ISO 18013-5), and WebAuthn/FIDO2.
COSE_Sign1 = [
protected : bstr, -- { 1: alg } where alg ∈ {-7 (ES256K), -8 (EdDSA)}
unprotected : {},
payload : bstr, -- CBOR-encoded sensor reading
signature : bstr -- ECDSA-secp256k1 or Ed25519 signature
]
The protocol supports two signature algorithms, both with native Plutus built-in verifiers:
| COSE alg | Curve | Plutus built-in | Notes |
|---|---|---|---|
| -47 (ES256K) | secp256k1 | verifyEcdsaSecp256k1Signature (CIP-49) |
Bitcoin/Ethereum compatible |
| -8 (EdDSA) | Ed25519 | verifyEd25519Signature (native) |
Cardano-native, cheapest on-chain |
The operator chooses the algorithm at deployment time. The on-chain validator reads the protected header to determine which built-in verifier to call. Both are supported by the NXP SE050 secure element.
See NFC Hardware — secure element alternatives for the full analysis of P-256 secure elements (OPTIGA Trust M, ATECC608B) and the ZK wrapper / off-chain verification alternatives. Choosing a P-256-only SE is possible but means either adding a ZK proving infrastructure or renouncing on-chain signature verification.
Payload structure¶
{
1: h'...', -- item_id (item public key hash, ByteString)
2: 142857000, -- valid_from (slot, from commitment)
3: 142900000, -- valid_until (slot, from commitment)
4: { ... }, -- state (sensor readings — schema varies by product)
5: 1 -- schema_version (unsigned int)
}
The item signs over (itemPubKey, validFrom, validUntil, state, schemaVersion). The validator checks fields 2 and 3 match the commitment in the leaf and that currentSlot is within range.
Fields 1-3 and 5 are the same for every product. Field 4 (state) is product-specific:
| Product | Field 3 contents |
|---|---|
| Battery | SoH, SoC, cycle count, voltage, current, temperature, cell voltages |
| Tyre (commercial) | Tread depth, casing condition, retread count |
| Cold chain | Temperature history, humidity |
| Industrial equipment | Vibration signature, operating hours |
CBOR with deterministic encoding (RFC 8949 §4.2) — integer-only values, integer keys, no floats. See Battery Payload Standard for the complete battery-specific schema.
Trust chain¶
graph LR
A[Physical<br/>phenomenon] -->|physics| B[Sensor]
B -->|I2C| C[Item<br/>controller]
C -->|CBOR| D[Secure<br/>Element]
D -->|COSE_Sign1| E[NFC]
E -->|tap| F[Phone]
F -->|reading| G[Operator API]
G -->|MPT update tx| H[Cardano]
| Link | Trust basis | Weakness |
|---|---|---|
| Phenomenon → Sensor | Physics | Sensor failure or physical tampering |
| Sensor → Controller | I2C bus on PCB | Compromised firmware could substitute readings |
| Controller → SE | I2C, CBOR format | SE signs whatever controller gives it |
| SE → signature | Private key in tamper-resistant hardware | Key extraction (expensive, destructive) |
| Phone → Operator API | HTTPS | Operator could delay (see adversarial path below) |
| Operator → Cardano | Tx submission | Operator is the oracle — they control the MPT |
Root of trust: the secure element vendor's key provisioning process.
Weakest link: controller → SE boundary. Mitigated by schema validation, plausibility checks, and cross-referencing with independent measurements.
Adversarial path¶
Open design problem
In the cooperative path, the operator controls the MPT and submits all transactions. If the operator refuses to process a valid reading, the user currently has no on-chain recourse — the operator is the oracle for their own MPT.
Possible future approaches:
- MPFS request mechanism: user creates a request UTxO that the operator is contractually obligated to process (locks ~1.5 ADA until processed)
- Timeout escalation: if the operator doesn't process a request within N slots, the user can trigger a penalty or move to a different operator
- CIP-118 nested transactions: user creates a sub-transaction that any batcher can wrap, removing the need for operator cooperation
This is deferred. The cooperative path is sufficient for the initial design where operators have a business incentive to process readings (they need the data for DPP compliance).
Future: CIP-118 (nested transactions)¶
CIP-118 introduces nested transactions in the Dijkstra ledger era. Actively being implemented — CIP merged January 2026, ledger code landing Q1-Q2 2026.
With CIP-118, the user could create a sub-transaction containing the signed reading, and any batcher (operator or third party) wraps it in a top-level transaction providing ADA. A CIP-112 guard script enforces the terms. This would eliminate the need for operator cooperation on the submission side, though the commitment phase still requires the operator to update their MPT.
Data visibility: on-chain or hash-only¶
The operator configures how much reading data appears on-chain. This is a per-operator deployment decision, not a protocol constraint.
Option A: Full data on-chain¶
The entire COSE_Sign1 object (including the CBOR payload) is included in the transaction — either as structured datum fields or in transaction metadata. Anyone can audit the reading history without the operator's cooperation.
| Pro | Con |
|---|---|
| Fully auditable by third parties | Larger tx size (~200-430 bytes per reading) |
| Survives operator shutdown | Higher tx fees |
| Enables on-chain plausibility checks | Sensor data is public (may be commercially sensitive) |
Option B: Hash-only on-chain¶
Only the hash of the COSE_Sign1 object is stored on-chain. The full reading data is held by the operator (and optionally shared via an API or IPFS).
| Pro | Con |
|---|---|
| Minimal tx size | Requires operator cooperation to access data |
| Lower tx fees | Data lost if operator disappears |
| Sensor data stays private | Third-party audits need operator API |
Validator impact¶
The on-chain validator always verifies the COSE signature (it needs the full COSE_Sign1 in the redeemer regardless). The difference is whether the full payload is also persisted on-chain in the output datum or metadata.
In both modes, the redeemer contains the full COSE_Sign1 for signature verification. In hash-only mode, only hash(coseSign1) is stored in the datum/metadata for future reference.
Applicability¶
This protocol is foundational for signing IoT sensor data wherever:
- A physical item has at least one sensor
- A secure element can be added (~$1.91 at volume)
- A user has physical access and an incentive to report
- The reading needs to be verifiable and timestamped
Batteries are the first concrete application. The protocol is product-agnostic — the only adaptation for a new product category is defining the CBOR payload schema (field 3) and the associated plausibility checks.