Files
Trusty/docs/twin-coupling.md

3.5 KiB

Twin Coupling -- Data Convention

Problem

A digital-twin computation ingests readings from one or more sensor chains and produces a derived result. The result is appended to its own trusty chain (separate entity_id). How does an external verifier confirm which sensor rows fed the computation?

Convention

The twin row carries a source_refs array inside its data field:

{
  "computation": "rolling_avg_temp_c",
  "result": 21.37,
  "source_refs": [
    {"chain_id": "chain_sensor-001_...", "sequence": 0, "row_hash": "ae1cfe445cda1f6d"},
    {"chain_id": "chain_sensor-001_...", "sequence": 1, "row_hash": "dee9856d170787cf"},
    {"chain_id": "chain_sensor-001_...", "sequence": 2, "row_hash": "26cd125f051243cf"}
  ]
}

Each entry is:

Field Source Purpose
chain_id AppendResult.chain_id Identifies which sensor chain
sequence AppendResult.sequence Row position within that chain
row_hash AppendResult.row_hash Tamper-evident fingerprint of the row

Because data is already part of the payload that row_hash commits to, the source_refs array is itself hash-protected. Altering any reference after the fact invalidates the twin row's row_hash and fails verify_chain.

Verification flow (two ProofBundles)

An external verifier needs no database access -- only two ProofBundle blobs exported via Trusty::export_bundle:

  1. sensor_bundle.verify(&sip_key) -- confirms the sensor chain is internally consistent.
  2. twin_bundle.verify(&sip_key) -- confirms the twin chain is internally consistent.
  3. Walk twin rows; for each source_refs entry, look up the matching row in sensor_bundle.rows by chain_id + sequence and assert row_hash matches.

If all three checks pass, the verifier knows:

  • The sensor readings are unaltered.
  • The twin computation row is unaltered.
  • The twin row commits to exactly those sensor row_hashes at write time.

See crates/trusty-sqlite/examples/twin_coupling.rs for a runnable end-to-end demo.

Design decisions

Why data field, not a new break_reason variant. break_reason marks loss-of-continuity (tamper, gap, domain mismatch). A twin derivation is a planned event, not an anomaly. Mixing the two pollutes breaks_since() as an alerting surface and requires callers to filter by break kind.

Why not a separate twin_reference table. A join table outside the hash surface cannot be verified from a ProofBundle alone. The verifier would need database access, breaking the self-contained export contract. source_refs in data is verifiable anywhere the bundle travels.

What this does NOT prove. The convention proves the twin row committed to those row_hashes at write time. It does not prove the computation is correct (the twin operator could have computed a wrong average and still referenced the right sensor rows). Computation correctness is out of scope for trusty; it belongs to a separate audit or reproducibility layer.

Multi-sensor / multi-chain refs

source_refs can mix chain_id values from different sensor entities. The verifier simply exports a ProofBundle per sensor entity and checks each ref against the appropriate bundle.

"source_refs": [
  {"chain_id": "chain_sensor-001_...", "sequence": 5, "row_hash": "..."},
  {"chain_id": "chain_sensor-002_...", "sequence": 3, "row_hash": "..."}
]