From 6ce7d6819ea9f18f1dcded01a5a8603b2199840e Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 21 Jun 2024 11:35:53 -0700 Subject: [PATCH] Coverage Point Calculator Integration with mobile-verifier (#830) * include coverage point calculator in mobile-verifier * derive Debug for nested types will help with the switchover * CoveragePoints uses CoveragePoint2 internally This makes coverage points async internally. When cleanup is done there will be a single instance of CoveragePoints. * add coverage map to compute rank * unignore mobile verifier integration tests * update with rename SubscriberThreshold -> RadioThreshold thresholds being met for a radio are not only about subscribers. * filter for boosted hexes during report generation It's the responsibility of the report to only include hexes with active boost values * calculator uses hex_assignments crate * clean values for reports * Start adding coverage-map in mobile-verifier Keep disctinction between SignalLevel in coverage and coverage-map, this way coverage-map doesn't need to bring in sqlx as a dependency * break out coverage map for drop in replacement testing * Remove coverage map trait * use RankedCoverage from coverage-map * Skip a radio for rewards if it cannot be constructed properly rewards should never fail. but we can skip a radio and have an alert that says something went wrong. * use new coverage_map for testing * bring up to date with coverage-point-calculator * remove unused fields now that coverage-map is stabilized * remove innner coverage map wrapper * rename after move from unwrapping * lift coverage points one more time * starting to conslidate radio information into single map * total coverage points is now provided pre-truncated * simplify coverage points to use only radio_infos * hotspot_points no longer needs to be &mut self * clean up trust_score construction and long types Move zipping trust scores with their distances to heartbeatreward. Leave I put the use statements above where they are in the function because the names of the types kind of overlap with the names used in the mobile verifier, and I wanted to make it extra clear to anyone reading, this function is transforming those values into calculator types. * name test function more explicitly * DateTime implements Copy, no need to Clone * remove need for cloning radio_info we can clone the speed_tests and trust_scores as they enter the calculator, but everything else can work with a reference for much longer. * Remove replaced coverage point code * rename coverage points constructor The constructor no longer tries to calculate points, it collects information about radios and coverage. All the pointing happens in into_rewards. * remove async where possible from CoveragePoints * remove unneccessary `this` binding * Rename CoveragePoints -> CoverageShares This matches with the other reward structs `MapperShares`, `ServiceProviderShares`. And reflects more that we're talking about `reward_shares`, where `coverage_points` is starting to mean points provided towards shares relating only to coverage (excluding backhaul (speedtests)). * remove unneeded derive for debug * remove location trust score calculation This all lives in the coverage-point-calculator now * rename to differentiate argument from local bindings * construct coverage map inside block scope We can insert coverage objects directly after iterating over them, radio_info only needs to know if the radio `is_indoor`. Moving that binding to the top helps hint that it's set by something in the block and returned. And we don't need to carry around covered_hexes for any longer than necessary. * function call now small enough to inline * rename coverage variables to match with struct * handle potential lack of speedtest for radio --- Cargo.lock | 2 + mobile_verifier/Cargo.toml | 2 + mobile_verifier/src/cli/reward_from_db.rs | 4 +- mobile_verifier/src/coverage.rs | 899 +----------------- mobile_verifier/src/heartbeats/mod.rs | 85 +- mobile_verifier/src/reward_shares.rs | 678 ++++++------- mobile_verifier/src/rewarder.rs | 6 +- .../tests/integrations/boosting_oracles.rs | 12 +- .../tests/integrations/heartbeats.rs | 7 - .../tests/integrations/modeled_coverage.rs | 111 ++- .../tests/integrations/seniority.rs | 1 - 11 files changed, 459 insertions(+), 1348 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0c2b9113..6f4e55426 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4758,6 +4758,8 @@ dependencies = [ "chrono", "clap 4.4.8", "config", + "coverage-map", + "coverage-point-calculator", "custom-tracing", "db-store", "derive_builder", diff --git a/mobile_verifier/Cargo.toml b/mobile_verifier/Cargo.toml index 75d86b1ec..cc80ba81a 100644 --- a/mobile_verifier/Cargo.toml +++ b/mobile_verifier/Cargo.toml @@ -58,6 +58,8 @@ regex = "1" humantime-serde = { workspace = true } custom-tracing = { path = "../custom_tracing" } hex-assignments = { path = "../hex_assignments" } +coverage-point-calculator = { path = "../coverage_point_calculator" } +coverage-map = { path = "../coverage_map" } [dev-dependencies] backon = "0" diff --git a/mobile_verifier/src/cli/reward_from_db.rs b/mobile_verifier/src/cli/reward_from_db.rs index ce0a08f21..a159eb9e5 100644 --- a/mobile_verifier/src/cli/reward_from_db.rs +++ b/mobile_verifier/src/cli/reward_from_db.rs @@ -1,7 +1,7 @@ use crate::{ heartbeats::HeartbeatReward, radio_threshold::VerifiedRadioThresholds, - reward_shares::{get_scheduled_tokens_for_poc, CoveragePoints}, + reward_shares::{get_scheduled_tokens_for_poc, CoverageShares}, speedtests_average::SpeedtestAverages, Settings, }; @@ -41,7 +41,7 @@ impl Cmd { let speedtest_averages = SpeedtestAverages::aggregate_epoch_averages(epoch.end, &pool).await?; - let reward_shares = CoveragePoints::aggregate_points( + let reward_shares = CoverageShares::new( &pool, heartbeats, &speedtest_averages, diff --git a/mobile_verifier/src/coverage.rs b/mobile_verifier/src/coverage.rs index 25f10de97..1b47810ea 100644 --- a/mobile_verifier/src/coverage.rs +++ b/mobile_verifier/src/coverage.rs @@ -17,25 +17,17 @@ use futures::{ TryFutureExt, TryStreamExt, }; use h3o::{CellIndex, LatLng}; -use helium_crypto::PublicKeyBinary; use helium_proto::services::{ mobile_config::NetworkKeyRole, poc_mobile::{self as proto, CoverageObjectValidity, SignalLevel as SignalLevelProto}, }; use hex_assignments::assignment::HexAssignments; use hextree::Cell; -use mobile_config::{ - boosted_hex_info::{BoostedHex, BoostedHexes}, - client::AuthorizationClient, -}; +use mobile_config::client::AuthorizationClient; use retainer::{entry::CacheReadGuard, Cache}; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; + use sqlx::{FromRow, PgPool, Pool, Postgres, QueryBuilder, Transaction, Type}; use std::{ - cmp::Ordering, - collections::{BTreeMap, BinaryHeap, HashMap}, - num::NonZeroU32, pin::pin, sync::Arc, time::{Duration, Instant}, @@ -65,6 +57,17 @@ impl From for SignalLevel { } } +impl From for coverage_map::SignalLevel { + fn from(value: SignalLevel) -> Self { + match value { + SignalLevel::None => coverage_map::SignalLevel::None, + SignalLevel::Low => coverage_map::SignalLevel::Low, + SignalLevel::Medium => coverage_map::SignalLevel::Medium, + SignalLevel::High => coverage_map::SignalLevel::High, + } + } +} + pub struct CoverageDaemon { pool: Pool, auth_client: AuthorizationClient, @@ -358,7 +361,7 @@ impl CoverageObject { } } -#[derive(Clone, FromRow)] +#[derive(Debug, Clone, FromRow)] pub struct HexCoverage { pub uuid: Uuid, #[sqlx(try_from = "i64")] @@ -373,132 +376,6 @@ pub struct HexCoverage { pub assignments: HexAssignments, } -#[derive(Eq, Debug)] -struct IndoorCoverageLevel { - radio_key: OwnedKeyType, - seniority_timestamp: DateTime, - hotspot: PublicKeyBinary, - signal_level: SignalLevel, - hex_assignments: HexAssignments, -} - -impl PartialEq for IndoorCoverageLevel { - fn eq(&self, other: &Self) -> bool { - self.seniority_timestamp == other.seniority_timestamp - } -} - -impl PartialOrd for IndoorCoverageLevel { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for IndoorCoverageLevel { - fn cmp(&self, other: &Self) -> Ordering { - self.seniority_timestamp.cmp(&other.seniority_timestamp) - } -} - -impl IndoorCoverageLevel { - fn coverage_points(&self) -> Decimal { - match (&self.radio_key, self.signal_level) { - (OwnedKeyType::Wifi(_), SignalLevel::High) => dec!(400), - (OwnedKeyType::Wifi(_), SignalLevel::Low) => dec!(100), - (OwnedKeyType::Cbrs(_), SignalLevel::High) => dec!(100), - (OwnedKeyType::Cbrs(_), SignalLevel::Low) => dec!(25), - _ => dec!(0), - } - } -} - -#[derive(Eq, Debug)] -struct OutdoorCoverageLevel { - radio_key: OwnedKeyType, - seniority_timestamp: DateTime, - hotspot: PublicKeyBinary, - signal_power: i32, - signal_level: SignalLevel, - hex_assignments: HexAssignments, -} - -impl PartialEq for OutdoorCoverageLevel { - fn eq(&self, other: &Self) -> bool { - self.signal_power == other.signal_power - && self.seniority_timestamp == other.seniority_timestamp - } -} - -impl PartialOrd for OutdoorCoverageLevel { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for OutdoorCoverageLevel { - fn cmp(&self, other: &Self) -> Ordering { - self.signal_power - .cmp(&other.signal_power) - .reverse() - .then_with(|| self.seniority_timestamp.cmp(&other.seniority_timestamp)) - } -} - -impl OutdoorCoverageLevel { - fn coverage_points(&self) -> Decimal { - match (&self.radio_key, self.signal_level) { - (OwnedKeyType::Wifi(_), SignalLevel::High) => dec!(16), - (OwnedKeyType::Wifi(_), SignalLevel::Medium) => dec!(8), - (OwnedKeyType::Wifi(_), SignalLevel::Low) => dec!(4), - (OwnedKeyType::Wifi(_), SignalLevel::None) => dec!(0), - (OwnedKeyType::Cbrs(_), SignalLevel::High) => dec!(4), - (OwnedKeyType::Cbrs(_), SignalLevel::Medium) => dec!(2), - (OwnedKeyType::Cbrs(_), SignalLevel::Low) => dec!(1), - (OwnedKeyType::Cbrs(_), SignalLevel::None) => dec!(0), - } - } -} - -#[derive(PartialEq, Debug)] -pub struct CoverageReward { - pub radio_key: OwnedKeyType, - pub points: CoverageRewardPoints, - pub hotspot: PublicKeyBinary, - pub boosted_hex_info: BoostedHex, -} - -impl CoverageReward { - fn has_rewards(&self) -> bool { - self.points.points() > Decimal::ZERO - } -} - -#[derive(PartialEq, Debug)] -pub struct CoverageRewardPoints { - pub boost_multiplier: NonZeroU32, - pub coverage_points: Decimal, - pub hex_assignments: HexAssignments, - pub rank: Option, -} - -impl CoverageRewardPoints { - pub fn points(&self) -> Decimal { - let oracle_multiplier = if self.boost_multiplier.get() > 1 { - dec!(1.0) - } else { - self.hex_assignments.boosting_multiplier() - }; - - let points = self.coverage_points * oracle_multiplier; - - if let Some(rank) = self.rank { - points * rank - } else { - points - } - } -} - #[async_trait::async_trait] pub trait CoveredHexStream { async fn covered_hex_stream<'a>( @@ -622,185 +499,6 @@ pub async fn clear_coverage_objects( Ok(()) } -type IndoorCellTree = HashMap>>; -type OutdoorCellTree = HashMap>; - -#[derive(Default, Debug)] -pub struct CoveredHexes { - indoor_cbrs: IndoorCellTree, - indoor_wifi: IndoorCellTree, - outdoor_cbrs: OutdoorCellTree, - outdoor_wifi: OutdoorCellTree, -} - -pub const MAX_INDOOR_RADIOS_PER_RES12_HEX: usize = 1; -pub const MAX_OUTDOOR_RADIOS_PER_RES12_HEX: usize = 3; -pub const OUTDOOR_REWARD_WEIGHTS: [Decimal; 3] = [dec!(1.0), dec!(0.50), dec!(0.25)]; - -impl CoveredHexes { - /// Aggregate the coverage. Returns whether or not any of the hexes are boosted - pub async fn aggregate_coverage( - &mut self, - hotspot: &PublicKeyBinary, - boosted_hexes: &BoostedHexes, - covered_hexes: impl Stream>, - ) -> Result { - let mut covered_hexes = std::pin::pin!(covered_hexes); - let mut boosted = false; - - while let Some(hex_coverage) = covered_hexes.next().await.transpose()? { - boosted |= boosted_hexes.is_boosted(&hex_coverage.hex); - match (hex_coverage.indoor, &hex_coverage.radio_key) { - (true, OwnedKeyType::Cbrs(_)) => { - insert_indoor_coverage(&mut self.indoor_cbrs, hotspot, hex_coverage); - } - (true, OwnedKeyType::Wifi(_)) => { - insert_indoor_coverage(&mut self.indoor_wifi, hotspot, hex_coverage); - } - (false, OwnedKeyType::Cbrs(_)) => { - insert_outdoor_coverage(&mut self.outdoor_cbrs, hotspot, hex_coverage); - } - (false, OwnedKeyType::Wifi(_)) => { - insert_outdoor_coverage(&mut self.outdoor_wifi, hotspot, hex_coverage); - } - } - } - - Ok(boosted) - } - - /// Returns the radios that should be rewarded for giving coverage. - pub fn into_coverage_rewards( - self, - boosted_hexes: &BoostedHexes, - epoch_start: DateTime, - ) -> impl Iterator + '_ { - let outdoor_cbrs_rewards = - into_outdoor_rewards(self.outdoor_cbrs, boosted_hexes, epoch_start); - - let outdoor_wifi_rewards = - into_outdoor_rewards(self.outdoor_wifi, boosted_hexes, epoch_start); - - let indoor_cbrs_rewards = into_indoor_rewards(self.indoor_cbrs, boosted_hexes, epoch_start); - let indoor_wifi_rewards = into_indoor_rewards(self.indoor_wifi, boosted_hexes, epoch_start); - - outdoor_cbrs_rewards - .chain(outdoor_wifi_rewards) - .chain(indoor_cbrs_rewards) - .chain(indoor_wifi_rewards) - .filter(CoverageReward::has_rewards) - } -} - -fn insert_indoor_coverage( - indoor: &mut IndoorCellTree, - hotspot: &PublicKeyBinary, - hex_coverage: HexCoverage, -) { - indoor - .entry(hex_coverage.hex) - .or_default() - .entry(hex_coverage.signal_level) - .or_default() - .push(IndoorCoverageLevel { - radio_key: hex_coverage.radio_key, - seniority_timestamp: hex_coverage.coverage_claim_time, - signal_level: hex_coverage.signal_level, - hotspot: hotspot.clone(), - hex_assignments: hex_coverage.assignments, - }) -} - -fn insert_outdoor_coverage( - outdoor: &mut OutdoorCellTree, - hotspot: &PublicKeyBinary, - hex_coverage: HexCoverage, -) { - outdoor - .entry(hex_coverage.hex) - .or_default() - .push(OutdoorCoverageLevel { - radio_key: hex_coverage.radio_key, - seniority_timestamp: hex_coverage.coverage_claim_time, - signal_level: hex_coverage.signal_level, - signal_power: hex_coverage.signal_power, - hotspot: hotspot.clone(), - hex_assignments: hex_coverage.assignments, - }); -} - -fn into_outdoor_rewards( - outdoor: OutdoorCellTree, - boosted_hexes: &BoostedHexes, - epoch_start: DateTime, -) -> impl Iterator + '_ { - outdoor.into_iter().flat_map(move |(hex, radios)| { - radios - .into_sorted_vec() - .into_iter() - .take(MAX_OUTDOOR_RADIOS_PER_RES12_HEX) - .zip(OUTDOOR_REWARD_WEIGHTS) - .map(move |(cl, rank)| { - let boost_multiplier = boosted_hexes - .get_current_multiplier(hex, epoch_start) - .unwrap_or(NonZeroU32::new(1).unwrap()); - - CoverageReward { - points: CoverageRewardPoints { - boost_multiplier, - coverage_points: cl.coverage_points(), - hex_assignments: cl.hex_assignments, - rank: Some(rank), - }, - hotspot: cl.hotspot, - radio_key: cl.radio_key, - boosted_hex_info: BoostedHex { - location: hex, - multiplier: boost_multiplier, - }, - } - }) - }) -} - -fn into_indoor_rewards( - indoor: IndoorCellTree, - boosted_hexes: &BoostedHexes, - epoch_start: DateTime, -) -> impl Iterator + '_ { - indoor - .into_iter() - .flat_map(move |(hex, mut radios)| { - radios.pop_last().map(move |(_, radios)| { - radios - .into_sorted_vec() - .into_iter() - .take(MAX_INDOOR_RADIOS_PER_RES12_HEX) - .map(move |cl| { - let boost_multiplier = boosted_hexes - .get_current_multiplier(hex, epoch_start) - .unwrap_or(NonZeroU32::new(1).unwrap()); - - CoverageReward { - points: CoverageRewardPoints { - boost_multiplier, - coverage_points: cl.coverage_points(), - hex_assignments: cl.hex_assignments, - rank: None, - }, - hotspot: cl.hotspot, - radio_key: cl.radio_key, - boosted_hex_info: BoostedHex { - location: hex, - multiplier: boost_multiplier, - }, - } - }) - }) - }) - .flatten() -} - type CoverageClaimTimeKey = ((String, HbType), Option); pub struct CoverageClaimTimeCache { @@ -955,572 +653,3 @@ impl CachedCoverageObject<'_> { }) } } - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use super::*; - use chrono::NaiveDate; - use futures::stream::iter; - use hex_assignments::Assignment; - use hextree::Cell; - - fn hex_assignments_mock() -> HexAssignments { - HexAssignments { - footfall: Assignment::A, - urbanized: Assignment::A, - landtype: Assignment::A, - } - } - - /// Test to ensure that if there are multiple radios with different signal levels - /// in a given hex, that the one with the highest signal level is chosen. - #[tokio::test] - async fn ensure_max_signal_level_selected() { - let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" - .parse() - .expect("failed owner parse"); - let mut covered_hexes = CoveredHexes::default(); - covered_hexes - .aggregate_coverage( - &owner, - &BoostedHexes::default(), - iter(vec![ - anyhow::Ok(indoor_cbrs_hex_coverage("1", SignalLevel::None, None)), - anyhow::Ok(indoor_cbrs_hex_coverage("2", SignalLevel::Low, None)), - anyhow::Ok(indoor_cbrs_hex_coverage("3", SignalLevel::High, None)), - anyhow::Ok(indoor_cbrs_hex_coverage("4", SignalLevel::Low, None)), - anyhow::Ok(indoor_cbrs_hex_coverage("5", SignalLevel::None, None)), - ]), - ) - .await - .unwrap(); - let rewards: Vec<_> = covered_hexes - .into_coverage_rewards(&BoostedHexes::default(), Utc::now()) - .collect(); - assert_eq!( - rewards, - vec![CoverageReward { - radio_key: OwnedKeyType::Cbrs("3".to_string()), - hotspot: owner, - points: CoverageRewardPoints { - coverage_points: dec!(100), - boost_multiplier: NonZeroU32::new(1).unwrap(), - hex_assignments: hex_assignments_mock(), - rank: None - }, - boosted_hex_info: BoostedHex { - location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - multiplier: NonZeroU32::new(1).unwrap(), - }, - }] - ); - } - - fn date(year: i32, month: u32, day: u32) -> DateTime { - NaiveDate::from_ymd_opt(year, month, day) - .unwrap() - .and_hms_opt(0, 0, 0) - .unwrap() - .and_utc() - } - - fn indoor_hex_coverage_with_date( - cbsd_id: &str, - signal_level: SignalLevel, - coverage_claim_time: DateTime, - ) -> HexCoverage { - HexCoverage { - uuid: Uuid::new_v4(), - hex: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - indoor: true, - radio_key: OwnedKeyType::Cbrs(cbsd_id.to_string()), - signal_level, - // Signal power is ignored for indoor radios: - signal_power: 0, - coverage_claim_time, - inserted_at: DateTime::::MIN_UTC, - assignments: hex_assignments_mock(), - } - } - - /// Test to ensure that if there are more than five radios with the highest signal - /// level in a given hex, that the five oldest radios are chosen. - #[tokio::test] - async fn ensure_oldest_radio_selected() { - let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" - .parse() - .expect("failed owner parse"); - let mut covered_hexes = CoveredHexes::default(); - covered_hexes - .aggregate_coverage( - &owner, - &BoostedHexes::default(), - iter(vec![ - anyhow::Ok(indoor_hex_coverage_with_date( - "1", - SignalLevel::High, - date(1980, 1, 1), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "2", - SignalLevel::High, - date(1970, 1, 5), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "3", - SignalLevel::High, - date(1990, 2, 2), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "4", - SignalLevel::High, - date(1970, 1, 4), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "5", - SignalLevel::High, - date(1975, 3, 3), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "6", - SignalLevel::High, - date(1970, 1, 3), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "7", - SignalLevel::High, - date(1974, 2, 2), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "8", - SignalLevel::High, - date(1970, 1, 2), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "9", - SignalLevel::High, - date(1976, 5, 2), - )), - anyhow::Ok(indoor_hex_coverage_with_date( - "10", - SignalLevel::High, - date(1970, 1, 1), - )), - ]), - ) - .await - .unwrap(); - let rewards: Vec<_> = covered_hexes - .into_coverage_rewards(&BoostedHexes::default(), Utc::now()) - .collect(); - assert_eq!( - rewards, - vec![CoverageReward { - radio_key: OwnedKeyType::Cbrs("10".to_string()), - hotspot: owner.clone(), - points: CoverageRewardPoints { - coverage_points: dec!(100), - boost_multiplier: NonZeroU32::new(1).unwrap(), - hex_assignments: hex_assignments_mock(), - rank: None - }, - boosted_hex_info: BoostedHex { - location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - multiplier: NonZeroU32::new(1).unwrap(), - }, - }] - ); - } - - #[tokio::test] - async fn ensure_outdoor_radios_ranked_by_power() { - let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" - .parse() - .expect("failed owner parse"); - let mut covered_hexes = CoveredHexes::default(); - covered_hexes - .aggregate_coverage( - &owner, - &BoostedHexes::default(), - iter(vec![ - anyhow::Ok(outdoor_cbrs_hex_coverage("1", -946, date(2022, 8, 1))), - anyhow::Ok(outdoor_cbrs_hex_coverage("2", -936, date(2022, 12, 5))), - anyhow::Ok(outdoor_cbrs_hex_coverage("3", -887, date(2022, 12, 2))), - anyhow::Ok(outdoor_cbrs_hex_coverage("4", -887, date(2022, 12, 1))), - anyhow::Ok(outdoor_cbrs_hex_coverage("5", -773, date(2023, 5, 1))), - ]), - ) - .await - .unwrap(); - let rewards: Vec<_> = covered_hexes - .into_coverage_rewards(&BoostedHexes::default(), Utc::now()) - .collect(); - assert_eq!( - rewards, - vec![ - CoverageReward { - radio_key: OwnedKeyType::Cbrs("5".to_string()), - hotspot: owner.clone(), - points: CoverageRewardPoints { - coverage_points: dec!(4), - rank: Some(dec!(1.0)), - boost_multiplier: NonZeroU32::new(1).unwrap(), - hex_assignments: hex_assignments_mock(), - }, - boosted_hex_info: BoostedHex { - location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - multiplier: NonZeroU32::new(1).unwrap(), - }, - }, - CoverageReward { - radio_key: OwnedKeyType::Cbrs("4".to_string()), - hotspot: owner.clone(), - points: CoverageRewardPoints { - coverage_points: dec!(4), - rank: Some(dec!(0.50)), - boost_multiplier: NonZeroU32::new(1).unwrap(), - hex_assignments: hex_assignments_mock(), - }, - boosted_hex_info: BoostedHex { - location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - multiplier: NonZeroU32::new(1).unwrap(), - }, - }, - CoverageReward { - radio_key: OwnedKeyType::Cbrs("3".to_string()), - hotspot: owner, - points: CoverageRewardPoints { - coverage_points: dec!(4), - rank: Some(dec!(0.25)), - boost_multiplier: NonZeroU32::new(1).unwrap(), - hex_assignments: hex_assignments_mock(), - }, - boosted_hex_info: BoostedHex { - location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - multiplier: NonZeroU32::new(1).unwrap(), - }, - } - ] - ); - } - - #[tokio::test] - async fn hip_105_ensure_all_types_get_rewards() -> anyhow::Result<()> { - let mut covered_hexes = CoveredHexes::default(); - let boosted_hexes = BoostedHexes::default(); - - let outdoor_cbrs_owner1 = - PublicKeyBinary::from_str("11eX55faMbqZB7jzN4p67m6w7ScPMH6ubnvCjCPLh72J49PaJEL")?; - covered_hexes - .aggregate_coverage( - &outdoor_cbrs_owner1, - &boosted_hexes, - iter(vec![ - anyhow::Ok(outdoor_cbrs_hex_coverage("oco1-1", -700, date(2024, 2, 20))), - anyhow::Ok(outdoor_cbrs_hex_coverage("oco1-2", -700, date(2024, 2, 21))), - anyhow::Ok(outdoor_cbrs_hex_coverage("oco1-3", -699, date(2024, 2, 23))), - anyhow::Ok(outdoor_cbrs_hex_coverage("oco1-4", -700, date(2024, 2, 19))), - ]), - ) - .await?; - - let indoor_cbrs_owner1 = - PublicKeyBinary::from_str("11PfLUsMAfsozjy2kcERF43UuhNAhicEQM8ioutFM322Eu37D4m")?; - covered_hexes - .aggregate_coverage( - &indoor_cbrs_owner1, - &boosted_hexes, - iter(vec![ - anyhow::Ok(indoor_cbrs_hex_coverage( - "ico1-1", - SignalLevel::High, - Some(date(2024, 2, 20)), - )), - anyhow::Ok(indoor_cbrs_hex_coverage( - "ico1-2", - SignalLevel::High, - Some(date(2024, 2, 21)), - )), - ]), - ) - .await?; - - let outdoor_wifi_owner1 = - PublicKeyBinary::from_str("1trSuseczBVbfpbJjefoFsuPazRSrzJjCXaKJPU9B3HKJvb6sdqTepcbY2zWBq2yMTt7Jsf7NZCm28ez856kDa5MT3Ja1gh8HyZWS8k9LCSFSWiDNG2YmcCpLgnhGrEw9FriCVPLuXaciQ2Fu9ztW7r1U1Pv64i3HvpkC4mmQWE9DSq7tgiNkNhNuWBA3Sf8KbtefMPofTxjCsfVCUKX2ow8MScn82CK6vWUZNUPonpTJydKVLNiMGfvceY1MsfXtHdx6bUCjoFoZNkikAzEpgArczJV9CdhkBjKX3xLVLpehdrBDGu8aBLfRbNJ4RRz9Gj4pHFnBhFq78tRGi1USpnf6Dohp9bA18qr4XdPJc59Qz")?; - covered_hexes - .aggregate_coverage( - &outdoor_wifi_owner1, - &boosted_hexes, - iter(vec![anyhow::Ok(outdoor_wifi_hex_coverage( - &outdoor_wifi_owner1, - -700, - date(2024, 2, 20), - ))]), - ) - .await?; - - let outdoor_wifi_owner2 = - PublicKeyBinary::from_str("1trSuseerjmxKaD43hSLPD1oWTt6Y6svqJX7WJecMwKKcRk1355AQp1GSkUNV7fnL9QYGpZSS378XoxmaHte5PCD64NYzJ1x7bBNdq6qBRFRDqTW1PGPMatjX3i18Y39hh8Ngsephg93YCZoVbvfGc5YMmtvxqqP4WXy4UxmiTZ6uuYzPV5U31piAFVxaUhTZtoQLCyLzAZEks8bj2cP6EyEFMecHb9Vq76d4qnXdjARvFim7xACkBKHTnAwEEN8wfWfGEw5QBQMfpvvSLUerL64xR72tT3SrM7qUXk9m7fTbLuwg8XfUKEs2iqhPSfSu2v4DpcKY7L4fvu8BT2WsMChC3xaPWWiibTVatoNLNxTH6")?; - covered_hexes - .aggregate_coverage( - &outdoor_wifi_owner2, - &boosted_hexes, - iter(vec![anyhow::Ok(outdoor_wifi_hex_coverage( - &outdoor_wifi_owner2, - -700, - date(2024, 2, 21), - ))]), - ) - .await?; - - let outdoor_wifi_owner3 = - PublicKeyBinary::from_str("1trSusefexd9C3purVPScg433RXrVb5kU9hLTTKjuc2dTju9udy2rsgAYUTjhxARa9ewZAW1PdsjsErGyaHNJKNDkjHfzuHZmPm7vWK3A13sckxRbwSBtBXAMg4nyChmoJ5JgZVeeHBtdYX69emPoDD8niKSx5vkkoBw5g1AYS7S4LfnpGhCtwKA8PzjjzE3ZY6dWQjm2oCut31ScsH9nZfBHriHpkTbNK8KttkFRU3ax3wdJXmN996PPbYsgm1wx8ctU9iU6Q7FvTvVkZTGfHHcH8J38YCuWB9LfizRSueKWNSPbfsrJgQe3otTYtdU7NDWWQzrpv3yATS2NJzyorKpjg8hH5J2krtU3KdByBgZdU")?; - covered_hexes - .aggregate_coverage( - &outdoor_wifi_owner3, - &boosted_hexes, - iter(vec![anyhow::Ok(outdoor_wifi_hex_coverage( - &outdoor_wifi_owner3, - -699, - date(2024, 2, 23), - ))]), - ) - .await?; - - let outdoor_wifi_owner4 = - PublicKeyBinary::from_str("1trSusf5rnmrUHqyv28ksYJGBBkHZi821ss7vLkBchUPi6vxDHpHGoscCftuHddxpaHgMacxD7fyHESf8Ht3JpRjebZnTzZMwqs6u6z2v8S7VZzxjv5KkaNZpX2CPYGYNfVRWC2tovSaUwEdc3P6Tyk6S9axAw7WM9pP2sxyEqWiCmyzhCnnd8xhZqaTKtiyoamvVTXqB1iZaUFX2KtSB6pLVGrDCUxGs7x4PrMrgAcPcdDF1jrF6s7EpAR9MjRHv6qxstoSHnGMpTeZaXLJEhySqtnSvyQEJaT218zuDSoHArKRUSQ9ViWE55T8hbwsVDusNDdayS4JG2fRMoDkj8LPYHvhMtVzQUDSg1ufFEFukh")?; - covered_hexes - .aggregate_coverage( - &outdoor_wifi_owner4, - &boosted_hexes, - iter(vec![anyhow::Ok(outdoor_wifi_hex_coverage( - &outdoor_wifi_owner4, - -700, - date(2024, 2, 19), - ))]), - ) - .await?; - - let indoor_wifi_owner1 = - PublicKeyBinary::from_str("1trSusf79ALaHuYSxUcvHLQEtHUgTnc25rfjyUUSKDs9AUwvXkJ7CQoGMrY7RhVpXyKYH4HaDnekRLYTUq5pczPk4XqnzXnACrwbY5CzhzdTSQkHRN2LuHvgQeySeh4LvjfhRP3Cru89zTGNNGMDXkpASuz3NkQx3ctqnTcdrjLgcBavQQmASofARxrqSPUz4UFTU1Gp4eRdaJgu1G7ys1f8NsjH5WU6bi5N4U5cWRVQkC7FEJZsGFn1sNferANVwkkSR2NLEpwYvL5qpGTYtk7zcqPrHY5hNC6jkjWhM5S4JPDYzZNcxRW28ekRCp2igJCqErA3APbcwkaZPXUxpqFJGWqu6GJf7aZRKz8R9cNAmd")?; - covered_hexes - .aggregate_coverage( - &indoor_wifi_owner1, - &boosted_hexes, - iter(vec![anyhow::Ok(indoor_wifi_hex_coverage( - &indoor_wifi_owner1, - SignalLevel::High, - date(2024, 2, 19), - ))]), - ) - .await?; - - let indoor_wifi_owner2 = - PublicKeyBinary::from_str("1trSusew4P9SVD2q9GjvNYf8e5qEH2NmZRQDnkXodQt1fmpcR9cG28iA3LJD476H31wrNcr6jbfBhoPdyJvfepQonxXH7kUDjpMcVJqfMZGeQyXQvaxxnPh4yzoPonpS5RM6VSmsk4WNczr6nBUa49ak1XM8s5DCGSRZEPqWhXfG9urQ8hgDSYdkd61eBcWmThRKLAfarT5cE1ZaeexFtUgjgRBUGd7ifCtchZTgkWTa9WVsMdpWTjFd8GUkaTekX1RWzFDtFyETGZHbD6wDut729EfoBuSKowJuwFv2LYZr7Cw4qKPmVboDBpem1ZramSq3PmatdrpNHHipXniz4Z1vtM1vfgtJ57o8BrWSQNuD9B")?; - covered_hexes - .aggregate_coverage( - &indoor_wifi_owner2, - &boosted_hexes, - iter(vec![anyhow::Ok(indoor_wifi_hex_coverage( - &indoor_wifi_owner2, - SignalLevel::High, - date(2024, 2, 20), - ))]), - ) - .await?; - - //Calculate coverage points - let rewards: Vec<_> = covered_hexes - .into_coverage_rewards(&boosted_hexes, Utc::now()) - .collect(); - - // assert outdoor cbrs radios - assert_eq!( - dec!(4), - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Cbrs("oco1-3".to_string())) - .unwrap() - .points - .points() - ); - - assert_eq!( - dec!(2), - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Cbrs("oco1-4".to_string())) - .unwrap() - .points - .points() - ); - - assert_eq!( - dec!(1), - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Cbrs("oco1-1".to_string())) - .unwrap() - .points - .points() - ); - - assert_eq!( - None, - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Cbrs("oco1-2".to_string())) - ); - - // assert indoor cbrs radios - assert_eq!( - dec!(100), - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Cbrs("ico1-1".to_string())) - .unwrap() - .points - .points() - ); - - assert_eq!( - None, - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Cbrs("ico1-2".to_string())) - ); - - //assert outdoor wifi radios - assert_eq!( - dec!(16), - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Wifi(outdoor_wifi_owner3.clone())) - .unwrap() - .points - .points() - ); - - assert_eq!( - dec!(8), - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Wifi(outdoor_wifi_owner4.clone())) - .unwrap() - .points - .points() - ); - - assert_eq!( - dec!(4), - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Wifi(outdoor_wifi_owner1.clone())) - .unwrap() - .points - .points() - ); - - assert_eq!( - None, - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Wifi(outdoor_wifi_owner2.clone())) - ); - - //assert indoor wifi radios - assert_eq!( - dec!(400), - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Wifi(indoor_wifi_owner1.clone())) - .unwrap() - .points - .points() - ); - - assert_eq!( - None, - rewards - .iter() - .find(|r| r.radio_key == OwnedKeyType::Wifi(indoor_wifi_owner2.clone())) - ); - - Ok(()) - } - - fn indoor_cbrs_hex_coverage( - cbsd_id: &str, - signal_level: SignalLevel, - coverage_claim_time: Option>, - ) -> HexCoverage { - HexCoverage { - uuid: Uuid::new_v4(), - hex: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - indoor: true, - radio_key: OwnedKeyType::Cbrs(cbsd_id.to_string()), - signal_level, - // Signal power is ignored for indoor radios: - signal_power: 0, - coverage_claim_time: coverage_claim_time.unwrap_or(DateTime::::MIN_UTC), - inserted_at: DateTime::::MIN_UTC, - assignments: hex_assignments_mock(), - } - } - - fn outdoor_cbrs_hex_coverage( - cbsd_id: &str, - signal_power: i32, - coverage_claim_time: DateTime, - ) -> HexCoverage { - HexCoverage { - uuid: Uuid::new_v4(), - hex: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - indoor: false, - radio_key: OwnedKeyType::Cbrs(cbsd_id.to_string()), - signal_power, - signal_level: SignalLevel::High, - coverage_claim_time, - inserted_at: DateTime::::MIN_UTC, - assignments: hex_assignments_mock(), - } - } - - fn outdoor_wifi_hex_coverage( - hotspot_key: &PublicKeyBinary, - signal_power: i32, - coverage_claim_time: DateTime, - ) -> HexCoverage { - HexCoverage { - uuid: Uuid::new_v4(), - hex: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - indoor: false, - radio_key: OwnedKeyType::Wifi(hotspot_key.clone()), - signal_power, - signal_level: SignalLevel::High, - coverage_claim_time, - inserted_at: DateTime::::MIN_UTC, - assignments: hex_assignments_mock(), - } - } - - fn indoor_wifi_hex_coverage( - hotspot_key: &PublicKeyBinary, - signal_level: SignalLevel, - coverage_claim_time: DateTime, - ) -> HexCoverage { - HexCoverage { - uuid: Uuid::new_v4(), - hex: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - indoor: true, - radio_key: OwnedKeyType::Wifi(hotspot_key.clone()), - signal_power: 0, - signal_level, - coverage_claim_time, - inserted_at: DateTime::::MIN_UTC, - assignments: hex_assignments_mock(), - } - } -} diff --git a/mobile_verifier/src/heartbeats/mod.rs b/mobile_verifier/src/heartbeats/mod.rs index 6352b6788..ea9cddc79 100644 --- a/mobile_verifier/src/heartbeats/mod.rs +++ b/mobile_verifier/src/heartbeats/mod.rs @@ -278,8 +278,6 @@ pub struct HeartbeatReward { pub coverage_object: Uuid, } -const RESTRICTIVE_MAX_DISTANCE: i64 = 50; - impl HeartbeatReward { pub fn key(&self) -> KeyType<'_> { match self.cbsd_id { @@ -299,39 +297,6 @@ impl HeartbeatReward { } } - pub fn trust_score_multiplier(&self, overlaps_boosted: bool) -> Decimal { - if self.cbsd_id.is_some() { - // If this is a cbrs radio, the trust score is always 1 - return dec!(1.0); - } - if overlaps_boosted { - // If we overlap a boosted hex, use the more restrictive distance - // check: - let distances = self.distances_to_asserted.as_ref().unwrap(); - let num_distances = Decimal::from(distances.len()); - distances - .iter() - .zip(self.trust_score_multipliers.iter()) - .map(|(distance, ts)| { - std::cmp::min( - if *distance > RESTRICTIVE_MAX_DISTANCE { - dec!(0.25) - } else { - dec!(1.0) - }, - *ts, - ) - }) - .sum::() - / num_distances - } else { - // If we don't overlap a boosted hex, just use the average of the - // trust scores: - let num_trust_scores = Decimal::from(self.trust_score_multipliers.len()); - self.trust_score_multipliers.iter().sum::() / num_trust_scores - } - } - pub fn validated<'a>( exec: impl sqlx::PgExecutor<'a> + Copy + 'a, epoch: &'a Range>, @@ -342,6 +307,18 @@ impl HeartbeatReward { .bind(MINIMUM_HEARTBEAT_COUNT) .fetch(exec) } + + pub fn iter_distances_and_scores(&self) -> impl Iterator { + let fallback: Vec = std::iter::repeat(0) + .take(self.trust_score_multipliers.len()) + .collect(); + + self.distances_to_asserted + .clone() + .unwrap_or(fallback) + .into_iter() + .zip(self.trust_score_multipliers.clone()) + } } #[derive(Clone)] @@ -947,44 +924,6 @@ mod test { use super::*; use proto::SeniorityUpdateReason::*; - #[test] - fn ensure_stricter_distance_check_in_trust_score_for_boosted_hexes() { - let mut heartbeat_reward = HeartbeatReward { - hotspot_key: "11sctWiP9r5wDJVuDe1Th4XSL2vaawaLLSQF8f8iokAoMAJHxqp" - .parse() - .unwrap(), - cbsd_id: None, - cell_type: CellType::CellTypeNone, - distances_to_asserted: Some(vec![RESTRICTIVE_MAX_DISTANCE + 1]), - trust_score_multipliers: vec![dec!(1.0)], - coverage_object: Uuid::new_v4(), - }; - // If the heartbeat is not in a boosted hex, the trust score should be 1.0: - assert_eq!(heartbeat_reward.trust_score_multiplier(false), dec!(1.0)); - // If the heartbeat does overlap a boosted hex, the trust score should be 0.25: - assert_eq!(heartbeat_reward.trust_score_multiplier(true), dec!(0.25)); - // Now we check that if we set the distance to asserted to be below the restrictive - // max, that we have a trust score of 1.0: - heartbeat_reward.distances_to_asserted = Some(vec![RESTRICTIVE_MAX_DISTANCE]); - assert_eq!(heartbeat_reward.trust_score_multiplier(true), dec!(1.0)); - } - - #[test] - fn test_averaging_of_trust_scores() { - let heartbeat_reward = HeartbeatReward { - hotspot_key: "11sctWiP9r5wDJVuDe1Th4XSL2vaawaLLSQF8f8iokAoMAJHxqp" - .parse() - .unwrap(), - cbsd_id: None, - cell_type: CellType::CellTypeNone, - distances_to_asserted: Some(vec![RESTRICTIVE_MAX_DISTANCE + 1, 0, 0, 0, 0]), - trust_score_multipliers: vec![dec!(1.0), dec!(0.25), dec!(1.0), dec!(1.0), dec!(0.25)], - coverage_object: Uuid::new_v4(), - }; - assert_eq!(heartbeat_reward.trust_score_multiplier(false), dec!(0.7)); - assert_eq!(heartbeat_reward.trust_score_multiplier(true), dec!(0.55)); - } - fn heartbeat(timestamp: DateTime, coverage_object: Uuid) -> ValidatedHeartbeat { ValidatedHeartbeat { cell_type: CellType::CellTypeNone, diff --git a/mobile_verifier/src/reward_shares.rs b/mobile_verifier/src/reward_shares.rs index 632df340e..d3608a843 100644 --- a/mobile_verifier/src/reward_shares.rs +++ b/mobile_verifier/src/reward_shares.rs @@ -1,9 +1,9 @@ use crate::{ - coverage::{CoverageReward, CoverageRewardPoints, CoveredHexStream, CoveredHexes}, + coverage::{CoveredHexStream, Seniority}, data_session::{HotspotMap, ServiceProviderDataSession}, - heartbeats::{HeartbeatReward, OwnedKeyType}, + heartbeats::HeartbeatReward, radio_threshold::VerifiedRadioThresholds, - speedtests_average::{SpeedtestAverage, SpeedtestAverages}, + speedtests_average::SpeedtestAverages, subscriber_location::SubscriberValidatedLocations, }; use chrono::{DateTime, Duration, Utc}; @@ -20,12 +20,12 @@ use helium_proto::{ ServiceProvider, }; use mobile_config::{ - boosted_hex_info::{BoostedHex, BoostedHexes}, + boosted_hex_info::BoostedHexes, client::{carrier_service_client::CarrierServiceVerifier, ClientError}, }; use rust_decimal::prelude::*; use rust_decimal_macros::dec; -use std::{collections::HashMap, num::NonZeroU32, ops::Range}; +use std::{collections::HashMap, ops::Range}; use uuid::Uuid; /// Total tokens emissions pool per 365 days or 366 days for a leap year @@ -375,197 +375,213 @@ pub fn dc_to_mobile_bones(dc_amount: Decimal, mobile_bone_price: Decimal) -> Dec .round_dp_with_strategy(DEFAULT_PREC, RoundingStrategy::ToPositiveInfinity) } -#[derive(Debug)] -struct CoverageRewardPointsWithMultiplier { - coverage_points: CoverageRewardPoints, - boosted_hex: BoostedHex, -} - -#[derive(Debug)] -struct RadioPoints { - location_trust_score_multiplier: Decimal, - coverage_object: Uuid, - seniority: DateTime, - // Boosted hexes are included in CoverageRewardPointsWithMultiplier, - // this gets included in the radio reward share proto - reward_points: Vec, -} - -impl RadioPoints { - fn new( - location_trust_score_multiplier: Decimal, - coverage_object: Uuid, - seniority: DateTime, - ) -> Self { - Self { - location_trust_score_multiplier, - seniority, - coverage_object, - reward_points: vec![], - } - } - - fn hex_points(&self) -> Decimal { - self.reward_points - .iter() - .map(|c| c.coverage_points.points() * Decimal::from(c.boosted_hex.multiplier.get())) - .sum::() - } - - fn coverage_points(&self) -> Decimal { - let coverage_points = self.hex_points(); - (self.location_trust_score_multiplier * coverage_points).max(Decimal::ZERO) - } - - fn poc_reward(&self, reward_per_share: Decimal, speedtest_multiplier: Decimal) -> Decimal { - reward_per_share * speedtest_multiplier * self.coverage_points() - } -} - -// pub type HotspotBoostedHexes = HashMap; - -#[derive(Debug, Default)] -struct HotspotPoints { - /// Points are multiplied by the multiplier to get shares. - /// Multiplier should never be zero. - speedtest_multiplier: Decimal, - radio_points: HashMap, RadioPoints>, -} +pub fn coverage_point_to_mobile_reward_share( + coverage_points: coverage_point_calculator::CoveragePoints, + reward_epoch: &Range>, + radio_id: &RadioId, + poc_reward: u64, + seniority_timestamp: DateTime, + coverage_object_uuid: Uuid, +) -> proto::MobileRewardShare { + let (hotspot_key, cbsd_id) = radio_id.clone(); + + let boosted_hexes = coverage_points + .covered_hexes + .iter() + .filter(|hex| hex.boosted_multiplier.is_some_and(|boost| boost > dec!(1))) + .map(|covered_hex| proto::BoostedHex { + location: covered_hex.hex.into_raw(), + multiplier: covered_hex.boosted_multiplier.unwrap().to_u32().unwrap(), + }) + .collect(); -impl HotspotPoints { - pub fn add_coverage_entry( - &mut self, - radio_key: OwnedKeyType, - hotspot: PublicKeyBinary, - points: CoverageRewardPoints, - boosted_hex_info: BoostedHex, - verified_radio_thresholds: &VerifiedRadioThresholds, - ) { - let cbsd_id = radio_key.clone().into_cbsd_id(); - let rp = self.radio_points.get_mut(&cbsd_id).unwrap(); - // need to consider requirements from hip93 & hip84 before applying any boost - // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting - // hip84: if radio has not met minimum data and subscriber thresholds, no boosting - let final_boost_info = if radio_key.is_wifi() - && rp.location_trust_score_multiplier < dec!(0.75) - || !verified_radio_thresholds.is_verified(hotspot, cbsd_id) - { - BoostedHex { - location: boosted_hex_info.location, - multiplier: NonZeroU32::new(1).unwrap(), - } - } else { - boosted_hex_info - }; + let to_proto_value = |value: Decimal| (value * dec!(1000)).to_u32().unwrap_or_default(); + let location_trust_score_multiplier = to_proto_value(coverage_points.location_trust_multiplier); + let speedtest_multiplier = to_proto_value(coverage_points.speedtest_multiplier); - rp.reward_points.push(CoverageRewardPointsWithMultiplier { - coverage_points: points, - boosted_hex: final_boost_info, - }); + let coverage_points = coverage_points + .total_coverage_points + .to_u64() + .unwrap_or_default(); + + let coverage_object = Vec::from(coverage_object_uuid.into_bytes()); + + proto::MobileRewardShare { + start_period: reward_epoch.start.encode_timestamp(), + end_period: reward_epoch.end.encode_timestamp(), + reward: Some(proto::mobile_reward_share::Reward::RadioReward( + proto::RadioReward { + hotspot_key: hotspot_key.into(), + cbsd_id: cbsd_id.unwrap_or_default(), + poc_reward, + coverage_points, + seniority_timestamp: seniority_timestamp.encode_timestamp(), + coverage_object, + location_trust_score_multiplier, + speedtest_multiplier, + boosted_hexes, + ..Default::default() + }, + )), } } -impl HotspotPoints { - pub fn new(speedtest_multiplier: Decimal) -> Self { - Self { - speedtest_multiplier, - radio_points: HashMap::new(), - } - } -} +type RadioId = (PublicKeyBinary, Option); -impl HotspotPoints { - pub fn total_points(&self) -> Decimal { - self.speedtest_multiplier - * self - .radio_points - .values() - .fold(Decimal::ZERO, |sum, radio| sum + radio.coverage_points()) - } +#[derive(Debug, Clone)] +struct RadioInfo { + radio_type: coverage_point_calculator::RadioType, + coverage_obj_uuid: Uuid, + seniority: Seniority, + trust_scores: Vec, + verified_radio_threshold: coverage_point_calculator::RadioThreshold, + speedtests: Vec, } #[derive(Debug)] -pub struct CoveragePoints { - coverage_points: HashMap, +pub struct CoverageShares { + coverage_map: coverage_map::CoverageMap, + radio_infos: HashMap, } -impl CoveragePoints { - pub async fn aggregate_points( +impl CoverageShares { + pub async fn new( hex_streams: &impl CoveredHexStream, heartbeats: impl Stream>, - speedtests: &SpeedtestAverages, + speedtest_averages: &SpeedtestAverages, boosted_hexes: &BoostedHexes, verified_radio_thresholds: &VerifiedRadioThresholds, reward_period: &Range>, - ) -> Result { + ) -> anyhow::Result { + let mut radio_infos: HashMap = HashMap::new(); + let mut coverage_map_builder = coverage_map::CoverageMapBuilder::default(); + + // The heartbearts query is written in a way that each radio is iterated a single time. let mut heartbeats = std::pin::pin!(heartbeats); - let mut covered_hexes = CoveredHexes::default(); - let mut coverage_points = HashMap::new(); while let Some(heartbeat) = heartbeats.next().await.transpose()? { - let speedtest_multiplier = speedtests - .get_average(&heartbeat.hotspot_key) - .as_ref() - .map_or(Decimal::ZERO, SpeedtestAverage::reward_multiplier); + let pubkey = heartbeat.hotspot_key.clone(); + let heartbeat_key = heartbeat.key(); + let cbsd_id = heartbeat_key.to_owned().into_cbsd_id(); + let key = (pubkey.clone(), cbsd_id.clone()); + let seniority = hex_streams - .fetch_seniority(heartbeat.key(), reward_period.end) - .await?; - let covered_hex_stream = hex_streams - .covered_hex_stream(heartbeat.key(), &heartbeat.coverage_object, &seniority) + .fetch_seniority(heartbeat_key, reward_period.end) .await?; - let overlaps_boosted = covered_hexes - .aggregate_coverage(&heartbeat.hotspot_key, boosted_hexes, covered_hex_stream) - .await?; - let opt_cbsd_id = heartbeat.key().to_owned().into_cbsd_id(); - coverage_points - .entry(heartbeat.hotspot_key.clone()) - .or_insert_with(|| HotspotPoints::new(speedtest_multiplier)) - .radio_points - .insert( - opt_cbsd_id, - RadioPoints::new( - heartbeat.trust_score_multiplier(overlaps_boosted), - heartbeat.coverage_object, - seniority.seniority_ts, - ), - ); - } - for CoverageReward { - radio_key, - points, - hotspot, - boosted_hex_info, - } in covered_hexes.into_coverage_rewards(boosted_hexes, reward_period.start) - { - // Guaranteed that points contains the given hotspot. - coverage_points - .get_mut(&hotspot) - .unwrap() - .add_coverage_entry( - radio_key, - hotspot, - points, - boosted_hex_info, - verified_radio_thresholds, - ) + let is_indoor = { + let mut is_indoor = false; + + let covered_hexes_stream = hex_streams + .covered_hex_stream(heartbeat_key, &heartbeat.coverage_object, &seniority) + .await?; + + let mut covered_hexes = vec![]; + let mut covered_hexes_stream = std::pin::pin!(covered_hexes_stream); + while let Some(hex_coverage) = covered_hexes_stream.next().await.transpose()? { + is_indoor = hex_coverage.indoor; + covered_hexes.push(coverage_map::UnrankedCoverage { + location: hex_coverage.hex, + signal_power: hex_coverage.signal_power, + signal_level: hex_coverage.signal_level.into(), + assignments: hex_coverage.assignments, + }); + } + + coverage_map_builder.insert_coverage_object(coverage_map::CoverageObject { + indoor: is_indoor, + hotspot_key: pubkey.clone().into(), + cbsd_id: cbsd_id.clone(), + seniority_timestamp: seniority.seniority_ts, + coverage: covered_hexes, + }); + + is_indoor + }; + + use coverage_point_calculator::RadioType; + let radio_type = match (is_indoor, cbsd_id.as_ref()) { + (true, None) => RadioType::IndoorWifi, + (true, Some(_)) => RadioType::IndoorCbrs, + (false, None) => RadioType::OutdoorWifi, + (false, Some(_)) => RadioType::OutdoorCbrs, + }; + + use coverage_point_calculator::{BytesPs, Speedtest}; + let speedtests = match speedtest_averages.get_average(&pubkey) { + Some(avg) => avg.speedtests, + None => vec![], + }; + let speedtests = speedtests + .iter() + .map(|test| Speedtest { + upload_speed: BytesPs::new(test.report.upload_speed), + download_speed: BytesPs::new(test.report.download_speed), + latency_millis: test.report.latency, + timestamp: test.report.timestamp, + }) + .collect(); + + use coverage_point_calculator::RadioThreshold; + let verified_radio_threshold = + match verified_radio_thresholds.is_verified(pubkey, cbsd_id) { + true => RadioThreshold::Verified, + false => RadioThreshold::Unverified, + }; + + use coverage_point_calculator::LocationTrust; + let trust_scores = heartbeat + .iter_distances_and_scores() + .map(|(distance, trust_score)| LocationTrust { + meters_to_asserted: distance as u32, + trust_score, + }) + .collect(); + + radio_infos.insert( + key, + RadioInfo { + radio_type, + coverage_obj_uuid: heartbeat.coverage_object, + seniority, + trust_scores, + verified_radio_threshold, + speedtests, + }, + ); } - Ok(Self { coverage_points }) - } - /// Only used for testing - pub fn hotspot_points(&self, hotspot: &PublicKeyBinary) -> Decimal { - self.coverage_points - .get(hotspot) - .map(HotspotPoints::total_points) - .unwrap_or(Decimal::ZERO) + let coverage_map = coverage_map_builder.build(boosted_hexes, reward_period.start); + + Ok(Self { + coverage_map, + radio_infos, + }) } - pub fn total_shares(&self) -> Decimal { - self.coverage_points - .values() - .fold(Decimal::ZERO, |sum, radio_points| { - sum + radio_points.total_points() - }) + fn coverage_points( + &self, + radio_id: &RadioId, + ) -> anyhow::Result { + let radio_info = self.radio_infos.get(radio_id).unwrap(); + + let hexes = { + let (pubkey, cbsd_id) = radio_id; + let ranked_coverage = match cbsd_id { + Some(cbsd_id) => self.coverage_map.get_cbrs_coverage(cbsd_id), + None => self.coverage_map.get_wifi_coverage(pubkey.as_ref()), + }; + ranked_coverage.to_vec() + }; + + let coverage_points = coverage_point_calculator::CoveragePoints::new( + radio_info.radio_type, + radio_info.verified_radio_threshold, + radio_info.speedtests.clone(), + radio_info.trust_scores.clone(), + hexes, + )?; + + Ok(coverage_points) } pub fn into_rewards( @@ -573,103 +589,61 @@ impl CoveragePoints { available_poc_rewards: Decimal, epoch: &'_ Range>, ) -> Option + '_> { - let total_shares = self.total_shares(); - available_poc_rewards - .checked_div(total_shares) - .map(|poc_rewards_per_share| { - tracing::info!(%poc_rewards_per_share); - let start_period = epoch.start.encode_timestamp(); - let end_period = epoch.end.encode_timestamp(); - self.coverage_points - .into_iter() - .flat_map(move |(hotspot_key, hotspot_points)| { - radio_points_into_rewards( - hotspot_key, - start_period, - end_period, - poc_rewards_per_share, - hotspot_points.speedtest_multiplier, - hotspot_points.radio_points.into_iter(), - ) - }) - .filter(|(poc_reward, _mobile_reward)| *poc_reward > 0) - }) - } -} + let mut total_shares: Decimal = dec!(0); + + let mut processed_radios = vec![]; + for (radio_id, radio_info) in self.radio_infos.iter() { + let points = match self.coverage_points(radio_id) { + Ok(points) => points, + Err(err) => { + tracing::error!( + pubkey = radio_id.0.to_string(), + ?err, + "could not reward radio" + ); + continue; + } + }; + + let seniority = radio_info.seniority.clone(); + let coverage_object_uuid = radio_info.coverage_obj_uuid; + + total_shares += points.reward_shares; + processed_radios.push((radio_id.clone(), points, seniority, coverage_object_uuid)); + } + + let Some(rewards_per_share) = available_poc_rewards.checked_div(total_shares) else { + // there are no shares, the rest are unallocated, return early + return None; + }; -fn radio_points_into_rewards( - hotspot_key: PublicKeyBinary, - start_period: u64, - end_period: u64, - poc_rewards_per_share: Decimal, - speedtest_multiplier: Decimal, - radio_points: impl Iterator, RadioPoints)>, -) -> impl Iterator { - radio_points.map(move |(cbsd_id, radio_points)| { - new_radio_reward( - cbsd_id, - &hotspot_key, - start_period, - end_period, - poc_rewards_per_share, - speedtest_multiplier, - radio_points, + Some( + processed_radios + .into_iter() + .map(move |(id, points, seniority, coverage_object_uuid)| { + let poc_reward = rewards_per_share * points.reward_shares; + let poc_reward = poc_reward.to_u64().unwrap_or_default(); + + let mobile_reward_share = coverage_point_to_mobile_reward_share( + points, + epoch, + &id, + poc_reward, + seniority.seniority_ts, + coverage_object_uuid, + ); + (poc_reward, mobile_reward_share) + }) + .filter(|(poc_reward, _mobile_reward)| *poc_reward > 0), ) - }) -} + } -#[allow(clippy::too_many_arguments)] -fn new_radio_reward( - cbsd_id: Option, - hotspot_key: &PublicKeyBinary, - start_period: u64, - end_period: u64, - poc_rewards_per_share: Decimal, - speedtest_multiplier: Decimal, - radio_points: RadioPoints, -) -> (u64, proto::MobileRewardShare) { - let poc_reward = radio_points.poc_reward(poc_rewards_per_share, speedtest_multiplier); - let hotspot_key: Vec = hotspot_key.clone().into(); - let cbsd_id = cbsd_id.unwrap_or_default(); - let poc_reward = poc_reward - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0); - let boosted_hexes = radio_points - .reward_points - .iter() - .filter(|radio_points| radio_points.boosted_hex.multiplier > NonZeroU32::new(1).unwrap()) - .map(|radio_points| proto::BoostedHex { - location: radio_points.boosted_hex.location.into_raw(), - multiplier: radio_points.boosted_hex.multiplier.get(), - }) - .collect(); - ( - poc_reward, - proto::MobileRewardShare { - start_period, - end_period, - reward: Some(proto::mobile_reward_share::Reward::RadioReward( - proto::RadioReward { - hotspot_key, - cbsd_id, - poc_reward, - coverage_points: radio_points.coverage_points().to_u64().unwrap_or(0), - seniority_timestamp: radio_points.seniority.encode_timestamp(), - coverage_object: Vec::from(radio_points.coverage_object.into_bytes()), - location_trust_score_multiplier: (radio_points.location_trust_score_multiplier - * dec!(1000)) - .to_u32() - .unwrap_or_default(), - speedtest_multiplier: (speedtest_multiplier * dec!(1000)) - .to_u32() - .unwrap_or_default(), - boosted_hexes, - ..Default::default() - }, - )), - }, - ) + /// Only used for testing + pub fn test_hotspot_reward_shares(&self, hotspot: &RadioId) -> Decimal { + self.coverage_points(hotspot) + .expect("reward shares for hotspot") + .reward_shares + } } pub fn get_total_scheduled_tokens(duration: Duration) -> Decimal { @@ -695,6 +669,7 @@ pub fn get_scheduled_tokens_for_oracles(duration: Duration) -> Decimal { #[cfg(test)] mod test { + use super::*; use hex_assignments::{assignment::HexAssignments, Assignment}; @@ -1394,7 +1369,7 @@ mod test { let mut allocated_poc_rewards = 0_u64; let epoch = (now - Duration::hours(1))..now; - for (reward_amount, mobile_reward) in CoveragePoints::aggregate_points( + for (reward_amount, mobile_reward) in CoverageShares::new( &hex_coverage, stream::iter(heartbeat_rewards), &speedtest_avgs, @@ -1566,7 +1541,7 @@ mod test { let duration = Duration::hours(1); let epoch = (now - duration)..now; let total_poc_rewards = get_scheduled_tokens_for_poc(epoch.end - epoch.start); - for (_reward_amount, mobile_reward) in CoveragePoints::aggregate_points( + for (_reward_amount, mobile_reward) in CoverageShares::new( &hex_coverage, stream::iter(heartbeat_rewards), &speedtest_avgs, @@ -1696,7 +1671,7 @@ mod test { let duration = Duration::hours(1); let epoch = (now - duration)..now; let total_poc_rewards = get_scheduled_tokens_for_poc(epoch.end - epoch.start); - for (_reward_amount, mobile_reward) in CoveragePoints::aggregate_points( + for (_reward_amount, mobile_reward) in CoverageShares::new( &hex_coverage, stream::iter(heartbeat_rewards), &speedtest_avgs, @@ -1826,7 +1801,7 @@ mod test { let duration = Duration::hours(1); let epoch = (now - duration)..now; let total_poc_rewards = get_scheduled_tokens_for_poc(epoch.end - epoch.start); - for (_reward_amount, mobile_reward) in CoveragePoints::aggregate_points( + for (_reward_amount, mobile_reward) in CoverageShares::new( &hex_coverage, stream::iter(heartbeat_rewards), &speedtest_avgs, @@ -1878,79 +1853,104 @@ mod test { .parse() .expect("failed gw2 parse"); - let c1 = "P27-SCE4255W2107CW5000014".to_string(); - let c2 = "P27-SCE4255W2107CW5000015".to_string(); - let c3 = "2AG32PBS3101S1202000464223GY0153".to_string(); - - let mut coverage_points = HashMap::new(); - - coverage_points.insert( - gw1.clone(), - HotspotPoints { - speedtest_multiplier: dec!(1.0), - radio_points: vec![( - Some(c1), - RadioPoints { - location_trust_score_multiplier: dec!(1.0), - seniority: DateTime::default(), - coverage_object: Uuid::new_v4(), - reward_points: vec![CoverageRewardPointsWithMultiplier { - coverage_points: CoverageRewardPoints { - boost_multiplier: NonZeroU32::new(1).unwrap(), - coverage_points: dec!(10.0), - hex_assignments: hex_assignments_mock(), - rank: None, - }, - boosted_hex: BoostedHex { - location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), - multiplier: NonZeroU32::new(1).unwrap(), - }, - }], + let now = Utc::now(); + let epoch = now - Duration::hours(1)..now; + + let uuid_1 = Uuid::new_v4(); + let uuid_2 = Uuid::new_v4(); + + let mut coverage_map = coverage_map::CoverageMapBuilder::default(); + coverage_map.insert_coverage_object(coverage_map::CoverageObject { + indoor: true, + hotspot_key: gw1.clone().into(), + cbsd_id: None, + seniority_timestamp: now, + coverage: vec![coverage_map::UnrankedCoverage { + location: Cell::from_raw(0x8c2681a3064dbff).expect("valid h3 cell"), + signal_power: 42, + signal_level: coverage_map::SignalLevel::High, + assignments: hex_assignments_mock(), + }], + }); + coverage_map.insert_coverage_object(coverage_map::CoverageObject { + indoor: true, + hotspot_key: gw2.clone().into(), + cbsd_id: None, + seniority_timestamp: now, + coverage: vec![coverage_map::UnrankedCoverage { + location: Cell::from_raw(0x8c2681a3064ddff).expect("valid h3 cell"), + signal_power: 42, + signal_level: coverage_map::SignalLevel::High, + assignments: hex_assignments_mock(), + }], + }); + let coverage_map = coverage_map.build(&BoostedHexes::default(), epoch.start); + + let mut radio_infos = HashMap::new(); + radio_infos.insert( + (gw1.clone(), None), + RadioInfo { + radio_type: coverage_point_calculator::RadioType::IndoorWifi, + coverage_obj_uuid: uuid_1, + trust_scores: vec![coverage_point_calculator::LocationTrust { + meters_to_asserted: 0, + trust_score: dec!(1), + }], + seniority: Seniority { + uuid: Uuid::new_v4(), + seniority_ts: now, + last_heartbeat: now, + inserted_at: now, + update_reason: 0, + }, + verified_radio_threshold: coverage_point_calculator::RadioThreshold::Verified, + speedtests: vec![ + coverage_point_calculator::Speedtest { + upload_speed: coverage_point_calculator::BytesPs::new(100_000_000), + download_speed: coverage_point_calculator::BytesPs::new(100_000_000), + latency_millis: 10, + timestamp: now, }, - )] - .into_iter() - .collect(), + coverage_point_calculator::Speedtest { + upload_speed: coverage_point_calculator::BytesPs::new(100_000_000), + download_speed: coverage_point_calculator::BytesPs::new(100_000_000), + latency_millis: 10, + timestamp: now, + }, + ], }, ); - coverage_points.insert( - gw2, - HotspotPoints { - speedtest_multiplier: dec!(1.0), - radio_points: vec![ - ( - Some(c2), - RadioPoints { - location_trust_score_multiplier: dec!(1.0), - seniority: DateTime::default(), - coverage_object: Uuid::new_v4(), - reward_points: vec![], - }, - ), - ( - Some(c3), - RadioPoints { - location_trust_score_multiplier: dec!(1.0), - reward_points: vec![], - seniority: DateTime::default(), - coverage_object: Uuid::new_v4(), - }, - ), - ] - .into_iter() - .collect(), + radio_infos.insert( + (gw2.clone(), None), + RadioInfo { + radio_type: coverage_point_calculator::RadioType::IndoorWifi, + coverage_obj_uuid: uuid_2, + trust_scores: vec![coverage_point_calculator::LocationTrust { + meters_to_asserted: 0, + trust_score: dec!(1), + }], + seniority: Seniority { + uuid: Uuid::new_v4(), + seniority_ts: now, + last_heartbeat: now, + inserted_at: now, + update_reason: 0, + }, + verified_radio_threshold: coverage_point_calculator::RadioThreshold::Verified, + speedtests: vec![], }, ); - let now = Utc::now(); - // We should never see any radio shares from owner2, since all of them are - // less than or equal to zero. - let coverage_points = CoveragePoints { coverage_points }; - let epoch = now - Duration::hours(1)..now; + let coverage_shares = CoverageShares { + coverage_map, + radio_infos, + }; let total_poc_rewards = get_scheduled_tokens_for_poc(epoch.end - epoch.start); + // gw2 does not have enough speedtests for a mulitplier let expected_hotspot = gw1; - for (_reward_amount, mobile_reward) in coverage_points + for (_reward_amount, mobile_reward) in coverage_shares .into_rewards(total_poc_rewards, &epoch) - .unwrap() + .expect("rewards output") { let radio_reward = match mobile_reward.reward { Some(proto::mobile_reward_share::Reward::RadioReward(radio_reward)) => radio_reward, @@ -1963,14 +1963,16 @@ mod test { #[tokio::test] async fn skip_empty_radio_rewards() { - let coverage_points = CoveragePoints { - coverage_points: HashMap::new(), - }; - let now = Utc::now(); let epoch = now - Duration::hours(1)..now; + let coverage_shares = CoverageShares { + coverage_map: coverage_map::CoverageMapBuilder::default() + .build(&BoostedHexes::default(), epoch.start), + radio_infos: HashMap::new(), + }; + let total_poc_rewards = get_scheduled_tokens_for_poc(epoch.end - epoch.start); - assert!(coverage_points + assert!(coverage_shares .into_rewards(total_poc_rewards, &epoch) .is_none()); } diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index c9c3792b3..e0ed055e9 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -3,7 +3,7 @@ use crate::{ coverage, data_session, heartbeats::{self, HeartbeatReward}, radio_threshold, - reward_shares::{self, CoveragePoints, MapperShares, ServiceProviderShares, TransferRewards}, + reward_shares::{self, CoverageShares, MapperShares, ServiceProviderShares, TransferRewards}, speedtests, speedtests_average::SpeedtestAverages, subscriber_location, telemetry, Settings, @@ -401,7 +401,7 @@ async fn reward_poc( let verified_radio_thresholds = radio_threshold::verified_radio_thresholds(pool, reward_period).await?; - let coverage_points = CoveragePoints::aggregate_points( + let coverage_shares = CoverageShares::new( pool, heartbeats, &speedtest_averages, @@ -412,7 +412,7 @@ async fn reward_poc( .await?; let unallocated_poc_amount = if let Some(mobile_reward_shares) = - coverage_points.into_rewards(total_poc_rewards, reward_period) + coverage_shares.into_rewards(total_poc_rewards, reward_period) { // handle poc reward outputs let mut allocated_poc_rewards = 0_u64; diff --git a/mobile_verifier/tests/integrations/boosting_oracles.rs b/mobile_verifier/tests/integrations/boosting_oracles.rs index a18fdeaec..1436437b5 100644 --- a/mobile_verifier/tests/integrations/boosting_oracles.rs +++ b/mobile_verifier/tests/integrations/boosting_oracles.rs @@ -22,7 +22,7 @@ use mobile_verifier::{ ValidatedHeartbeat, }, radio_threshold::VerifiedRadioThresholds, - reward_shares::CoveragePoints, + reward_shares::CoverageShares, speedtests::Speedtest, speedtests_average::{SpeedtestAverage, SpeedtestAverages}, GatewayResolution, GatewayResolver, @@ -350,7 +350,8 @@ async fn test_footfall_and_urbanization_and_landtype(pool: PgPool) -> anyhow::Re .build()?; let _ = common::set_unassigned_oracle_boosting_assignments(&pool, &hex_boost_data).await?; - let heartbeats = heartbeats(12, start, &owner, &cbsd_id, 0.0, 0.0, uuid); + let heartbeat_owner = owner.clone(); + let heartbeats = heartbeats(12, start, &heartbeat_owner, &cbsd_id, 0.0, 0.0, uuid); let coverage_objects = CoverageObjectCache::new(&pool); let coverage_claim_time_cache = CoverageClaimTimeCache::new(); @@ -399,7 +400,7 @@ async fn test_footfall_and_urbanization_and_landtype(pool: PgPool) -> anyhow::Re let speedtest_avgs = SpeedtestAverages { averages }; let heartbeats = HeartbeatReward::validated(&pool, &epoch); - let coverage_points = CoveragePoints::aggregate_points( + let coverage_shares = CoverageShares::new( &pool, heartbeats, &speedtest_avgs, @@ -450,7 +451,10 @@ async fn test_footfall_and_urbanization_and_landtype(pool: PgPool) -> anyhow::Re // ----------------------------------------------- // = 1,073 - assert_eq!(coverage_points.hotspot_points(&owner), dec!(1073.0)); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner, Some(cbsd_id.clone()))), + dec!(1073.0) + ); Ok(()) } diff --git a/mobile_verifier/tests/integrations/heartbeats.rs b/mobile_verifier/tests/integrations/heartbeats.rs index d79f259d5..c827e9f44 100644 --- a/mobile_verifier/tests/integrations/heartbeats.rs +++ b/mobile_verifier/tests/integrations/heartbeats.rs @@ -12,7 +12,6 @@ use sqlx::PgPool; use uuid::Uuid; #[sqlx::test] -#[ignore] async fn test_save_wifi_heartbeat(pool: PgPool) -> anyhow::Result<()> { let coverage_object = Uuid::new_v4(); let heartbeat = ValidatedHeartbeat { @@ -50,7 +49,6 @@ async fn test_save_wifi_heartbeat(pool: PgPool) -> anyhow::Result<()> { } #[sqlx::test] -#[ignore] async fn test_save_cbrs_heartbeat(pool: PgPool) -> anyhow::Result<()> { let coverage_object = Uuid::new_v4(); let heartbeat = ValidatedHeartbeat { @@ -88,7 +86,6 @@ async fn test_save_cbrs_heartbeat(pool: PgPool) -> anyhow::Result<()> { } #[sqlx::test] -#[ignore] async fn only_fetch_latest_hotspot(pool: PgPool) -> anyhow::Result<()> { let cbsd_id = "P27-SCE4255W120200039521XGB0103".to_string(); let coverage_object = Uuid::new_v4(); @@ -156,7 +153,6 @@ VALUES } #[sqlx::test] -#[ignore] async fn ensure_hotspot_does_not_affect_count(pool: PgPool) -> anyhow::Result<()> { let cbsd_id = "P27-SCE4255W120200039521XGB0103".to_string(); let coverage_object = Uuid::new_v4(); @@ -212,7 +208,6 @@ VALUES } #[sqlx::test] -#[ignore] async fn ensure_minimum_count(pool: PgPool) -> anyhow::Result<()> { let cbsd_id = "P27-SCE4255W120200039521XGB0103".to_string(); let coverage_object = Uuid::new_v4(); @@ -253,7 +248,6 @@ VALUES } #[sqlx::test] -#[ignore] async fn ensure_wifi_hotspots_are_rewarded(pool: PgPool) -> anyhow::Result<()> { let early_coverage_object = Uuid::new_v4(); let latest_coverage_object = Uuid::new_v4(); @@ -305,7 +299,6 @@ VALUES } #[sqlx::test] -#[ignore] async fn ensure_wifi_hotspots_use_average_location_trust_score(pool: PgPool) -> anyhow::Result<()> { let early_coverage_object = Uuid::new_v4(); let latest_coverage_object = Uuid::new_v4(); diff --git a/mobile_verifier/tests/integrations/modeled_coverage.rs b/mobile_verifier/tests/integrations/modeled_coverage.rs index b59b5e6e5..e8ab799ba 100644 --- a/mobile_verifier/tests/integrations/modeled_coverage.rs +++ b/mobile_verifier/tests/integrations/modeled_coverage.rs @@ -22,7 +22,7 @@ use mobile_verifier::{ ValidatedHeartbeat, }, radio_threshold::VerifiedRadioThresholds, - reward_shares::CoveragePoints, + reward_shares::CoverageShares, speedtests::Speedtest, speedtests_average::{SpeedtestAverage, SpeedtestAverages}, GatewayResolution, GatewayResolver, IsAuthorized, @@ -48,7 +48,6 @@ const BOOST_HEX_PUBKEY: &str = "J9JiLTpjaShxL8eMvUs8txVw6TZ36E38SiJ89NxnMbLU"; const BOOST_CONFIG_PUBKEY: &str = "BZM1QTud72B2cpTW7PhEnFmRX7ZWzvY7DpPpNJJuDrWG"; #[sqlx::test] -#[ignore] async fn test_save_wifi_coverage_object(pool: PgPool) -> anyhow::Result<()> { let cache = CoverageObjectCache::new(&pool); let uuid = Uuid::new_v4(); @@ -116,7 +115,6 @@ async fn test_save_wifi_coverage_object(pool: PgPool) -> anyhow::Result<()> { } #[sqlx::test] -#[ignore] async fn test_save_cbrs_coverage_object(pool: PgPool) -> anyhow::Result<()> { let cache = CoverageObjectCache::new(&pool); let uuid = Uuid::new_v4(); @@ -180,7 +178,6 @@ async fn test_save_cbrs_coverage_object(pool: PgPool) -> anyhow::Result<()> { } #[sqlx::test] -#[ignore] async fn test_coverage_object_save_updates(pool: PgPool) -> anyhow::Result<()> { let cache = CoverageObjectCache::new(&pool); let uuid = Uuid::new_v4(); @@ -448,7 +445,6 @@ async fn process_input( } #[sqlx::test] -#[ignore] async fn scenario_one(pool: PgPool) -> anyhow::Result<()> { let start: DateTime = "2022-01-01 00:00:00.000000000 UTC".parse()?; let end: DateTime = "2022-01-02 00:00:00.000000000 UTC".parse()?; @@ -496,7 +492,7 @@ async fn scenario_one(pool: PgPool) -> anyhow::Result<()> { let reward_period = start..end; let heartbeats = HeartbeatReward::validated(&pool, &reward_period); - let coverage_points = CoveragePoints::aggregate_points( + let coverage_shares = CoverageShares::new( &pool, heartbeats, &speedtest_avgs, @@ -506,13 +502,15 @@ async fn scenario_one(pool: PgPool) -> anyhow::Result<()> { ) .await?; - assert_eq!(coverage_points.hotspot_points(&owner), dec!(250)); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner, Some(cbsd_id))), + dec!(250) + ); Ok(()) } #[sqlx::test] -#[ignore] async fn scenario_two(pool: PgPool) -> anyhow::Result<()> { let start: DateTime = "2022-02-01 00:00:00.000000000 UTC".parse()?; let end: DateTime = "2022-02-02 00:00:00.000000000 UTC".parse()?; @@ -596,7 +594,7 @@ async fn scenario_two(pool: PgPool) -> anyhow::Result<()> { let reward_period = start..end; let heartbeats = HeartbeatReward::validated(&pool, &reward_period); - let coverage_points = CoveragePoints::aggregate_points( + let coverage_shares = CoverageShares::new( &pool, heartbeats, &speedtest_avgs, @@ -606,14 +604,19 @@ async fn scenario_two(pool: PgPool) -> anyhow::Result<()> { ) .await?; - assert_eq!(coverage_points.hotspot_points(&owner_1), dec!(112.5)); - assert_eq!(coverage_points.hotspot_points(&owner_2), dec!(250)); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_1, Some(cbsd_id_1))), + dec!(112.5) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_2, Some(cbsd_id_2))), + dec!(250) + ); Ok(()) } #[sqlx::test] -#[ignore] async fn scenario_three(pool: PgPool) -> anyhow::Result<()> { let start: DateTime = "2022-02-01 00:00:00.000000000 UTC".parse()?; let end: DateTime = "2022-02-02 00:00:00.000000000 UTC".parse()?; @@ -879,7 +882,7 @@ async fn scenario_three(pool: PgPool) -> anyhow::Result<()> { let reward_period = start..end; let heartbeats = HeartbeatReward::validated(&pool, &reward_period); - let coverage_points = CoveragePoints::aggregate_points( + let coverage_shares = CoverageShares::new( &pool, heartbeats, &speedtest_avgs, @@ -889,18 +892,35 @@ async fn scenario_three(pool: PgPool) -> anyhow::Result<()> { ) .await?; - assert_eq!(coverage_points.hotspot_points(&owner_1), dec!(0)); - assert_eq!(coverage_points.hotspot_points(&owner_2), dec!(0)); - assert_eq!(coverage_points.hotspot_points(&owner_3), dec!(0)); - assert_eq!(coverage_points.hotspot_points(&owner_4), dec!(250)); - assert_eq!(coverage_points.hotspot_points(&owner_5), dec!(0)); - assert_eq!(coverage_points.hotspot_points(&owner_6), dec!(0)); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_1, Some(cbsd_id_1))), + dec!(0) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_2, Some(cbsd_id_2))), + dec!(0) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_3, Some(cbsd_id_3))), + dec!(0) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_4, Some(cbsd_id_4))), + dec!(250) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_5, Some(cbsd_id_5))), + dec!(0) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_6, Some(cbsd_id_6))), + dec!(0) + ); Ok(()) } #[sqlx::test] -#[ignore] async fn scenario_four(pool: PgPool) -> anyhow::Result<()> { let start: DateTime = "2022-01-01 00:00:00.000000000 UTC".parse()?; let end: DateTime = "2022-01-02 00:00:00.000000000 UTC".parse()?; @@ -951,7 +971,7 @@ async fn scenario_four(pool: PgPool) -> anyhow::Result<()> { let reward_period = start..end; let heartbeats = HeartbeatReward::validated(&pool, &reward_period); - let coverage_points = CoveragePoints::aggregate_points( + let coverage_shares = CoverageShares::new( &pool, heartbeats, &speedtest_avgs, @@ -961,13 +981,15 @@ async fn scenario_four(pool: PgPool) -> anyhow::Result<()> { ) .await?; - assert_eq!(coverage_points.hotspot_points(&owner), dec!(19)); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner, Some(cbsd_id))), + dec!(19) + ); Ok(()) } #[sqlx::test] -#[ignore] async fn scenario_five(pool: PgPool) -> anyhow::Result<()> { let start: DateTime = "2022-02-01 00:00:00.000000000 UTC".parse()?; let end: DateTime = "2022-02-02 00:00:00.000000000 UTC".parse()?; @@ -1050,7 +1072,7 @@ async fn scenario_five(pool: PgPool) -> anyhow::Result<()> { let reward_period = start..end; let heartbeats = HeartbeatReward::validated(&pool, &reward_period); - let coverage_points = CoveragePoints::aggregate_points( + let coverage_shares = CoverageShares::new( &pool, heartbeats, &speedtest_avgs, @@ -1061,16 +1083,18 @@ async fn scenario_five(pool: PgPool) -> anyhow::Result<()> { .await?; assert_eq!( - coverage_points.hotspot_points(&owner_1), + coverage_shares.test_hotspot_reward_shares(&(owner_1, Some(cbsd_id_1))), dec!(19) * dec!(0.5) ); - assert_eq!(coverage_points.hotspot_points(&owner_2), dec!(8)); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_2, Some(cbsd_id_2))), + dec!(8) + ); Ok(()) } #[sqlx::test] -#[ignore] async fn scenario_six(pool: PgPool) -> anyhow::Result<()> { let start: DateTime = "2022-02-01 00:00:00.000000000 UTC".parse()?; let end: DateTime = "2022-02-02 00:00:00.000000000 UTC".parse()?; @@ -1297,7 +1321,7 @@ async fn scenario_six(pool: PgPool) -> anyhow::Result<()> { let reward_period = start..end; let heartbeats = HeartbeatReward::validated(&pool, &reward_period); - let coverage_points = CoveragePoints::aggregate_points( + let coverage_shares = CoverageShares::new( &pool, heartbeats, &speedtest_avgs, @@ -1307,18 +1331,35 @@ async fn scenario_six(pool: PgPool) -> anyhow::Result<()> { ) .await?; - assert_eq!(coverage_points.hotspot_points(&owner_1), dec!(0)); - assert_eq!(coverage_points.hotspot_points(&owner_2), dec!(62.5)); - assert_eq!(coverage_points.hotspot_points(&owner_3), dec!(0)); - assert_eq!(coverage_points.hotspot_points(&owner_4), dec!(0)); - assert_eq!(coverage_points.hotspot_points(&owner_5), dec!(0)); - assert_eq!(coverage_points.hotspot_points(&owner_6), dec!(0)); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_1, Some(cbsd_id_1))), + dec!(0) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_2, Some(cbsd_id_2))), + dec!(62.5) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_3, Some(cbsd_id_3))), + dec!(0) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_4, Some(cbsd_id_4))), + dec!(0) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_5, Some(cbsd_id_5))), + dec!(0) + ); + assert_eq!( + coverage_shares.test_hotspot_reward_shares(&(owner_6, Some(cbsd_id_6))), + dec!(0) + ); Ok(()) } #[sqlx::test] -#[ignore] async fn ensure_lower_trust_score_for_distant_heartbeats(pool: PgPool) -> anyhow::Result<()> { let owner_1: PublicKeyBinary = "11xtYwQYnvkFYnJ9iZ8kmnetYKwhdi87Mcr36e1pVLrhBMPLjV9" .parse() diff --git a/mobile_verifier/tests/integrations/seniority.rs b/mobile_verifier/tests/integrations/seniority.rs index e3f1950dd..54bb59605 100644 --- a/mobile_verifier/tests/integrations/seniority.rs +++ b/mobile_verifier/tests/integrations/seniority.rs @@ -10,7 +10,6 @@ use sqlx::PgPool; use uuid::Uuid; #[sqlx::test] -#[ignore] async fn test_seniority_updates(pool: PgPool) -> anyhow::Result<()> { let coverage_object = Uuid::new_v4(); let mut heartbeat = ValidatedHeartbeat {