diff --git a/Cargo.lock b/Cargo.lock index 82682b1dc..c4f081968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1649,6 +1649,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit_field" version = "0.10.1" @@ -3215,8 +3230,8 @@ dependencies = [ "serde_json", "sha2 0.10.8", "sqlx", - "strum 0.24.1", - "strum_macros 0.24.3", + "strum", + "strum_macros", "task-manager", "tempfile", "thiserror", @@ -3816,8 +3831,8 @@ dependencies = [ "prost-build", "serde", "serde_json", - "strum 0.26.3", - "strum_macros 0.26.4", + "strum", + "strum_macros", "tonic", "tonic-build", ] @@ -5167,6 +5182,7 @@ dependencies = [ "once_cell", "poc-metrics", "price", + "proptest", "prost", "rand 0.8.5", "regex", @@ -6050,6 +6066,26 @@ dependencies = [ "triggered", ] +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.5.0", + "lazy_static", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.0", + "rand_xorshift", + "regex-syntax 0.8.3", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "prost" version = "0.12.4" @@ -6192,6 +6228,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" version = "0.10.2" @@ -6326,6 +6368,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -6900,6 +6951,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.11" @@ -8647,35 +8710,13 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -dependencies = [ - "strum_macros 0.24.3", -] - [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck 0.4.0", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", + "strum_macros", ] [[package]] @@ -9284,6 +9325,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c52b4cb7830f995903b2fcff3f523d21efc1c11f6c1596dd544b7925a64ff56" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -9476,6 +9523,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/mobile_verifier/Cargo.toml b/mobile_verifier/Cargo.toml index 29d0989ca..84e6c61b3 100644 --- a/mobile_verifier/Cargo.toml +++ b/mobile_verifier/Cargo.toml @@ -63,3 +63,4 @@ coverage-map = { path = "../coverage_map" } [dev-dependencies] backon = "0" +proptest = "1.5.0" diff --git a/mobile_verifier/src/service_provider/reward.rs b/mobile_verifier/src/service_provider/reward.rs index c22a03e65..3c9e7e7cc 100644 --- a/mobile_verifier/src/service_provider/reward.rs +++ b/mobile_verifier/src/service_provider/reward.rs @@ -284,7 +284,7 @@ mod tests { use file_store::promotion_reward::Entity; use helium_proto::services::poc_mobile::{MobileRewardShare, PromotionReward}; - use crate::service_provider::promotions::rewards::PromotionRewardShare; + use crate::service_provider::{self, promotions::rewards::PromotionRewardShare}; use super::*; @@ -617,4 +617,108 @@ mod tests { .collect() } } + + use proptest::prelude::*; + + prop_compose! { + fn arb_share()(sp_id in 0..10_i32, ent_id in 0..200u8, shares in 1..=100u64) -> PromotionRewardShare { + PromotionRewardShare { + service_provider_id: sp_id, + rewardable_entity: Entity::SubscriberId(vec![ent_id]), + shares + } + } + } + + prop_compose! { + fn arb_dc_session()( + sp_id in 0..10_i32, + // below 1 trillion + dc_session in (0..=1_000_000_000_000_u64).prop_map(Decimal::from) + ) -> (i32, Decimal) { + (sp_id, dc_session) + } + } + + prop_compose! { + fn arb_fund()(sp_id in 0..10_i32, bps in arb_bps()) -> (i32, u16) { + (sp_id, bps) + } + } + + prop_compose! { + fn arb_bps()(bps in 0..=10_000u16) -> u16 { bps } + } + + proptest! { + // #![proptest_config(ProptestConfig::with_cases(100_000))] + + #[test] + fn single_provider_does_not_overallocate( + dc_session in any::<u64>().prop_map(Decimal::from), + fund_bps in arb_bps(), + shares in prop::collection::vec(arb_share(), 0..10), + total_allocation in any::<u64>().prop_map(Decimal::from) + ) { + + let sp_infos = ServiceProviderRewardInfos::new( + ServiceProviderDCSessions::from([(0, dc_session)]), + ServiceProviderFunds::from([(0, fund_bps)]), + ServiceProviderPromotions::from(shares), + total_allocation, + dec!(0.00001), + epoch() + ); + + let total_perc= sp_infos.total_percent(); + assert!(total_perc <= dec!(1)); + + let mut allocated = dec!(0); + for (amount, _) in sp_infos.iter_rewards() { + allocated += Decimal::from(amount); + } + assert!(allocated <= total_allocation); + } + + #[test] + fn multiple_provider_does_not_overallocate( + dc_sessions in prop::collection::vec(arb_dc_session(), 0..10), + funds in prop::collection::vec(arb_fund(), 0..10), + promotions in prop::collection::vec(arb_share(), 0..100), + ) { + let epoch = epoch(); + let total_allocation = service_provider::get_scheduled_tokens(&epoch); + + let sp_infos = ServiceProviderRewardInfos::new( + ServiceProviderDCSessions::from(dc_sessions), + ServiceProviderFunds::from(funds), + ServiceProviderPromotions::from(promotions), + total_allocation, + dec!(0.00001), + epoch + ); + + let total_perc= sp_infos.total_percent(); + prop_assert!(total_perc <= dec!(1)); + + let mut allocated = dec!(0); + for (amount, _) in sp_infos.iter_rewards() { + allocated += Decimal::from(amount); + } + prop_assert!(allocated <= total_allocation); + } + + } + + impl RewardInfo { + fn total_percent(&self) -> Decimal { + self.realized_dc_perc + self.realized_promo_perc + self.matched_promo_perc + } + } + + impl ServiceProviderRewardInfos { + fn total_percent(&self) -> Decimal { + self.coll.iter().map(|x| x.total_percent()).sum() + } + } }