field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

compatibility.rs (7548B)


      1 //! Frozen compatibility across application FFI history transfer. All stores are
      2 //! isolated fixtures; signer keys are generated in memory and never serialized.
      3 
      4 use secp256k1::{Keypair, Message, Secp256k1};
      5 use serde_json::Value;
      6 use std::path::Path;
      7 use tera_ffi::{
      8     FfiAddCommandType, FfiAddDraftInput, FfiCancellationPolicy, FfiDraftStatusRecord,
      9     FfiQueuePolicyRecord, FfiRelaySatisfaction, HostSigningOutcome, HostSigningRequest,
     10     HostSigningResult, MOBILE_FFI_SCHEMA_VERSION, ProtectedDataAvailability,
     11     SignerAvailabilityRecord, SignerStatusRecord, TeraHostSigner, TeraRuntime,
     12 };
     13 
     14 fn fixture() -> Value {
     15     serde_json::from_str(include_str!(
     16         "../../../../test-fixtures/tera-compatibility.v1.json"
     17     ))
     18     .expect("checked synthetic fixture")
     19 }
     20 
     21 fn field<'a>(fixture: &'a Value, key: &str) -> &'a str {
     22     fixture[key].as_str().expect("fixture string")
     23 }
     24 
     25 fn input(fixture: &Value) -> FfiAddDraftInput {
     26     FfiAddDraftInput {
     27         schema_version: MOBILE_FFI_SCHEMA_VERSION,
     28         command_type: FfiAddCommandType::CreateUpdate,
     29         content: field(fixture, "content").to_owned(),
     30         identifier: None,
     31         title: None,
     32         summary: None,
     33         location: None,
     34         event_timing: None,
     35         event_start_date: None,
     36         event_end_date: None,
     37         event_start_unix_s: None,
     38         event_end_unix_s: None,
     39         event_timezone: None,
     40         price_amount: None,
     41         currency: None,
     42         unit: None,
     43         quantity: None,
     44         food_published_at_unix_s: None,
     45         food_status: None,
     46         media: Vec::new(),
     47     }
     48 }
     49 
     50 async fn runtime(root: &Path, public_key: &str, signer: Option<Keypair>) -> TeraRuntime {
     51     std::fs::create_dir_all(root.join("radroots/users").join(public_key))
     52         .expect("isolated application owner directory");
     53     let fixture = fixture();
     54     let generation = field(&fixture["queued_update"], "source_generation").to_owned();
     55     if let Some(keypair) = signer {
     56         TeraRuntime::with_host_signer(
     57             root.to_string_lossy().into_owned(),
     58             public_key.to_owned(),
     59             generation,
     60             1_800_000_000_000,
     61             ProtectedDataAvailability::Available,
     62             Box::new(EphemeralSigner(keypair)),
     63         )
     64         .await
     65         .expect("runtime with ephemeral fixture signer")
     66     } else {
     67         TeraRuntime::new(
     68             root.to_string_lossy().into_owned(),
     69             public_key.to_owned(),
     70             generation,
     71             1_800_000_000_000,
     72             ProtectedDataAvailability::Available,
     73         )
     74         .await
     75         .expect("runtime using current persistent reader")
     76     }
     77 }
     78 
     79 async fn queue(runtime: &TeraRuntime, fixture: &Value) -> FfiDraftStatusRecord {
     80     let id = field(fixture, "draft_id");
     81     let persisted = fixture["persisted_at_unix_ms"].as_u64().unwrap();
     82     let saved = runtime
     83         .phase1_save_draft(
     84             id.to_owned(),
     85             input(fixture),
     86             fixture["authored_at_unix_s"].as_u64().unwrap(),
     87             None,
     88             persisted,
     89         )
     90         .await
     91         .expect("save synthetic draft through real FFI");
     92     let policy = FfiQueuePolicyRecord {
     93         schema_version: MOBILE_FFI_SCHEMA_VERSION,
     94         relay_urls: fixture["relay_urls"]
     95             .as_array()
     96             .unwrap()
     97             .iter()
     98             .map(|value| value.as_str().unwrap().to_owned())
     99             .collect(),
    100         satisfaction: FfiRelaySatisfaction::AllAccepted,
    101         delivery_deadline_unix_ms: fixture["delivery_deadline_unix_ms"].as_u64().unwrap(),
    102         cancellation: FfiCancellationPolicy::LocalCooperative,
    103     };
    104     runtime
    105         .phase1_queue_draft(id.to_owned(), saved.revision, policy, persisted + 1)
    106         .await
    107         .expect("queue locally without starting relay delivery")
    108 }
    109 
    110 #[tokio::test]
    111 async fn queued_update_reopens_with_frozen_operation_and_card_identity() {
    112     let fixture = fixture()["queued_update"].clone();
    113     let root = tempfile::tempdir().unwrap();
    114     let public_key = field(&fixture, "public_key");
    115     let first = runtime(root.path(), public_key, None).await;
    116     let queued = queue(&first, &fixture).await;
    117     assert_eq!(
    118         queued.operation_id.as_deref(),
    119         Some(field(&fixture, "expected_operation_id"))
    120     );
    121     assert_eq!(queued.card_id, field(&fixture, "expected_card_id"));
    122     first.shutdown().await.unwrap();
    123     drop(first);
    124     let reopened = runtime(root.path(), public_key, None).await;
    125     let restored = reopened
    126         .phase1_draft_status(field(&fixture, "draft_id").to_owned())
    127         .await
    128         .expect("existing persisted draft reader");
    129     assert_eq!(restored, queued);
    130     let recovered = reopened
    131         .phase1_recover_draft_queue(field(&fixture, "draft_id").to_owned(), 1_900_000_000_002)
    132         .await
    133         .expect("existing queued operation reader");
    134     assert_eq!(recovered, queued);
    135     reopened.shutdown().await.unwrap();
    136 }
    137 
    138 #[tokio::test]
    139 async fn signed_operation_reopens_without_replacing_its_author_or_identity() {
    140     let fixture = fixture()["queued_update"].clone();
    141     let root = tempfile::tempdir().unwrap();
    142     let keypair = Keypair::new(&Secp256k1::new(), &mut secp256k1::rand::thread_rng());
    143     let public_key = keypair.x_only_public_key().0.to_string();
    144     let first = runtime(root.path(), &public_key, Some(keypair)).await;
    145     let queued = queue(&first, &fixture).await;
    146     let signed = first
    147         .phase1_sign_queued_draft(queued.draft_id.clone(), queued.revision)
    148         .await
    149         .expect("sign and durably admit the fixed operation");
    150     assert_eq!(signed.operation_id, queued.operation_id);
    151     assert_eq!(signed.card_id, queued.card_id);
    152     assert_eq!(signed.author_public_key, public_key);
    153     assert_eq!(signed.settlement.unwrap().signed, 1);
    154     first.shutdown().await.unwrap();
    155     drop(first);
    156     // Reopening has no signer. Reading must preserve the already signed facts.
    157     let reopened = runtime(root.path(), &public_key, None).await;
    158     let restored = reopened
    159         .phase1_draft_status(signed.draft_id.clone())
    160         .await
    161         .unwrap();
    162     assert_eq!(restored, signed);
    163     reopened.shutdown().await.unwrap();
    164 }
    165 
    166 struct EphemeralSigner(Keypair);
    167 
    168 #[async_trait::async_trait]
    169 impl TeraHostSigner for EphemeralSigner {
    170     async fn signer_status(&self) -> SignerStatusRecord {
    171         SignerStatusRecord {
    172             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    173             availability: SignerAvailabilityRecord::Ready,
    174         }
    175     }
    176 
    177     async fn sign(&self, request: HostSigningRequest) -> HostSigningResult {
    178         assert_eq!(request.public_key, self.0.x_only_public_key().0.to_string());
    179         let digest: [u8; 32] = request
    180             .event_id_digest
    181             .try_into()
    182             .expect("exact event digest");
    183         let signature =
    184             Secp256k1::new().sign_schnorr_no_aux_rand(&Message::from_digest(digest), &self.0);
    185         HostSigningResult {
    186             schema_version: MOBILE_FFI_SCHEMA_VERSION,
    187             outcome: HostSigningOutcome::Signed,
    188             operation_id: request.operation_id,
    189             signer_request_id: request.signer_request_id,
    190             public_key: request.public_key,
    191             purpose: request.purpose,
    192             signature_hex: Some(signature.to_string()),
    193             completed_at_unix_ms: std::time::SystemTime::now()
    194                 .duration_since(std::time::UNIX_EPOCH)
    195                 .unwrap()
    196                 .as_millis()
    197                 .try_into()
    198                 .unwrap(),
    199         }
    200     }
    201 }