Skip to content

Commit

Permalink
HIP-119: Location Trust Score from maximum asserted distance differen…
Browse files Browse the repository at this point in the history
…ce (#840)

* Calculate Location Trust Score from asserted distance in heartbeat

HIP-119 introduces a new set of tables for location trust scores based on `(radio_type, distance_to_asserted)`.

This also allows for a new minimum trust score multiplier of `0x`.

* Remove use of max_distance_to_asserted

This value is now contained within the function `asserted_distance_to_trust_multiplier` in the coverage point calculator.

* Update test for increased allowable location trust multipliers

All location trust scores used to be 1.0 or 0.25. HIP-119 adds 0.00 as a multiplier possibility based on distance.

* typo in test name

* Use lowest possible location trust multiplier value for bad case scenarios

* Pass location trust to determine boost eligibility

Location Trust Scores are no longer reduced based on the presence of a
boosted hex. However, having an average distance from an asserted
location past 50m can cause a radio to be ineligible for boosted rewards.

* namespace location consts to provide more context

Both constants have to do with service provider boosting in regards to a
radios location trust scores. Namespacing allows for not needing to
shove all possible context into a top level name.

* make service provider boosting module

It didn't feel quite correct to have half the service provider boosting
code in lib.rs and the other in location.rs.

The constants have to with location trust scores, but they do not get
used there.

* Remove trust score tests that expect score modification from boosting

The distance to asserted no longer changes trust scores in the presence
of a boosted hex. Service provider boosting eligibility is determined
from the distance to asserted, that is tested at the top level of this crate.

* Test Boosting does not apply when too far away

* consolidate seeding heartbeats v1 and v3

The only difference was the location distance from asserted and assigned
location trust multipliers.

Refactoring heartbeat seeding further is left as an exercise to the next
person who has a need to change these tests.

* Test being too far from asserted location removes service provider boosting

HIP-119 removes the part of calculating coverage points that degrades a
trust score when a radio covers a boosted hex and is more than 50m away
from their asserted location.

Now, being +50m away from an asserted location makes a radio not
eligible for receiving service provider boosted rewards. They continue
to receive the full force of their location trust score.

Being an Indoor radio 100m away from an asserted location is enough to
keep a location trust multiplier of 1.0x, but not receive boosted rewards.

* add hip-125 mentions in the docs

* Try to link to relevant hips when possible

Adding links to multiple places so you don't have to know a secret
location where they exist.

* Active boosting ineligibility takes precendence over passive ineligibility

* Remove answered questions

Both were answered yes

* New location scores apply to Wifi only

CBRS is always trusted for location. The location trust score is intercepted earlier when validating heartbeats. If for some reason CBRS radios do make it to this function, they will receive a good trust score anyways.

* ServiceProvider -> SP for brevity
  • Loading branch information
michaeldjeffrey authored Jul 17, 2024
1 parent fe83fa3 commit 3e142e9
Show file tree
Hide file tree
Showing 14 changed files with 543 additions and 396 deletions.
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

0 comments on commit 3e142e9

Please sign in to comment.