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 }