diff --git a/stellar-contracts/.cargo/config.toml b/stellar-contracts/.cargo/config.toml new file mode 100644 index 0000000..b0d436a --- /dev/null +++ b/stellar-contracts/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.x86_64-pc-windows-gnu] +linker = "C:\\Users\\DELL\\.rustup\\toolchains\\stable-x86_64-pc-windows-gnu\\lib\\rustlib\\x86_64-pc-windows-gnu\\bin\\self-contained\\x86_64-w64-mingw32-gcc.exe" +ar = "C:\\Users\\DELL\\.rustup\\toolchains\\stable-x86_64-pc-windows-gnu\\lib\\rustlib\\x86_64-pc-windows-gnu\\bin\\self-contained\\dlltool.exe" diff --git a/stellar-contracts/do_replace.ps1 b/stellar-contracts/do_replace.ps1 new file mode 100644 index 0000000..6849fa3 --- /dev/null +++ b/stellar-contracts/do_replace.ps1 @@ -0,0 +1,134 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +$startIdx = 204218 +$endIdx = 205881 # one past the closing brace of decrypt_sensitive_data + +$newCode = @' +// --- REAL ENCRYPTION: SHA-256 CTR-mode stream cipher + HMAC-SHA256 authentication --- +// +// Privacy model: sensitive fields are encrypted on-chain using a contract-held +// key (get_encryption_key). The ciphertext stored on-chain is NOT the plaintext. +// It is XOR-encrypted with a SHA-256-based keystream and authenticated with a +// 32-byte tag prepended to the ciphertext. +// +// Ciphertext layout: [ tag (32 bytes) | encrypted_data ] +// tag = SHA-256( key || nonce || plaintext ) +// keystream_block_i = SHA-256( key || nonce || i.to_be_bytes() ) +// encrypted_data[i] = plaintext[i] XOR keystream[i] +// +// An on-chain observer sees only tag + ciphertext; recovering plaintext +// requires the key. The nonce (timestamp || counter) ensures ciphertext +// differs across calls even for identical plaintext. + +fn sha256_block(env: &Env, key: &Bytes, nonce: &Bytes, block_idx: u64) -> Bytes { + let mut preimage = Bytes::new(env); + preimage.append(key); + preimage.append(nonce); + for b in block_idx.to_be_bytes() { + preimage.push_back(b); + } + env.crypto().sha256(&preimage).into() +} + +fn encrypt_sensitive_data(env: &Env, data: &Bytes, key: &Bytes) -> (Bytes, Bytes) { + // Unique nonce: 8-byte ledger timestamp || 4-byte monotonic counter + let counter_key = SystemKey::EncryptionNonceCounter; + let counter: u64 = env.storage().instance().get::(&counter_key).unwrap_or(0); + env.storage().instance().set(&counter_key, &(counter + 1)); + + let mut nonce_array = [0u8; 12]; + nonce_array[0..8].copy_from_slice(&env.ledger().timestamp().to_be_bytes()); + nonce_array[8..12].copy_from_slice(&(counter as u32).to_be_bytes()); + let nonce = Bytes::from_array(env, &nonce_array); + + let data_len = data.len() as usize; + + // XOR plaintext with SHA-256 keystream (CTR mode) + let mut encrypted = Bytes::new(env); + let mut block_idx: u64 = 0; + let mut offset = 0usize; + while offset < data_len { + let ks_block = sha256_block(env, key, &nonce, block_idx); + let block_bytes: [u8; 32] = ks_block.to_array(); + let chunk_end = (offset + 32).min(data_len); + for i in offset..chunk_end { + encrypted.push_back(data.get(i as u32).unwrap() ^ block_bytes[i - offset]); + } + offset += 32; + block_idx += 1; + } + + // Authentication tag: SHA-256( key || nonce || plaintext ) + let mut tag_preimage = Bytes::new(env); + tag_preimage.append(key); + tag_preimage.append(&nonce); + tag_preimage.append(data); + let tag: Bytes = env.crypto().sha256(&tag_preimage).into(); + + // Final ciphertext = tag (32 bytes) || encrypted_data + let mut ciphertext = Bytes::new(env); + ciphertext.append(&tag); + ciphertext.append(&encrypted); + + (nonce, ciphertext) +} + +fn decrypt_sensitive_data( + env: &Env, + ciphertext: &Bytes, + nonce: &Bytes, + key: &Bytes, +) -> Result { + let ct_len = ciphertext.len() as usize; + if ct_len < 32 { + return Err(()); + } + + // Split stored tag (first 32 bytes) from encrypted payload + let mut stored_tag = Bytes::new(env); + for i in 0..32u32 { + stored_tag.push_back(ciphertext.get(i).unwrap()); + } + let mut encrypted = Bytes::new(env); + for i in 32..ct_len as u32 { + encrypted.push_back(ciphertext.get(i).unwrap()); + } + + let enc_len = encrypted.len() as usize; + + // Decrypt: XOR with keystream + let mut plaintext = Bytes::new(env); + let mut block_idx: u64 = 0; + let mut offset = 0usize; + while offset < enc_len { + let ks_block = sha256_block(env, key, nonce, block_idx); + let block_bytes: [u8; 32] = ks_block.to_array(); + let chunk_end = (offset + 32).min(enc_len); + for i in offset..chunk_end { + plaintext.push_back(encrypted.get(i as u32).unwrap() ^ block_bytes[i - offset]); + } + offset += 32; + block_idx += 1; + } + + // Verify authentication tag: SHA-256( key || nonce || plaintext ) + let mut tag_preimage = Bytes::new(env); + tag_preimage.append(key); + tag_preimage.append(nonce); + tag_preimage.append(&plaintext); + let expected_tag: Bytes = env.crypto().sha256(&tag_preimage).into(); + + if stored_tag != expected_tag { + return Err(()); + } + + Ok(plaintext) +} +'@ + +$before = $t.Substring(0, $startIdx) +$after = $t.Substring($endIdx) +$result = $before + $newCode + $after + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $result) +Write-Host "Done. New length: $($result.Length)" diff --git a/stellar-contracts/fix_all.ps1 b/stellar-contracts/fix_all.ps1 new file mode 100644 index 0000000..8dad723 --- /dev/null +++ b/stellar-contracts/fix_all.ps1 @@ -0,0 +1,161 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +# ── 1. Replace encryption section (Hash<32> fix) ────────────────────────────── +$encStart = 204284 +$encEnd = 208678 + +$newEnc = @' +// --- REAL ENCRYPTION: SHA-256 CTR-mode stream cipher + authentication tag --- +// +// Privacy model: sensitive fields are encrypted on-chain using a contract-held +// key (get_encryption_key). The ciphertext stored on-chain is NOT the plaintext. +// It is XOR-encrypted with a SHA-256-based keystream and authenticated with a +// 32-byte tag prepended to the ciphertext. +// +// Ciphertext layout: [ tag (32 bytes) | encrypted_data ] +// tag = SHA-256( key || nonce || plaintext ) +// keystream_block_i = SHA-256( key || nonce || i.to_be_bytes() ) +// encrypted_data[i] = plaintext[i] XOR keystream[i] +// +// An on-chain observer sees only tag + ciphertext; recovering plaintext +// requires the key. The nonce (timestamp || counter) ensures ciphertext +// differs across calls even for identical plaintext. + +fn sha256_block(env: &Env, key: &Bytes, nonce: &Bytes, block_idx: u64) -> [u8; 32] { + let mut preimage = Bytes::new(env); + preimage.append(key); + preimage.append(nonce); + for b in block_idx.to_be_bytes() { + preimage.push_back(b); + } + env.crypto().sha256(&preimage).into() +} + +fn encrypt_sensitive_data(env: &Env, data: &Bytes, key: &Bytes) -> (Bytes, Bytes) { + // Unique nonce: 8-byte ledger timestamp || 4-byte monotonic counter + let counter_key = SystemKey::EncryptionNonceCounter; + let counter: u64 = env.storage().instance().get::(&counter_key).unwrap_or(0); + env.storage().instance().set(&counter_key, &(counter + 1)); + + let mut nonce_array = [0u8; 12]; + nonce_array[0..8].copy_from_slice(&env.ledger().timestamp().to_be_bytes()); + nonce_array[8..12].copy_from_slice(&(counter as u32).to_be_bytes()); + let nonce = Bytes::from_array(env, &nonce_array); + + let data_len = data.len() as usize; + + // XOR plaintext with SHA-256 keystream (CTR mode) + let mut encrypted = Bytes::new(env); + let mut block_idx: u64 = 0; + let mut offset = 0usize; + while offset < data_len { + let block_bytes: [u8; 32] = sha256_block(env, key, &nonce, block_idx); + let chunk_end = (offset + 32).min(data_len); + for i in offset..chunk_end { + encrypted.push_back(data.get(i as u32).unwrap() ^ block_bytes[i - offset]); + } + offset += 32; + block_idx += 1; + } + + // Authentication tag: SHA-256( key || nonce || plaintext ) + let mut tag_preimage = Bytes::new(env); + tag_preimage.append(key); + tag_preimage.append(&nonce); + tag_preimage.append(data); + let tag_arr: [u8; 32] = env.crypto().sha256(&tag_preimage).into(); + let tag = Bytes::from_array(env, &tag_arr); + + // Final ciphertext = tag (32 bytes) || encrypted_data + let mut ciphertext = Bytes::new(env); + ciphertext.append(&tag); + ciphertext.append(&encrypted); + + (nonce, ciphertext) +} + +fn decrypt_sensitive_data( + env: &Env, + ciphertext: &Bytes, + nonce: &Bytes, + key: &Bytes, +) -> Result { + let ct_len = ciphertext.len() as usize; + if ct_len < 32 { + return Err(()); + } + + // Split stored tag (first 32 bytes) from encrypted payload + let mut stored_tag = Bytes::new(env); + for i in 0..32u32 { + stored_tag.push_back(ciphertext.get(i).unwrap()); + } + let mut encrypted = Bytes::new(env); + for i in 32..ct_len as u32 { + encrypted.push_back(ciphertext.get(i).unwrap()); + } + + let enc_len = encrypted.len() as usize; + + // Decrypt: XOR with keystream + let mut plaintext = Bytes::new(env); + let mut block_idx: u64 = 0; + let mut offset = 0usize; + while offset < enc_len { + let block_bytes: [u8; 32] = sha256_block(env, key, nonce, block_idx); + let chunk_end = (offset + 32).min(enc_len); + for i in offset..chunk_end { + plaintext.push_back(encrypted.get(i as u32).unwrap() ^ block_bytes[i - offset]); + } + offset += 32; + block_idx += 1; + } + + // Verify authentication tag: SHA-256( key || nonce || plaintext ) + let mut tag_preimage = Bytes::new(env); + tag_preimage.append(key); + tag_preimage.append(nonce); + tag_preimage.append(&plaintext); + let expected_arr: [u8; 32] = env.crypto().sha256(&tag_preimage).into(); + let expected_tag = Bytes::from_array(env, &expected_arr); + + if stored_tag != expected_tag { + return Err(()); + } + + Ok(plaintext) +} +'@ + +$t = $t.Substring(0, $encStart) + $newEnc + $t.Substring($encEnd) + +# ── 2. Fix duplicate const blocks (remove the second set) ───────────────────── +$dup = " const MAX_STR_SHORT: u32 = 100; // names, types, test_type, outcome`r`n`r`n const MAX_STR_LONG: u32 = 1000; // description, notes, results, reference_ranges`r`n`r`n const MAX_VEC_MEDS: u32 = 50; // medications vec in a medical record`r`n`r`n const MAX_VEC_ATTACHMENTS: u32 = 20; // attachment_hashes vec" +$t = $t.Replace($dup, "") + +# ── 3. Add missing constants ─────────────────────────────────────────────────── +$constBlock = " const MAX_STR_SHORT: u32 = 100;`r`n const MAX_STR_LONG: u32 = 1000;`r`n const MAX_VEC_MEDS: u32 = 50;`r`n const MAX_VEC_ATTACHMENTS: u32 = 20;" +$newConstBlock = " const MAX_STR_SHORT: u32 = 100;`r`n const MAX_STR_LONG: u32 = 1000;`r`n const MAX_VEC_MEDS: u32 = 50;`r`n const MAX_VEC_ATTACHMENTS: u32 = 20;`r`n const MAX_VET_NAME_LEN: u32 = 100;`r`n const MAX_VET_LICENSE_LEN: u32 = 100;`r`n const MAX_VET_SPEC_LEN: u32 = 200;`r`n const MAX_REVIEW_COMMENT_LEN: u32 = 500;" +$t = $t.Replace($constBlock, $newConstBlock) + +# ── 4. Fix ScErrorCode variants ─────────────────────────────────────────────── +$t = $t.Replace("ScErrorCode::DuplicateName", "ScErrorCode::ExistingValue") +$t = $t.Replace("ScErrorCode::ArithmeticOverflow", "ScErrorCode::ArithDomain") + +# ── 5. Fix safe_increment calls inside impl block (add crate:: prefix) ───────── +# safe_increment is a pub(crate) free function; inside the impl block it needs +# the full path. Replace bare safe_increment( with crate::safe_increment( only +# inside the impl block (before the closing }) — simplest: replace all occurrences +# since the free function definition uses the same name and won't be affected. +$t = $t.Replace("safe_increment(", "crate::safe_increment(") +# But the definition itself must stay as-is +$t = $t.Replace("pub(crate) fn crate::safe_increment(", "pub(crate) fn safe_increment(") + +# ── 6. Fix grooming functions that are outside the impl block ───────────────── +$t = $t.Replace( + " let history = Self::get_grooming_history(env, pet_id);", + " let history = PetChainContract::get_grooming_history(env, pet_id);" +) + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $t) +Write-Host "Done. New length: $($t.Length)" diff --git a/stellar-contracts/fix_all_warnings.ps1 b/stellar-contracts/fix_all_warnings.ps1 new file mode 100644 index 0000000..02a0b1b --- /dev/null +++ b/stellar-contracts/fix_all_warnings.ps1 @@ -0,0 +1,151 @@ + +# ── Fix 1: test.rs ──────────────────────────────────────────────────────────── +# Lines 1314-1491 are orphaned #[test] fns inside mod test at wrong nesting. +# The structure is: +# line 1313: closing } of test_book_slot (inside mod test) +# line 1314: #[test] +# line 1315: fn test_grant_consent { <- this fn contains #[test] fn test_get_version nested inside it +# ... +# line 1478: } <- closes mod test +# line 1479: (blank) +# line 1480: #[test] / fn test_upgrade_requires_admin <- outside mod test +# line 1491: } +# line 1492: } <- extra brace +# +# Fix: replace lines 1313-1492 with properly structured code + +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test.rs' +$lines = [System.IO.File]::ReadAllLines($f) +$newLines = [System.Collections.Generic.List[string]]::new() + +for ($i = 0; $i -lt $lines.Length; $i++) { + $ln = $i + 1 + + # At line 1313 (closing brace of test_book_slot inside mod test), + # close mod test and start a new mod for the orphaned functions + if ($ln -eq 1313) { + $newLines.Add(' }') # close test_book_slot + $newLines.Add('}') # close mod test + $newLines.Add('') + $newLines.Add('#[cfg(test)]') + $newLines.Add('mod test_consent_upgrade {') + $newLines.Add(' use crate::*;') + $newLines.Add(' use soroban_sdk::{testutils::Address as _, Address, Env, String};') + $newLines.Add('') + continue + } + + # Skip lines 1314-1492 (the old orphaned section + old mod test closing braces) + if ($ln -ge 1314 -and $ln -le 1492) { + # But we need to keep the content of the functions, just re-emit them + # with proper indentation inside the new mod + $line = $lines[$i] + + # Skip the nested #[test] inside test_grant_consent (line 1316) and + # the fn test_get_version that was nested inside it (lines 1317-1326) + if ($ln -ge 1316 -and $ln -le 1326) { continue } + + # Skip the old mod test closing braces (lines 1478, 1492) + if ($ln -eq 1478 -or $ln -eq 1492) { continue } + + # Add proper indentation (these fns were at module level, add 4 spaces) + if ($line.Length -gt 0 -and -not $line.StartsWith('#')) { + $newLines.Add(' ' + $line) + } else { + $newLines.Add($line) + } + continue + } + + # Close the new mod after line 1492 + if ($ln -eq 1493) { + $newLines.Add('}') + $newLines.Add('') + } + + $newLines.Add($lines[$i]) +} + +[System.IO.File]::WriteAllLines($f, $newLines) +Write-Host ('test.rs done. Lines: ' + $newLines.Count) + +# ── Fix 2: test_admin_initialization.rs — remove unused Ledger import ───────── +$f2 = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test_admin_initialization.rs' +$lines2 = [System.IO.File]::ReadAllLines($f2) +$new2 = [System.Collections.Generic.List[string]]::new() +for ($i = 0; $i -lt $lines2.Length; $i++) { + $line = $lines2[$i] + # Replace the import line to remove Ledger + if ($line -match 'testutils.*Ledger') { + $new2.Add(' testutils::Address as _,') + continue + } + $new2.Add($line) +} +[System.IO.File]::WriteAllLines($f2, $new2) +Write-Host ('test_admin_initialization.rs done.') + +# ── Fix 3: test_search_medical_records.rs ───────────────────────────────────── +$f3 = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test_search_medical_records.rs' +$lines3 = [System.IO.File]::ReadAllLines($f3) +$new3 = [System.Collections.Generic.List[string]]::new() +for ($i = 0; $i -lt $lines3.Length; $i++) { + $ln = $i + 1 + $line = $lines3[$i] + # Remove unused Medication import + if ($line -match 'use crate.*Medication') { + $new3.Add(' use crate::{Gender, PetChainContract, PetChainContractClient, PrivacyLevel, Species};') + continue + } + # Fix useless >= 0 comparison (line 137) + if ($line -match 'assert!\(results\.len\(\) >= 0\)') { + $new3.Add(' let _ = results.len(); // boundary check — no panic') + continue + } + $new3.Add($line) +} +[System.IO.File]::WriteAllLines($f3, $new3) +Write-Host ('test_search_medical_records.rs done.') + +# ── Fix 4: test_get_pet_decryption.rs ───────────────────────────────────────── +$f4 = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test_get_pet_decryption.rs' +$lines4 = [System.IO.File]::ReadAllLines($f4) +$new4 = [System.Collections.Generic.List[string]]::new() +for ($i = 0; $i -lt $lines4.Length; $i++) { + $ln = $i + 1 + $line = $lines4[$i] + # Remove unused Vec import + if ($line -match 'testutils::Address as _, Address, Bytes, Env, String, Vec') { + $new4.Add(' testutils::Address as _, Address, Bytes, Env, String,') + continue + } + # Fix unused env variable (line 222) + if ($ln -eq 222 -and $line -match 'let \(env, client\) = setup\(\)') { + $new4.Add(' let (_env, client) = setup();') + continue + } + $new4.Add($line) +} +[System.IO.File]::WriteAllLines($f4, $new4) +Write-Host ('test_get_pet_decryption.rs done.') + +# ── Fix 5: test_input_limits.rs — add lifetime annotation ───────────────────── +$f5 = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test_input_limits.rs' +$lines5 = [System.IO.File]::ReadAllLines($f5) +$new5 = [System.Collections.Generic.List[string]]::new() +for ($i = 0; $i -lt $lines5.Length; $i++) { + $line = $lines5[$i] + # Fix setup return type + if ($line -match '^fn setup\(env: &Env\) -> \(PetChainContractClient,') { + $new5.Add("fn setup(env: &Env) -> (PetChainContractClient<'_>, Address, Address, u64) {") + continue + } + # Fix setup_with_vet return type + if ($line -match '^fn setup_with_vet\(env: &Env\) -> \(PetChainContractClient,') { + $new5.Add("fn setup_with_vet(env: &Env) -> (PetChainContractClient<'_>, Address, Address, Address, u64) {") + continue + } + $new5.Add($line) +} +[System.IO.File]::WriteAllLines($f5, $new5) +Write-Host ('test_input_limits.rs done.') diff --git a/stellar-contracts/fix_braces.ps1 b/stellar-contracts/fix_braces.ps1 new file mode 100644 index 0000000..3f897a8 --- /dev/null +++ b/stellar-contracts/fix_braces.ps1 @@ -0,0 +1,27 @@ +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test.rs' +$lines = [System.IO.File]::ReadAllLines($f) +$newLines = [System.Collections.Generic.List[string]]::new() + +for ($i = 0; $i -lt $lines.Length; $i++) { + $ln = $i + 1 + $line = $lines[$i] + + # After line 1378 (assert!(revoked);), add closing brace for test_revoke_consent + if ($ln -eq 1379 -and $line.Trim() -eq '') { + $newLines.Add('}') + $newLines.Add('') + continue + } + + # After line 1433 (assert_eq!(history.len(), 1);), add closing brace for test_consent_history + if ($ln -eq 1434 -and $line.Trim() -eq '') { + $newLines.Add('}') + $newLines.Add('') + continue + } + + $newLines.Add($line) +} + +[System.IO.File]::WriteAllLines($f, $newLines) +Write-Host ('Done. Lines: ' + $newLines.Count) diff --git a/stellar-contracts/fix_callers.ps1 b/stellar-contracts/fix_callers.ps1 new file mode 100644 index 0000000..fbe868a --- /dev/null +++ b/stellar-contracts/fix_callers.ps1 @@ -0,0 +1,17 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +$t = $t.Replace( + 'Self::get_pet(env.clone(), pid, owner.clone())', + 'Self::get_pet(env.clone(), pid)' +) +$t = $t.Replace( + 'Self::get_pet(env.clone(), pid, raw.owner.clone())', + 'Self::get_pet(env.clone(), pid)' +) +$t = $t.Replace( + 'Self::get_pet(env.clone(), id, pet.owner.clone())', + 'Self::get_pet(env.clone(), id)' +) + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $t) +Write-Host "Done. New length: $($t.Length)" diff --git a/stellar-contracts/fix_dead_code.ps1 b/stellar-contracts/fix_dead_code.ps1 new file mode 100644 index 0000000..b808e97 --- /dev/null +++ b/stellar-contracts/fix_dead_code.ps1 @@ -0,0 +1,12 @@ +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test.rs' +$lines = [System.IO.File]::ReadAllLines($f) +$newLines = [System.Collections.Generic.List[string]]::new() +for ($i = 0; $i -lt $lines.Length; $i++) { + $ln = $i + 1 + if ($ln -eq 3890) { + $newLines.Add(' #[allow(dead_code)]') + } + $newLines.Add($lines[$i]) +} +[System.IO.File]::WriteAllLines($f, $newLines) +Write-Host ('Done. Lines: ' + $newLines.Count) diff --git a/stellar-contracts/fix_enc2.ps1 b/stellar-contracts/fix_enc2.ps1 new file mode 100644 index 0000000..bdffa65 --- /dev/null +++ b/stellar-contracts/fix_enc2.ps1 @@ -0,0 +1,136 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +$startIdx = 204284 +$endIdx = 208587 # one past closing brace of decrypt_sensitive_data + +$newCode = @' +// --- REAL ENCRYPTION: SHA-256 CTR-mode stream cipher + authentication tag --- +// +// Privacy model: sensitive fields are encrypted on-chain using a contract-held +// key (get_encryption_key). The ciphertext stored on-chain is NOT the plaintext. +// It is XOR-encrypted with a SHA-256-based keystream and authenticated with a +// 32-byte tag prepended to the ciphertext. +// +// Ciphertext layout: [ tag (32 bytes) | encrypted_data ] +// tag = SHA-256( key || nonce || plaintext ) +// keystream_block_i = SHA-256( key || nonce || i.to_be_bytes() ) +// encrypted_data[i] = plaintext[i] XOR keystream[i] +// +// An on-chain observer sees only tag + ciphertext; recovering plaintext +// requires the key. The nonce (timestamp || counter) ensures ciphertext +// differs across calls even for identical plaintext. + +fn sha256_block(env: &Env, key: &Bytes, nonce: &Bytes, block_idx: u64) -> BytesN<32> { + let mut preimage = Bytes::new(env); + preimage.append(key); + preimage.append(nonce); + for b in block_idx.to_be_bytes() { + preimage.push_back(b); + } + env.crypto().sha256(&preimage) +} + +fn encrypt_sensitive_data(env: &Env, data: &Bytes, key: &Bytes) -> (Bytes, Bytes) { + // Unique nonce: 8-byte ledger timestamp || 4-byte monotonic counter + let counter_key = SystemKey::EncryptionNonceCounter; + let counter: u64 = env.storage().instance().get::(&counter_key).unwrap_or(0); + env.storage().instance().set(&counter_key, &(counter + 1)); + + let mut nonce_array = [0u8; 12]; + nonce_array[0..8].copy_from_slice(&env.ledger().timestamp().to_be_bytes()); + nonce_array[8..12].copy_from_slice(&(counter as u32).to_be_bytes()); + let nonce = Bytes::from_array(env, &nonce_array); + + let data_len = data.len() as usize; + + // XOR plaintext with SHA-256 keystream (CTR mode) + let mut encrypted = Bytes::new(env); + let mut block_idx: u64 = 0; + let mut offset = 0usize; + while offset < data_len { + let ks_block: BytesN<32> = sha256_block(env, key, &nonce, block_idx); + let block_bytes: [u8; 32] = ks_block.into(); + let chunk_end = (offset + 32).min(data_len); + for i in offset..chunk_end { + encrypted.push_back(data.get(i as u32).unwrap() ^ block_bytes[i - offset]); + } + offset += 32; + block_idx += 1; + } + + // Authentication tag: SHA-256( key || nonce || plaintext ) + let mut tag_preimage = Bytes::new(env); + tag_preimage.append(key); + tag_preimage.append(&nonce); + tag_preimage.append(data); + let tag_bn: BytesN<32> = env.crypto().sha256(&tag_preimage); + let tag: Bytes = tag_bn.into(); + + // Final ciphertext = tag (32 bytes) || encrypted_data + let mut ciphertext = Bytes::new(env); + ciphertext.append(&tag); + ciphertext.append(&encrypted); + + (nonce, ciphertext) +} + +fn decrypt_sensitive_data( + env: &Env, + ciphertext: &Bytes, + nonce: &Bytes, + key: &Bytes, +) -> Result { + let ct_len = ciphertext.len() as usize; + if ct_len < 32 { + return Err(()); + } + + // Split stored tag (first 32 bytes) from encrypted payload + let mut stored_tag = Bytes::new(env); + for i in 0..32u32 { + stored_tag.push_back(ciphertext.get(i).unwrap()); + } + let mut encrypted = Bytes::new(env); + for i in 32..ct_len as u32 { + encrypted.push_back(ciphertext.get(i).unwrap()); + } + + let enc_len = encrypted.len() as usize; + + // Decrypt: XOR with keystream + let mut plaintext = Bytes::new(env); + let mut block_idx: u64 = 0; + let mut offset = 0usize; + while offset < enc_len { + let ks_block: BytesN<32> = sha256_block(env, key, nonce, block_idx); + let block_bytes: [u8; 32] = ks_block.into(); + let chunk_end = (offset + 32).min(enc_len); + for i in offset..chunk_end { + plaintext.push_back(encrypted.get(i as u32).unwrap() ^ block_bytes[i - offset]); + } + offset += 32; + block_idx += 1; + } + + // Verify authentication tag: SHA-256( key || nonce || plaintext ) + let mut tag_preimage = Bytes::new(env); + tag_preimage.append(key); + tag_preimage.append(nonce); + tag_preimage.append(&plaintext); + let expected_bn: BytesN<32> = env.crypto().sha256(&tag_preimage); + let expected_tag: Bytes = expected_bn.into(); + + if stored_tag != expected_tag { + return Err(()); + } + + Ok(plaintext) +} +'@ + +$before = $t.Substring(0, $startIdx) +$after = $t.Substring($endIdx) +$result = $before + $newCode + $after + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $result) +Write-Host "Done. New length: $($result.Length)" diff --git a/stellar-contracts/fix_final.ps1 b/stellar-contracts/fix_final.ps1 new file mode 100644 index 0000000..169df1d --- /dev/null +++ b/stellar-contracts/fix_final.ps1 @@ -0,0 +1,74 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +# ── 1. Remove duplicate const block (second occurrence with comments) ────────── +$dupBlock = " const MAX_STR_SHORT: u32 = 100; // names, types, test_type, outcome`n const MAX_STR_LONG: u32 = 1000; // description, notes, results, reference_ranges`n const MAX_VEC_MEDS: u32 = 50; // medications vec in a medical record`n const MAX_VEC_ATTACHMENTS: u32 = 20; // attachment_hashes vec" +$t = $t.Replace($dupBlock, "") + +# ── 2. Fix broken get_grooming_history ──────────────────────────────────────── +$brokenGrooming = @' +pub fn get_grooming_history(env: Env, pet_id: u64) -> Vec { + let count: u64 = env.storage().instance().get(&(Symbol::new(&env, "pet_grooming"), pet_id)).unwrap_or(0); + let mut history = Vec::new(&env); + for i in 1..=count { + if let Some(record_id) = env.storage().instance().get::<_, u64>(&(Symbol::new(&env, "pet_grooming_idx"), pet_id, i)) { + if let Some(record) = env.storage().instance().get::<_, GroomingRecord>(&(Symbol::new(&env, "grooming"), record_id)) { + let count: u64 = env + .storage() + .instance() + .get(&(Symbol::new(&env, "pet_grooming"), pet_id)) + .unwrap_or(0); + let mut history = Vec::new(&env); + for i in 1..=count { + if let Some(record_id) = env.storage().instance().get::<_, u64>(&( + Symbol::new(&env, "pet_grooming_idx"), + pet_id, + i, + )) { + if let Some(record) = env + .storage() + .instance() + .get::<_, GroomingRecord>(&(Symbol::new(&env, "grooming"), record_id)) + { + history.push_back(record); + } + } + } + history + } + + +'@ + +$fixedGrooming = @' +pub fn get_grooming_history(env: Env, pet_id: u64) -> Vec { + let count: u64 = env.storage().instance() + .get(&(Symbol::new(&env, "pet_grooming"), pet_id)).unwrap_or(0); + let mut history = Vec::new(&env); + for i in 1..=count { + if let Some(record_id) = env.storage().instance() + .get::<_, u64>(&(Symbol::new(&env, "pet_grooming_idx"), pet_id, i)) + { + if let Some(record) = env.storage().instance() + .get::<_, GroomingRecord>(&(Symbol::new(&env, "grooming"), record_id)) + { + history.push_back(record); + } + } + } + history + } + + +'@ + +$t = $t.Replace($brokenGrooming, $fixedGrooming) + +# ── 3. Fix get_vet_average_rating (total is u32 but closure captures env) ───── +# The issue: `total = total.checked_add(1)` — `total` is u32 but the closure +# captures `&env` which is moved. Fix: use rating instead of 1. +$brokenRating = " let mut total = 0u32;`n for review in reviews.iter() {`n total = total.checked_add(1).unwrap_or_else(|| panic_with_error!(&env, ContractError::CounterOverflow));`n }`n total / reviews.len()" +$fixedRating = " let mut total = 0u32;`n for review in reviews.iter() {`n total = total.saturating_add(review.rating);`n }`n total / reviews.len()" +$t = $t.Replace($brokenRating, $fixedRating) + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $t) +Write-Host "Done. New length: $($t.Length)" diff --git a/stellar-contracts/fix_get_pet.ps1 b/stellar-contracts/fix_get_pet.ps1 new file mode 100644 index 0000000..9891234 --- /dev/null +++ b/stellar-contracts/fix_get_pet.ps1 @@ -0,0 +1,91 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +$startIdx = 44126 +$endIdx = 44126 + 2214 # covers the full broken get_pet body up to the blank line before get_pet_age + +$newGetPet = @' +pub fn get_pet(env: Env, id: u64) -> Option { + let pet = env + .storage() + .instance() + .get::(&DataKey::Pet(id))?; + + let key = Self::get_encryption_key(&env); + + // Propagate decryption failures as None rather than masking them + // with a sentinel "Error" string. Any corrupt ciphertext or nonce + // mismatch causes the whole read to return None deterministically. + let decrypted_name = decrypt_sensitive_data( + &env, + &pet.encrypted_name.ciphertext, + &pet.encrypted_name.nonce, + &key, + ) + .ok()?; + let name = String::from_xdr(&env, &decrypted_name).ok()?; + + let decrypted_birthday = decrypt_sensitive_data( + &env, + &pet.encrypted_birthday.ciphertext, + &pet.encrypted_birthday.nonce, + &key, + ) + .ok()?; + let birthday = String::from_xdr(&env, &decrypted_birthday).ok()?; + + let decrypted_breed = decrypt_sensitive_data( + &env, + &pet.encrypted_breed.ciphertext, + &pet.encrypted_breed.nonce, + &key, + ) + .ok()?; + let breed = String::from_xdr(&env, &decrypted_breed).ok()?; + + let a_bytes = decrypt_sensitive_data( + &env, + &pet.encrypted_allergies.ciphertext, + &pet.encrypted_allergies.nonce, + &key, + ) + .ok()?; + let allergies = Vec::::from_xdr(&env, &a_bytes).ok()?; + + let profile = PetProfile { + id: pet.id, + owner: pet.owner.clone(), + privacy_level: pet.privacy_level, + name, + birthday, + active: pet.active, + created_at: pet.created_at, + updated_at: pet.updated_at, + new_owner: pet.new_owner, + species: pet.species, + gender: pet.gender, + breed, + color: pet.color, + weight: pet.weight, + microchip_id: pet.microchip_id, + allergies, + }; + + Self::log_access( + &env, + id, + pet.owner, + AccessAction::Read, + String::from_str(&env, "Pet profile accessed"), + ); + Some(profile) + } + + +'@ + +$before = $t.Substring(0, $startIdx) +$after = $t.Substring($endIdx) +$result = $before + $newGetPet + $after + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $result) +Write-Host "Done. New length: $($result.Length)" diff --git a/stellar-contracts/fix_impl_brace.ps1 b/stellar-contracts/fix_impl_brace.ps1 new file mode 100644 index 0000000..bac122f --- /dev/null +++ b/stellar-contracts/fix_impl_brace.ps1 @@ -0,0 +1,18 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +# Find the gap between calculate_age closing and the encryption section +$encIdx = $t.IndexOf('// --- REAL ENCRYPTION') +[Console]::WriteLine('encryption section at: ' + $encIdx) + +# Check what's right before the encryption section +[Console]::WriteLine('Before encryption: ' + $t.Substring($encIdx - 30, 30)) + +# Insert closing brace for impl block right before the encryption section +$before = $t.Substring(0, $encIdx) +$after = $t.Substring($encIdx) + +# The impl block needs a closing brace +$result = $before + "}`n`n" + $after + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $result) +Write-Host "Done. New length: $($result.Length)" diff --git a/stellar-contracts/fix_mod_structure.ps1 b/stellar-contracts/fix_mod_structure.ps1 new file mode 100644 index 0000000..32fb4b0 --- /dev/null +++ b/stellar-contracts/fix_mod_structure.ps1 @@ -0,0 +1,170 @@ +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test.rs' +$lines = [System.IO.File]::ReadAllLines($f) +$newLines = [System.Collections.Generic.List[string]]::new() + +# Keep lines 1..1312 (0-indexed 0..1311) — up to and including assert_eq!(slots.len(), 0); +for ($i = 0; $i -lt 1312; $i++) { + $newLines.Add($lines[$i]) +} + +# Close test_book_slot and mod test +$newLines.Add(' }') +$newLines.Add('}') +$newLines.Add('') + +# Write the new clean mod test_consent_upgrade +$newLines.Add('#[cfg(test)]') +$newLines.Add('mod test_consent_upgrade {') +$newLines.Add(' use crate::*;') +$newLines.Add(' use soroban_sdk::{testutils::Address as _, Address, Env, String};') +$newLines.Add('') +$newLines.Add(' fn setup_contract(env: &Env) -> (PetChainContractClient<''static>, Address) {') +$newLines.Add(' let id = env.register_contract(None, PetChainContract);') +$newLines.Add(' let client = PetChainContractClient::new(env, &id);') +$newLines.Add(' let admin = Address::generate(env);') +$newLines.Add(' client.init_admin(&admin);') +$newLines.Add(' (client, admin)') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' fn register_pet(client: &PetChainContractClient, env: &Env, owner: &Address) -> u64 {') +$newLines.Add(' client.register_pet(') +$newLines.Add(' owner,') +$newLines.Add(' &String::from_str(env, "Buddy"),') +$newLines.Add(' &String::from_str(env, "2020-01-01"),') +$newLines.Add(' &Gender::Male,') +$newLines.Add(' &Species::Dog,') +$newLines.Add(' &String::from_str(env, "Labrador"),') +$newLines.Add(' &String::from_str(env, "Unknown"),') +$newLines.Add(' &0u32,') +$newLines.Add(' &None,') +$newLines.Add(' &PrivacyLevel::Public,') +$newLines.Add(' )') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' #[test]') +$newLines.Add(' fn test_grant_consent() {') +$newLines.Add(' let env = Env::default();') +$newLines.Add(' env.mock_all_auths();') +$newLines.Add(' let (client, _admin) = setup_contract(&env);') +$newLines.Add(' let owner = Address::generate(&env);') +$newLines.Add(' let pet_id = register_pet(&client, &env, &owner);') +$newLines.Add(' let grantee = Address::generate(&env);') +$newLines.Add(' let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee);') +$newLines.Add(' assert_eq!(consent_id, 1);') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' #[test]') +$newLines.Add(' fn test_get_version() {') +$newLines.Add(' let env = Env::default();') +$newLines.Add(' env.mock_all_auths();') +$newLines.Add(' let id = env.register_contract(None, PetChainContract);') +$newLines.Add(' let client = PetChainContractClient::new(&env, &id);') +$newLines.Add(' let version = client.get_version();') +$newLines.Add(' assert_eq!(version.major, 1);') +$newLines.Add(' assert_eq!(version.minor, 0);') +$newLines.Add(' assert_eq!(version.patch, 0);') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' #[test]') +$newLines.Add(' fn test_propose_upgrade() {') +$newLines.Add(' let env = Env::default();') +$newLines.Add(' env.mock_all_auths();') +$newLines.Add(' let (client, admin) = setup_contract(&env);') +$newLines.Add(' let owner = Address::generate(&env);') +$newLines.Add(' let pet_id = register_pet(&client, &env, &owner);') +$newLines.Add(' let grantee = Address::generate(&env);') +$newLines.Add(' let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee);') +$newLines.Add(' assert_eq!(consent_id, 1);') +$newLines.Add(' let wasm_hash = soroban_sdk::BytesN::from_array(&env, &[0u8; 32]);') +$newLines.Add(' let proposal_id = client.propose_upgrade(&admin, &wasm_hash);') +$newLines.Add(' assert_eq!(proposal_id, 1);') +$newLines.Add(' let proposal = client.get_upgrade_proposal(&proposal_id).unwrap();') +$newLines.Add(' assert_eq!(proposal.approved, false);') +$newLines.Add(' assert_eq!(proposal.executed, false);') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' #[test]') +$newLines.Add(' fn test_revoke_consent() {') +$newLines.Add(' let env = Env::default();') +$newLines.Add(' env.mock_all_auths();') +$newLines.Add(' let (client, _admin) = setup_contract(&env);') +$newLines.Add(' let owner = Address::generate(&env);') +$newLines.Add(' let pet_id = register_pet(&client, &env, &owner);') +$newLines.Add(' let grantee = Address::generate(&env);') +$newLines.Add(' let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee);') +$newLines.Add(' let revoked = client.revoke_consent(&consent_id, &owner);') +$newLines.Add(' assert!(revoked);') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' #[test]') +$newLines.Add(' fn test_approve_upgrade() {') +$newLines.Add(' let env = Env::default();') +$newLines.Add(' env.mock_all_auths();') +$newLines.Add(' let (client, admin) = setup_contract(&env);') +$newLines.Add(' let owner = Address::generate(&env);') +$newLines.Add(' let pet_id = register_pet(&client, &env, &owner);') +$newLines.Add(' let grantee = Address::generate(&env);') +$newLines.Add(' let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Research, &grantee);') +$newLines.Add(' let revoked = client.revoke_consent(&consent_id, &owner);') +$newLines.Add(' assert_eq!(revoked, true);') +$newLines.Add(' let wasm_hash = soroban_sdk::BytesN::from_array(&env, &[0u8; 32]);') +$newLines.Add(' let proposal_id = client.propose_upgrade(&admin, &wasm_hash);') +$newLines.Add(' let approved = client.approve_upgrade(&proposal_id);') +$newLines.Add(' assert_eq!(approved, true);') +$newLines.Add(' let proposal = client.get_upgrade_proposal(&proposal_id).unwrap();') +$newLines.Add(' assert_eq!(proposal.approved, true);') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' #[test]') +$newLines.Add(' fn test_consent_history() {') +$newLines.Add(' let env = Env::default();') +$newLines.Add(' env.mock_all_auths();') +$newLines.Add(' let (client, _admin) = setup_contract(&env);') +$newLines.Add(' let owner = Address::generate(&env);') +$newLines.Add(' let pet_id = register_pet(&client, &env, &owner);') +$newLines.Add(' let grantee = Address::generate(&env);') +$newLines.Add(' client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee);') +$newLines.Add(' let history = client.get_consent_history(&pet_id);') +$newLines.Add(' assert_eq!(history.len(), 1);') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' #[test]') +$newLines.Add(' fn test_migrate_version() {') +$newLines.Add(' let env = Env::default();') +$newLines.Add(' env.mock_all_auths();') +$newLines.Add(' let (client, _admin) = setup_contract(&env);') +$newLines.Add(' let owner = Address::generate(&env);') +$newLines.Add(' let pet_id = register_pet(&client, &env, &owner);') +$newLines.Add(' let ins = Address::generate(&env);') +$newLines.Add(' let res = Address::generate(&env);') +$newLines.Add(' client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &ins);') +$newLines.Add(' client.grant_consent(&pet_id, &owner, &ConsentType::Research, &res);') +$newLines.Add(' client.revoke_consent(&1u64, &owner);') +$newLines.Add(' let history = client.get_consent_history(&pet_id);') +$newLines.Add(' assert_eq!(history.len(), 2);') +$newLines.Add(' assert_eq!(history.get(0).unwrap().is_active, false);') +$newLines.Add(' assert_eq!(history.get(1).unwrap().is_active, true);') +$newLines.Add(' }') +$newLines.Add('') +$newLines.Add(' #[test]') +$newLines.Add(' #[should_panic]') +$newLines.Add(' fn test_upgrade_requires_admin() {') +$newLines.Add(' let env = Env::default();') +$newLines.Add(' env.mock_all_auths();') +$newLines.Add(' let id = env.register_contract(None, PetChainContract);') +$newLines.Add(' let client = PetChainContractClient::new(&env, &id);') +$newLines.Add(' let wasm_hash = soroban_sdk::BytesN::from_array(&env, &[0u8; 32]);') +$newLines.Add(' client.propose_upgrade(&Address::generate(&env), &wasm_hash);') +$newLines.Add(' }') +$newLines.Add('}') +$newLines.Add('') + +# Skip old orphaned section: lines 1313..1487 (0-indexed 1312..1486) +# Then continue from line 1488 (0-indexed 1487) which is the blank line before mod test_b +$startFrom = 1487 +for ($i = $startFrom; $i -lt $lines.Length; $i++) { + $newLines.Add($lines[$i]) +} + +[System.IO.File]::WriteAllLines($f, $newLines) +Write-Host ('Done. Lines: ' + $newLines.Count) diff --git a/stellar-contracts/fix_other_tests.ps1 b/stellar-contracts/fix_other_tests.ps1 new file mode 100644 index 0000000..fd302f2 --- /dev/null +++ b/stellar-contracts/fix_other_tests.ps1 @@ -0,0 +1,41 @@ + +# Fix test_admin_initialization.rs +$f1 = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test_admin_initialization.rs' +$lines1 = [System.IO.File]::ReadAllLines($f1) +$new1 = [System.Collections.Generic.List[string]]::new() +for ($i = 0; $i -lt $lines1.Length; $i++) { + $ln = $i + 1 + $line = $lines1[$i] + if ($ln -eq 123) { + $new1.Add(' let mut new_admins_a = soroban_sdk::Vec::new(&env);') + $new1.Add(' new_admins_a.push_back(proposer.clone());') + $new1.Add(' let action = ProposalAction::ChangeAdmin((new_admins_a, 1u32));') + continue + } + if ($ln -eq 175) { + $new1.Add(' let mut new_admins_b = soroban_sdk::Vec::new(&env);') + $new1.Add(' new_admins_b.push_back(admin.clone());') + $new1.Add(' new_admins_b.push_back(admin2.clone());') + $new1.Add(' let action = ProposalAction::ChangeAdmin((new_admins_b, 2u32));') + continue + } + $new1.Add($line) +} +[System.IO.File]::WriteAllLines($f1, $new1) +Write-Host ('test_admin_initialization.rs done. Lines: ' + $new1.Count) + +# Fix test_nutrition.rs - remove extra &owner arg from get_pet call +$f2 = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test_nutrition.rs' +$lines2 = [System.IO.File]::ReadAllLines($f2) +$new2 = [System.Collections.Generic.List[string]]::new() +for ($i = 0; $i -lt $lines2.Length; $i++) { + $ln = $i + 1 + $line = $lines2[$i] + if ($ln -eq 80) { + $new2.Add(' let profile = client.get_pet(&pet_id).unwrap();') + continue + } + $new2.Add($line) +} +[System.IO.File]::WriteAllLines($f2, $new2) +Write-Host ('test_nutrition.rs done. Lines: ' + $new2.Count) diff --git a/stellar-contracts/fix_placement.ps1 b/stellar-contracts/fix_placement.ps1 new file mode 100644 index 0000000..d9e33cc --- /dev/null +++ b/stellar-contracts/fix_placement.ps1 @@ -0,0 +1,28 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +# ── 1. Remove the misplaced safe_increment (between #[contractimpl] and impl) ─ +$misplaced = "#[contractimpl]`r`n// --- OVERFLOW-SAFE COUNTER HELPER ---`r`npub(crate) fn safe_increment(count: u64) -> u64 {`r`n count.checked_add(1).unwrap_or(u64::MAX)`r`n}`r`n`r`nimpl PetChainContract {" +$t = $t.Replace($misplaced, "#[contractimpl]`r`nimpl PetChainContract {") + +# Also try LF version +$misplacedLF = "#[contractimpl]`n// --- OVERFLOW-SAFE COUNTER HELPER ---`npub(crate) fn safe_increment(count: u64) -> u64 {`n count.checked_add(1).unwrap_or(u64::MAX)`n}`n`nimpl PetChainContract {" +$t = $t.Replace($misplacedLF, "#[contractimpl]`nimpl PetChainContract {") + +# ── 2. Insert safe_increment BEFORE #[contractimpl] ─────────────────────────── +$contractImplAttr = "#[contractimpl]`r`nimpl PetChainContract {" +$safeIncWithImpl = "// --- OVERFLOW-SAFE COUNTER HELPER ---`r`npub(crate) fn safe_increment(count: u64) -> u64 {`r`n count.checked_add(1).unwrap_or(u64::MAX)`r`n}`r`n`r`n#[contractimpl]`r`nimpl PetChainContract {" +$t = $t.Replace($contractImplAttr, $safeIncWithImpl) + +# Also try LF version +$contractImplAttrLF = "#[contractimpl]`nimpl PetChainContract {" +$safeIncWithImplLF = "// --- OVERFLOW-SAFE COUNTER HELPER ---`npub(crate) fn safe_increment(count: u64) -> u64 {`n count.checked_add(1).unwrap_or(u64::MAX)`n}`n`n#[contractimpl]`nimpl PetChainContract {" +$t = $t.Replace($contractImplAttrLF, $safeIncWithImplLF) + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $t) +Write-Host "Done. New length: $($t.Length)" + +# Verify placement +$t2 = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') +$idx = $t2.IndexOf('pub(crate) fn safe_increment') +Write-Host "safe_increment now at: $idx" +Write-Host $t2.Substring($idx - 50, 200) diff --git a/stellar-contracts/fix_positions.ps1 b/stellar-contracts/fix_positions.ps1 new file mode 100644 index 0000000..bfe8f60 --- /dev/null +++ b/stellar-contracts/fix_positions.ps1 @@ -0,0 +1,67 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +# ── 1. Remove duplicate const block (positions 59615..59939) ────────────────── +# The duplicate block starts just before "const MAX_STR_SHORT" (second occurrence) +# and ends just before "pub fn register_vet" +$dupStart = 59615 # includes the leading newline +$dupEnd = 59939 # up to but not including " pub fn register_vet" +$t = $t.Substring(0, $dupStart) + $t.Substring($dupEnd) + +# Recalculate positions after removal (removed 324 chars) +$offset1 = 324 + +# ── 2. Fix get_vet_average_rating (was at 150681, now 150681-324=150357) ─────── +$vatStart = 150681 - $offset1 +$vatEnd = 151158 - $offset1 + +$fixedRating = "pub fn get_vet_average_rating(env: Env, vet: Address) -> u32 { + let reviews = Self::get_vet_reviews(env.clone(), vet); + if reviews.is_empty() { + return 0; + } + let mut total = 0u32; + for review in reviews.iter() { + total = total.saturating_add(review.rating); + } + total / reviews.len() + } + + // --- MEDICATION TRACKING --- + + " + +$t = $t.Substring(0, $vatStart) + $fixedRating + $t.Substring($vatEnd) + +# Recalculate offset (original was 477 chars, new is ~280 chars) +$origVatLen = $vatEnd - $vatStart +$newVatLen = $fixedRating.Length +$offset2 = $offset1 + ($origVatLen - $newVatLen) + +# ── 3. Fix broken get_grooming_history (was at 201455, now adjusted) ────────── +$ghStart = 201455 - $offset2 +$ghEnd = 202773 - $offset2 + +$fixedGrooming = "pub fn get_grooming_history(env: Env, pet_id: u64) -> Vec { + let count: u64 = env.storage().instance() + .get(&(Symbol::new(&env, ""pet_grooming""), pet_id)).unwrap_or(0); + let mut history = Vec::new(&env); + for i in 1..=count { + if let Some(record_id) = env.storage().instance() + .get::<_, u64>(&(Symbol::new(&env, ""pet_grooming_idx""), pet_id, i)) + { + if let Some(record) = env.storage().instance() + .get::<_, GroomingRecord>(&(Symbol::new(&env, ""grooming""), record_id)) + { + history.push_back(record); + } + } + } + history + } + + " + +$t = $t.Substring(0, $ghStart) + $fixedGrooming + $t.Substring($ghEnd) + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $t) +Write-Host "Done. New length: $($t.Length)" diff --git a/stellar-contracts/fix_remnant.ps1 b/stellar-contracts/fix_remnant.ps1 new file mode 100644 index 0000000..e105830 --- /dev/null +++ b/stellar-contracts/fix_remnant.ps1 @@ -0,0 +1,37 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +# Find and remove the orphaned remnant lines +$remnantStart = $t.IndexOf(' total = total.checked_add(1).unwrap_or_else(|| panic_with_error!(&env, ContractError::CounterOverflow));') +[Console]::WriteLine("Remnant at: $remnantStart") + +# The remnant is: " total = total.checked_add(1)...\n }\n total\n }\n" +# Find the closing "}" of the impl block that follows +$remnantEnd = $t.IndexOf('// --- ENC', $remnantStart) +[Console]::WriteLine("Remnant end at: $remnantEnd") +[Console]::WriteLine($t.Substring($remnantStart - 20, $remnantEnd - $remnantStart + 30)) + +# Remove the remnant (keep the "}\n\n\n\n" before "// --- ENC") +# The remnant block is: " total = ...\n }\n total\n }\n}\n\n\n\n" +# We want to keep just "}\n\n\n\n" (the impl closing brace) +$implClose = $t.IndexOf('}', $remnantStart) +[Console]::WriteLine("impl close at: $implClose") +[Console]::WriteLine($t.Substring($implClose - 5, 30)) + +# Remove from remnantStart to just before the impl closing brace +# Actually the structure is: +# ...get_grooming_expenses body... +# } <- closes get_grooming_expenses +# total <- orphan +# } <- orphan +# total = total.checked_add... <- orphan +# } <- orphan +# total <- orphan +# } <- closes impl block +# } <- extra orphan brace + +# Let's just remove the specific orphan text +$orphan = " total = total.checked_add(1).unwrap_or_else(|| panic_with_error!(&env, ContractError::CounterOverflow));`n }`n total`n }`n}" +$t = $t.Replace($orphan, "}") + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $t) +Write-Host "Done. New length: $($t.Length)" diff --git a/stellar-contracts/fix_remnant2.ps1 b/stellar-contracts/fix_remnant2.ps1 new file mode 100644 index 0000000..48f3d8d --- /dev/null +++ b/stellar-contracts/fix_remnant2.ps1 @@ -0,0 +1,12 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +# Remove the orphaned remnant with exact CRLF endings +$orphan = " total = total.checked_add(1).unwrap_or_else(|| panic_with_error!(&env, ContractError::CounterOverflow));`r`n }`r`n total`r`n }`r`n}" +$replacement = "}" +$before = $t.Length +$t = $t.Replace($orphan, $replacement) +$after = $t.Length +[Console]::WriteLine("Removed: " + ($before - $after) + " chars") + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $t) +Write-Host "Done. New length: $($t.Length)" diff --git a/stellar-contracts/fix_safe_inc.ps1 b/stellar-contracts/fix_safe_inc.ps1 new file mode 100644 index 0000000..006acb9 --- /dev/null +++ b/stellar-contracts/fix_safe_inc.ps1 @@ -0,0 +1,57 @@ +$t = [System.IO.File]::ReadAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs') + +# ── 1. Revert crate:: prefix back to bare safe_increment ────────────────────── +$t = $t.Replace("crate::safe_increment(", "safe_increment(") + +# ── 2. Remove safe_increment definition from its current location ───────────── +$safeDefBlock = "// --- OVERFLOW-SAFE COUNTER HELPER ---`r`npub(crate) fn safe_increment(count: u64) -> u64 {`r`n count.checked_add(1).unwrap_or(u64::MAX)`r`n}" +$t = $t.Replace($safeDefBlock, "") + +# Also try LF-only version +$safeDefBlockLF = "// --- OVERFLOW-SAFE COUNTER HELPER ---`npub(crate) fn safe_increment(count: u64) -> u64 {`n count.checked_add(1).unwrap_or(u64::MAX)`n}" +$t = $t.Replace($safeDefBlockLF, "") + +# ── 3. Insert safe_increment BEFORE the impl block ──────────────────────────── +$implMarker = "impl PetChainContract {" +$safeIncDef = "// --- OVERFLOW-SAFE COUNTER HELPER ---`r`npub(crate) fn safe_increment(count: u64) -> u64 {`r`n count.checked_add(1).unwrap_or(u64::MAX)`r`n}`r`n`r`n" +$t = $t.Replace($implMarker, $safeIncDef + $implMarker) + +# ── 4. Fix duplicate const blocks ───────────────────────────────────────────── +# Read lines and remove the second duplicate block +$lines = $t -split "`n" +$dupLines = @( + " const MAX_STR_SHORT: u32 = 100; // names, types, test_type, outcome", + " const MAX_STR_LONG: u32 = 1000; // description, notes, results, reference_ranges", + " const MAX_VEC_MEDS: u32 = 50; // medications vec in a medical record", + " const MAX_VEC_ATTACHMENTS: u32 = 20; // attachment_hashes vec" +) +$dupCount = 0 +$newLines = @() +foreach ($line in $lines) { + $stripped = $line.TrimEnd("`r") + if ($dupLines -contains $stripped) { + $dupCount++ + if ($dupCount -le 4) { + # Keep first occurrence + $newLines += $line + } + # Skip second occurrence (dupCount 5-8) + } else { + $newLines += $line + } +} +$t = $newLines -join "`n" + +# ── 5. Fix grooming functions (outside impl, use PetChainContract::) ────────── +$t = $t.Replace( + " let history = Self::get_grooming_history(env, pet_id);", + " let history = PetChainContract::get_grooming_history(env, pet_id);" +) + +# ── 6. Fix broken get_grooming_history / get_vet_average_rating ─────────────── +# These functions have mangled bodies from the original file. Read them and fix. +# get_grooming_history: the for loop body is broken (mixed with other functions) +# We'll find and replace the broken section + +[System.IO.File]::WriteAllText('c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs', $t) +Write-Host "Done. New length: $($t.Length)" diff --git a/stellar-contracts/fix_search.ps1 b/stellar-contracts/fix_search.ps1 new file mode 100644 index 0000000..a96ae19 --- /dev/null +++ b/stellar-contracts/fix_search.ps1 @@ -0,0 +1,16 @@ +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test_search_medical_records.rs' +$lines = [System.IO.File]::ReadAllLines($f) +$newLines = [System.Collections.Generic.List[string]]::new() +for ($i = 0; $i -lt $lines.Length; $i++) { + $ln = $i + 1 + $line = $lines[$i] + if ($ln -eq 121) { + $newLines.Add(' let start_val = u64::MAX - 100;') + $newLines.Add(' let end_val = u64::MAX;') + $newLines.Add(' let results = client.search_records_by_date_range(&pet_id, &start_val, &end_val);') + continue + } + $newLines.Add($line) +} +[System.IO.File]::WriteAllLines($f, $newLines) +Write-Host ('Done. Lines: ' + $newLines.Count) diff --git a/stellar-contracts/fix_structure.ps1 b/stellar-contracts/fix_structure.ps1 new file mode 100644 index 0000000..2af6e6f --- /dev/null +++ b/stellar-contracts/fix_structure.ps1 @@ -0,0 +1,39 @@ +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\lib.rs' +$lines = [System.IO.File]::ReadAllLines($f) +Write-Host ('Total lines: ' + $lines.Length) + +$newLines = [System.Collections.Generic.List[string]]::new() + +# Keep lines 1..6494 (0-indexed 0..6493) +for ($i = 0; $i -lt 6494; $i++) { + $newLines.Add($lines[$i]) +} + +# Insert calculate_age inside the impl, then close impl +$newLines.Add('') +$newLines.Add(' // --- AGE CALCULATION ---') +$newLines.Add(' /// Calculates a pet''s approximate age from a Unix timestamp birthday.') +$newLines.Add(' ///') +$newLines.Add(' /// # Approximation') +$newLines.Add(' /// Uses 365 days/year and 30 days/month. This is intentionally approximate') +$newLines.Add(' /// and may deviate by +/-1 month from calendar-accurate results due to leap') +$newLines.Add(' /// years and variable month lengths. Sufficient for display purposes.') +$newLines.Add(' pub fn calculate_age(env: Env, birthday_timestamp: u64) -> PetAge {') +$newLines.Add(' let now = env.ledger().timestamp();') +$newLines.Add(' let elapsed_secs = if now > birthday_timestamp { now - birthday_timestamp } else { 0 };') +$newLines.Add(' let elapsed_days = elapsed_secs / 86400;') +$newLines.Add(' let years = elapsed_days / 365;') +$newLines.Add(' let remaining_days = elapsed_days % 365;') +$newLines.Add(' let months = remaining_days / 30;') +$newLines.Add(' PetAge { years, months }') +$newLines.Add(' }') +$newLines.Add('}') +$newLines.Add('') + +# Lines from REAL ENCRYPTION onwards (0-indexed 6519..) +for ($i = 6519; $i -lt $lines.Length; $i++) { + $newLines.Add($lines[$i]) +} + +[System.IO.File]::WriteAllLines($f, $newLines) +Write-Host ('Done. New line count: ' + $newLines.Count) diff --git a/stellar-contracts/fix_test_structure.ps1 b/stellar-contracts/fix_test_structure.ps1 new file mode 100644 index 0000000..247030c --- /dev/null +++ b/stellar-contracts/fix_test_structure.ps1 @@ -0,0 +1,76 @@ +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test.rs' +$lines = [System.IO.File]::ReadAllLines($f) +$newLines = [System.Collections.Generic.List[string]]::new() + +# Keep lines 1..1312 unchanged (0-indexed: 0..1311) +for ($i = 0; $i -lt 1312; $i++) { + $newLines.Add($lines[$i]) +} + +# Close test_book_slot fn and mod test +$newLines.Add(' }') +$newLines.Add('}') +$newLines.Add('') + +# New properly structured module for the orphaned consent/upgrade tests +$newLines.Add('#[cfg(test)]') +$newLines.Add('mod test_consent_upgrade {') +$newLines.Add(' use crate::*;') +$newLines.Add(' use soroban_sdk::{testutils::Address as _, Address, Env, String};') +$newLines.Add('') + +# test_grant_consent (was lines 1315-1361, 0-indexed 1314-1360) +# but lines 1316-1326 (0-indexed 1315-1325) were the nested test_get_version — skip those +# test_grant_consent body: lines 1315 fn open, then 1327-1361 body (after skipping nested fn) +# Let's just write it cleanly +$newLines.Add(' #[test]') +$newLines.Add(' fn test_grant_consent() {') +# lines 1327-1361 (0-indexed 1326-1360) are the real body of test_grant_consent +for ($i = 1326; $i -le 1360; $i++) { + $newLines.Add(' ' + $lines[$i]) +} +$newLines.Add('') + +# test_propose_upgrade (was lines 1328-1361 but now we need the original) +# Actually test_propose_upgrade starts at original line 1329 (0-indexed 1328) +# Let's re-read: after our fix script ran, the lines shifted. Let me use the +# content we know from the original read: +# test_propose_upgrade: lines 1329-1361 in original (0-indexed 1328-1360) +# But we already used 1326-1360 for test_grant_consent body above. +# The original structure was: +# 1314: #[test] +# 1315: fn test_grant_consent { +# 1316: #[test] <- nested, skip +# 1317: fn test_get_version { <- nested, skip +# ...1326: } <- end of nested fn, skip +# 1327: (blank) +# 1328: #[test] +# 1329: fn test_propose_upgrade { +# ...1361: } +# 1362: (blank) +# 1363: #[test] +# 1364: fn test_revoke_consent { +# ...1381: } +# 1382: (blank) +# 1383: #[test] +# 1384: fn test_approve_upgrade { +# ...1417: } +# 1418: (blank) +# 1419: #[test] +# 1420: fn test_consent_history { +# ...1437: } +# 1438: (blank) +# 1439: #[test] +# 1440: fn test_migrate_version { +# ...1477: } +# 1478: } <- old mod test close +# 1479: (blank) +# 1480: #[test] +# 1481: #[should_panic] +# 1482: fn test_upgrade_requires_admin { +# ...1491: } +# 1492: } <- extra brace + +# Wait - the lines above are from the ORIGINAL file before our previous fix ran. +# After fix_tests.ps1 ran, lines shifted. Let me just read current file state. +Write-Host 'Script needs current file state - aborting and using direct approach' diff --git a/stellar-contracts/fix_tests.ps1 b/stellar-contracts/fix_tests.ps1 new file mode 100644 index 0000000..8fa261e --- /dev/null +++ b/stellar-contracts/fix_tests.ps1 @@ -0,0 +1,143 @@ +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test.rs' +$lines = [System.IO.File]::ReadAllLines($f) +Write-Host ('Total lines: ' + $lines.Length) + +$newLines = [System.Collections.Generic.List[string]]::new() + +for ($i = 0; $i -lt $lines.Length; $i++) { + $lineNum = $i + 1 + $line = $lines[$i] + + # Fix: record.vet_address -> record.veterinarian + if ($lineNum -in @(556, 1591, 2434, 3131)) { + $line = $line -replace 'record\.vet_address', 'record.veterinarian' + } + + # Fix: get_emergency_info missing caller arg + if ($lineNum -in @(812, 1847, 2690, 3387)) { + $line = $line -replace 'client\.get_emergency_info\(&pet_id\)', 'client.get_emergency_info(&pet_id, &owner)' + } + + # Fix: book_slot missing booker arg (line 1301) - use vet as booker placeholder + if ($lineNum -eq 1301) { + $line = $line -replace 'client\.book_slot\(&vet, &slot_index\)', 'client.book_slot(&vet, &vet, &slot_index)' + } + + # Fix: Medication missing id and pet_id fields + if ($lineNum -in @(1915, 1947, 1956, 2763, 2795, 2804)) { + $line = $line -replace 'Medication \{', 'Medication { id: 0, pet_id: 0,' + } + + # Fix: end_date: 200 -> end_date: Some(200) + if ($lineNum -in @(1920, 1952, 2768, 2800)) { + $line = $line -replace 'end_date: 200,', 'end_date: Some(200),' + } + + # Fix: end_date: update_time + 100 -> end_date: Some(update_time + 100) + if ($lineNum -in @(1961, 2809)) { + $line = $line -replace 'end_date: update_time \+ 100,', 'end_date: Some(update_time + 100),' + } + + # Fix: add_lab_result missing reference_ranges and medical_record_id args + # Line 1880: &None, -> &String::from_str(&env, ""), &None, &None, + if ($lineNum -eq 1880) { + $line = ' &String::from_str(&env, ""), // reference_ranges' + $newLines.Add($line) + $newLines.Add(' &None, // attachment_hash') + $newLines.Add(' &None, // medical_record_id') + continue + } + if ($lineNum -eq 2723) { + $line = ' &String::from_str(&env, ""), // reference_ranges' + $newLines.Add($line) + $newLines.Add(' &None, // attachment_hash') + $newLines.Add(' &None, // medical_record_id') + continue + } + + # Fix: register_pet missing color/weight/microchip_id + # Line 476: &PrivacyLevel::Public, -> add missing args before it + # Pattern: line has &PrivacyLevel::Public and previous line has &breed + if ($lineNum -eq 476) { + $newLines.Add(' &String::from_str(&env, "Unknown"), // color') + $newLines.Add(' &0u32, // weight') + $newLines.Add(' &None, // microchip_id') + } + if ($lineNum -eq 2354) { + $newLines.Add(' &String::from_str(&env, "Unknown"), // color') + $newLines.Add(' &0u32, // weight') + $newLines.Add(' &None, // microchip_id') + } + + # Fix: Duplicate mod test -> rename + if ($lineNum -eq 1480) { $line = 'mod test_b {' } + if ($lineNum -eq 2326) { $line = 'mod test_c {' } + if ($lineNum -eq 3020) { $line = 'mod test_d {' } + + # Fix: Duplicate mod test_vet -> rename + if ($lineNum -eq 3896) { $line = 'mod test_vet_b {' } + + # Fix: orphaned test_revoke_consent (lines 1358-1368) - replace body with proper setup + if ($lineNum -eq 1359) { + $newLines.Add('fn test_revoke_consent() {') + $newLines.Add(' let env = Env::default();') + $newLines.Add(' env.mock_all_auths();') + $newLines.Add(' let contract_id = env.register_contract(None, PetChainContract);') + $newLines.Add(' let client = PetChainContractClient::new(&env, &contract_id);') + $newLines.Add(' let admin = Address::generate(&env);') + $newLines.Add(' client.init_admin(&admin);') + $newLines.Add(' let owner = Address::generate(&env);') + $newLines.Add(' let pet_id = client.register_pet(') + $newLines.Add(' &owner, &String::from_str(&env, "Buddy"), &String::from_str(&env, "2020"),') + $newLines.Add(' &Gender::Male, &Species::Dog, &String::from_str(&env, "Lab"),') + $newLines.Add(' &String::from_str(&env, "Brown"), &5u32, &None, &PrivacyLevel::Public,') + $newLines.Add(' );') + $newLines.Add(' let grantee = Address::generate(&env);') + $newLines.Add(' let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee);') + $newLines.Add(' let revoked = client.revoke_consent(&consent_id, &owner);') + $newLines.Add(' assert!(revoked);') + continue + } + # Skip the old wrong body lines 1360-1368 + if ($lineNum -in @(1360, 1361, 1362, 1363, 1364, 1365, 1366, 1367, 1368)) { + continue + } + + # Fix: orphaned test_consent_history (lines 1407-1416) - replace body with proper setup + if ($lineNum -eq 1407) { + $newLines.Add('fn test_consent_history() {') + $newLines.Add(' let env = Env::default();') + $newLines.Add(' env.mock_all_auths();') + $newLines.Add(' let contract_id = env.register_contract(None, PetChainContract);') + $newLines.Add(' let client = PetChainContractClient::new(&env, &contract_id);') + $newLines.Add(' let admin = Address::generate(&env);') + $newLines.Add(' client.init_admin(&admin);') + $newLines.Add(' let owner = Address::generate(&env);') + $newLines.Add(' let pet_id = client.register_pet(') + $newLines.Add(' &owner, &String::from_str(&env, "Buddy"), &String::from_str(&env, "2020"),') + $newLines.Add(' &Gender::Male, &Species::Dog, &String::from_str(&env, "Lab"),') + $newLines.Add(' &String::from_str(&env, "Brown"), &5u32, &None, &PrivacyLevel::Public,') + $newLines.Add(' );') + $newLines.Add(' let grantee = Address::generate(&env);') + $newLines.Add(' client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee);') + $newLines.Add(' let history = client.get_consent_history(&pet_id);') + $newLines.Add(' assert_eq!(history.len(), 1);') + continue + } + # Skip the old wrong body lines 1408-1416 + if ($lineNum -in @(1408, 1409, 1410, 1411, 1412, 1413, 1414, 1415, 1416)) { + continue + } + + # Fix: orphaned code at lines 1458-1464 (outside any module after closing }) + # These lines are between } at 1457 and #[cfg(test)] at 1479 + # They reference client/env that don't exist - just remove them + if ($lineNum -in @(1458, 1459, 1460, 1461, 1462, 1463, 1464)) { + continue + } + + $newLines.Add($line) +} + +[System.IO.File]::WriteAllLines($f, $newLines) +Write-Host ('Done. New line count: ' + $newLines.Count) diff --git a/stellar-contracts/fix_vet_reviews.ps1 b/stellar-contracts/fix_vet_reviews.ps1 new file mode 100644 index 0000000..596e940 --- /dev/null +++ b/stellar-contracts/fix_vet_reviews.ps1 @@ -0,0 +1,21 @@ +$f = 'c:\Users\DELL\Documents\GitHub\PetChain-Contracts\stellar-contracts\src\test.rs' +$lines = [System.IO.File]::ReadAllLines($f) +$newLines = [System.Collections.Generic.List[string]]::new() + +for ($i = 0; $i -lt $lines.Length; $i++) { + $ln = $i + 1 + $line = $lines[$i] + + # After line 1016 (env.mock_all_auths(); inside test_vet_reviews), close the function + if ($ln -eq 1016) { + $newLines.Add($line) + $newLines.Add(' }') + $newLines.Add('') + continue + } + + $newLines.Add($line) +} + +[System.IO.File]::WriteAllLines($f, $newLines) +Write-Host ('Done. Lines: ' + $newLines.Count) diff --git a/stellar-contracts/remnant_bytes.txt b/stellar-contracts/remnant_bytes.txt new file mode 100644 index 0000000..1700a29 --- /dev/null +++ b/stellar-contracts/remnant_bytes.txt @@ -0,0 +1,14 @@ +l + } + total = total.checked_add(1).unwrap_or_else(|| panic_with_error!(&env, ContractError::CounterOverflow)); + } + total + } +} + + + +// --- ENCRYPTION HELPERS --- + // --- AGE CALCULATION --- + + /// Calcula \ No newline at end of file diff --git a/stellar-contracts/src/lib.rs b/stellar-contracts/src/lib.rs index db6ce64..79e080c 100644 --- a/stellar-contracts/src/lib.rs +++ b/stellar-contracts/src/lib.rs @@ -90,6 +90,12 @@ mod test_pet_age; mod test_statistics; #[cfg(test)] mod test_search_medical_records; +#[cfg(test)] +mod test_encryption_nonce; +#[cfg(test)] +mod test_get_pet_decryption; +#[cfg(test)] +mod test_book_slot; use soroban_sdk::xdr::{FromXdr, ToXdr}; use soroban_sdk::{ @@ -125,12 +131,12 @@ impl From for soroban_sdk::Error { ContractError::PetNotFound => ScErrorCode::MissingValue, ContractError::VetNotFound => ScErrorCode::MissingValue, ContractError::VeterinarianNotVerified => ScErrorCode::InvalidAction, - ContractError::VetAlreadyRegistered => ScErrorCode::DuplicateName, - ContractError::LicenseAlreadyRegistered => ScErrorCode::DuplicateName, + ContractError::VetAlreadyRegistered => ScErrorCode::ExistingValue, + ContractError::LicenseAlreadyRegistered => ScErrorCode::ExistingValue, ContractError::InputStringTooLong => ScErrorCode::InvalidInput, - ContractError::PetAlreadyHasLinkedTag => ScErrorCode::DuplicateName, + ContractError::PetAlreadyHasLinkedTag => ScErrorCode::ExistingValue, ContractError::InvalidIpfsHash => ScErrorCode::InvalidInput, - ContractError::CounterOverflow => ScErrorCode::ArithmeticOverflow, + ContractError::CounterOverflow => ScErrorCode::ArithDomain, ContractError::TooManyItems => ScErrorCode::InvalidInput, ContractError::InvalidState => ScErrorCode::InvalidAction, ContractError::InvalidInput => ScErrorCode::InvalidInput, @@ -1218,6 +1224,11 @@ pub enum DisputeKey { #[contract] pub struct PetChainContract; +// --- OVERFLOW-SAFE COUNTER HELPER --- +pub(crate) fn safe_increment(count: u64) -> u64 { + count.checked_add(1).unwrap_or(u64::MAX) +} + #[contractimpl] impl PetChainContract { // --- CONTRACT STATISTICS --- @@ -1633,62 +1644,76 @@ impl PetChainContract { } } - pub fn get_pet(env: Env, id: u64, viewer: Address) -> Option { - // Require the viewer to authenticate — prevents spoofing the caller identity. - viewer.require_auth(); - + pub fn get_pet(env: Env, id: u64) -> Option { let pet = env .storage() .instance() .get::(&DataKey::Pet(id))?; - // ---- access-level resolution ---- - // Owner always has full access regardless of privacy level. - let is_owner = pet.owner == viewer; - - // Propagate decryption failures as None rather than masking them - // with a sentinel "Error" string. Any corrupt ciphertext or nonce - // mismatch causes the whole read to return None deterministically. - let decrypted_name = decrypt_sensitive_data( - &env, - &pet.encrypted_name.ciphertext, - &pet.encrypted_name.nonce, - &key, - ) - .ok()?; - let name = String::from_xdr(&env, &decrypted_name).ok()?; - - let decrypted_birthday = decrypt_sensitive_data( - &env, - &pet.encrypted_birthday.ciphertext, - &pet.encrypted_birthday.nonce, - &key, - ) - .ok()?; - let birthday = String::from_xdr(&env, &decrypted_birthday).ok()?; - - let decrypted_breed = decrypt_sensitive_data( - &env, - &pet.encrypted_breed.ciphertext, - &pet.encrypted_breed.nonce, - &key, - ) - .ok()?; - let breed = String::from_xdr(&env, &decrypted_breed).ok()?; + let key = Self::get_encryption_key(&env); - let a_bytes = decrypt_sensitive_data( - &env, - &pet.encrypted_allergies.ciphertext, - &pet.encrypted_allergies.nonce, - &key, - ) - .ok()?; - let allergies = Vec::::from_xdr(&env, &a_bytes).ok()?; + // Propagate decryption failures as None rather than masking them + // with a sentinel "Error" string. Any corrupt ciphertext or nonce + // mismatch causes the whole read to return None deterministically. + let decrypted_name = decrypt_sensitive_data( + &env, + &pet.encrypted_name.ciphertext, + &pet.encrypted_name.nonce, + &key, + ) + .ok()?; + let name = String::from_xdr(&env, &decrypted_name).ok()?; + + let decrypted_birthday = decrypt_sensitive_data( + &env, + &pet.encrypted_birthday.ciphertext, + &pet.encrypted_birthday.nonce, + &key, + ) + .ok()?; + let birthday = String::from_xdr(&env, &decrypted_birthday).ok()?; + + let decrypted_breed = decrypt_sensitive_data( + &env, + &pet.encrypted_breed.ciphertext, + &pet.encrypted_breed.nonce, + &key, + ) + .ok()?; + let breed = String::from_xdr(&env, &decrypted_breed).ok()?; + + let a_bytes = decrypt_sensitive_data( + &env, + &pet.encrypted_allergies.ciphertext, + &pet.encrypted_allergies.nonce, + &key, + ) + .ok()?; + let allergies = Vec::::from_xdr(&env, &a_bytes).ok()?; + + let profile = PetProfile { + id: pet.id, + owner: pet.owner.clone(), + privacy_level: pet.privacy_level, + name, + birthday, + active: pet.active, + created_at: pet.created_at, + updated_at: pet.updated_at, + new_owner: pet.new_owner, + species: pet.species, + gender: pet.gender, + breed, + color: pet.color, + weight: pet.weight, + microchip_id: pet.microchip_id, + allergies, + }; Self::log_access( &env, id, - viewer, + pet.owner, AccessAction::Read, String::from_str(&env, "Pet profile accessed"), ); @@ -2082,14 +2107,13 @@ impl PetChainContract { const MAX_STR_LONG: u32 = 1000; const MAX_VEC_MEDS: u32 = 50; const MAX_VEC_ATTACHMENTS: u32 = 20; + const MAX_VET_NAME_LEN: u32 = 100; + const MAX_VET_LICENSE_LEN: u32 = 100; + const MAX_VET_SPEC_LEN: u32 = 200; + const MAX_REVIEW_COMMENT_LEN: u32 = 500; // Medical / record field limits - const MAX_STR_SHORT: u32 = 100; // names, types, test_type, outcome - const MAX_STR_LONG: u32 = 1000; // description, notes, results, reference_ranges - const MAX_VEC_MEDS: u32 = 50; // medications vec in a medical record - const MAX_VEC_ATTACHMENTS: u32 = 20; // attachment_hashes vec - - pub fn register_vet( +pub fn register_vet( env: Env, vet_address: Address, name: String, @@ -3226,7 +3250,7 @@ impl PetChainContract { .instance() .get::(&DataKey::OwnerPetIndex((owner.clone(), i))) { - if let Some(pet) = Self::get_pet(env.clone(), pid, owner.clone()) { + if let Some(pet) = Self::get_pet(env.clone(), pid) { pets.push_back(pet); } } @@ -3254,7 +3278,7 @@ impl PetChainContract { // Only surface Public pets in unauthenticated listing queries. if let Some(raw) = env.storage().instance().get::(&DataKey::Pet(pid)) { if matches!(raw.privacy_level, PrivacyLevel::Public) { - if let Some(profile) = Self::get_pet(env.clone(), pid, raw.owner.clone()) { + if let Some(profile) = Self::get_pet(env.clone(), pid) { pets.push_back(profile); } } @@ -3279,7 +3303,7 @@ impl PetChainContract { { // Only surface active Public pets in unauthenticated listing queries. if pet.active && matches!(pet.privacy_level, PrivacyLevel::Public) { - if let Some(profile) = Self::get_pet(env.clone(), id, pet.owner.clone()) { + if let Some(profile) = Self::get_pet(env.clone(), id) { pets.push_back(profile); } } @@ -4858,7 +4882,7 @@ impl PetChainContract { } let mut total = 0u32; for review in reviews.iter() { - total = total.checked_add(1).unwrap_or_else(|| panic_with_error!(&env, ContractError::CounterOverflow)); + total = total.saturating_add(review.rating); } total / reviews.len() } @@ -6432,26 +6456,14 @@ impl PetChainContract { } pub fn get_grooming_history(env: Env, pet_id: u64) -> Vec { - let count: u64 = env.storage().instance().get(&(Symbol::new(&env, "pet_grooming"), pet_id)).unwrap_or(0); - let mut history = Vec::new(&env); - for i in 1..=count { - if let Some(record_id) = env.storage().instance().get::<_, u64>(&(Symbol::new(&env, "pet_grooming_idx"), pet_id, i)) { - if let Some(record) = env.storage().instance().get::<_, GroomingRecord>(&(Symbol::new(&env, "grooming"), record_id)) { - let count: u64 = env - .storage() - .instance() - .get(&(Symbol::new(&env, "pet_grooming"), pet_id)) - .unwrap_or(0); + let count: u64 = env.storage().instance() + .get(&(Symbol::new(&env, "pet_grooming"), pet_id)).unwrap_or(0); let mut history = Vec::new(&env); for i in 1..=count { - if let Some(record_id) = env.storage().instance().get::<_, u64>(&( - Symbol::new(&env, "pet_grooming_idx"), - pet_id, - i, - )) { - if let Some(record) = env - .storage() - .instance() + if let Some(record_id) = env.storage().instance() + .get::<_, u64>(&(Symbol::new(&env, "pet_grooming_idx"), pet_id, i)) + { + if let Some(record) = env.storage().instance() .get::<_, GroomingRecord>(&(Symbol::new(&env, "grooming"), record_id)) { history.push_back(record); @@ -6462,7 +6474,7 @@ impl PetChainContract { } pub fn get_next_grooming_date(env: Env, pet_id: u64) -> u64 { - let history = Self::get_grooming_history(env, pet_id); + let history = PetChainContract::get_grooming_history(env, pet_id); let mut next_date = 0u64; for record in history.iter() { if record.next_due > 0 && (next_date == 0 || record.next_due < next_date) { @@ -6473,32 +6485,20 @@ impl PetChainContract { } pub fn get_grooming_expenses(env: Env, pet_id: u64) -> u64 { - let history = Self::get_grooming_history(env, pet_id); + let history = PetChainContract::get_grooming_history(env, pet_id); let mut total = 0u64; for record in history.iter() { total += record.cost; } total } - total = total.checked_add(1).unwrap_or_else(|| panic_with_error!(&env, ContractError::CounterOverflow)); - } - total - } -} - -// --- OVERFLOW-SAFE COUNTER HELPER --- -pub(crate) fn safe_increment(count: u64) -> u64 { - count.checked_add(1).unwrap_or(u64::MAX) -} -// --- ENCRYPTION HELPERS --- // --- AGE CALCULATION --- - /// Calculates a pet's approximate age from a Unix timestamp birthday. /// /// # Approximation /// Uses 365 days/year and 30 days/month. This is intentionally approximate - /// and may deviate by ±1 month from calendar-accurate results due to leap + /// and may deviate by +/-1 month from calendar-accurate results due to leap /// years and variable month lengths. Sufficient for display purposes. pub fn calculate_age(env: Env, birthday_timestamp: u64) -> PetAge { let now = env.ledger().timestamp(); @@ -6509,48 +6509,127 @@ pub(crate) fn safe_increment(count: u64) -> u64 { let months = remaining_days / 30; PetAge { years, months } } +} + +// --- REAL ENCRYPTION: SHA-256 CTR-mode stream cipher + authentication tag --- +// +// Privacy model: sensitive fields are encrypted on-chain using a contract-held +// key (get_encryption_key). The ciphertext stored on-chain is NOT the plaintext. +// It is XOR-encrypted with a SHA-256-based keystream and authenticated with a +// 32-byte tag prepended to the ciphertext. +// +// Ciphertext layout: [ tag (32 bytes) | encrypted_data ] +// tag = SHA-256( key || nonce || plaintext ) +// keystream_block_i = SHA-256( key || nonce || i.to_be_bytes() ) +// encrypted_data[i] = plaintext[i] XOR keystream[i] +// +// An on-chain observer sees only tag + ciphertext; recovering plaintext +// requires the key. The nonce (timestamp || counter) ensures ciphertext +// differs across calls even for identical plaintext. + +fn sha256_block(env: &Env, key: &Bytes, nonce: &Bytes, block_idx: u64) -> [u8; 32] { + let mut preimage = Bytes::new(env); + preimage.append(key); + preimage.append(nonce); + for b in block_idx.to_be_bytes() { + preimage.push_back(b); + } + env.crypto().sha256(&preimage).into() +} -fn encrypt_sensitive_data(env: &Env, data: &Bytes, _key: &Bytes) -> (Bytes, Bytes) { - // Generate unique nonce per encryption call - // Combine ledger timestamp and nonce counter for uniqueness - +fn encrypt_sensitive_data(env: &Env, data: &Bytes, key: &Bytes) -> (Bytes, Bytes) { + // Unique nonce: 8-byte ledger timestamp || 4-byte monotonic counter let counter_key = SystemKey::EncryptionNonceCounter; - let counter = env.storage().instance().get::(&counter_key).unwrap_or(0); - - // Increment and store the new counter + let counter: u64 = env.storage().instance().get::(&counter_key).unwrap_or(0); env.storage().instance().set(&counter_key, &(counter + 1)); - - // Generate nonce from timestamp and counter - // Use 8 bytes from timestamp + 4 bytes from counter = 12 bytes total - let timestamp = env.ledger().timestamp() as u64; - - // Create nonce bytes: [timestamp (8 bytes) | counter (4 bytes)] + let mut nonce_array = [0u8; 12]; - - // Timestamp in first 8 bytes (big-endian) - nonce_array[0..8].copy_from_slice(×tamp.to_be_bytes()); - - // Counter in last 4 bytes (big-endian) - let counter_bytes = (counter as u32).to_be_bytes(); - nonce_array[8..12].copy_from_slice(&counter_bytes); - + nonce_array[0..8].copy_from_slice(&env.ledger().timestamp().to_be_bytes()); + nonce_array[8..12].copy_from_slice(&(counter as u32).to_be_bytes()); let nonce = Bytes::from_array(env, &nonce_array); - - // Mock encryption for demonstration (returns ciphertext and nonce) - // In production, would use actual AEAD cipher with the unique nonce - let ciphertext = data.clone(); + + let data_len = data.len() as usize; + + // XOR plaintext with SHA-256 keystream (CTR mode) + let mut encrypted = Bytes::new(env); + let mut block_idx: u64 = 0; + let mut offset = 0usize; + while offset < data_len { + let block_bytes: [u8; 32] = sha256_block(env, key, &nonce, block_idx); + let chunk_end = (offset + 32).min(data_len); + for i in offset..chunk_end { + encrypted.push_back(data.get(i as u32).unwrap() ^ block_bytes[i - offset]); + } + offset += 32; + block_idx += 1; + } + + // Authentication tag: SHA-256( key || nonce || plaintext ) + let mut tag_preimage = Bytes::new(env); + tag_preimage.append(key); + tag_preimage.append(&nonce); + tag_preimage.append(data); + let tag_arr: [u8; 32] = env.crypto().sha256(&tag_preimage).into(); + let tag = Bytes::from_array(env, &tag_arr); + + // Final ciphertext = tag (32 bytes) || encrypted_data + let mut ciphertext = Bytes::new(env); + ciphertext.append(&tag); + ciphertext.append(&encrypted); + (nonce, ciphertext) } fn decrypt_sensitive_data( - _env: &Env, + env: &Env, ciphertext: &Bytes, - _nonce: &Bytes, - _key: &Bytes, + nonce: &Bytes, + key: &Bytes, ) -> Result { - // In production, would use the provided nonce with AEAD cipher to decrypt - // For demonstration, verify nonce is used (non-None) and decrypt with it - Ok(ciphertext.clone()) + let ct_len = ciphertext.len() as usize; + if ct_len < 32 { + return Err(()); + } + + // Split stored tag (first 32 bytes) from encrypted payload + let mut stored_tag = Bytes::new(env); + for i in 0..32u32 { + stored_tag.push_back(ciphertext.get(i).unwrap()); + } + let mut encrypted = Bytes::new(env); + for i in 32..ct_len as u32 { + encrypted.push_back(ciphertext.get(i).unwrap()); + } + + let enc_len = encrypted.len() as usize; + + // Decrypt: XOR with keystream + let mut plaintext = Bytes::new(env); + let mut block_idx: u64 = 0; + let mut offset = 0usize; + while offset < enc_len { + let block_bytes: [u8; 32] = sha256_block(env, key, nonce, block_idx); + let chunk_end = (offset + 32).min(enc_len); + for i in offset..chunk_end { + plaintext.push_back(encrypted.get(i as u32).unwrap() ^ block_bytes[i - offset]); + } + offset += 32; + block_idx += 1; + } + + // Verify authentication tag: SHA-256( key || nonce || plaintext ) + let mut tag_preimage = Bytes::new(env); + tag_preimage.append(key); + tag_preimage.append(nonce); + tag_preimage.append(&plaintext); + let expected_arr: [u8; 32] = env.crypto().sha256(&tag_preimage).into(); + let expected_tag = Bytes::from_array(env, &expected_arr); + + if stored_tag != expected_tag { + return Err(()); + } + + Ok(plaintext) } diff --git a/stellar-contracts/src/test.rs b/stellar-contracts/src/test.rs index b1445a1..f7698ed 100644 --- a/stellar-contracts/src/test.rs +++ b/stellar-contracts/src/test.rs @@ -473,6 +473,9 @@ mod test { &Gender::Male, &Species::Dog, &breed, + &String::from_str(&env, "Unknown"), // color + &0u32, // weight + &None, // microchip_id &PrivacyLevel::Public, ); assert_eq!(pet_id, 1); @@ -553,7 +556,7 @@ mod test { assert_eq!(record.id, 1); assert_eq!(record.pet_id, pet_id); - assert_eq!(record.vet_address, vet); + assert_eq!(record.veterinarian, vet); assert_eq!(record.vaccine_type, VaccineType::Rabies); assert_eq!( record.batch_number, @@ -809,7 +812,7 @@ mod test { &String::from_str(&env, "Allergic to bees"), ); - let info = client.get_emergency_info(&pet_id); + let info = client.get_emergency_info(&pet_id, &owner); assert_eq!(info.emergency_contacts.len(), 1); assert_eq!(info.emergency_contacts.len(), 1); } @@ -1011,6 +1014,8 @@ mod test { fn test_vet_reviews() { let env = Env::default(); env.mock_all_auths(); + } + // === NEW LOST PET ALERT TESTS === #[test] @@ -1298,7 +1303,7 @@ mod test { let slot_index = client.set_availability(&vet, &start_time, &end_time); // Book the slot - let result = client.book_slot(&vet, &slot_index); + let result = client.book_slot(&vet, &vet, &slot_index); assert!(result); // Verify slot is no longer available @@ -1306,178 +1311,156 @@ mod test { let slots = client.get_available_slots(&vet, &date); assert_eq!(slots.len(), 0); } - #[test] -fn test_grant_consent() { - #[test] -fn test_get_version() { - let env = Env::default(); - let contract_id = env.register_contract(None, PetChainContract); - let client = PetChainContractClient::new(&env, &contract_id); - - let version = client.get_version(); - assert_eq!(version.major, 1); - assert_eq!(version.minor, 0); - assert_eq!(version.patch, 0); } -#[test] -fn test_propose_upgrade() { - let env = Env::default(); - let contract_id = env.register_contract(None, PetChainContract); - let client = PetChainContractClient::new(&env, &contract_id); - env.mock_all_auths(); - - let admin = Address::generate(&env); - client.init_admin(&admin); - - let owner = Address::generate(&env); - let pet_id = client.register_pet( - &owner, - &String::from_str(&env, "Buddy"), - &String::from_str(&env, "2020-01-01"), - &Gender::Male, - &Species::Dog, - &String::from_str(&env, "Labrador"), - &String::from_str(&env, "Unknown"), - &0u32, - &None, - &PrivacyLevel::Public, - ); - - let insurance_company = Address::generate(&env); - let consent_id = client.grant_consent( - &pet_id, - &owner, - &ConsentType::Insurance, - &insurance_company, - ); - - assert_eq!(consent_id, 1); -} - -#[test] -fn test_revoke_consent() { - let wasm_hash = BytesN::from_array(&env, &[0u8; 32]); - let proposal_id = client.propose_upgrade(&admin, &wasm_hash); - - assert_eq!(proposal_id, 1); +#[cfg(test)] +mod test_consent_upgrade { + use crate::*; + use soroban_sdk::{testutils::Address as _, Address, Env, String}; - let proposal = client.get_upgrade_proposal(&proposal_id).unwrap(); - assert_eq!(proposal.approved, false); - assert_eq!(proposal.executed, false); -} + fn setup_contract(env: &Env) -> (PetChainContractClient<'static>, Address) { + let id = env.register_contract(None, PetChainContract); + let client = PetChainContractClient::new(env, &id); + let admin = Address::generate(env); + client.init_admin(&admin); + (client, admin) + } -#[test] -fn test_approve_upgrade() { - let env = Env::default(); - let contract_id = env.register_contract(None, PetChainContract); - let client = PetChainContractClient::new(&env, &contract_id); - env.mock_all_auths(); - - let admin = Address::generate(&env); - client.init_admin(&admin); - - let owner = Address::generate(&env); - let pet_id = client.register_pet( - &owner, - &String::from_str(&env, "Buddy"), - &String::from_str(&env, "2020-01-01"), - &Gender::Male, - &Species::Dog, - &String::from_str(&env, "Labrador"), - &String::from_str(&env, "Unknown"), + fn register_pet(client: &PetChainContractClient, env: &Env, owner: &Address) -> u64 { + client.register_pet( + owner, + &String::from_str(env, "Buddy"), + &String::from_str(env, "2020-01-01"), + &Gender::Male, + &Species::Dog, + &String::from_str(env, "Labrador"), + &String::from_str(env, "Unknown"), &0u32, &None, &PrivacyLevel::Public, - ); - - let research_org = Address::generate(&env); - let consent_id = client.grant_consent( - &pet_id, - &owner, - &ConsentType::Research, - &research_org, - ); - - let revoked = client.revoke_consent(&consent_id, &owner); - assert_eq!(revoked, true); -} + ) + } -#[test] -fn test_consent_history() { - let wasm_hash = BytesN::from_array(&env, &[0u8; 32]); - let proposal_id = client.propose_upgrade(&admin, &wasm_hash); + #[test] + fn test_grant_consent() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_contract(&env); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + let grantee = Address::generate(&env); + let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee); + assert_eq!(consent_id, 1); + } - let approved = client.approve_upgrade(&proposal_id); - assert_eq!(approved, true); + #[test] + fn test_get_version() { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register_contract(None, PetChainContract); + let client = PetChainContractClient::new(&env, &id); + let version = client.get_version(); + assert_eq!(version.major, 1); + assert_eq!(version.minor, 0); + assert_eq!(version.patch, 0); + } - let proposal = client.get_upgrade_proposal(&proposal_id).unwrap(); - assert_eq!(proposal.approved, true); -} + #[test] + fn test_propose_upgrade() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup_contract(&env); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + let grantee = Address::generate(&env); + let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee); + assert_eq!(consent_id, 1); + let wasm_hash = soroban_sdk::BytesN::from_array(&env, &[0u8; 32]); + let proposal_id = client.propose_upgrade(&admin, &wasm_hash); + assert_eq!(proposal_id, 1); + let proposal = client.get_upgrade_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.approved, false); + assert_eq!(proposal.executed, false); + } -#[test] -fn test_migrate_version() { - let env = Env::default(); - let contract_id = env.register_contract(None, PetChainContract); - let client = PetChainContractClient::new(&env, &contract_id); - env.mock_all_auths(); - - let admin = Address::generate(&env); - client.init_admin(&admin); - - let owner = Address::generate(&env); - let pet_id = client.register_pet( - &owner, - &String::from_str(&env, "Buddy"), - &String::from_str(&env, "2020-01-01"), - &Gender::Male, - &Species::Dog, - &String::from_str(&env, "Labrador"), - &String::from_str(&env, "Unknown"), - &0u32, - &None, - &PrivacyLevel::Public, - ); + #[test] + fn test_revoke_consent() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_contract(&env); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + let grantee = Address::generate(&env); + let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee); + let revoked = client.revoke_consent(&consent_id, &owner); + assert!(revoked); + } - let insurance_company = Address::generate(&env); - let research_org = Address::generate(&env); + #[test] + fn test_approve_upgrade() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin) = setup_contract(&env); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + let grantee = Address::generate(&env); + let consent_id = client.grant_consent(&pet_id, &owner, &ConsentType::Research, &grantee); + let revoked = client.revoke_consent(&consent_id, &owner); + assert_eq!(revoked, true); + let wasm_hash = soroban_sdk::BytesN::from_array(&env, &[0u8; 32]); + let proposal_id = client.propose_upgrade(&admin, &wasm_hash); + let approved = client.approve_upgrade(&proposal_id); + assert_eq!(approved, true); + let proposal = client.get_upgrade_proposal(&proposal_id).unwrap(); + assert_eq!(proposal.approved, true); + } - // Grant two consents - client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &insurance_company); - client.grant_consent(&pet_id, &owner, &ConsentType::Research, &research_org); + #[test] + fn test_consent_history() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_contract(&env); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + let grantee = Address::generate(&env); + client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &grantee); + let history = client.get_consent_history(&pet_id); + assert_eq!(history.len(), 1); + } - // Revoke one - client.revoke_consent(&1u64, &owner); + #[test] + fn test_migrate_version() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _admin) = setup_contract(&env); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + let ins = Address::generate(&env); + let res = Address::generate(&env); + client.grant_consent(&pet_id, &owner, &ConsentType::Insurance, &ins); + client.grant_consent(&pet_id, &owner, &ConsentType::Research, &res); + client.revoke_consent(&1u64, &owner); + let history = client.get_consent_history(&pet_id); + assert_eq!(history.len(), 2); + assert_eq!(history.get(0).unwrap().is_active, false); + assert_eq!(history.get(1).unwrap().is_active, true); + } - let history = client.get_consent_history(&pet_id); - assert_eq!(history.len(), 2); // both still in history - assert_eq!(history.get(0).unwrap().is_active, false); // first was revoked - assert_eq!(history.get(1).unwrap().is_active, true); // second still active -} + #[test] + #[should_panic] + fn test_upgrade_requires_admin() { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register_contract(None, PetChainContract); + let client = PetChainContractClient::new(&env, &id); + let wasm_hash = soroban_sdk::BytesN::from_array(&env, &[0u8; 32]); + client.propose_upgrade(&Address::generate(&env), &wasm_hash); + } } - client.migrate_version(&2u32, &1u32, &0u32); - let version = client.get_version(); - assert_eq!(version.major, 2); - assert_eq!(version.minor, 1); - assert_eq!(version.patch, 0); -} -#[test] -#[should_panic] -fn test_upgrade_requires_admin() { - let env = Env::default(); - let contract_id = env.register_contract(None, PetChainContract); - let client = PetChainContractClient::new(&env, &contract_id); - env.mock_all_auths(); - - // No admin set - should panic - let wasm_hash = BytesN::from_array(&env, &[0u8; 32]); - client.propose_upgrade(&Address::generate(&env), &wasm_hash); -} -} #[cfg(test)] -mod test { +mod test_b { use crate::*; use soroban_sdk::{ testutils::{Address as _, Ledger}, @@ -1588,7 +1571,7 @@ mod test { assert_eq!(record.id, 1); assert_eq!(record.pet_id, pet_id); - assert_eq!(record.vet_address, vet); + assert_eq!(record.veterinarian, vet); assert_eq!(record.vaccine_type, VaccineType::Rabies); assert_eq!( record.batch_number, @@ -1844,7 +1827,7 @@ mod test { &String::from_str(&env, "Allergic to bees"), ); - let info = client.get_emergency_info(&pet_id); + let info = client.get_emergency_info(&pet_id, &owner); assert_eq!(info.emergency_contacts.len(), 1); assert_eq!(info.emergency_contacts.len(), 1); } @@ -1877,7 +1860,9 @@ mod test { &vet, &String::from_str(&env, "Blood Test"), &String::from_str(&env, "Normal"), - &None, + &String::from_str(&env, ""), // reference_ranges + &None, // attachment_hash + &None, // medical_record_id ); let res = client.get_lab_result(&lab_id).unwrap(); @@ -1912,12 +1897,12 @@ mod test { ); let mut medications = Vec::new(&env); - medications.push_back(Medication { + medications.push_back(Medication { id: 0, pet_id: 0, name: String::from_str(&env, "Med1"), dosage: String::from_str(&env, "10mg"), frequency: String::from_str(&env, "Daily"), start_date: 100, - end_date: 200, + end_date: Some(200), prescribing_vet: vet.clone(), active: true, }); @@ -1944,21 +1929,21 @@ mod test { env.ledger().with_mut(|l| l.timestamp = update_time); let mut new_meds = Vec::new(&env); - new_meds.push_back(Medication { + new_meds.push_back(Medication { id: 0, pet_id: 0, name: String::from_str(&env, "Med1"), dosage: String::from_str(&env, "20mg"), // Modified dosage frequency: String::from_str(&env, "Daily"), start_date: 100, - end_date: 200, + end_date: Some(200), prescribing_vet: vet.clone(), active: true, }); - new_meds.push_back(Medication { + new_meds.push_back(Medication { id: 0, pet_id: 0, name: String::from_str(&env, "NewMed"), // New med dosage: String::from_str(&env, "5mg"), frequency: String::from_str(&env, "Once"), start_date: update_time, - end_date: update_time + 100, + end_date: Some(update_time + 100), prescribing_vet: vet.clone(), active: true, }); @@ -2323,7 +2308,7 @@ mod test { } } #[cfg(test)] -mod test { +mod test_c { use crate::*; use soroban_sdk::{ testutils::{Address as _, Ledger}, @@ -2351,6 +2336,9 @@ mod test { &Gender::Male, &Species::Dog, &breed, + &String::from_str(&env, "Unknown"), // color + &0u32, // weight + &None, // microchip_id &PrivacyLevel::Public, ); assert_eq!(pet_id, 1); @@ -2431,7 +2419,7 @@ mod test { assert_eq!(record.id, 1); assert_eq!(record.pet_id, pet_id); - assert_eq!(record.vet_address, vet); + assert_eq!(record.veterinarian, vet); assert_eq!(record.vaccine_type, VaccineType::Rabies); assert_eq!( record.batch_number, @@ -2687,7 +2675,7 @@ mod test { &String::from_str(&env, "Allergic to bees"), ); - let info = client.get_emergency_info(&pet_id); + let info = client.get_emergency_info(&pet_id, &owner); assert_eq!(info.emergency_contacts.len(), 1); assert_eq!(info.emergency_contacts.len(), 1); } @@ -2720,7 +2708,9 @@ mod test { &vet, &String::from_str(&env, "Blood Test"), &String::from_str(&env, "Normal"), - &None, + &String::from_str(&env, ""), // reference_ranges + &None, // attachment_hash + &None, // medical_record_id ); let res = client.get_lab_result(&lab_id).unwrap(); @@ -2760,12 +2750,12 @@ mod test { ); let mut medications = Vec::new(&env); - medications.push_back(Medication { + medications.push_back(Medication { id: 0, pet_id: 0, name: String::from_str(&env, "Med1"), dosage: String::from_str(&env, "10mg"), frequency: String::from_str(&env, "Daily"), start_date: 100, - end_date: 200, + end_date: Some(200), prescribing_vet: vet.clone(), active: true, }); @@ -2792,21 +2782,21 @@ mod test { env.ledger().with_mut(|l| l.timestamp = update_time); let mut new_meds = Vec::new(&env); - new_meds.push_back(Medication { + new_meds.push_back(Medication { id: 0, pet_id: 0, name: String::from_str(&env, "Med1"), dosage: String::from_str(&env, "20mg"), // Modified dosage frequency: String::from_str(&env, "Daily"), start_date: 100, - end_date: 200, + end_date: Some(200), prescribing_vet: vet.clone(), active: true, }); - new_meds.push_back(Medication { + new_meds.push_back(Medication { id: 0, pet_id: 0, name: String::from_str(&env, "NewMed"), // New med dosage: String::from_str(&env, "5mg"), frequency: String::from_str(&env, "Once"), start_date: update_time, - end_date: update_time + 100, + end_date: Some(update_time + 100), prescribing_vet: vet.clone(), active: true, }); @@ -3017,7 +3007,7 @@ mod test { } } #[cfg(test)] -mod test { +mod test_d { use crate::*; use soroban_sdk::{ testutils::{Address as _, Ledger}, @@ -3128,7 +3118,7 @@ mod test { assert_eq!(record.id, 1); assert_eq!(record.pet_id, pet_id); - assert_eq!(record.vet_address, vet); + assert_eq!(record.veterinarian, vet); assert_eq!(record.vaccine_type, VaccineType::Rabies); assert_eq!( record.batch_number, @@ -3384,7 +3374,7 @@ mod test { &String::from_str(&env, "Allergic to bees"), ); - let info = client.get_emergency_info(&pet_id); + let info = client.get_emergency_info(&pet_id, &owner); assert_eq!(info.emergency_contacts.len(), 1); assert_eq!(info.emergency_contacts.len(), 1); } @@ -3893,10 +3883,11 @@ mod test { // ============================================================ #[cfg(test)] -mod test_vet { +mod test_vet_b { use crate::{Gender, PetChainContract, PetChainContractClient, PrivacyLevel, Species}; use soroban_sdk::{testutils::Address as _, Address, Env, String}; + #[allow(dead_code)] fn register_test_pet( client: &PetChainContractClient, env: &Env, diff --git a/stellar-contracts/src/test_admin_initialization.rs b/stellar-contracts/src/test_admin_initialization.rs index 067542a..5c6ea89 100644 --- a/stellar-contracts/src/test_admin_initialization.rs +++ b/stellar-contracts/src/test_admin_initialization.rs @@ -1,6 +1,6 @@ use crate::*; use soroban_sdk::{ - testutils::{Address as _, Ledger}, + testutils::Address as _, Env, }; @@ -120,7 +120,9 @@ fn test_propose_action_without_admin_initialization() { let proposer = Address::generate(&env); // Create a simple proposal action - let action = ProposalAction::UpdateThreshold(2u32); + let mut new_admins_a = soroban_sdk::Vec::new(&env); + new_admins_a.push_back(proposer.clone()); + let action = ProposalAction::ChangeAdmin((new_admins_a, 1u32)); // Try to propose action without initializing admin - should panic with typed error client.propose_action(&proposer, &action, &3600u64); @@ -172,7 +174,10 @@ fn test_multisig_admin_methods_work_after_initialization() { client.init_multisig(&admin, &admins, &1u32); // Now proposing action should work - let action = ProposalAction::UpdateThreshold(2u32); + let mut new_admins_b = soroban_sdk::Vec::new(&env); + new_admins_b.push_back(admin.clone()); + new_admins_b.push_back(admin2.clone()); + let action = ProposalAction::ChangeAdmin((new_admins_b, 2u32)); let proposal_id = client.propose_action(&admin, &action, &3600u64); assert_eq!(proposal_id, 1u64); diff --git a/stellar-contracts/src/test_encryption_nonce.rs b/stellar-contracts/src/test_encryption_nonce.rs index a2bf8e3..8318e28 100644 --- a/stellar-contracts/src/test_encryption_nonce.rs +++ b/stellar-contracts/src/test_encryption_nonce.rs @@ -1,174 +1,224 @@ #[cfg(test)] mod tests { - use soroban_sdk::{Bytes, Env, Symbol}; + use crate::{ + Gender, PetChainContract, PetChainContractClient, PrivacyLevel, Species, + }; + use soroban_sdk::{testutils::Address as _, Address, Env, String}; - // Import the encryption functions from lib.rs - // These are tests for the nonce uniqueness fix - - #[test] - fn test_nonce_uniqueness_basic() { - // Test that two encryptions produce different nonces + fn setup() -> (Env, PetChainContractClient<'static>) { let env = Env::default(); + env.mock_all_auths(); + let id = env.register_contract(None, PetChainContract); + let client = PetChainContractClient::new(&env, &id); + (env, client) + } - let data = Bytes::from_slice(&env, b"sensitive_data"); - let key = Bytes::from_slice(&env, b"encryption_key_32_bytes_long!"); - - // Call the encryption function twice in the same test - // Note: In Soroban, we'd need to expose the encryption function or call it through the contract - // For now, this demonstrates the test structure - - // First encryption - // let (nonce1, cipher1) = crate::encrypt_sensitive_data(&env, &data, &key); - - // Second encryption - // let (nonce2, cipher2) = crate::encrypt_sensitive_data(&env, &data, &key); - - // Assert nonces are different - // assert_ne!(nonce1, nonce2, "Nonces should be unique across calls"); + fn register_pet(client: &PetChainContractClient, env: &Env, owner: &Address) -> u64 { + client.register_pet( + owner, + &String::from_str(env, "Buddy"), + &String::from_str(env, "1609459200"), + &Gender::Male, + &Species::Dog, + &String::from_str(env, "Labrador"), + &String::from_str(env, "Brown"), + &25u32, + &None, + &PrivacyLevel::Public, + ) } + /// Ciphertext stored on-chain must differ from the plaintext name. + /// With the SHA-256 CTR cipher the stored bytes are tag||XOR(plaintext), + /// which is never equal to the raw XDR of the name string. #[test] - fn test_nonce_derivation_components() { - // Test that nonce components are derived correctly from timestamp and counter - let env = Env::default(); - - // Get initial timestamp - let timestamp_before = env.ledger().timestamp(); - - let data = Bytes::from_slice(&env, b"test_data"); - let key = Bytes::from_slice(&env, b"test_key"); - - // In a real contract call, the nonce would be generated - // Verify timestamp + counter composition: - // - First 8 bytes: ledger timestamp (big-endian) - // - Last 4 bytes: encryption counter (big-endian) - - let expected_nonce_size = 12; - assert_eq!( - expected_nonce_size, 12, - "Nonce should always be 12 bytes for AEAD ciphers" + fn test_ciphertext_differs_from_plaintext() { + let (env, client) = setup(); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + + // Read the raw Pet from storage and compare ciphertext to the + // XDR-encoded plaintext name. + let pet: crate::Pet = env + .storage() + .instance() + .get(&crate::DataKey::Pet(pet_id)) + .unwrap(); + + use soroban_sdk::xdr::ToXdr; + let name_xdr = String::from_str(&env, "Buddy").to_xdr(&env); + + // The ciphertext must NOT equal the raw plaintext bytes. + assert_ne!( + pet.encrypted_name.ciphertext, + name_xdr, + "ciphertext must not equal plaintext" ); } + /// Two separate registrations of pets with the same name must produce + /// different ciphertexts because each call uses a unique nonce + /// (timestamp || monotonic counter). #[test] - fn test_nonce_incremental_counter() { - // Test that nonce counter increments properly - let env = Env::default(); - - // Simulate multiple encryption calls - // Each should have an incremented counter in the last 4 bytes - let data = Bytes::from_slice(&env, b"data"); - let key = Bytes::from_slice(&env, b"key"); - - // With multiple calls, the counter portion would increment: - // Call 1: counter = 0 - // Call 2: counter = 1 - // Call 3: counter = 2 - // etc. - - // This ensures uniqueness even if timestamp doesn't change + fn test_same_plaintext_different_ciphertexts() { + let (env, client) = setup(); + let owner1 = Address::generate(&env); + let owner2 = Address::generate(&env); + + let pet1 = register_pet(&client, &env, &owner1); + let pet2 = register_pet(&client, &env, &owner2); + + let p1: crate::Pet = env + .storage() + .instance() + .get(&crate::DataKey::Pet(pet1)) + .unwrap(); + let p2: crate::Pet = env + .storage() + .instance() + .get(&crate::DataKey::Pet(pet2)) + .unwrap(); + + // Same plaintext ("Buddy") but different nonces → different ciphertexts. + assert_ne!( + p1.encrypted_name.ciphertext, + p2.encrypted_name.ciphertext, + "same plaintext must produce different ciphertexts due to unique nonces" + ); + assert_ne!( + p1.encrypted_name.nonce, + p2.encrypted_name.nonce, + "nonces must be unique across calls" + ); } + /// Nonce must be exactly 12 bytes (8-byte timestamp || 4-byte counter). #[test] - fn test_encryption_ciphertext_uniqueness() { - // Test requirement: Two encryptions of the same data produce different output - let env = Env::default(); + fn test_nonce_is_12_bytes() { + let (env, client) = setup(); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + + let pet: crate::Pet = env + .storage() + .instance() + .get(&crate::DataKey::Pet(pet_id)) + .unwrap(); - let data = Bytes::from_slice(&env, b"same_data"); - let key = Bytes::from_slice(&env, b"same_key"); - - // Note: In current mock implementation, ciphertext is data.clone() - // So both would be identical in ciphertext. - // When real AEAD cipher is implemented: - // - With unique nonce per call - // - Ciphertext will differ even for same plaintext - // let (nonce1, cipher1) = crate::encrypt_sensitive_data(&env, &data, &key); - // let (nonce2, cipher2) = crate::encrypt_sensitive_data(&env, &data, &key); - // assert_ne!(cipher1, cipher2, "Ciphertexts should differ with unique nonces"); + assert_eq!( + pet.encrypted_name.nonce.len(), + 12, + "nonce must be exactly 12 bytes" + ); } + /// Ciphertext must be at least 33 bytes: 32-byte auth tag + ≥1 byte payload. #[test] - fn test_decryption_with_nonce() { - // Test that decryption can use the provided nonce - let env = Env::default(); - - let plaintext = Bytes::from_slice(&env, b"secret_message"); - let key = Bytes::from_slice(&env, b"encryption_key"); - - // Encrypt - // let (nonce, ciphertext) = crate::encrypt_sensitive_data(&env, &plaintext, &key); - - // Decrypt with the same nonce - // let result = crate::decrypt_sensitive_data(&env, &ciphertext, &nonce, &key); - // assert!(result.is_ok(), "Decryption should succeed with correct nonce"); - // assert_eq!(result.unwrap(), plaintext, "Decrypted data should match original"); + fn test_ciphertext_has_auth_tag_prefix() { + let (env, client) = setup(); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + + let pet: crate::Pet = env + .storage() + .instance() + .get(&crate::DataKey::Pet(pet_id)) + .unwrap(); + + assert!( + pet.encrypted_name.ciphertext.len() > 32, + "ciphertext must contain 32-byte auth tag plus encrypted payload" + ); } + /// Decryption round-trip: get_pet must return the original plaintext name. #[test] - fn test_decryption_fails_with_wrong_nonce() { - // Test that decryption fails if a wrong nonce is provided - let env = Env::default(); - - let plaintext = Bytes::from_slice(&env, b"secret_message"); - let key = Bytes::from_slice(&env, b"encryption_key"); + fn test_decryption_roundtrip() { + let (env, client) = setup(); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); - // In real AEAD implementation, using wrong nonce should cause authentication failure - // This is a critical security property that prevents tampering - - // Encrypt - // let (nonce, ciphertext) = crate::encrypt_sensitive_data(&env, &plaintext, &key); - - // Try decrypt with wrong nonce - // let wrong_nonce = Bytes::from_array(&env, &[1u8; 12]); - // let result = crate::decrypt_sensitive_data(&env, &ciphertext, &wrong_nonce, &key); - // assert!(result.is_err(), "Decryption should fail with wrong nonce"); + let profile = client.get_pet(&pet_id).unwrap(); + assert_eq!( + profile.name, + String::from_str(&env, "Buddy"), + "decrypted name must match original plaintext" + ); + assert_eq!( + profile.breed, + String::from_str(&env, "Labrador"), + "decrypted breed must match original plaintext" + ); } + /// Corrupt ciphertext (wrong auth tag) must cause get_pet to return None, + /// not a partial profile — proving the auth tag is actually verified. #[test] - fn test_nonce_uniqueness_across_multiple_calls() { - // Test that multiple sequential calls produce unique nonces - let env = Env::default(); - - let data = Bytes::from_slice(&env, b"data"); - let key = Bytes::from_slice(&env, b"key"); - - // In a real scenario with contract invocation: - // let nonce1 = encrypt(...).0; // counter = 0 - // let nonce2 = encrypt(...).0; // counter = 1 - // let nonce3 = encrypt(...).0; // counter = 2 - // ... - // Each nonce should be unique due to incrementing counter - - // Extract counter portion from each nonce (last 4 bytes) - // Verify they form sequence: 0, 1, 2, ... + fn test_tampered_ciphertext_rejected() { + let (env, client) = setup(); + let owner = Address::generate(&env); + let pet_id = register_pet(&client, &env, &owner); + + // Flip the first byte of the auth tag to simulate tampering. + let mut pet: crate::Pet = env + .storage() + .instance() + .get(&crate::DataKey::Pet(pet_id)) + .unwrap(); + + let first_byte = pet.encrypted_name.ciphertext.get(0).unwrap(); + let mut tampered = soroban_sdk::Bytes::new(&env); + tampered.push_back(first_byte ^ 0xFF); // flip all bits of first byte + for i in 1..pet.encrypted_name.ciphertext.len() { + tampered.push_back(pet.encrypted_name.ciphertext.get(i).unwrap()); + } + pet.encrypted_name.ciphertext = tampered; + env.storage() + .instance() + .set(&crate::DataKey::Pet(pet_id), &pet); + + let result = client.get_pet(&pet_id); + assert!( + result.is_none(), + "tampered ciphertext must be rejected — auth tag verification must fail" + ); } + /// Counter increments: the nonce counter portion (last 4 bytes) must + /// increase with each successive encryption call. #[test] - fn test_nonce_format_validation() { - // Test that nonce has correct format - // - Exactly 12 bytes - // - First 8 bytes: valid timestamp - // - Last 4 bytes: valid counter - let env = Env::default(); - - let data = Bytes::from_slice(&env, b"data"); - let key = Bytes::from_slice(&env, b"key"); - - // let (nonce, _) = crate::encrypt_sensitive_data(&env, &data, &key); - - // Extract components - // let nonce_bytes: [u8; 12] = nonce.to_array().unwrap(); - - // Extract timestamp (first 8 bytes) - // let timestamp_bytes: [u8; 8] = nonce_bytes[0..8].try_into().unwrap(); - // let timestamp_from_nonce = u64::from_be_bytes(timestamp_bytes); - - // Extract counter (last 4 bytes) - // let counter_bytes: [u8; 4] = nonce_bytes[8..12].try_into().unwrap(); - // let counter_from_nonce = u32::from_be_bytes(counter_bytes); - - // Verify components are reasonable - // assert!(timestamp_from_nonce > 0, "Timestamp in nonce should be positive"); - // assert!(counter_from_nonce >= 0, "Counter in nonce should be non-negative"); + fn test_nonce_counter_increments() { + let (env, client) = setup(); + let owner1 = Address::generate(&env); + let owner2 = Address::generate(&env); + let owner3 = Address::generate(&env); + + let p1 = register_pet(&client, &env, &owner1); + let p2 = register_pet(&client, &env, &owner2); + let p3 = register_pet(&client, &env, &owner3); + + let get_counter = |pet_id: u64| -> u32 { + let pet: crate::Pet = env + .storage() + .instance() + .get(&crate::DataKey::Pet(pet_id)) + .unwrap(); + // Counter is in the last 4 bytes of the 12-byte nonce of the + // *first* encrypted field (encrypted_name). + // Each register_pet call encrypts 6 fields, so counters advance by 6. + let n = pet.encrypted_name.nonce; + let b0 = n.get(8).unwrap() as u32; + let b1 = n.get(9).unwrap() as u32; + let b2 = n.get(10).unwrap() as u32; + let b3 = n.get(11).unwrap() as u32; + (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 + }; + + let c1 = get_counter(p1); + let c2 = get_counter(p2); + let c3 = get_counter(p3); + + assert!(c2 > c1, "counter must increase between registrations"); + assert!(c3 > c2, "counter must increase between registrations"); } } diff --git a/stellar-contracts/src/test_get_pet_decryption.rs b/stellar-contracts/src/test_get_pet_decryption.rs index c435e69..371d3d6 100644 --- a/stellar-contracts/src/test_get_pet_decryption.rs +++ b/stellar-contracts/src/test_get_pet_decryption.rs @@ -15,7 +15,7 @@ mod test_get_pet_decryption { PrivacyLevel, Species, }; use soroban_sdk::{ - testutils::Address as _, Address, Bytes, Env, String, Vec, + testutils::Address as _, Address, Bytes, Env, String, }; // ---- helpers ---- @@ -219,7 +219,7 @@ mod test_get_pet_decryption { /// A non-existent pet must still return None (regression guard). #[test] fn test_nonexistent_pet_returns_none() { - let (env, client) = setup(); + let (_env, client) = setup(); assert!(client.get_pet(&9999u64).is_none()); } } diff --git a/stellar-contracts/src/test_input_limits.rs b/stellar-contracts/src/test_input_limits.rs index e443d6b..661aa87 100644 --- a/stellar-contracts/src/test_input_limits.rs +++ b/stellar-contracts/src/test_input_limits.rs @@ -11,7 +11,7 @@ fn repeat(env: &Env, byte: u8, n: usize) -> String { String::from_bytes(env, &buf[..n]) } -fn setup(env: &Env) -> (PetChainContractClient, Address, Address, u64) { +fn setup(env: &Env) -> (PetChainContractClient<'_>, Address, Address, u64) { let contract_id = env.register_contract(None, PetChainContract); let client = PetChainContractClient::new(env, &contract_id); @@ -36,7 +36,7 @@ fn setup(env: &Env) -> (PetChainContractClient, Address, Address, u64) { (client, admin, owner, pet_id) } -fn setup_with_vet(env: &Env) -> (PetChainContractClient, Address, Address, Address, u64) { +fn setup_with_vet(env: &Env) -> (PetChainContractClient<'_>, Address, Address, Address, u64) { let (client, admin, owner, pet_id) = setup(env); let vet = Address::generate(env); client.register_vet( diff --git a/stellar-contracts/src/test_nutrition.rs b/stellar-contracts/src/test_nutrition.rs index 8e95163..36c35bb 100644 --- a/stellar-contracts/src/test_nutrition.rs +++ b/stellar-contracts/src/test_nutrition.rs @@ -77,6 +77,6 @@ fn test_weight_entries_and_pet_update() { let w_history = client.get_weight_history(&pet_id); assert_eq!(w_history.len(), 2); - let profile = client.get_pet(&pet_id, &owner).unwrap(); + let profile = client.get_pet(&pet_id).unwrap(); assert_eq!(profile.weight, 8u32); } diff --git a/stellar-contracts/src/test_search_medical_records.rs b/stellar-contracts/src/test_search_medical_records.rs index 9172315..b10efe5 100644 --- a/stellar-contracts/src/test_search_medical_records.rs +++ b/stellar-contracts/src/test_search_medical_records.rs @@ -4,7 +4,7 @@ #[cfg(test)] mod test_search_medical_records { - use crate::{Gender, Medication, PetChainContract, PetChainContractClient, PrivacyLevel, Species}; + use crate::{Gender, PetChainContract, PetChainContractClient, PrivacyLevel, Species}; use soroban_sdk::{testutils::Address as _, Address, Env, String, Vec}; // ---- helpers ---- @@ -118,7 +118,9 @@ mod test_search_medical_records { add_record(&client, &env, pet_id, &vet, "Flu"); // Range in the far future — should match nothing - let results = client.search_records_by_date_range(&pet_id, &u64::MAX - 100, &u64::MAX); + let start_val = u64::MAX - 100; + let end_val = u64::MAX; + let results = client.search_records_by_date_range(&pet_id, &start_val, &end_val); assert_eq!(results.len(), 0); } @@ -132,7 +134,7 @@ mod test_search_medical_records { // Exact timestamp as both start and end should still match let results = client.search_records_by_date_range(&pet_id, &ts, &ts); // At least one record should fall within the boundary - assert!(results.len() >= 0); // boundary check — no panic + let _ = results.len(); // boundary check — no panic } // ---- search by vet ----