// ============================================================ // EXAMPLE: twin_coupling // PURPOSE: Demonstrate the data-convention pattern for linking a // digital-twin computation row back to the sensor rows // that fed it. Sensor chain and twin chain are independent // trusty entities. The twin row carries a `source_refs` // array in its `data` field so an external verifier can // check the linkage from two ProofBundles alone -- no // extra tables, no schema changes. // // See `docs/twin-coupling.md` for the conceptual doc. // // RUN: cargo run -p trusty-sqlite --example twin_coupling // ============================================================ use serde_json::{json, Value}; use std::path::PathBuf; use trusty_core::SecretKey; use trusty_sqlite::Trusty; fn tmp_db_path() -> PathBuf { let mut p = std::env::temp_dir(); let nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_nanos()) .unwrap_or(0); p.push(format!("trusty_twin_coupling_demo_{nanos}.db")); p } fn main() { let path = tmp_db_path(); println!("trusty twin-coupling demo"); println!("db: {}\n", path.display()); let sip_key = SecretKey::new(*b"0123456789abcdef"); let mut t = Trusty::open(&path, sip_key.clone()).expect("open trusty"); // ---------------------------------------------------------------- // step 1: append sensor readings to the sensor chain // ---------------------------------------------------------------- println!("step 1: append three sensor readings"); let r0 = t .append("sensor-001", &json!({"temp_c": 21.1}), "iot://sensor-001") .expect("append r0"); println!( " seq={} chain_id={:.16}... row_hash={}", r0.sequence, r0.chain_id, r0.row_hash ); let r1 = t .append("sensor-001", &json!({"temp_c": 21.4}), "iot://sensor-001") .expect("append r1"); println!( " seq={} chain_id={:.16}... row_hash={}", r1.sequence, r1.chain_id, r1.row_hash ); let r2 = t .append("sensor-001", &json!({"temp_c": 21.6}), "iot://sensor-001") .expect("append r2"); println!( " seq={} chain_id={:.16}... row_hash={}", r2.sequence, r2.chain_id, r2.row_hash ); // ---------------------------------------------------------------- // step 2: twin computation cites the three sensor rows by // chain_id + sequence + row_hash (data convention) // ---------------------------------------------------------------- println!("\nstep 2: append twin computation row with source_refs"); let avg = (21.1_f64 + 21.4 + 21.6) / 3.0; let twin_data = json!({ "computation": "rolling_avg_temp_c", "result": (avg * 100.0).round() / 100.0, "source_refs": [ {"chain_id": r0.chain_id, "sequence": r0.sequence, "row_hash": r0.row_hash}, {"chain_id": r1.chain_id, "sequence": r1.sequence, "row_hash": r1.row_hash}, {"chain_id": r2.chain_id, "sequence": r2.sequence, "row_hash": r2.row_hash}, ] }); let tw = t .append("twin-001", &twin_data, "omniverse://twin-001") .expect("append twin"); println!(" seq={} row_hash={}", tw.sequence, tw.row_hash); // ---------------------------------------------------------------- // step 3: verify both chains independently // ---------------------------------------------------------------- println!("\nstep 3: verify both chains"); let sensor_report = t.verify("sensor-001").expect("verify sensor"); println!( " sensor-001: ok={} rows={}", sensor_report.rows_valid, sensor_report.row_count ); assert!(sensor_report.rows_valid, "sensor chain verification failed"); let twin_report = t.verify("twin-001").expect("verify twin"); println!( " twin-001: ok={} rows={}", twin_report.rows_valid, twin_report.row_count ); assert!(twin_report.rows_valid, "twin chain verification failed"); // ---------------------------------------------------------------- // step 4: external-verifier cross-chain check // Export both ProofBundles; walk twin source_refs and // confirm each referenced row_hash is present and matches // in the sensor bundle. // ---------------------------------------------------------------- println!("\nstep 4: cross-chain verify from ProofBundles"); let sensor_bundle = t .export_bundle("sensor-001", None, None) .expect("sensor bundle"); let twin_bundle = t .export_bundle("twin-001", None, None) .expect("twin bundle"); let sensor_verify = sensor_bundle.verify(&sip_key); let twin_verify = twin_bundle.verify(&sip_key); println!( " sensor bundle verify: ok={} rows={}", sensor_verify.rows_valid, sensor_verify.row_count ); println!( " twin bundle verify: ok={} rows={}", twin_verify.rows_valid, twin_verify.row_count ); assert!(sensor_verify.rows_valid); assert!(twin_verify.rows_valid); // Walk twin rows; for each source_ref, confirm the sensor bundle // contains a row at that sequence with a matching row_hash. let mut refs_checked = 0usize; for twin_row in twin_bundle.rows.iter().filter(|r| r.sequence >= 0) { let payload: Value = twin_row .raw_payload .as_ref() .and_then(|p| serde_json::from_str(p).ok()) .expect("twin row has no payload"); let data = payload.get("data").expect("payload has no data"); let refs = match data.get("source_refs").and_then(|v| v.as_array()) { Some(r) => r, None => continue, }; for sr in refs { let ref_chain_id = sr["chain_id"].as_str().expect("chain_id"); let ref_seq = sr["sequence"].as_i64().expect("sequence"); let ref_hash = sr["row_hash"].as_str().expect("row_hash"); let matched = sensor_bundle.rows.iter().find(|r| { r.chain_id == ref_chain_id && r.sequence == ref_seq && r.row_hash.as_deref() == Some(ref_hash) }); match matched { Some(m) => { println!( " source_ref ok: seq={} row_hash={} (sensor chain_id={:.16}...)", m.sequence, ref_hash, ref_chain_id ); refs_checked += 1; } None => { panic!( "source_ref not found in sensor bundle: chain_id={} seq={} row_hash={}", ref_chain_id, ref_seq, ref_hash ); } } } } println!("\n {refs_checked} source_refs verified across ProofBundles"); assert_eq!(refs_checked, 3, "expected 3 source_refs checked"); println!("\ndone. (cleanup: rm {})", path.display()); }