Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HIP-119: Location Trust Score from maximum asserted distance difference #840

Merged
merged 19 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4616f1b
Calculate Location Trust Score from asserted distance in heartbeat
michaeldjeffrey Jul 8, 2024
f48cbb2
Remove use of max_distance_to_asserted
michaeldjeffrey Jul 9, 2024
8e9b254
Update test for increased allowable location trust multipliers
michaeldjeffrey Jul 9, 2024
48167e5
typo in test name
michaeldjeffrey Jul 9, 2024
5f3d97c
Use lowest possible location trust multiplier value for bad case scen…
michaeldjeffrey Jul 9, 2024
63d2881
Pass location trust to determine boost eligibility
michaeldjeffrey Jul 9, 2024
41222c0
namespace location consts to provide more context
michaeldjeffrey Jul 9, 2024
480690f
make service provider boosting module
michaeldjeffrey Jul 9, 2024
f16bfa8
Remove trust score tests that expect score modification from boosting
michaeldjeffrey Jul 9, 2024
159b0b3
Test Boosting does not apply when too far away
michaeldjeffrey Jul 9, 2024
e5b68cd
consolidate seeding heartbeats v1 and v3
michaeldjeffrey Jul 9, 2024
fbfeb65
Test being too far from asserted location removes service provider bo…
michaeldjeffrey Jul 9, 2024
051f289
add hip-125 mentions in the docs
michaeldjeffrey Jul 10, 2024
adb6439
Try to link to relevant hips when possible
michaeldjeffrey Jul 10, 2024
3d7a1cd
Active boosting ineligibility takes precendence over passive ineligib…
michaeldjeffrey Jul 11, 2024
1a0d007
Merged origin/main into mj/hip-119-location-trust
michaeldjeffrey Jul 16, 2024
dab79b8
Remove answered questions
michaeldjeffrey Jul 16, 2024
c3e8194
New location scores apply to Wifi only
michaeldjeffrey Jul 16, 2024
861bd8f
ServiceProvider -> SP for brevity
michaeldjeffrey Jul 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 137 additions & 48 deletions coverage_point_calculator/src/lib.rs

Large diffs are not rendered by default.

143 changes: 45 additions & 98 deletions coverage_point_calculator/src/location.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -17,20 +11,44 @@ pub struct LocationTrust {
pub trust_score: Decimal,
}

pub(crate) fn clean_trust_scores(
trust_scores: Vec<LocationTrust>,
ranked_coverage: &[RankedCoverage],
) -> Vec<LocationTrust> {
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 {
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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]
Expand All @@ -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<RankedCoverage> {
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));
}
}
37 changes: 37 additions & 0 deletions coverage_point_calculator/src/service_provider_boosting.rs
Original file line number Diff line number Diff line change
@@ -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,
}
14 changes: 7 additions & 7 deletions coverage_point_calculator/tests/coverage_point_calculator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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![
Expand Down Expand Up @@ -372,7 +372,7 @@ fn indoor_cbrs_radio(
) -> Result<CoveragePoints> {
CoveragePoints::new(
RadioType::IndoorCbrs,
ServiceProviderBoostedRewardEligibility::Eligible,
SPBoostedRewardEligibility::Eligible,
Speedtest::mock(speedtest_tier),
vec![],
coverage.to_owned(),
Expand All @@ -385,7 +385,7 @@ fn outdoor_cbrs_radio(
) -> Result<CoveragePoints> {
CoveragePoints::new(
RadioType::OutdoorCbrs,
ServiceProviderBoostedRewardEligibility::Eligible,
SPBoostedRewardEligibility::Eligible,
Speedtest::mock(speedtest_tier),
vec![],
coverage.to_owned(),
Expand Down
5 changes: 0 additions & 5 deletions mobile_verifier/src/heartbeats/cbrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ pub struct CbrsHeartbeatDaemon<GIR, GFV> {
pool: sqlx::Pool<sqlx::Postgres>,
gateway_info_resolver: GIR,
heartbeats: Receiver<FileInfoStream<CbrsHeartbeatIngestReport>>,
max_distance_to_asserted: u32,
max_distance_to_coverage: u32,
heartbeat_sink: FileSinkClient,
seniority_sink: FileSinkClient,
Expand Down Expand Up @@ -65,7 +64,6 @@ where
pool,
gateway_resolver,
cbrs_heartbeats,
settings.max_asserted_distance_deviation,
settings.max_distance_from_coverage,
valid_heartbeats,
seniority_updates,
Expand All @@ -83,7 +81,6 @@ where
pool: sqlx::Pool<sqlx::Postgres>,
gateway_info_resolver: GIR,
heartbeats: Receiver<FileInfoStream<CbrsHeartbeatIngestReport>>,
max_distance_to_asserted: u32,
max_distance_to_coverage: u32,
heartbeat_sink: FileSinkClient,
seniority_sink: FileSinkClient,
Expand All @@ -93,7 +90,6 @@ where
pool,
gateway_info_resolver,
heartbeats,
max_distance_to_asserted,
max_distance_to_coverage,
heartbeat_sink,
seniority_sink,
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 20 additions & 11 deletions mobile_verifier/src/heartbeats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DateTime<Utc>>,
geofence: &impl GeofenceValidator,
Expand Down Expand Up @@ -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,
Expand All @@ -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<DateTime<Utc>>,
geofence: &'a impl GeofenceValidator,
Expand All @@ -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,
Expand Down
Loading