diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 2e23ee181..8e891d737 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -24,6 +24,7 @@ //! - [HIP-98][qos-score] //! - states 30m requirement for boosted hexes [HIP-107][prevent-gaming] //! - increase Boosted hex restriction, 30m -> 50m [Pull Request][boosted-hex-restriction] +//! - Maximum Asserted Distance Difference [HIP-119][location-gaming] //! //! - [CoveragePoints::speedtest_multiplier] //! - [HIP-74][modeled-coverage] @@ -32,7 +33,7 @@ //! //! ## Notable Conditions: //! - [LocationTrust] -//! - If a Radio covers any boosted hexes, [LocationTrust] scores must meet distance requirements, or be degraded. +//! - The average distance to asserted must be <=50m to be eligible for boosted rewards. //! - CBRS Radio's location is always trusted because of GPS. //! //! - [Speedtest] @@ -43,6 +44,10 @@ //! - If a Radio is not [BoostedHexStatus::Eligible], boost values are removed before calculations. //! - If a Hex is boosted by a Provider, the Oracle Assignment multiplier is automatically 1x. //! +//! - [ServiceProviderBoostedRewardEligibility] +//! - Radio must pass at least 1mb of data from 3 unique phones [HIP-84][provider-boosting] +//! - Service Provider can invalidate boosted rewards of a hotspot [HIP-125][provider-banning] +//! //! [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios //! [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md //! [wifi-aps]: https://github.com/helium/HIP/blob/main/0093-addition-of-wifi-aps-to-mobile-subdao.md @@ -53,18 +58,23 @@ //! [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md //! [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage //! [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 +//! [location-gaming]: https://github.com/helium/HIP/blob/main/0119-closing-gaming-loopholes-within-the-mobile-network.md +//! [provider-banning]: https://github.com/helium/HIP/blob/main/0125-temporary-anti-gaming-measures-for-boosted-hexes.md //! pub use crate::{ hexes::{CoveredHex, HexPoints}, - location::LocationTrust, + location::{asserted_distance_to_trust_multiplier, LocationTrust}, + service_provider_boosting::SPBoostedRewardEligibility, speedtest::{BytesPs, Speedtest, SpeedtestTier}, }; use coverage_map::SignalLevel; use rust_decimal::Decimal; use rust_decimal_macros::dec; +use service_provider_boosting::{MAX_AVERAGE_DISTANCE, MIN_WIFI_TRUST_MULTIPLIER}; mod hexes; mod location; +mod service_provider_boosting; mod speedtest; pub type Result = std::result::Result; @@ -122,7 +132,7 @@ pub struct CoveragePoints { /// Input Radio Type pub radio_type: RadioType, /// Input ServiceProviderBoostedRewardEligibility - pub service_provider_boosted_reward_eligibility: ServiceProviderBoostedRewardEligibility, + pub service_provider_boosted_reward_eligibility: SPBoostedRewardEligibility, /// Derived Eligibility for Boosted Hex Rewards pub boosted_hex_eligibility: BoostedHexStatus, /// Speedtests used in calculcation @@ -136,18 +146,18 @@ pub struct CoveragePoints { impl CoveragePoints { pub fn new( radio_type: RadioType, - service_provider_boosted_reward_eligibility: ServiceProviderBoostedRewardEligibility, + service_provider_boosted_reward_eligibility: SPBoostedRewardEligibility, speedtests: Vec, - trust_scores: Vec, + location_trust_scores: Vec, ranked_coverage: Vec, ) -> Result { - let location_trust_scores = location::clean_trust_scores(trust_scores, &ranked_coverage); let location_trust_multiplier = location::multiplier(radio_type, &location_trust_scores); let boost_eligibility = BoostedHexStatus::new( - &radio_type, + radio_type, location_trust_multiplier, - &service_provider_boosted_reward_eligibility, + &location_trust_scores, + service_provider_boosted_reward_eligibility, ); let covered_hexes = @@ -181,7 +191,7 @@ impl CoveragePoints { /// value referred to as "shares". /// /// Ref: - /// https://github.com/helium/proto/blob/master/src/service/poc_mobile.proto + /// /// `message radio_reward` pub fn coverage_points_v1(&self) -> Decimal { let total_coverage_points = self.coverage_points.base + self.boosted_points(); @@ -209,39 +219,48 @@ impl CoveragePoints { match self.boosted_hex_eligibility { BoostedHexStatus::Eligible => self.coverage_points.boosted, BoostedHexStatus::WifiLocationScoreBelowThreshold(_) => dec!(0), + BoostedHexStatus::AverageAssertedDistanceOverLimit(_) => dec!(0), BoostedHexStatus::RadioThresholdNotMet => dec!(0), BoostedHexStatus::ServiceProviderBanned => dec!(0), } } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BoostedHexStatus { Eligible, WifiLocationScoreBelowThreshold(Decimal), + AverageAssertedDistanceOverLimit(Decimal), RadioThresholdNotMet, ServiceProviderBanned, } impl BoostedHexStatus { fn new( - radio_type: &RadioType, - location_trust_score: Decimal, - service_provider_boosted_reward_eligibility: &ServiceProviderBoostedRewardEligibility, + radio_type: RadioType, + location_trust_multiplier: Decimal, + location_trust_scores: &[LocationTrust], + service_provider_boosted_reward_eligibility: SPBoostedRewardEligibility, ) -> Self { - // hip-93: if radio is wifi & location_trust score multiplier < 0.75, no boosting - if radio_type.is_wifi() && location_trust_score < dec!(0.75) { - return Self::WifiLocationScoreBelowThreshold(location_trust_score); - } - - // hip-84: if radio has not met minimum data and subscriber thresholds, no boosting match service_provider_boosted_reward_eligibility { - ServiceProviderBoostedRewardEligibility::Eligible => Self::Eligible, - ServiceProviderBoostedRewardEligibility::ServiceProviderBanned => { - Self::ServiceProviderBanned - } - ServiceProviderBoostedRewardEligibility::RadioThresholdNotMet => { - Self::RadioThresholdNotMet + // hip-125: if radio has been banned by service provider, no boosting + SPBoostedRewardEligibility::ServiceProviderBanned => Self::ServiceProviderBanned, + // hip-84: if radio has not met minimum data and subscriber thresholds, no boosting + SPBoostedRewardEligibility::RadioThresholdNotMet => Self::RadioThresholdNotMet, + SPBoostedRewardEligibility::Eligible => { + // hip-93: if radio is wifi & location_trust score multiplier < 0.75, no boosting + if radio_type.is_wifi() && location_trust_multiplier < MIN_WIFI_TRUST_MULTIPLIER { + return Self::WifiLocationScoreBelowThreshold(location_trust_multiplier); + } + + // hip-119: if the average distance to asserted is beyond 50m, no boosting + let average_distance = + location::average_distance(radio_type, location_trust_scores); + if average_distance > MAX_AVERAGE_DISTANCE { + return Self::AverageAssertedDistanceOverLimit(average_distance); + } + + Self::Eligible } } } @@ -315,13 +334,6 @@ impl RadioType { } } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ServiceProviderBoostedRewardEligibility { - Eligible, - ServiceProviderBanned, - RadioThresholdNotMet, -} - #[cfg(test)] mod tests { @@ -346,7 +358,7 @@ mod tests { ) { let wifi = CoveragePoints::new( RadioType::IndoorWifi, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), vec![RankedCoverage { @@ -368,7 +380,7 @@ mod tests { #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { - let calculate_wifi = |eligibility: ServiceProviderBoostedRewardEligibility| { + let calculate_wifi = |eligibility: SPBoostedRewardEligibility| { CoveragePoints::new( RadioType::IndoorWifi, eligibility, @@ -393,13 +405,12 @@ mod tests { // Radio meeting the threshold is eligible for boosted hexes. // Boosted hex provides radio with more than base_points. - let verified_wifi = calculate_wifi(ServiceProviderBoostedRewardEligibility::Eligible); + let verified_wifi = calculate_wifi(SPBoostedRewardEligibility::Eligible); assert_eq!(base_points * dec!(5), verified_wifi.coverage_points_v1()); // Radio not meeting the threshold is not eligible for boosted hexes. // Boost from hex is not applied, radio receives base points. - let unverified_wifi = - calculate_wifi(ServiceProviderBoostedRewardEligibility::RadioThresholdNotMet); + let unverified_wifi = calculate_wifi(SPBoostedRewardEligibility::RadioThresholdNotMet); assert_eq!(base_points, unverified_wifi.coverage_points_v1()); } @@ -408,7 +419,7 @@ mod tests { let calculate_wifi = |location_trust_scores: Vec| { CoveragePoints::new( RadioType::IndoorWifi, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_scores, vec![RankedCoverage { @@ -441,12 +452,48 @@ mod tests { assert!(untrusted_wifi.coverage_points_v1() < base_points); } + #[test] + fn hip_119_radio_with_past_50m_from_asserted_receives_no_boosted_hexes() { + let calculate_wifi = |location_trust_scores: Vec| { + CoveragePoints::new( + RadioType::IndoorWifi, + SPBoostedRewardEligibility::Eligible, + speedtest_maximum(), + location_trust_scores, + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: NonZeroU32::new(5), + }], + ) + .expect("indoor wifi with location scores") + }; + + let base_points = RadioType::IndoorWifi + .base_coverage_points(&SignalLevel::High) + .unwrap(); + + // Radio with distance to asserted under the limit is eligible for boosted hexes. + // Boosted hex provides radio with more than base_points. + let trusted_wifi = calculate_wifi(location_trust_with_asserted_distance(&[0, 49])); + assert!(trusted_wifi.total_shares() > base_points); + + // Radio with distance to asserted over the limit is not eligible for boosted hexes. + // Boost from hex is not applied. + let untrusted_wifi = calculate_wifi(location_trust_with_asserted_distance(&[50, 51])); + assert_eq!(untrusted_wifi.total_shares(), base_points); + } + #[test] fn speedtests_effect_reward_shares() { let calculate_indoor_cbrs = |speedtests: Vec| { CoveragePoints::new( RadioType::IndoorCbrs, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtests, location_trust_maximum(), vec![RankedCoverage { @@ -534,7 +581,7 @@ mod tests { use Assignment::*; let indoor_cbrs = CoveragePoints::new( RadioType::IndoorCbrs, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), vec![ @@ -591,7 +638,7 @@ mod tests { ) { let outdoor_wifi = CoveragePoints::new( radio_type, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), vec![RankedCoverage { @@ -620,7 +667,7 @@ mod tests { ) { let indoor_wifi = CoveragePoints::new( radio_type, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), vec![ @@ -663,7 +710,7 @@ mod tests { // Location scores are averaged together let indoor_wifi = CoveragePoints::new( RadioType::IndoorWifi, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_with_scores(&[dec!(0.1), dec!(0.2), dec!(0.3), dec!(0.4)]), vec![RankedCoverage { @@ -707,7 +754,7 @@ mod tests { ]; let indoor_wifi = CoveragePoints::new( RadioType::IndoorWifi, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), covered_hexes.clone(), @@ -730,7 +777,7 @@ mod tests { ) { let outdoor_cbrs = CoveragePoints::new( RadioType::OutdoorCbrs, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), vec![RankedCoverage { @@ -757,7 +804,7 @@ mod tests { ) { let indoor_cbrs = CoveragePoints::new( RadioType::IndoorCbrs, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), vec![RankedCoverage { @@ -786,7 +833,7 @@ mod tests { ) { let outdoor_wifi = CoveragePoints::new( RadioType::OutdoorWifi, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), vec![RankedCoverage { @@ -813,7 +860,7 @@ mod tests { ) { let indoor_wifi = CoveragePoints::new( RadioType::IndoorWifi, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtest_maximum(), location_trust_maximum(), vec![RankedCoverage { @@ -831,6 +878,36 @@ mod tests { assert_eq!(expected, indoor_wifi.coverage_points_v1()); } + #[test] + fn wifi_with_bad_location_boosted_hex_status_prioritizes_service_provider_statuses() { + let bad_location = vec![LocationTrust { + meters_to_asserted: 100, + trust_score: dec!(0.0), + }]; + + let wifi_bad_trust_score = |sp_status: SPBoostedRewardEligibility| { + BoostedHexStatus::new( + RadioType::IndoorWifi, + location::multiplier(RadioType::IndoorWifi, &bad_location), + &bad_location, + sp_status, + ) + }; + + assert_eq!( + wifi_bad_trust_score(SPBoostedRewardEligibility::Eligible), + BoostedHexStatus::WifiLocationScoreBelowThreshold(dec!(0)), + ); + assert_eq!( + wifi_bad_trust_score(SPBoostedRewardEligibility::ServiceProviderBanned), + BoostedHexStatus::ServiceProviderBanned + ); + assert_eq!( + wifi_bad_trust_score(SPBoostedRewardEligibility::RadioThresholdNotMet), + BoostedHexStatus::RadioThresholdNotMet + ); + } + fn hex_location() -> hextree::Cell { hextree::Cell::from_raw(0x8c2681a3064edff).unwrap() } @@ -896,6 +973,18 @@ mod tests { .collect() } + fn location_trust_with_asserted_distance(distances_to_asserted: &[u32]) -> Vec { + distances_to_asserted + .to_owned() + .iter() + .copied() + .map(|meters_to_asserted| LocationTrust { + meters_to_asserted, + trust_score: dec!(1.0), + }) + .collect() + } + fn pubkey() -> Vec { vec![1] } diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index a75ef88eb..ad4ac179d 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -1,14 +1,8 @@ -use coverage_map::RankedCoverage; use rust_decimal::Decimal; use rust_decimal_macros::dec; use crate::RadioType; -/// When a Radio is covering any boosted hexes, it's trust score location must -/// be within this distance to it's asserted location. Otherwise the trust_score -/// will be capped at 0.25x. -const RESTRICTIVE_MAX_DISTANCE: Meters = 50; - type Meters = u32; #[derive(Debug, Clone, PartialEq)] @@ -17,20 +11,44 @@ pub struct LocationTrust { pub trust_score: Decimal, } -pub(crate) fn clean_trust_scores( - trust_scores: Vec, - ranked_coverage: &[RankedCoverage], -) -> Vec { - let any_boosted_hexes = ranked_coverage.iter().any(|hex| hex.boosted.is_some()); - - if any_boosted_hexes { - trust_scores - .into_iter() - .map(LocationTrust::into_boosted) - .collect() - } else { - trust_scores +/// Returns the trust multiplier for a given radio type and distance to it's asserted location. +/// +/// [HIP-119: Gaming Loopholes][gaming-loopholes] +/// +/// [gaming-loopholes]: https://github.com/helium/HIP/blob/main/0119-closing-gaming-loopholes-within-the-mobile-network.md#maximum-asserted-distance-difference +pub fn asserted_distance_to_trust_multiplier( + radio_type: RadioType, + meters_to_asserted: Meters, +) -> Decimal { + match radio_type { + RadioType::IndoorWifi => match meters_to_asserted { + 0..=200 => dec!(1.00), + 201..=300 => dec!(0.25), + _ => dec!(0.00), + }, + RadioType::OutdoorWifi => match meters_to_asserted { + 0..=75 => dec!(1.00), + 76..=100 => dec!(0.25), + _ => dec!(0.00), + }, + RadioType::IndoorCbrs => dec!(1.0), + RadioType::OutdoorCbrs => dec!(1.0), + } +} + +pub(crate) fn average_distance(radio_type: RadioType, trust_scores: &[LocationTrust]) -> Decimal { + // CBRS radios are always trusted because they have internal GPS + if radio_type.is_cbrs() { + return dec!(0); } + + let count = Decimal::from(trust_scores.len()); + let sum: Decimal = trust_scores + .iter() + .map(|l| Decimal::from(l.meters_to_asserted)) + .sum(); + + sum / count } pub(crate) fn multiplier(radio_type: RadioType, trust_scores: &[LocationTrust]) -> Decimal { @@ -45,35 +63,18 @@ pub(crate) fn multiplier(radio_type: RadioType, trust_scores: &[LocationTrust]) scores / count } -impl LocationTrust { - fn into_boosted(self) -> Self { - // Cap multipliers to 0.25x when a radio covers _any_ boosted hex - // and it's distance to asserted is above the threshold. - let trust_score = if self.meters_to_asserted > RESTRICTIVE_MAX_DISTANCE { - dec!(0.25).min(self.trust_score) - } else { - self.trust_score - }; - - LocationTrust { - trust_score, - meters_to_asserted: self.meters_to_asserted, - } - } -} - #[cfg(test)] mod tests { - use std::num::NonZeroU32; - - use coverage_map::SignalLevel; - use hex_assignments::{assignment::HexAssignments, Assignment}; use super::*; #[test] - fn all_locations_within_max_boosted_distance() { + fn distance_does_not_effect_multiplier() { let trust_scores = vec![ + LocationTrust { + meters_to_asserted: 0, + trust_score: dec!(0.5), + }, LocationTrust { meters_to_asserted: 49, trust_score: dec!(0.5), @@ -82,17 +83,6 @@ mod tests { meters_to_asserted: 50, trust_score: dec!(0.5), }, - ]; - let boosted = clean_trust_scores(trust_scores.clone(), &boosted_ranked_coverage()); - let unboosted = clean_trust_scores(trust_scores, &[]); - - assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &boosted)); - assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &unboosted)); - } - - #[test] - fn all_locations_past_max_boosted_distance() { - let trust_scores = vec![ LocationTrust { meters_to_asserted: 51, trust_score: dec!(0.5), @@ -101,36 +91,13 @@ mod tests { meters_to_asserted: 100, trust_score: dec!(0.5), }, - ]; - - let boosted = clean_trust_scores(trust_scores.clone(), &boosted_ranked_coverage()); - let unboosted = clean_trust_scores(trust_scores, &[]); - - assert_eq!(dec!(0.25), multiplier(RadioType::IndoorWifi, &boosted)); - assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &unboosted)); - } - - #[test] - fn locations_around_max_boosted_distance() { - let trust_scores = vec![ LocationTrust { - meters_to_asserted: 50, - trust_score: dec!(0.5), - }, - LocationTrust { - meters_to_asserted: 51, + meters_to_asserted: 99999, trust_score: dec!(0.5), }, ]; - let boosted = clean_trust_scores(trust_scores.clone(), &boosted_ranked_coverage()); - let unboosted = clean_trust_scores(trust_scores, &[]); - - // location past distance limit trust score is degraded - let degraded_mult = (dec!(0.5) + dec!(0.25)) / dec!(2); - assert_eq!(degraded_mult, multiplier(RadioType::IndoorWifi, &boosted)); - // location past distance limit trust score is untouched - assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &unboosted)); + assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &trust_scores)); } #[test] @@ -143,26 +110,6 @@ mod tests { trust_score: dec!(0), }]; - let boosted = clean_trust_scores(trust_scores.clone(), &boosted_ranked_coverage()); - let unboosted = clean_trust_scores(trust_scores, &[]); - - assert_eq!(dec!(1), multiplier(RadioType::IndoorCbrs, &boosted)); - assert_eq!(dec!(1), multiplier(RadioType::IndoorCbrs, &unboosted)); - } - - fn boosted_ranked_coverage() -> Vec { - vec![RankedCoverage { - hex: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), - rank: 1, - hotspot_key: vec![], - cbsd_id: None, - signal_level: SignalLevel::High, - assignments: HexAssignments { - footfall: Assignment::A, - landtype: Assignment::A, - urbanized: Assignment::A, - }, - boosted: NonZeroU32::new(5), - }] + assert_eq!(dec!(1), multiplier(RadioType::IndoorCbrs, &trust_scores)); } } diff --git a/coverage_point_calculator/src/service_provider_boosting.rs b/coverage_point_calculator/src/service_provider_boosting.rs new file mode 100644 index 000000000..af31b524a --- /dev/null +++ b/coverage_point_calculator/src/service_provider_boosting.rs @@ -0,0 +1,37 @@ +use rust_decimal::Decimal; +use rust_decimal_macros::dec; + +// In order for the Wi-Fi access point to be eligible for boosted hex rewards +// as described in HIP84 the location trust score needs to be 0.75 or higher. +// +// [HIP-93: Add Wifi to Mobile Dao][add-wifi-aps] +// +// [add-wifi-aps]: https://github.com/helium/HIP/blob/main/0093-addition-of-wifi-aps-to-mobile-subdao.md#341-indoor-access-points-rewards +pub(crate) const MIN_WIFI_TRUST_MULTIPLIER: Decimal = dec!(0.75); + +// In order for access points to be eligible for boosted Service Provider +// rewards defined in HIP-84, the asserted distances must be 50 meters or +// less than the reported location from external services for both indoor +// and outdoor Access Points. +// +// [HIP-119: Gaming Loopholes][gaming-loopholes] +// +// [gaming-loopholes]: https://github.com/helium/HIP/blob/main/0119-closing-gaming-loopholes-within-the-mobile-network.md#maximum-asserted-distance-for-boosted-hexes +pub(crate) const MAX_AVERAGE_DISTANCE: Decimal = dec!(50); + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SPBoostedRewardEligibility { + Eligible, + /// Service Provider can invalidate boosted rewards of a hotspot + /// + /// [HIP-125: Anti gaming measures][anti-gaming] + /// + /// [anti-gaming]: https://github.com/helium/HIP/blob/main/0125-temporary-anti-gaming-measures-for-boosted-hexes.md + ServiceProviderBanned, + /// Radio must pass at least 1mb of data from 3 unique phones. + /// + /// [HIP-84: Provider Hex Boosting][provider-boosting] + /// + /// [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md + RadioThresholdNotMet, +} diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index aba3323ed..f8de92fc9 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -3,8 +3,8 @@ use std::num::NonZeroU32; use chrono::Utc; use coverage_map::{BoostedHexMap, RankedCoverage, SignalLevel, UnrankedCoverage}; use coverage_point_calculator::{ - BytesPs, CoveragePoints, LocationTrust, RadioType, Result, - ServiceProviderBoostedRewardEligibility, Speedtest, SpeedtestTier, + BytesPs, CoveragePoints, LocationTrust, RadioType, Result, SPBoostedRewardEligibility, + Speedtest, SpeedtestTier, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; @@ -52,7 +52,7 @@ fn base_radio_coverage_points() { ] { let coverage_points = CoveragePoints::new( radio_type, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, speedtests.clone(), location_trust_scores.clone(), hexes.clone(), @@ -113,7 +113,7 @@ fn radios_with_coverage() { ] { let coverage_points = CoveragePoints::new( radio_type, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, default_speedtests.clone(), default_location_trust_scores.clone(), base_hex_iter.clone().take(num_hexes).collect(), @@ -240,7 +240,7 @@ fn cbrs_outdoor_with_mixed_signal_level_coverage() -> Result { let radio = CoveragePoints::new( RadioType::OutdoorCbrs, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, Speedtest::mock(SpeedtestTier::Good), vec![], // Location Trust is ignored for Cbrs vec![ @@ -372,7 +372,7 @@ fn indoor_cbrs_radio( ) -> Result { CoveragePoints::new( RadioType::IndoorCbrs, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, Speedtest::mock(speedtest_tier), vec![], coverage.to_owned(), @@ -385,7 +385,7 @@ fn outdoor_cbrs_radio( ) -> Result { CoveragePoints::new( RadioType::OutdoorCbrs, - ServiceProviderBoostedRewardEligibility::Eligible, + SPBoostedRewardEligibility::Eligible, Speedtest::mock(speedtest_tier), vec![], coverage.to_owned(), diff --git a/mobile_verifier/src/heartbeats/cbrs.rs b/mobile_verifier/src/heartbeats/cbrs.rs index fa3cdf3a6..2d7e8f0a1 100644 --- a/mobile_verifier/src/heartbeats/cbrs.rs +++ b/mobile_verifier/src/heartbeats/cbrs.rs @@ -28,7 +28,6 @@ pub struct CbrsHeartbeatDaemon { pool: sqlx::Pool, gateway_info_resolver: GIR, heartbeats: Receiver>, - max_distance_to_asserted: u32, max_distance_to_coverage: u32, heartbeat_sink: FileSinkClient, seniority_sink: FileSinkClient, @@ -65,7 +64,6 @@ where pool, gateway_resolver, cbrs_heartbeats, - settings.max_asserted_distance_deviation, settings.max_distance_from_coverage, valid_heartbeats, seniority_updates, @@ -83,7 +81,6 @@ where pool: sqlx::Pool, gateway_info_resolver: GIR, heartbeats: Receiver>, - max_distance_to_asserted: u32, max_distance_to_coverage: u32, heartbeat_sink: FileSinkClient, seniority_sink: FileSinkClient, @@ -93,7 +90,6 @@ where pool, gateway_info_resolver, heartbeats, - max_distance_to_asserted, max_distance_to_coverage, heartbeat_sink, seniority_sink, @@ -170,7 +166,6 @@ where &self.gateway_info_resolver, coverage_object_cache, location_cache, - self.max_distance_to_asserted, self.max_distance_to_coverage, &epoch, &self.geofence, diff --git a/mobile_verifier/src/heartbeats/mod.rs b/mobile_verifier/src/heartbeats/mod.rs index 2e21a68a9..985fcae52 100644 --- a/mobile_verifier/src/heartbeats/mod.rs +++ b/mobile_verifier/src/heartbeats/mod.rs @@ -373,7 +373,6 @@ impl ValidatedHeartbeat { gateway_info_resolver: &impl GatewayResolver, coverage_object_cache: &CoverageObjectCache, last_location_cache: &LocationCache, - max_distance_to_asserted: u32, max_distance_to_coverage: u32, epoch: &Range>, geofence: &impl GeofenceValidator, @@ -538,17 +537,29 @@ impl ValidatedHeartbeat { true } }; + let distance_to_asserted = asserted_latlng.distance_m(hb_latlng).round() as i64; - let location_trust_score_multiplier = if is_valid - // The heartbeat location to asserted location must be less than the max_distance_to_asserted value: - && distance_to_asserted <= max_distance_to_asserted as i64 - // The heartbeat location to every associated coverage hex must be less than max_distance_to_coverage: - && coverage_object.max_distance_m(hb_latlng).round() as u32 <= max_distance_to_coverage - { - dec!(1.0) + let max_distance = coverage_object.max_distance_m(hb_latlng).round() as u32; + + let location_trust_score_multiplier = if !is_valid { + dec!(0) + } else if max_distance >= max_distance_to_coverage { + // Furthest hex in Heartbeat exceeds allowed coverage distance + dec!(0) } else { - dec!(0.25) + // HIP-119 maximum asserted distance check + use coverage_point_calculator::{ + asserted_distance_to_trust_multiplier, RadioType, + }; + let radio_type = match (heartbeat.hb_type, coverage_object.meta.indoor) { + (HbType::Cbrs, true) => RadioType::IndoorCbrs, + (HbType::Cbrs, false) => RadioType::OutdoorCbrs, + (HbType::Wifi, true) => RadioType::IndoorWifi, + (HbType::Wifi, false) => RadioType::OutdoorWifi, + }; + asserted_distance_to_trust_multiplier(radio_type, distance_to_asserted as u32) }; + Ok(Self::new( heartbeat, cell_type, @@ -575,7 +586,6 @@ impl ValidatedHeartbeat { gateway_info_resolver: &'a impl GatewayResolver, coverage_object_cache: &'a CoverageObjectCache, last_location_cache: &'a LocationCache, - max_distance_to_asserted: u32, max_distance_to_coverage: u32, epoch: &'a Range>, geofence: &'a impl GeofenceValidator, @@ -586,7 +596,6 @@ impl ValidatedHeartbeat { gateway_info_resolver, coverage_object_cache, last_location_cache, - max_distance_to_asserted, max_distance_to_coverage, epoch, geofence, diff --git a/mobile_verifier/src/heartbeats/wifi.rs b/mobile_verifier/src/heartbeats/wifi.rs index 7456af788..f40da6e24 100644 --- a/mobile_verifier/src/heartbeats/wifi.rs +++ b/mobile_verifier/src/heartbeats/wifi.rs @@ -27,7 +27,6 @@ pub struct WifiHeartbeatDaemon { pool: sqlx::Pool, gateway_info_resolver: GIR, heartbeats: Receiver>, - max_distance_to_asserted: u32, max_distance_to_coverage: u32, heartbeat_sink: FileSinkClient, seniority_sink: FileSinkClient, @@ -63,7 +62,6 @@ where pool, gateway_resolver, wifi_heartbeats, - settings.max_asserted_distance_deviation, settings.max_distance_from_coverage, valid_heartbeats, seniority_updates, @@ -81,7 +79,6 @@ where pool: sqlx::Pool, gateway_info_resolver: GIR, heartbeats: Receiver>, - max_distance_to_asserted: u32, max_distance_to_coverage: u32, heartbeat_sink: FileSinkClient, seniority_sink: FileSinkClient, @@ -91,7 +88,6 @@ where pool, gateway_info_resolver, heartbeats, - max_distance_to_asserted, max_distance_to_coverage, heartbeat_sink, seniority_sink, @@ -161,7 +157,6 @@ where &self.gateway_info_resolver, coverage_object_cache, location_cache, - self.max_distance_to_asserted, self.max_distance_to_coverage, &epoch, &self.geofence, diff --git a/mobile_verifier/src/reward_shares.rs b/mobile_verifier/src/reward_shares.rs index 7496acbb7..5a35cdfbc 100644 --- a/mobile_verifier/src/reward_shares.rs +++ b/mobile_verifier/src/reward_shares.rs @@ -8,7 +8,7 @@ use crate::{ subscriber_location::SubscriberValidatedLocations, }; use chrono::{DateTime, Duration, Utc}; -use coverage_point_calculator::ServiceProviderBoostedRewardEligibility; +use coverage_point_calculator::SPBoostedRewardEligibility; use file_store::traits::TimestampEncode; use futures::{Stream, StreamExt}; use helium_crypto::PublicKeyBinary; @@ -427,7 +427,7 @@ struct RadioInfo { coverage_obj_uuid: Uuid, seniority: Seniority, trust_scores: Vec, - sp_boosted_reward_eligibility: ServiceProviderBoostedRewardEligibility, + sp_boosted_reward_eligibility: SPBoostedRewardEligibility, speedtests: Vec, } @@ -2035,7 +2035,7 @@ mod test { inserted_at: now, update_reason: 0, }, - sp_boosted_reward_eligibility: ServiceProviderBoostedRewardEligibility::Eligible, + sp_boosted_reward_eligibility: SPBoostedRewardEligibility::Eligible, speedtests: vec![ coverage_point_calculator::Speedtest { upload_speed: coverage_point_calculator::BytesPs::new(100_000_000), @@ -2068,7 +2068,7 @@ mod test { inserted_at: now, update_reason: 0, }, - sp_boosted_reward_eligibility: ServiceProviderBoostedRewardEligibility::Eligible, + sp_boosted_reward_eligibility: SPBoostedRewardEligibility::Eligible, speedtests: vec![], }, ); diff --git a/mobile_verifier/src/rewarder/boosted_hex_eligibility.rs b/mobile_verifier/src/rewarder/boosted_hex_eligibility.rs index 21f327fea..3db3ea19e 100644 --- a/mobile_verifier/src/rewarder/boosted_hex_eligibility.rs +++ b/mobile_verifier/src/rewarder/boosted_hex_eligibility.rs @@ -1,4 +1,4 @@ -use coverage_point_calculator::ServiceProviderBoostedRewardEligibility; +use coverage_point_calculator::SPBoostedRewardEligibility; use helium_crypto::PublicKeyBinary; use crate::{radio_threshold::VerifiedRadioThresholds, sp_boosted_rewards_bans::BannedRadios}; @@ -21,13 +21,13 @@ impl BoostedHexEligibility { &self, key: PublicKeyBinary, cbsd_id_opt: Option, - ) -> ServiceProviderBoostedRewardEligibility { + ) -> SPBoostedRewardEligibility { if self.banned_radios.contains(&key, cbsd_id_opt.as_deref()) { - ServiceProviderBoostedRewardEligibility::ServiceProviderBanned + SPBoostedRewardEligibility::ServiceProviderBanned } else if self.radio_thresholds.is_verified(key, cbsd_id_opt) { - ServiceProviderBoostedRewardEligibility::Eligible + SPBoostedRewardEligibility::Eligible } else { - ServiceProviderBoostedRewardEligibility::RadioThresholdNotMet + SPBoostedRewardEligibility::RadioThresholdNotMet } } } @@ -56,14 +56,14 @@ mod tests { let eligibility = boosted_hex_eligibility.eligibility(pub_key.clone(), None); assert_eq!( - ServiceProviderBoostedRewardEligibility::ServiceProviderBanned, + SPBoostedRewardEligibility::ServiceProviderBanned, eligibility ); let eligibility = boosted_hex_eligibility.eligibility(pub_key, Some(cbsd_id)); assert_eq!( - ServiceProviderBoostedRewardEligibility::ServiceProviderBanned, + SPBoostedRewardEligibility::ServiceProviderBanned, eligibility ); } @@ -81,14 +81,14 @@ mod tests { let eligibility = boosted_hex_eligibility.eligibility(pub_key.clone(), None); assert_eq!( - ServiceProviderBoostedRewardEligibility::RadioThresholdNotMet, + SPBoostedRewardEligibility::RadioThresholdNotMet, eligibility ); let eligibility = boosted_hex_eligibility.eligibility(pub_key, Some(cbsd_id)); assert_eq!( - ServiceProviderBoostedRewardEligibility::RadioThresholdNotMet, + SPBoostedRewardEligibility::RadioThresholdNotMet, eligibility ); } @@ -109,17 +109,11 @@ mod tests { let eligibility = boosted_hex_eligibility.eligibility(pub_key.clone(), None); - assert_eq!( - ServiceProviderBoostedRewardEligibility::Eligible, - eligibility - ); + assert_eq!(SPBoostedRewardEligibility::Eligible, eligibility); let eligibility = boosted_hex_eligibility.eligibility(pub_key, Some(cbsd_id)); - assert_eq!( - ServiceProviderBoostedRewardEligibility::Eligible, - eligibility - ); + assert_eq!(SPBoostedRewardEligibility::Eligible, eligibility); } fn generate_keypair() -> Keypair { diff --git a/mobile_verifier/src/settings.rs b/mobile_verifier/src/settings.rs index 1a3949ebd..54a31d9dd 100644 --- a/mobile_verifier/src/settings.rs +++ b/mobile_verifier/src/settings.rs @@ -38,11 +38,6 @@ pub struct Settings { /// its respective coverage object #[serde(default = "default_max_distance_from_coverage")] pub max_distance_from_coverage: u32, - /// Max distance in meters between the asserted location of a WIFI hotspot - /// and the lat/lng defined in a heartbeat - /// beyond which its location weight will be reduced - #[serde(default = "default_max_asserted_distance_deviation")] - pub max_asserted_distance_deviation: u32, /// Directory in which new oracle boosting data sets are downloaded into pub data_sets_directory: PathBuf, /// Poll duration for new data sets @@ -66,10 +61,6 @@ fn default_max_distance_from_coverage() -> u32 { 2000 } -fn default_max_asserted_distance_deviation() -> u32 { - 100 -} - fn default_log() -> String { "mobile_verifier=debug,poc_store=info".to_string() } diff --git a/mobile_verifier/tests/integrations/boosting_oracles.rs b/mobile_verifier/tests/integrations/boosting_oracles.rs index 0cf60efeb..d5d00d844 100644 --- a/mobile_verifier/tests/integrations/boosting_oracles.rs +++ b/mobile_verifier/tests/integrations/boosting_oracles.rs @@ -362,7 +362,6 @@ async fn test_footfall_and_urbanization_and_landtype(pool: PgPool) -> anyhow::Re &coverage_objects, &location_cache, 2000, - 2000, &epoch, &MockGeofence, )); diff --git a/mobile_verifier/tests/integrations/hex_boosting.rs b/mobile_verifier/tests/integrations/hex_boosting.rs index a0058ec75..a6de082e5 100644 --- a/mobile_verifier/tests/integrations/hex_boosting.rs +++ b/mobile_verifier/tests/integrations/hex_boosting.rs @@ -724,7 +724,23 @@ async fn test_reduced_location_score_with_boosted_hexes(pool: PgPool) -> anyhow: // seed all the things let mut txn = pool.clone().begin().await?; - seed_heartbeats_v3(epoch.start, &mut txn).await?; + seed_heartbeats_with_location_trust( + epoch.start, + &mut txn, + HotspotLocationTrust { + meters: 10, + multiplier: dec!(1.0), + }, + HotspotLocationTrust { + meters: 10, + multiplier: dec!(1.0), + }, + HotspotLocationTrust { + meters: 300, + multiplier: dec!(0.25), + }, + ) + .await?; seed_speedtests(epoch.end, &mut txn).await?; seed_radio_thresholds(epoch.start, &mut txn).await?; txn.commit().await?; @@ -871,6 +887,185 @@ async fn test_reduced_location_score_with_boosted_hexes(pool: PgPool) -> anyhow: Ok(()) } +#[sqlx::test] +async fn test_distance_from_asserted_removes_boosting_but_not_location_trust( + pool: PgPool, +) -> anyhow::Result<()> { + let (mobile_rewards_client, mut mobile_rewards) = common::create_file_sink(); + let (speedtest_avg_client, _speedtest_avg_server) = common::create_file_sink(); + let now = Utc::now(); + let epoch = (now - ChronoDuration::hours(24))..now; + let epoch_duration = epoch.end - epoch.start; + let boost_period_length = Duration::days(30); + + // seed all the things + let mut txn = pool.clone().begin().await?; + seed_heartbeats_with_location_trust( + epoch.start, + &mut txn, + // hotspot 1 can receive boosting + HotspotLocationTrust { + meters: 10, + multiplier: dec!(1.0), + }, + // hotspot 2 can receive boosting but has no boosted hexes + HotspotLocationTrust { + meters: 10, + multiplier: dec!(1.0), + }, + // hotspot 3 is too far for boosting + HotspotLocationTrust { + meters: 100, + multiplier: dec!(1.0), + }, + ) + .await?; + seed_speedtests(epoch.end, &mut txn).await?; + seed_radio_thresholds(epoch.start, &mut txn).await?; + txn.commit().await?; + update_assignments(&pool).await?; + + // setup boosted hex where reward start time is in the second period length + let multipliers1 = vec![NonZeroU32::new(2).unwrap()]; + let start_ts_1 = epoch.start; + let end_ts_1 = start_ts_1 + (boost_period_length * multipliers1.len() as i32); + + // setup boosted hex where no start or end time is set + let multipliers2 = vec![NonZeroU32::new(2).unwrap()]; + + let boosted_hexes = vec![ + BoostedHexInfo { + // hotspot 1's location + location: Cell::from_raw(0x8a1fb466d2dffff_u64)?, + start_ts: Some(start_ts_1), + end_ts: Some(end_ts_1), + period_length: boost_period_length, + multipliers: multipliers1, + boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), + boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), + version: 0, + }, + BoostedHexInfo { + // hotspot 3's location + location: Cell::from_raw(0x8c2681a306607ff_u64)?, + start_ts: None, + end_ts: None, + period_length: boost_period_length, + multipliers: multipliers2, + boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), + boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), + version: 0, + }, + ]; + + let hex_boosting_client = MockHexBoostingClient::new(boosted_hexes); + let total_poc_emissions = reward_shares::get_scheduled_tokens_for_poc(epoch_duration) + .to_u64() + .unwrap(); + + let (_, rewards) = tokio::join!( + // run rewards for poc and dc + rewarder::reward_poc_and_dc( + &pool, + &hex_boosting_client, + &mobile_rewards_client, + &speedtest_avg_client, + &epoch, + dec!(0.0001) + ), + receive_expected_rewards_maybe_unallocated( + &mut mobile_rewards, + ExpectUnallocated::NoWhenValue(total_poc_emissions) + ) + ); + + let Ok((poc_rewards, unallocated_reward)) = rewards else { + panic!("no rewards received"); + }; + + let mut poc_rewards = poc_rewards.iter(); + let hotspot_2 = poc_rewards.next().unwrap(); // full location trust NO boosts + let hotspot_1 = poc_rewards.next().unwrap(); // full location trust 1 boost + let hotspot_3 = poc_rewards.next().unwrap(); // reduced location trust 1 boost + assert_eq!( + None, + poc_rewards.next(), + "Received more hotspots than expected in rewards" + ); + assert_eq!( + HOTSPOT_1.to_string(), + PublicKeyBinary::from(hotspot_1.hotspot_key.clone()).to_string() + ); + assert_eq!( + HOTSPOT_2.to_string(), + PublicKeyBinary::from(hotspot_2.hotspot_key.clone()).to_string() + ); + assert_eq!( + HOTSPOT_3.to_string(), + PublicKeyBinary::from(hotspot_3.hotspot_key.clone()).to_string() + ); + + // Calculating expected rewards + let (regular_poc, boosted_poc) = get_poc_allocation_buckets(epoch_duration); + + // Here's how we get the regular shares per coverage points + // | base coverage point | speedtest | location | total | + // |---------------------|-----------|----------|-------| + // | 400 | 0.75 | 1.00 | 300 | + // | 400 | 0.75 | 1.00 | 300 | + // | 400 | 0.75 | 1.00 | 300 | + // |---------------------|-----------|----------|-------| + // | 900 | + let regular_share = regular_poc / dec!(900); + + // Boosted hexes are 2x, only one radio qualifies based on the location trust + // 300 * 1 == 300 + // To get points _only_ from boosting. + let boosted_share = boosted_poc / dec!(300); + + let exp_reward_1 = + rounded(regular_share * dec!(300)) + rounded(boosted_share * dec!(300) * dec!(1)); + let exp_reward_2 = + rounded(regular_share * dec!(300)) + rounded(boosted_share * dec!(300) * dec!(0)); + let exp_reward_3 = + rounded(regular_share * dec!(300)) + rounded(boosted_share * dec!(300) * dec!(0)); + + assert_eq!(exp_reward_1, hotspot_1.poc_reward); + assert_eq!(exp_reward_2, hotspot_2.poc_reward); + assert_eq!(exp_reward_3, hotspot_3.poc_reward); + + // assert the number of boosted hexes for each radio + //hotspot 1 has one boosted hex + assert_eq!(1, hotspot_1.boosted_hexes.len()); + //hotspot 2 has no boosted hexes + assert_eq!(0, hotspot_2.boosted_hexes.len()); + // hotspot 3 has a boosted location but as its location trust score + // is reduced the boost does not get applied + assert_eq!(0, hotspot_3.boosted_hexes.len()); + + // assert the hex boost multiplier values + // assert_eq!(2, hotspot_1.boosted_hexes[0].multiplier); + assert_eq!(2, hotspot_1.boosted_hexes[0].multiplier); + assert_eq!(0x8a1fb466d2dffff_u64, hotspot_1.boosted_hexes[0].location); + + // confirm the total rewards allocated matches expectations + let poc_sum = hotspot_1.poc_reward + hotspot_2.poc_reward + hotspot_3.poc_reward; + let total = poc_sum + unallocated_reward.amount; + + let expected_sum = reward_shares::get_scheduled_tokens_for_poc(epoch.end - epoch.start) + .to_u64() + .unwrap(); + assert_eq!(expected_sum, total); + + // confirm the rewarded percentage amount matches expectations + let daily_total = reward_shares::get_total_scheduled_tokens(epoch.end - epoch.start); + let percent = (Decimal::from(total) / daily_total) + .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); + assert_eq!(percent, dec!(0.6)); + + Ok(()) +} + #[sqlx::test] async fn test_poc_with_cbrs_and_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { let (mobile_rewards_client, mut mobile_rewards) = common::create_file_sink(); @@ -1143,100 +1338,24 @@ async fn seed_heartbeats_v1( ts: DateTime, txn: &mut Transaction<'_, Postgres>, ) -> anyhow::Result<()> { - for n in 0..24 { - let hotspot_key1: PublicKeyBinary = HOTSPOT_1.to_string().parse().unwrap(); - let cov_obj_1 = create_coverage_object( - ts + ChronoDuration::hours(n), - None, - hotspot_key1.clone(), - 0x8a1fb466d2dffff_u64, - true, - ); - let wifi_heartbeat1 = ValidatedHeartbeat { - heartbeat: Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot_key1, - cbsd_id: None, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(cov_obj_1.coverage_object.uuid), - location_validation_timestamp: Some(ts - ChronoDuration::hours(24)), - timestamp: ts + ChronoDuration::hours(n), - }, - cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(10), - coverage_meta: None, - location_trust_score_multiplier: dec!(1.0), - validity: HeartbeatValidity::Valid, - }; - - let hotspot_key2: PublicKeyBinary = HOTSPOT_2.to_string().parse().unwrap(); - let cov_obj_2 = create_coverage_object( - ts + ChronoDuration::hours(n), - None, - hotspot_key2.clone(), - 0x8a1fb49642dffff_u64, - true, - ); - let wifi_heartbeat2 = ValidatedHeartbeat { - heartbeat: Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot_key2, - cbsd_id: None, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(cov_obj_2.coverage_object.uuid), - location_validation_timestamp: Some(ts - ChronoDuration::hours(24)), - timestamp: ts + ChronoDuration::hours(n), - }, - cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(10), - coverage_meta: None, - location_trust_score_multiplier: dec!(1.0), - validity: HeartbeatValidity::Valid, - }; - - let hotspot_key3: PublicKeyBinary = HOTSPOT_3.to_string().parse().unwrap(); - let cov_obj_3 = create_coverage_object( - ts + ChronoDuration::hours(n), - None, - hotspot_key3.clone(), - 0x8c2681a306607ff_u64, - true, - ); - let wifi_heartbeat3 = ValidatedHeartbeat { - heartbeat: Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot_key3, - cbsd_id: None, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(cov_obj_3.coverage_object.uuid), - location_validation_timestamp: Some(ts - ChronoDuration::hours(24)), - timestamp: ts + ChronoDuration::hours(n), - }, - cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(10), - coverage_meta: None, - location_trust_score_multiplier: dec!(1.0), - validity: HeartbeatValidity::Valid, - }; - - save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat1, txn).await?; - save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat2, txn).await?; - save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat3, txn).await?; - - wifi_heartbeat1.save(txn).await?; - wifi_heartbeat2.save(txn).await?; - wifi_heartbeat3.save(txn).await?; + seed_heartbeats_with_location_trust( + ts, + txn, + HotspotLocationTrust { + meters: 10, + multiplier: dec!(1.0), + }, + HotspotLocationTrust { + meters: 10, + multiplier: dec!(1.0), + }, + HotspotLocationTrust { + meters: 10, + multiplier: dec!(1.0), + }, + ) + .await?; - cov_obj_1.save(txn).await?; - cov_obj_2.save(txn).await?; - cov_obj_3.save(txn).await?; - } Ok(()) } @@ -1342,13 +1461,18 @@ async fn seed_heartbeats_v2( Ok(()) } -async fn seed_heartbeats_v3( +struct HotspotLocationTrust { + meters: i64, + multiplier: Decimal, +} + +async fn seed_heartbeats_with_location_trust( ts: DateTime, txn: &mut Transaction<'_, Postgres>, + hs_1_location: HotspotLocationTrust, + hs_2_location: HotspotLocationTrust, + hs_3_location: HotspotLocationTrust, ) -> anyhow::Result<()> { - // HOTSPOT 1 has full location trust score - // HOTSPOT 2 has full location trust score - // HOTSPOT 3 has reduced location trust score for n in 0..24 { let hotspot_key1: PublicKeyBinary = HOTSPOT_1.to_string().parse().unwrap(); let cov_obj_1 = create_coverage_object( @@ -1371,9 +1495,9 @@ async fn seed_heartbeats_v3( timestamp: ts + ChronoDuration::hours(n), }, cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(10), + distance_to_asserted: Some(hs_1_location.meters), coverage_meta: None, - location_trust_score_multiplier: dec!(1.0), + location_trust_score_multiplier: hs_1_location.multiplier, validity: HeartbeatValidity::Valid, }; @@ -1398,9 +1522,9 @@ async fn seed_heartbeats_v3( timestamp: ts + ChronoDuration::hours(n), }, cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(10), + distance_to_asserted: Some(hs_2_location.meters), coverage_meta: None, - location_trust_score_multiplier: dec!(1.0), + location_trust_score_multiplier: hs_2_location.multiplier, validity: HeartbeatValidity::Valid, }; @@ -1425,9 +1549,9 @@ async fn seed_heartbeats_v3( timestamp: ts + ChronoDuration::hours(n), }, cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(300), + distance_to_asserted: Some(hs_3_location.meters), coverage_meta: None, - location_trust_score_multiplier: dec!(0.25), + location_trust_score_multiplier: hs_3_location.multiplier, validity: HeartbeatValidity::Valid, }; diff --git a/mobile_verifier/tests/integrations/last_location.rs b/mobile_verifier/tests/integrations/last_location.rs index c5ba6a150..5ba87e932 100644 --- a/mobile_verifier/tests/integrations/last_location.rs +++ b/mobile_verifier/tests/integrations/last_location.rs @@ -42,7 +42,7 @@ impl GatewayResolver for AllOwnersValid { } #[sqlx::test] -async fn heatbeat_uses_last_good_location_when_invalid_location( +async fn heartbeat_uses_last_good_location_when_invalid_location( pool: PgPool, ) -> anyhow::Result<()> { let hotspot = PublicKeyBinary::from_str(PUB_KEY)?; @@ -63,7 +63,6 @@ async fn heatbeat_uses_last_good_location_when_invalid_location( &AllOwnersValid, &coverage_objects, &location_cache, - 1, u32::MAX, &(epoch_start..epoch_end), &MockGeofence, @@ -82,7 +81,6 @@ async fn heatbeat_uses_last_good_location_when_invalid_location( &AllOwnersValid, &coverage_objects, &location_cache, - 1, u32::MAX, &(epoch_start..epoch_end), &MockGeofence, @@ -108,7 +106,7 @@ async fn heatbeat_uses_last_good_location_when_invalid_location( } #[sqlx::test] -async fn heatbeat_will_use_last_good_location_from_db(pool: PgPool) -> anyhow::Result<()> { +async fn heartbeat_will_use_last_good_location_from_db(pool: PgPool) -> anyhow::Result<()> { let hotspot = PublicKeyBinary::from_str(PUB_KEY)?; let epoch_start = Utc::now() - Duration::days(1); let epoch_end = epoch_start + Duration::days(2); @@ -127,7 +125,6 @@ async fn heatbeat_will_use_last_good_location_from_db(pool: PgPool) -> anyhow::R &AllOwnersValid, &coverage_objects, &location_cache, - 1, u32::MAX, &(epoch_start..epoch_end), &MockGeofence, @@ -151,7 +148,6 @@ async fn heatbeat_will_use_last_good_location_from_db(pool: PgPool) -> anyhow::R &AllOwnersValid, &coverage_objects, &location_cache, - 1, u32::MAX, &(epoch_start..epoch_end), &MockGeofence, @@ -177,7 +173,7 @@ async fn heatbeat_will_use_last_good_location_from_db(pool: PgPool) -> anyhow::R } #[sqlx::test] -async fn heatbeat_does_not_use_last_good_location_when_more_than_12_hours( +async fn heartbeat_does_not_use_last_good_location_when_more_than_12_hours( pool: PgPool, ) -> anyhow::Result<()> { let hotspot = PublicKeyBinary::from_str(PUB_KEY)?; @@ -199,7 +195,6 @@ async fn heatbeat_does_not_use_last_good_location_when_more_than_12_hours( &AllOwnersValid, &coverage_objects, &location_cache, - 1, u32::MAX, &(epoch_start..epoch_end), &MockGeofence, @@ -218,7 +213,6 @@ async fn heatbeat_does_not_use_last_good_location_when_more_than_12_hours( &AllOwnersValid, &coverage_objects, &location_cache, - 1, u32::MAX, &(epoch_start..epoch_end), &MockGeofence, @@ -227,7 +221,7 @@ async fn heatbeat_does_not_use_last_good_location_when_more_than_12_hours( assert_eq!( validated_heartbeat_2.location_trust_score_multiplier, - dec!(0.25) + dec!(0.00) ); Ok(()) diff --git a/mobile_verifier/tests/integrations/modeled_coverage.rs b/mobile_verifier/tests/integrations/modeled_coverage.rs index 4d5c64432..0a66fb322 100644 --- a/mobile_verifier/tests/integrations/modeled_coverage.rs +++ b/mobile_verifier/tests/integrations/modeled_coverage.rs @@ -6,6 +6,7 @@ use file_store::{ wifi_heartbeat::{WifiHeartbeat, WifiHeartbeatIngestReport}, }; use futures::stream::{self, StreamExt}; +use h3o::{CellIndex, LatLng}; use helium_crypto::PublicKeyBinary; use helium_proto::services::{ mobile_config::NetworkKeyRole, @@ -416,7 +417,6 @@ async fn process_input( &coverage_objects, &location_cache, 2000, - 2000, epoch, &MockGeofence, )); @@ -1382,26 +1382,15 @@ async fn ensure_lower_trust_score_for_distant_heartbeats(pool: PgPool) -> anyhow coverage_object.save(&mut transaction).await?; transaction.commit().await?; - let hb_1 = WifiHeartbeatIngestReport { - report: WifiHeartbeat { - pubkey: owner_1.clone(), - lon: -105.2715848904, - lat: 40.0194278140, - timestamp: DateTime::::MIN_UTC, - location_validation_timestamp: Some(DateTime::::MIN_UTC), - operation_mode: true, - coverage_object: Vec::from(coverage_object_uuid.into_bytes()), - }, - received_timestamp: Utc::now(), - }; - - let hb_1: Heartbeat = hb_1.into(); + let max_covered_distance = 5_000; + let coverage_object_cache = CoverageObjectCache::new(&pool); + let location_cache = LocationCache::new(&pool); - let hb_2 = WifiHeartbeatIngestReport { + let mk_heartbeat = |latlng: LatLng| WifiHeartbeatIngestReport { report: WifiHeartbeat { pubkey: owner_1.clone(), - lon: -105.2344693282443, - lat: 40.033526907035935, + lon: latlng.lng(), + lat: latlng.lat(), timestamp: DateTime::::MIN_UTC, location_validation_timestamp: Some(DateTime::::MIN_UTC), operation_mode: true, @@ -1410,70 +1399,54 @@ async fn ensure_lower_trust_score_for_distant_heartbeats(pool: PgPool) -> anyhow received_timestamp: Utc::now(), }; - let hb_2: Heartbeat = hb_2.into(); - - let coverage_object_cache = CoverageObjectCache::new(&pool); - let location_cache = LocationCache::new(&pool); - - let validated_hb_1 = ValidatedHeartbeat::validate( - hb_1, - &AllOwnersValid, - &coverage_object_cache, - &location_cache, - 2000, - 2000, - &(DateTime::::MIN_UTC..DateTime::::MAX_UTC), - &MockGeofence, - ) - .await - .unwrap(); - - assert_eq!(validated_hb_1.location_trust_score_multiplier, dec!(1.0)); - - let validated_hb_2 = ValidatedHeartbeat::validate( - hb_2.clone(), - &AllOwnersValid, - &coverage_object_cache, - &location_cache, - 1000000, - 2000, - &(DateTime::::MIN_UTC..DateTime::::MAX_UTC), - &MockGeofence, - ) - .await - .unwrap(); + let validate = |latlng: LatLng| { + ValidatedHeartbeat::validate( + mk_heartbeat(latlng).into(), + &AllOwnersValid, + &coverage_object_cache, + &location_cache, + max_covered_distance, + &(DateTime::::MIN_UTC..DateTime::::MAX_UTC), + &MockGeofence, + ) + }; - assert_eq!(validated_hb_2.location_trust_score_multiplier, dec!(0.25)); + let covered_cell_index: CellIndex = "8c2681a3064d9ff".parse()?; + let covered_latlng = LatLng::from(covered_cell_index); - let validated_hb_2 = ValidatedHeartbeat::validate( - hb_2.clone(), - &AllOwnersValid, - &coverage_object_cache, - &location_cache, - 2000, - 1000000, - &(DateTime::::MIN_UTC..DateTime::::MAX_UTC), - &MockGeofence, - ) - .await - .unwrap(); + // Constrain distances by only moving vertically + let near_latlng = LatLng::new(40.0194278140, -105.272)?; // 35m + let med_latlng = LatLng::new(40.0194278140, -105.274)?; // 205m + let far_latlng = LatLng::new(40.0194278140, -105.3)?; // 2,419m + let past_latlng = LatLng::new(40.0194278140, 105.2715848904)?; // 10,591,975m - assert_eq!(validated_hb_2.location_trust_score_multiplier, dec!(0.25)); + // It's easy to gloss over floats, let make sure the distances are within the ranges we expect. + assert!((0.0..=200.0).contains(&covered_latlng.distance_m(near_latlng))); // Indoor low distance <= 200 + assert!((200.0..=300.0).contains(&covered_latlng.distance_m(med_latlng))); // Indoor Medium distance <= 300 + assert!(covered_latlng.distance_m(far_latlng) > 300.0); // Indoor Over Distance => 300 + assert!(covered_latlng.distance_m(past_latlng) > max_covered_distance as f64); // Indoor past max distance => max_distance - let validated_hb_2 = ValidatedHeartbeat::validate( - hb_2.clone(), - &AllOwnersValid, - &coverage_object_cache, - &location_cache, - 1000000, - 1000000, - &(DateTime::::MIN_UTC..DateTime::::MAX_UTC), - &MockGeofence, - ) - .await - .unwrap(); + let low_dist_validated = validate(near_latlng).await?; + let med_dist_validated = validate(med_latlng).await?; + let far_dist_validated = validate(far_latlng).await?; + let past_dist_validated = validate(past_latlng).await?; - assert_eq!(validated_hb_2.location_trust_score_multiplier, dec!(1.0)); + assert_eq!( + low_dist_validated.location_trust_score_multiplier, + dec!(1.0) + ); + assert_eq!( + med_dist_validated.location_trust_score_multiplier, + dec!(0.25) + ); + assert_eq!( + far_dist_validated.location_trust_score_multiplier, + dec!(0.00) + ); + assert_eq!( + past_dist_validated.location_trust_score_multiplier, + dec!(0.00) + ); Ok(()) }