93 lines
3.5 KiB
Markdown
93 lines
3.5 KiB
Markdown
# 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:
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
"source_refs": [
|
|
{"chain_id": "chain_sensor-001_...", "sequence": 5, "row_hash": "..."},
|
|
{"chain_id": "chain_sensor-002_...", "sequence": 3, "row_hash": "..."}
|
|
]
|
|
```
|