@@ -166,7 +166,7 @@ pub enum GroomingKey {
166166use soroban_sdk:: xdr:: { FromXdr , ToXdr } ;
167167use soroban_sdk:: {
168168 contract, contracterror, contractimpl, contracttype, panic_with_error, Address , Bytes , BytesN ,
169- Env , Map , String , Symbol , Vec ,
169+ Env , IntoVal , Map , String , Symbol , Val , Vec ,
170170} ;
171171
172172// Bounded-module split (Issue #1146, phase 1): storage keys and value
@@ -203,7 +203,13 @@ mod test_breeding_genetics;
203203#[ cfg( test) ]
204204mod test_pet_birthday_validation;
205205#[ cfg( test) ]
206- mod test_microchip_normalization;
206+ mod test_persistent_ttl_policy;
207+ #[ cfg( test) ]
208+ mod test_access_grant_index_invariants;
209+ #[ cfg( test) ]
210+ mod test_medical_record_hashing;
211+ #[ cfg( test) ]
212+ mod test_medical_event_timestamps;
207213#[ cfg( test) ]
208214mod test_verify_claim_document;
209215#[ cfg( test) ]
@@ -370,6 +376,40 @@ const MAX_LAB_RESULTS_LEN: u32 = 1_000;
370376/// Maximum byte length of a `LabResult::reference_ranges`.
371377const MAX_LAB_REF_RANGES_LEN : u32 = 500 ;
372378
379+ /// TTL-extension policy for persistent storage entries (Issue #1154).
380+ ///
381+ /// Persistent entries (audit/access logs, breeding records, ...) are billed
382+ /// separately from instance storage and, unlike instance storage, are not
383+ /// automatically kept alive by every contract invocation: each entry's TTL
384+ /// must be extended explicitly or it can be archived/expire out from under
385+ /// the contract. `PERSISTENT_TTL_THRESHOLD` is the minimum remaining TTL (in
386+ /// ledgers) below which we proactively bump it back up to
387+ /// `PERSISTENT_TTL_EXTEND_TO` on every write (and on reads of
388+ /// long-lived/critical records) so records that are written once and read
389+ /// rarely still survive.
390+ ///
391+ /// At Stellar's ~5s ledger close time, `PERSISTENT_TTL_EXTEND_TO` of
392+ /// ~1,036,800 ledgers is roughly 60 days; `PERSISTENT_TTL_THRESHOLD` bumps
393+ /// as soon as the entry has less than ~30 days of life left, well within the
394+ /// network's max TTL extension window.
395+ const PERSISTENT_TTL_THRESHOLD : u32 = 518_400 ; // ~30 days
396+ const PERSISTENT_TTL_EXTEND_TO : u32 = 1_036_800 ; // ~60 days
397+
398+ /// Maximum allowed clock skew (seconds) for a medical-event timestamp that is
399+ /// reported as having already occurred (e.g. `administered_at`), measured
400+ /// relative to the current ledger time. This is deliberately generous (on
401+ /// the order of decades) so it only rejects clearly nonsensical/corrupt
402+ /// future dates (e.g. a caller passing a millisecond timestamp, or a typo
403+ /// adding extra digits) without constraining legitimate historical or
404+ /// synthetic test timestamps, which need not track real-world wall-clock
405+ /// time. (Issue #1174)
406+ const MAX_EVENT_FUTURE_SKEW : u64 = 100 * 365 * 24 * 60 * 60 ; // ~100 years
407+
408+ /// Furthest a vaccination's `next_due_date` / `expires_at` may be scheduled
409+ /// past `administered_at`, to catch fat-fingered far-future dates while
410+ /// still allowing multi-year vaccination schedules. (Issue #1174)
411+ const MAX_EVENT_HORIZON : u64 = 50 * 365 * 24 * 60 * 60 ; // ~50 years
412+
373413/// Maximum byte length of a `Dispute::reason`.
374414const MAX_DISPUTE_REASON_LEN : u32 = 500 ;
375415
@@ -523,6 +563,11 @@ pub enum ContractError {
523563 NotDisputeStakeholder = 166 ,
524564 NotInEvidencePhase = 167 ,
525565 NotDisputeParty = 168 ,
566+
567+ /// A medical-event timestamp fell outside the allowed domain relative to
568+ /// ledger time (too far in the past, too far in the future, or with a
569+ /// due/expiry date before the event it describes). (Issue #1174)
570+ InvalidTimestamp = 169 ,
526571}
527572
528573// --- MULTI-LANGUAGE ERROR REGISTRY (Issue #684) ---
@@ -3811,6 +3856,21 @@ impl PetChainContract {
38113856 result
38123857 }
38133858
3859+ /// Extend the TTL of a persistent-storage entry per the archival policy
3860+ /// defined by `PERSISTENT_TTL_THRESHOLD` / `PERSISTENT_TTL_EXTEND_TO`.
3861+ /// (Issue #1154). Call this after every `persistent().set(...)` (and on
3862+ /// reads of records that must remain reachable even when written once
3863+ /// and read rarely) so critical persistent records are not silently
3864+ /// archived/expired by the ledger.
3865+ fn bump_persistent_ttl < K > ( env : & Env , key : & K )
3866+ where
3867+ K : IntoVal < Env , Val > ,
3868+ {
3869+ env. storage ( )
3870+ . persistent ( )
3871+ . extend_ttl ( key, PERSISTENT_TTL_THRESHOLD , PERSISTENT_TTL_EXTEND_TO ) ;
3872+ }
3873+
38143874 fn log_access ( env : & Env , pet_id : u64 , user : Address , action : AccessAction , details : String ) {
38153875 let key = ( Symbol :: new ( env, "access_logs" ) , pet_id) ;
38163876 let mut logs: Vec < AccessLog > = env
@@ -3839,6 +3899,7 @@ impl PetChainContract {
38393899
38403900 logs. push_back ( log) ;
38413901 env. storage ( ) . persistent ( ) . set ( & key, & logs) ;
3902+ Self :: bump_persistent_ttl ( env, & key) ;
38423903 }
38433904
38443905 /// Read access log entries for a pet. Visible to the pet owner or any admin.
@@ -3859,10 +3920,13 @@ impl PetChainContract {
38593920 }
38603921
38613922 let key = ( Symbol :: new ( & env, "access_logs" ) , pet_id) ;
3862- env. storage ( )
3923+ let logs = env
3924+ . storage ( )
38633925 . persistent ( )
38643926 . get ( & key)
3865- . unwrap_or ( Vec :: new ( & env) )
3927+ . unwrap_or ( Vec :: new ( & env) ) ;
3928+ Self :: bump_persistent_ttl ( & env, & key) ;
3929+ logs
38663930 }
38673931
38683932 fn require_admin ( env : & Env ) {
@@ -7153,6 +7217,25 @@ impl PetChainContract {
71537217 . get ( & DataKey :: Pet ( pet_id) )
71547218 . unwrap_or_else ( || env. panic_with_error ( ContractError :: PetNotFound ) ) ;
71557219
7220+ let now = env. ledger ( ) . timestamp ( ) ;
7221+
7222+ // Validate medical-event timestamps against ledger time (Issue #1174).
7223+ // `administered_at` must not be further in the future than the
7224+ // allowed clock-skew tolerance relative to the current ledger time.
7225+ if administered_at > now. saturating_add ( MAX_EVENT_FUTURE_SKEW ) {
7226+ panic_with_error ! ( & env, ContractError :: InvalidTimestamp ) ;
7227+ }
7228+ // `next_due_date` and `expires_at` (when set) describe follow-up
7229+ // dates and must not precede the event they follow, nor sit
7230+ // absurdly far beyond it.
7231+ let max_future = administered_at. saturating_add ( MAX_EVENT_HORIZON ) ;
7232+ if next_due_date != 0 && ( next_due_date < administered_at || next_due_date > max_future) {
7233+ panic_with_error ! ( & env, ContractError :: InvalidTimestamp ) ;
7234+ }
7235+ if expires_at != 0 && ( expires_at < administered_at || expires_at > max_future) {
7236+ panic_with_error ! ( & env, ContractError :: InvalidTimestamp ) ;
7237+ }
7238+
71567239 // Check storage quota (Issue #676)
71577240 Self :: increment_pet_storage ( & env, pet_id) ;
71587241
@@ -7164,7 +7247,6 @@ impl PetChainContract {
71647247 let vaccine_id = vaccine_count
71657248 . checked_add ( 1 )
71667249 . unwrap_or_else ( || panic_with_error ! ( & env, ContractError :: CounterOverflow ) ) ;
7167- let now = env. ledger ( ) . timestamp ( ) ;
71687250 let key = PetChainContract :: get_encryption_key ( & env) ;
71697251
71707252 let vname_bytes = vaccine_name. to_xdr ( & env) ;
@@ -10266,6 +10348,7 @@ impl PetChainContract {
1026610348 }
1026710349 logs. push_back ( log) ;
1026810350 env. storage ( ) . persistent ( ) . set ( & log_key, & logs) ;
10351+ Self :: bump_persistent_ttl ( & env, & log_key) ;
1026910352
1027010353 Self :: write_emergency_audit ( & env, pet_id, caller, reason_code) ;
1027110354
@@ -10298,6 +10381,7 @@ impl PetChainContract {
1029810381 pet_id,
1029910382 } ) ;
1030010383 env. storage ( ) . persistent ( ) . set ( & audit_key, & entries) ;
10384+ Self :: bump_persistent_ttl ( env, & audit_key) ;
1030110385 }
1030210386
1030310387 fn is_admin_address ( env : & Env , caller : & Address ) -> bool {
@@ -12148,6 +12232,7 @@ impl PetChainContract {
1214812232 env. storage ( )
1214912233 . persistent ( )
1215012234 . set ( & ActivityKey :: PetActivityStreak ( pet_id) , & streak) ;
12235+ Self :: bump_persistent_ttl ( & env, & ActivityKey :: PetActivityStreak ( pet_id) ) ;
1215112236
1215212237 activity_id
1215312238 }
@@ -12245,9 +12330,11 @@ impl PetChainContract {
1224512330 env. storage ( )
1224612331 . persistent ( )
1224712332 . set ( & BreedingKey :: BreedingRecord ( id) , & record) ;
12333+ Self :: bump_persistent_ttl ( & env, & BreedingKey :: BreedingRecord ( id) ) ;
1224812334 env. storage ( )
1224912335 . persistent ( )
1225012336 . set ( & BreedingKey :: BreedingRecordCount , & id) ;
12337+ Self :: bump_persistent_ttl ( & env, & BreedingKey :: BreedingRecordCount ) ;
1225112338
1225212339 Self :: inc_pet_breeding_count ( & env, sire_id) ;
1225312340 Self :: inc_pet_breeding_count ( & env, dam_id) ;
@@ -12264,6 +12351,7 @@ impl PetChainContract {
1226412351 env. storage ( )
1226512352 . persistent ( )
1226612353 . set ( & BreedingKey :: PetBreedingCount ( pet_id) , & safe_increment ( count) ) ;
12354+ Self :: bump_persistent_ttl ( env, & BreedingKey :: PetBreedingCount ( pet_id) ) ;
1226712355 }
1226812356
1226912357 pub fn add_offspring ( env : Env , record_id : u64 , offspring_id : u64 ) -> bool {
@@ -12288,15 +12376,17 @@ impl PetChainContract {
1228812376 }
1228912377
1229012378 // Store parent pair for pedigree queries (COI, lineage)
12291- env. storage ( ) . persistent ( ) . set (
12292- & BreedingKey :: ParentPair ( offspring_id) ,
12293- & ( record. sire_id , record. dam_id ) ,
12294- ) ;
12379+ let parent_pair_key = BreedingKey :: ParentPair ( offspring_id) ;
12380+ env. storage ( )
12381+ . persistent ( )
12382+ . set ( & parent_pair_key, & ( record. sire_id , record. dam_id ) ) ;
12383+ Self :: bump_persistent_ttl ( & env, & parent_pair_key) ;
1229512384
1229612385 record. offspring_count = record. offspring_count . saturating_add ( 1 ) ;
1229712386 env. storage ( )
1229812387 . persistent ( )
1229912388 . set ( & BreedingKey :: BreedingRecord ( record_id) , & record) ;
12389+ Self :: bump_persistent_ttl ( & env, & BreedingKey :: BreedingRecord ( record_id) ) ;
1230012390
1230112391 let count = env
1230212392 . storage ( )
@@ -12306,6 +12396,7 @@ impl PetChainContract {
1230612396 env. storage ( )
1230712397 . persistent ( )
1230812398 . set ( & BreedingKey :: PetOffspringCount ( offspring_id) , & ( count + 1 ) ) ;
12399+ Self :: bump_persistent_ttl ( & env, & BreedingKey :: PetOffspringCount ( offspring_id) ) ;
1230912400
1231012401 true
1231112402 }
@@ -12831,6 +12922,83 @@ impl PetChainContract {
1283112922 res. deleted . len ( )
1283212923 }
1283312924
12925+ /// Build the canonical, versioned preimage bytes for a [`MedicalRecord`]
12926+ /// (Issue #1169).
12927+ ///
12928+ /// Off-chain clients (in any language with a Stellar/Soroban XDR codec)
12929+ /// need to be able to reproduce the exact same commitment a contract
12930+ /// computes for a medical record, independent of storage/audit
12931+ /// metadata that can change without the clinical facts changing. The
12932+ /// canonical encoding is:
12933+ ///
12934+ /// ```text
12935+ /// sha256(
12936+ /// b"petchain:medical-record:v1" (26-byte literal domain tag)
12937+ /// || pet_id as 8-byte big-endian u64
12938+ /// || vet_address as its XDR-encoded `ScAddress`
12939+ /// || diagnosis as its XDR-encoded `ScString`
12940+ /// || treatment as its XDR-encoded `ScString`
12941+ /// || medications as its XDR-encoded `ScVec` (fixed struct field order)
12942+ /// || notes as its XDR-encoded `ScString`
12943+ /// || date as 8-byte big-endian u64 (clinical event time)
12944+ /// )
12945+ /// ```
12946+ ///
12947+ /// Fields are concatenated in this fixed order with no separators
12948+ /// (XDR-encoded values are already self-delimiting/length-prefixed, and
12949+ /// the two `u64` fields have a fixed 8-byte width, so the encoding is
12950+ /// unambiguous). `id`, `updated_at`, `attachment_hashes`, and
12951+ /// `deleted_at` are intentionally excluded: they are ledger
12952+ /// bookkeeping/audit metadata, not clinical content, so the commitment
12953+ /// stays stable across non-clinical housekeeping mutations (e.g. an
12954+ /// attachment being added, or a soft-delete).
12955+ ///
12956+ /// The `v1` domain tag is part of the preimage precisely so that any
12957+ /// future change to the field set, order, or encoding can ship as a
12958+ /// `v2` tag without silently colliding with existing `v1` commitments
12959+ /// clients may have already anchored off-chain.
12960+ fn canonical_medical_record_preimage ( env : & Env , record : & MedicalRecord ) -> Bytes {
12961+ let mut preimage = Bytes :: new ( env) ;
12962+ for byte in b"petchain:medical-record:v1" {
12963+ preimage. push_back ( * byte) ;
12964+ }
12965+ for byte in record. pet_id . to_be_bytes ( ) {
12966+ preimage. push_back ( byte) ;
12967+ }
12968+ for byte in record. vet_address . to_xdr ( env) . iter ( ) {
12969+ preimage. push_back ( byte) ;
12970+ }
12971+ for byte in record. diagnosis . to_xdr ( env) . iter ( ) {
12972+ preimage. push_back ( byte) ;
12973+ }
12974+ for byte in record. treatment . to_xdr ( env) . iter ( ) {
12975+ preimage. push_back ( byte) ;
12976+ }
12977+ for byte in record. medications . to_xdr ( env) . iter ( ) {
12978+ preimage. push_back ( byte) ;
12979+ }
12980+ for byte in record. notes . to_xdr ( env) . iter ( ) {
12981+ preimage. push_back ( byte) ;
12982+ }
12983+ for byte in record. date . to_be_bytes ( ) {
12984+ preimage. push_back ( byte) ;
12985+ }
12986+ preimage
12987+ }
12988+
12989+ /// Compute the canonical hash commitment for a stored medical record.
12990+ /// See [`Self::canonical_medical_record_preimage`] for the exact
12991+ /// versioned encoding. (Issue #1169)
12992+ pub fn get_medical_record_hash ( env : Env , record_id : u64 ) -> BytesN < 32 > {
12993+ let record: MedicalRecord = env
12994+ . storage ( )
12995+ . instance ( )
12996+ . get ( & MedicalKey :: MedicalRecord ( record_id) )
12997+ . unwrap_or_else ( || panic_with_error ! ( & env, ContractError :: RecordNotFound ) ) ;
12998+ let preimage = Self :: canonical_medical_record_preimage ( & env, & record) ;
12999+ env. crypto ( ) . sha256 ( & preimage) . into ( )
13000+ }
13001+
1283413002 pub fn add_medical_record (
1283513003 env : Env ,
1283613004 pet_id : u64 ,
0 commit comments