diff --git a/Cargo.lock b/Cargo.lock index f4f6b08..b6ea00d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,7 +147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand", ] [[package]] @@ -180,21 +180,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bitflags" version = "2.13.0" @@ -295,7 +280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -527,7 +512,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.6.4", + "rand_core", "serde", "sha2", "subtle", @@ -552,7 +537,7 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core 0.6.4", + "rand_core", "sec1", "subtle", "zeroize", @@ -564,16 +549,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "escape-bytes" version = "0.1.1" @@ -586,19 +561,13 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - [[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -644,29 +613,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - [[package]] name = "group" version = "0.13.0" @@ -674,7 +620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -846,12 +792,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "log" version = "0.4.29" @@ -907,6 +847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -988,30 +929,20 @@ dependencies = [ [[package]] name = "proptest" -version = "1.8.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ - "bit-set", - "bit-vec", "bitflags", "lazy_static", "num-traits", - "rand 0.9.4", - "rand_chacha 0.9.0", + "rand", + "rand_chacha", "rand_xorshift", "regex-syntax", - "rusty-fork", - "tempfile", "unarray", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" version = "1.0.45" @@ -1021,18 +952,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" version = "0.8.5" @@ -1040,18 +959,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", + "rand_chacha", + "rand_core", ] [[package]] @@ -1061,17 +970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -1080,25 +979,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", + "getrandom", ] [[package]] name = "rand_xorshift" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -1146,37 +1036,12 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - [[package]] name = "schemars" version = "0.9.0" @@ -1328,7 +1193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1393,7 +1258,7 @@ dependencies = [ "ed25519-dalek", "elliptic-curve", "generic-array", - "getrandom 0.2.17", + "getrandom", "hex-literal", "hmac", "k256", @@ -1401,8 +1266,8 @@ dependencies = [ "num-integer", "num-traits", "p256", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", "sec1", "sha2", "sha3", @@ -1454,7 +1319,7 @@ dependencies = [ "ctor", "derive_arbitrary", "ed25519-dalek", - "rand 0.8.5", + "rand", "rustc_version", "serde", "serde_json", @@ -1609,19 +1474,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1697,30 +1549,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.115" @@ -1862,21 +1696,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - [[package]] name = "zerocopy" version = "0.8.48" diff --git a/Cargo.toml b/Cargo.toml index f293376..b9c96b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ doctest = false soroban-sdk = "=22.0.11" [dev-dependencies] -proptest = "=1.8.0" +proptest = { version = "=1.5.0", default-features = false, features = ["std", "alloc"] } soroban-sdk = { version = "=22.0.11", features = ["testutils"] } [profile.release] diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index d9ef384..72ba285 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -21,30 +21,34 @@ cargo tarpaulin --out Html --out Stdout ## Property Tests for Timestamp Ordering (Issue #136) -New file: `tests/proptest_timestamps.rs` +File: `tests/proptest_timestamps.rs` What is validated: -- Valid ordering property: `start < end` succeeds for randomized inputs. -- Invalid ordering property: `start >= end` rejects with `Error::InvalidTimestamps`. -- Duration bound property: `(end - start) > MAX_VAULT_DURATION` rejects with `Error::DurationTooLong`. +- **Valid ordering property**: `start < end` succeeds for randomized inputs and randomized ledger times. +- **Invalid ordering property**: `start >= end` rejects with `Error::InvalidTimestamps`. +- **Duration bound property**: `(end - start) > MAX_VAULT_DURATION` rejects with `Error::DurationTooLong`. +- **Duration boundary property**: Densely samples the exact boundary where `duration == MAX_VAULT_DURATION` is accepted, and `duration == MAX_VAULT_DURATION + 1` is rejected with `Error::DurationTooLong`. +- **Past-start rejection property**: Verifies that any vault creation with `start_timestamp < current_time` is rejected with `Error::InvalidTimestamp`. Strategy design (controlled randomness): -- Valid cases use `(start_offset, duration)` with: +- **Randomized Ledger Time**: The current time `now` is generated via a randomized strategy `0u64..20_000_000_000u64` to sample different ledger dates. +- **Valid cases** use `(now, start_offset, duration)` with: - `start = now + start_offset` - `duration in 1..=MAX_VAULT_DURATION` - `end = start + duration` -- Invalid ordering uses `end = start.saturating_sub(backoff)` to guarantee `end <= start`. -- Overflow risk is avoided by bounded ranges for `start_offset` and `duration`. +- **Invalid ordering** uses `end = start.saturating_sub(backoff)` to guarantee `end <= start`. +- **Past-start cases** use `start = now - past_offset` (where `past_offset >= 1` and `now >= 1_000_000`) to guarantee `start < now`. +- **Overflow risk** is entirely avoided by setting upper limits on `now` and `start_offset` such that `now + start_offset + duration` is mathematically guaranteed to stay well below `u64::MAX`. Explicit edge vectors included: -- `start == end` (reject) +- `start == end` (reject with `Error::InvalidTimestamps`) - `start = 0`, `end = 1` with ledger time at `0` (accept) - `duration == MAX_VAULT_DURATION` (accept) +- `start == now` (accept) -## Test Coverage: 95%+ Achieved ✅ - **32 comprehensive tests** - All passing - **92.16% line coverage** (47/51 lines) diff --git a/tests/proptest_timestamps.rs b/tests/proptest_timestamps.rs index 312daee..d8e37e8 100644 --- a/tests/proptest_timestamps.rs +++ b/tests/proptest_timestamps.rs @@ -47,7 +47,8 @@ proptest! { #[test] fn prop_create_vault_accepts_valid_ordering( - start_offset in 0u64..1_000_000, + now in 0u64..20_000_000_000u64, + start_offset in 0u64..1_000_000_000, duration in 1u64..=MAX_VAULT_DURATION, amount in MIN_AMOUNT..=MAX_AMOUNT, ) { @@ -58,10 +59,9 @@ proptest! { let failure = Address::generate(&env); let milestone = BytesN::from_array(&env, &[0u8; 32]); - let now = 1_725_000_000u64; env.ledger().set_timestamp(now); - // Overflow-safe by construction: start <= now + 1_000_000 and duration <= MAX_VAULT_DURATION. + // Bounded to avoid u64 overflow: now <= 20_000_000_000, start_offset <= 1_000_000_000, duration <= MAX_VAULT_DURATION. let start = now + start_offset; let end = start + duration; @@ -88,6 +88,7 @@ proptest! { #[test] fn prop_create_vault_rejects_start_gte_end( + now in 0u64..20_000_000_000u64, start_offset in 0u64..1_000_000, backoff in 0u64..1_000_000, amount in MIN_AMOUNT..=MAX_AMOUNT, @@ -98,7 +99,6 @@ proptest! { let success = Address::generate(&env); let failure = Address::generate(&env); - let now = 1_725_000_000u64; env.ledger().set_timestamp(now); let start = now + start_offset; @@ -123,6 +123,7 @@ proptest! { #[test] fn prop_create_vault_rejects_duration_above_max( + now in 0u64..20_000_000_000u64, start_offset in 0u64..10_000, extra in 1u64..10_000, amount in MIN_AMOUNT..=MAX_AMOUNT, @@ -133,7 +134,6 @@ proptest! { let success = Address::generate(&env); let failure = Address::generate(&env); - let now = 1_725_000_000u64; env.ledger().set_timestamp(now); let start = now + start_offset; @@ -155,8 +155,131 @@ proptest! { assert_contract_error(result, Error::DurationTooLong); } + + #[test] + fn prop_duration_boundary( + now in 0u64..20_000_000_000u64, + start_offset in 0u64..1_000_000, + amount in MIN_AMOUNT..=MAX_AMOUNT, + ) { + let (env, client, usdc, usdc_asset) = setup(); + + let creator = Address::generate(&env); + let success = Address::generate(&env); + let failure = Address::generate(&env); + let milestone = BytesN::from_array(&env, &[3u8; 32]); + + env.ledger().set_timestamp(now); + + let start = now + start_offset; + + // 1. Assert duration == MAX_VAULT_DURATION accepts + { + let end_valid = start + MAX_VAULT_DURATION; + usdc_asset.mint(&creator, &amount); + let vault_id = client.create_vault( + &usdc, + &creator, + &amount, + &start, + &end_valid, + &milestone, + &None, + &success, + &failure, + ); + let vault = client.get_vault_state(&vault_id).expect("vault should exist"); + prop_assert_eq!(vault.start_timestamp, start); + prop_assert_eq!(vault.end_timestamp, end_valid); + prop_assert_eq!(vault.end_timestamp - vault.start_timestamp, MAX_VAULT_DURATION); + } + + // 2. Assert duration == MAX_VAULT_DURATION + 1 rejects with DurationTooLong + { + let end_invalid = start + MAX_VAULT_DURATION + 1; + usdc_asset.mint(&creator, &amount); + let result = client.try_create_vault( + &usdc, + &creator, + &amount, + &start, + &end_invalid, + &milestone, + &None, + &success, + &failure, + ); + assert_contract_error(result, Error::DurationTooLong); + } + } + + #[test] + fn prop_past_start_rejected( + now in 1_000_000u64..20_000_000_000u64, + past_offset in 1u64..1_000_000, + duration in 1u64..=MAX_VAULT_DURATION, + amount in MIN_AMOUNT..=MAX_AMOUNT, + ) { + let (env, client, usdc, usdc_asset) = setup(); + + let creator = Address::generate(&env); + let success = Address::generate(&env); + let failure = Address::generate(&env); + let milestone = BytesN::from_array(&env, &[4u8; 32]); + + env.ledger().set_timestamp(now); + + let start = now - past_offset; + let end = start + duration; + + usdc_asset.mint(&creator, &amount); + + let result = client.try_create_vault( + &usdc, + &creator, + &amount, + &start, + &end, + &milestone, + &None, + &success, + &failure, + ); + + assert_contract_error(result, Error::InvalidTimestamp); + } +} + +#[test] +fn edge_start_eq_now_succeeds() { + let (env, client, usdc, usdc_asset) = setup(); + let creator = Address::generate(&env); + let now = 1_725_000_000u64; + env.ledger().set_timestamp(now); + + usdc_asset.mint(&creator, &MIN_AMOUNT); + + let start = now; + let end = start + 1; + + let id = client.create_vault( + &usdc, + &creator, + &MIN_AMOUNT, + &start, + &end, + &BytesN::from_array(&env, &[6u8; 32]), + &None, + &Address::generate(&env), + &Address::generate(&env), + ); + + let vault = client.get_vault_state(&id).unwrap(); + assert_eq!(vault.start_timestamp, start); + assert_eq!(vault.end_timestamp, end); } + #[test] fn edge_start_eq_end_rejected() { let (env, client, usdc, usdc_asset) = setup();