diff --git a/Cargo.lock b/Cargo.lock index b2324b41f..745f8386f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1590,7 +1590,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "beacon" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#009aeadc1fcd3028cb5b52676a44d7920f1d162b" +source = "git+https://github.com/helium/proto?branch=master#659cd817e882090de83e4debb3df63f99b06a70a" dependencies = [ "base64 0.21.7", "byteorder", @@ -3556,7 +3556,7 @@ dependencies = [ [[package]] name = "helium-proto" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#009aeadc1fcd3028cb5b52676a44d7920f1d162b" +source = "git+https://github.com/helium/proto?branch=master#659cd817e882090de83e4debb3df63f99b06a70a" dependencies = [ "bytes", "prost", @@ -6630,7 +6630,7 @@ dependencies = [ "futures", "helium-anchor-gen", "helium-crypto", - "itertools 0.10.5", + "itertools 0.12.0", "metrics", "serde", "sha2 0.10.6", diff --git a/file_store/src/cli/dump.rs b/file_store/src/cli/dump.rs index 33b5b4e95..e0017c989 100644 --- a/file_store/src/cli/dump.rs +++ b/file_store/src/cli/dump.rs @@ -1,5 +1,6 @@ use crate::{ cli::print_json, + coverage::CoverageObject, file_source, heartbeat::{CbrsHeartbeat, CbrsHeartbeatIngestReport}, iot_packet::IotValidPacket, @@ -23,7 +24,7 @@ use helium_proto::{ }, poc_mobile::{ mobile_reward_share::Reward, CellHeartbeatIngestReportV1, CellHeartbeatReqV1, - Heartbeat, InvalidDataTransferIngestReportV1, MobileRewardShare, + CoverageObjectV1, Heartbeat, InvalidDataTransferIngestReportV1, MobileRewardShare, OracleBoostingReportV1, RadioRewardShare, SpeedtestAvg, SpeedtestIngestReportV1, SpeedtestReqV1, }, @@ -355,7 +356,17 @@ impl Cmd { "timestamp": report.timestamp.to_timestamp()?, }))? } - _ => (), + FileType::CoverageObject => { + let coverage = CoverageObjectV1::decode(msg)?; + let coverage = CoverageObject::try_from(coverage.coverage_object.unwrap())?; + print_json(&json!({ + "pub_key": coverage.pub_key, + "uuid": coverage.uuid, + "coverage_claim_time": coverage.coverage_claim_time, + "coverage": coverage.coverage, + }))?; + } + missing_filetype => println!("No dump for {missing_filetype}"), } } diff --git a/file_store/src/cli/dump_mobile_rewards.rs b/file_store/src/cli/dump_mobile_rewards.rs new file mode 100644 index 000000000..56ba0fccc --- /dev/null +++ b/file_store/src/cli/dump_mobile_rewards.rs @@ -0,0 +1,68 @@ +use crate::cli::print_json; +use crate::{file_source, Result, Settings}; +use futures::stream::StreamExt; +use helium_crypto::PublicKey; +use helium_proto::services::poc_mobile::mobile_reward_share::Reward::*; +use helium_proto::services::poc_mobile::MobileRewardShare; +use prost::Message; +use serde_json::json; +use std::path::PathBuf; + +#[derive(Debug, clap::Args)] +pub struct Cmd { + path: PathBuf, +} + +impl Cmd { + pub async fn run(&self, _settings: &Settings) -> Result { + let mut file_stream = file_source::source([&self.path]); + + let mut radio_reward = vec![]; + let mut gateway_reward = vec![]; + let mut subscriber_reward = vec![]; + let mut service_provider_reward = vec![]; + let mut unallocated_reward = vec![]; + + while let Some(result) = file_stream.next().await { + let msg = result?; + let reward = MobileRewardShare::decode(msg)?; + match reward.reward { + Some(r) => match r { + RadioReward(reward) => radio_reward.push(json!({ + "hotspot_key": PublicKey::try_from(reward.hotspot_key)?.to_string(), + "cbsd_id": reward.cbsd_id, + "poc_reward": reward.poc_reward, + "boosted_hexes": reward.boosted_hexes, + })), + GatewayReward(reward) => gateway_reward.push(json!({ + "hotspot_key": PublicKey::try_from(reward.hotspot_key)?.to_string(), + "dc_transfer_reward": reward.dc_transfer_reward, + })), + SubscriberReward(reward) => subscriber_reward.push(json!({ + "subscriber_id": uuid::Uuid::from_slice(&reward.subscriber_id).unwrap(), + "discovery_location_amount": reward.discovery_location_amount, + })), + ServiceProviderReward(reward) => service_provider_reward.push(json!({ + "service_provider": reward.service_provider_id, + "amount": reward.amount, + })), + UnallocatedReward(reward) => unallocated_reward.push(json!({ + "unallocated_reward_type": reward.reward_type, + "amount": reward.amount, + })), + }, + None => todo!(), + } + } + + print_json(&json!({ + "radio_reward": radio_reward, + "gateway_reward": gateway_reward, + "subscriber_reward": subscriber_reward, + "service_provider_reward": service_provider_reward, + "unallocated_reward": unallocated_reward, + }))?; + + Ok(()) + } +} diff --git a/file_store/src/cli/mod.rs b/file_store/src/cli/mod.rs index d47fd8345..5eaccb9f3 100644 --- a/file_store/src/cli/mod.rs +++ b/file_store/src/cli/mod.rs @@ -1,5 +1,6 @@ pub mod bucket; pub mod dump; +pub mod dump_mobile_rewards; pub mod info; use crate::Result; diff --git a/file_store/src/main.rs b/file_store/src/main.rs index 6c61c6f83..0f56edce2 100644 --- a/file_store/src/main.rs +++ b/file_store/src/main.rs @@ -1,6 +1,6 @@ use clap::Parser; use file_store::{ - cli::{bucket, dump, info}, + cli::{bucket, dump, dump_mobile_rewards, info}, Result, Settings, }; use std::path; @@ -29,6 +29,7 @@ pub enum Cmd { Info(info::Cmd), Dump(dump::Cmd), Bucket(Box), + DumpMobileRewards(dump_mobile_rewards::Cmd), } impl Cmd { @@ -37,6 +38,7 @@ impl Cmd { Cmd::Info(cmd) => cmd.run(&settings).await, Cmd::Dump(cmd) => cmd.run(&settings).await, Cmd::Bucket(cmd) => cmd.run(&settings).await, + Cmd::DumpMobileRewards(cmd) => cmd.run(&settings).await, } } } diff --git a/mobile_config/src/settings.rs b/mobile_config/src/settings.rs index a7d3f7a0b..dfba3af10 100644 --- a/mobile_config/src/settings.rs +++ b/mobile_config/src/settings.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use config::{Config, Environment, File}; use serde::Deserialize; use std::{ @@ -65,7 +66,9 @@ impl Settings { } pub fn signing_keypair(&self) -> anyhow::Result { - let data = std::fs::read(&self.signing_keypair).map_err(helium_crypto::Error::from)?; + let data = std::fs::read(&self.signing_keypair) + .map_err(helium_crypto::Error::from) + .with_context(|| format!("reading keypair from settings: {}", self.signing_keypair))?; Ok(helium_crypto::Keypair::try_from(&data[..])?) } diff --git a/mobile_verifier/migrations/29_footfall.sql b/mobile_verifier/migrations/29_footfall.sql new file mode 100644 index 000000000..a8d4235ee --- /dev/null +++ b/mobile_verifier/migrations/29_footfall.sql @@ -0,0 +1 @@ +ALTER TABLE hexes ADD COLUMN footfall oracle_assignment; \ No newline at end of file diff --git a/mobile_verifier/src/boosting_oracles/assignment.rs b/mobile_verifier/src/boosting_oracles/assignment.rs index 8956c55b2..4be121708 100644 --- a/mobile_verifier/src/boosting_oracles/assignment.rs +++ b/mobile_verifier/src/boosting_oracles/assignment.rs @@ -40,18 +40,7 @@ impl fmt::Display for Assignment { } } -pub fn urbanization_multiplier(urbanization: Assignment) -> Decimal { - use Assignment::*; - - match urbanization { - A => dec!(1.0), - B => dec!(0.25), - C => dec!(0.0), - } -} - -#[allow(dead_code)] -pub fn urbanization_and_footfall_multiplier( +pub fn footfall_and_urbanization_multiplier( footfall: Assignment, urbanization: Assignment, ) -> Decimal { diff --git a/mobile_verifier/src/boosting_oracles/mod.rs b/mobile_verifier/src/boosting_oracles/mod.rs index 95f4f5d82..882089cbe 100644 --- a/mobile_verifier/src/boosting_oracles/mod.rs +++ b/mobile_verifier/src/boosting_oracles/mod.rs @@ -2,11 +2,70 @@ pub mod assignment; use std::collections::HashMap; -use crate::geofence::GeofenceValidator; +use crate::{ + geofence::{Geofence, GeofenceValidator}, + Settings, +}; pub use assignment::Assignment; use hextree::disktree::DiskTreeMap; -pub trait DiskTreeLike: Send + Sync + 'static { +pub fn make_hex_boost_data( + settings: &Settings, + usa_geofence: Geofence, +) -> anyhow::Result> { + let urban_disktree = DiskTreeMap::open(&settings.urbanization_data_set)?; + let footfall_disktree = DiskTreeMap::open(&settings.footfall_data_set)?; + + let urbanization = UrbanizationData::new(urban_disktree, usa_geofence); + let footfall_data = FootfallData::new(footfall_disktree); + let hex_boost_data = HexBoostData::new(urbanization, footfall_data); + + Ok(hex_boost_data) +} + +pub trait HexAssignment: Send + Sync { + fn assignment(&self, cell: hextree::Cell) -> anyhow::Result; +} + +pub struct HexBoostData { + pub urbanization: Urban, + pub footfall: Foot, +} + +pub struct UrbanizationData { + urbanized: Urban, + usa_geofence: Geo, +} + +pub struct FootfallData { + footfall: Foot, +} + +impl HexBoostData { + pub fn new(urbanization: Urban, footfall: Foot) -> Self { + Self { + urbanization, + footfall, + } + } +} + +impl UrbanizationData { + pub fn new(urbanized: Urban, usa_geofence: Geo) -> Self { + Self { + urbanized, + usa_geofence, + } + } +} + +impl FootfallData { + pub fn new(footfall: Foot) -> Self { + Self { footfall } + } +} + +trait DiskTreeLike: Send + Sync { fn get(&self, cell: hextree::Cell) -> hextree::Result>; } @@ -16,54 +75,61 @@ impl DiskTreeLike for DiskTreeMap { } } -impl DiskTreeLike for HashMap> { +impl DiskTreeLike for std::collections::HashSet { fn get(&self, cell: hextree::Cell) -> hextree::Result> { - Ok(self.get(&cell).map(|x| (cell, x.as_slice()))) + match self.contains(&cell) { + true => Ok(Some((cell, &[]))), + false => Ok(None), + } } } -pub struct MockDiskTree; +impl HexAssignment for UrbanizationData +where + Urban: DiskTreeLike, + Geo: GeofenceValidator, +{ + fn assignment(&self, cell: hextree::Cell) -> anyhow::Result { + if !self.usa_geofence.in_valid_region(&cell) { + return Ok(Assignment::C); + } -impl DiskTreeLike for MockDiskTree { - fn get(&self, cell: hextree::Cell) -> hextree::Result> { - Ok(Some((cell, &[]))) + match self.urbanized.get(cell)?.is_some() { + true => Ok(Assignment::A), + false => Ok(Assignment::B), + } } } -pub struct Urbanization { - urbanized: DT, - usa_geofence: GF, -} +impl HexAssignment for FootfallData +where + Foot: DiskTreeLike, +{ + fn assignment(&self, cell: hextree::Cell) -> anyhow::Result { + let Some((_, vals)) = self.footfall.get(cell)? else { + return Ok(Assignment::C); + }; -impl Urbanization { - pub fn new(urbanized: DT, usa_geofence: GF) -> Self { - Self { - urbanized, - usa_geofence, + match vals { + &[x] if x >= 1 => Ok(Assignment::A), + &[0] => Ok(Assignment::B), + other => anyhow::bail!("unexpected disktree data: {cell:?} {other:?}"), } } } -impl Urbanization -where - DT: DiskTreeLike, - GF: GeofenceValidator, -{ - fn is_urbanized(&self, location: u64) -> anyhow::Result { - let cell = hextree::Cell::from_raw(location)?; - let result = self.urbanized.get(cell)?; - Ok(result.is_some()) +impl HexAssignment for Assignment { + fn assignment(&self, _cell: hextree::Cell) -> anyhow::Result { + Ok(*self) } +} - pub fn hex_assignment(&self, hex: u64) -> anyhow::Result { - let assignment = if self.usa_geofence.in_valid_region(&hex) { - if self.is_urbanized(hex)? { - Assignment::A - } else { - Assignment::B - } - } else { - Assignment::C +impl HexAssignment for HashMap { + fn assignment(&self, cell: hextree::Cell) -> anyhow::Result { + let assignment = match self.get(&cell) { + Some(true) => Assignment::A, + Some(false) => Assignment::B, + None => Assignment::C, }; Ok(assignment) } diff --git a/mobile_verifier/src/cli/server.rs b/mobile_verifier/src/cli/server.rs index b11280ded..c2fe059ea 100644 --- a/mobile_verifier/src/cli/server.rs +++ b/mobile_verifier/src/cli/server.rs @@ -1,10 +1,17 @@ use crate::{ - boosting_oracles::Urbanization, coverage::CoverageDaemon, data_session::DataSessionIngestor, - geofence::Geofence, heartbeats::cbrs::HeartbeatDaemon as CellHeartbeatDaemon, - heartbeats::wifi::HeartbeatDaemon as WifiHeartbeatDaemon, + boosting_oracles, + coverage::CoverageDaemon, + data_session::DataSessionIngestor, + geofence::Geofence, + heartbeats::{ + cbrs::HeartbeatDaemon as CellHeartbeatDaemon, wifi::HeartbeatDaemon as WifiHeartbeatDaemon, + }, invalidated_radio_threshold::InvalidatedRadioThresholdIngestor, - radio_threshold::RadioThresholdIngestor, rewarder::Rewarder, speedtests::SpeedtestDaemon, - subscriber_location::SubscriberLocationIngestor, telemetry, Settings, + radio_threshold::RadioThresholdIngestor, + rewarder::Rewarder, + speedtests::SpeedtestDaemon, + subscriber_location::SubscriberLocationIngestor, + telemetry, Settings, }; use anyhow::Result; use chrono::Duration; @@ -17,7 +24,7 @@ use file_store::{ speedtest::CellSpeedtestIngestReport, wifi_heartbeat::WifiHeartbeatIngestReport, FileStore, FileType, }; -use hextree::disktree::DiskTreeMap; + use mobile_config::client::{ entity_client::EntityClient, hex_boosting_client::HexBoostingClient, AuthorizationClient, CarrierServiceClient, GatewayClient, @@ -213,13 +220,11 @@ impl Cmd { .create() .await?; - let disktree = DiskTreeMap::open(&settings.urbanization_data_set)?; - let urbanization = Urbanization::new(disktree, usa_geofence); - + let hex_boost_data = boosting_oracles::make_hex_boost_data(settings, usa_geofence)?; let coverage_daemon = CoverageDaemon::new( pool.clone(), auth_client.clone(), - urbanization, + hex_boost_data, coverage_objs, valid_coverage_objs, oracle_boosting_reports, diff --git a/mobile_verifier/src/coverage.rs b/mobile_verifier/src/coverage.rs index 06346871f..8a47caa12 100644 --- a/mobile_verifier/src/coverage.rs +++ b/mobile_verifier/src/coverage.rs @@ -1,8 +1,7 @@ use crate::{ boosting_oracles::{ - assignment::urbanization_multiplier, Assignment, DiskTreeLike, Urbanization, + assignment::footfall_and_urbanization_multiplier, Assignment, HexAssignment, HexBoostData, }, - geofence::GeofenceValidator, heartbeats::{HbType, KeyType, OwnedKeyType}, IsAuthorized, }; @@ -67,25 +66,29 @@ impl From for SignalLevel { } } -pub struct CoverageDaemon { +pub struct CoverageDaemon +where + Urban: HexAssignment, + Foot: HexAssignment, +{ pool: Pool, auth_client: AuthorizationClient, - urbanization: Urbanization, + hex_boost_data: HexBoostData, coverage_objs: Receiver>, initial_boosting_reports: Option>, coverage_obj_sink: FileSinkClient, oracle_boosting_sink: FileSinkClient, } -impl CoverageDaemon +impl CoverageDaemon where - DT: DiskTreeLike, - GF: GeofenceValidator, + Urban: HexAssignment, + Foot: HexAssignment, { pub async fn new( pool: PgPool, auth_client: AuthorizationClient, - urbanization: Urbanization, + hex_boost_data: HexBoostData, coverage_objs: Receiver>, coverage_obj_sink: FileSinkClient, oracle_boosting_sink: FileSinkClient, @@ -94,7 +97,7 @@ where let unassigned_hexes = UnassignedHex::fetch(&pool); let initial_boosting_reports = Some( - set_oracle_boosting_assignments(unassigned_hexes, &urbanization, &pool) + set_oracle_boosting_assignments(unassigned_hexes, &hex_boost_data, &pool) .await? .collect(), ); @@ -102,7 +105,7 @@ where Ok(Self { pool, auth_client, - urbanization, + hex_boost_data, coverage_objs, coverage_obj_sink, oracle_boosting_sink, @@ -166,7 +169,7 @@ where // done in two steps to improve the testability of the assignments. let unassigned_hexes = UnassignedHex::fetch(&self.pool); let boosting_reports = - set_oracle_boosting_assignments(unassigned_hexes, &self.urbanization, &self.pool) + set_oracle_boosting_assignments(unassigned_hexes, &self.hex_boost_data, &self.pool) .await?; self.oracle_boosting_sink .write_all(boosting_reports) @@ -189,80 +192,107 @@ pub struct UnassignedHex { impl UnassignedHex { pub fn fetch(pool: &PgPool) -> impl Stream> + '_ { sqlx::query_as( - "SELECT uuid, hex, signal_level, signal_power FROM hexes WHERE urbanized IS NULL", + "SELECT uuid, hex, signal_level, signal_power FROM hexes WHERE urbanized IS NULL OR footfall IS NULL", ) .fetch(pool) } + + fn to_location_string(&self) -> String { + format!("{:x}", self.hex) + } } -pub async fn set_oracle_boosting_assignments<'a>( - unassigned_hexes: impl Stream>, - urbanization: &Urbanization>, - pool: &'a PgPool, +pub async fn set_oracle_boosting_assignments( + unassigned_urbinization_hexes: impl Stream>, + hex_boost_data: &HexBoostData, + pool: &PgPool, ) -> anyhow::Result> { - const NUMBER_OF_FIELDS_IN_QUERY: u16 = 5; + let now = Utc::now(); + + let boost_results = + initialize_unassigned_hexes(unassigned_urbinization_hexes, hex_boost_data, pool).await?; + + Ok(boost_results + .into_iter() + .map( + move |(coverage_object, assignments)| proto::OracleBoostingReportV1 { + coverage_object: Vec::from(coverage_object.into_bytes()), + assignments, + timestamp: now.encode_timestamp(), + }, + )) +} + +async fn initialize_unassigned_hexes( + unassigned_urbinization_hexes: impl Stream>, + hex_boost_data: &HexBoostData, + pool: &Pool, +) -> Result>, anyhow::Error> { + const NUMBER_OF_FIELDS_IN_QUERY: u16 = 6; const ASSIGNMENTS_MAX_BATCH_ENTRIES: usize = (u16::MAX / NUMBER_OF_FIELDS_IN_QUERY) as usize; - let now = Utc::now(); let mut boost_results = HashMap::>::new(); - let mut unassigned_hexes = pin!(unassigned_hexes.try_chunks(ASSIGNMENTS_MAX_BATCH_ENTRIES)); + + let mut unassigned_hexes = + pin!(unassigned_urbinization_hexes.try_chunks(ASSIGNMENTS_MAX_BATCH_ENTRIES)); while let Some(hexes) = unassigned_hexes.try_next().await? { let hexes: anyhow::Result> = hexes .into_iter() .map(|hex| { - let urbanized = urbanization.hex_assignment(hex.hex)?; - let location = format!("{:x}", hex.hex); - let assignment_multiplier = (urbanization_multiplier(urbanized) * dec!(1000)) - .to_u32() - .unwrap_or(0); + let cell = hextree::Cell::from_raw(hex.hex)?; + let urbanized = hex_boost_data.urbanization.assignment(cell)?; + let footfall = hex_boost_data.footfall.assignment(cell)?; + + let location = hex.to_location_string(); + let assignment_multiplier = + (footfall_and_urbanization_multiplier(footfall, urbanized) * dec!(1000)) + .to_u32() + .unwrap_or(0); boost_results.entry(hex.uuid).or_default().push( proto::OracleBoostingHexAssignment { location, urbanized: urbanized.into(), + footfall: footfall.into(), assignment_multiplier, }, ); - Ok((hex, urbanized)) + Ok((hex, urbanized, footfall)) }) .collect(); - QueryBuilder::new("INSERT INTO hexes (uuid, hex, signal_level, signal_power, urbanized)") - .push_values(hexes?, |mut b, (hex, urbanized)| { - b.push_bind(hex.uuid) - .push_bind(hex.hex as i64) - .push_bind(hex.signal_level) - .push_bind(hex.signal_power) - .push_bind(urbanized); - }) - .push( - r#" - ON CONFLICT (uuid, hex) DO UPDATE SET - urbanized = EXCLUDED.urbanized - "#, - ) - .build() - .execute(pool) - .await?; + QueryBuilder::new( + "INSERT INTO hexes (uuid, hex, signal_level, signal_power, urbanized, footfall)", + ) + .push_values(hexes?, |mut b, (hex, urbanized, footfall)| { + b.push_bind(hex.uuid) + .push_bind(hex.hex as i64) + .push_bind(hex.signal_level) + .push_bind(hex.signal_power) + .push_bind(urbanized) + .push_bind(footfall); + }) + .push( + r#" + ON CONFLICT (uuid, hex) DO UPDATE SET + urbanized = EXCLUDED.urbanized, + footfall = EXCLUDED.footfall + "#, + ) + .build() + .execute(pool) + .await?; } - Ok(boost_results - .into_iter() - .map( - move |(coverage_object, assignments)| proto::OracleBoostingReportV1 { - coverage_object: Vec::from(coverage_object.into_bytes()), - assignments, - timestamp: now.encode_timestamp(), - }, - )) + Ok(boost_results) } -impl ManagedTask for CoverageDaemon +impl ManagedTask for CoverageDaemon where - DT: DiskTreeLike, - GF: GeofenceValidator, + Urban: HexAssignment + 'static, + Foot: HexAssignment + 'static, { fn start_task( self: Box, @@ -422,6 +452,7 @@ pub struct HexCoverage { pub coverage_claim_time: DateTime, pub inserted_at: DateTime, pub urbanized: Assignment, + pub footfall: Assignment, } #[derive(Eq, Debug)] @@ -431,6 +462,7 @@ struct IndoorCoverageLevel { hotspot: PublicKeyBinary, signal_level: SignalLevel, urbanized: Assignment, + footfall: Assignment, } impl PartialEq for IndoorCoverageLevel { @@ -469,6 +501,7 @@ struct OutdoorCoverageLevel { signal_power: i32, signal_level: SignalLevel, urbanized: Assignment, + footfall: Assignment, } impl PartialEq for OutdoorCoverageLevel { @@ -507,11 +540,44 @@ impl OutdoorCoverageLevel { #[derive(PartialEq, Debug)] pub struct CoverageReward { pub radio_key: OwnedKeyType, - pub points: Decimal, + 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 urbanized: Assignment, + pub footfall: Assignment, + pub rank: Option, +} + +impl CoverageRewardPoints { + pub fn points(&self) -> Decimal { + let oracle_multiplier = if self.boost_multiplier.get() > 1 { + dec!(1.0) + } else { + footfall_and_urbanization_multiplier(self.footfall, self.urbanized) + }; + + 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>( @@ -570,7 +636,7 @@ impl CoveredHexStream for Pool { Ok( sqlx::query_as( r#" - SELECT co.uuid, h.hex, co.indoor, co.radio_key, h.signal_level, h.signal_power, co.coverage_claim_time, co.inserted_at, h.urbanized + SELECT co.uuid, h.hex, co.indoor, co.radio_key, h.signal_level, h.signal_power, co.coverage_claim_time, co.inserted_at, h.urbanized, h.footfall FROM coverage_objects co INNER JOIN hexes h on co.uuid = h.uuid WHERE co.radio_key = $1 @@ -698,7 +764,7 @@ impl CoveredHexes { .chain(outdoor_wifi_rewards) .chain(indoor_cbrs_rewards) .chain(indoor_wifi_rewards) - .filter(|r| r.points > Decimal::ZERO) + .filter(CoverageReward::has_rewards) } } @@ -718,6 +784,7 @@ fn insert_indoor_coverage( signal_level: hex_coverage.signal_level, hotspot: hotspot.clone(), urbanized: hex_coverage.urbanized, + footfall: hex_coverage.footfall, }) } @@ -736,6 +803,7 @@ fn insert_outdoor_coverage( signal_power: hex_coverage.signal_power, hotspot: hotspot.clone(), urbanized: hex_coverage.urbanized, + footfall: hex_coverage.footfall, }); } @@ -751,20 +819,18 @@ fn into_outdoor_rewards( .take(MAX_OUTDOOR_RADIOS_PER_RES12_HEX) .zip(OUTDOOR_REWARD_WEIGHTS) .map(move |(cl, rank)| { - let (boost_multiplier, oracle_multiplier) = boosted_hexes + let boost_multiplier = boosted_hexes .get_current_multiplier(hex.into(), epoch_start) - .map_or_else( - || { - ( - NonZeroU32::new(1).unwrap(), - urbanization_multiplier(cl.urbanized), - ) - }, - |multiplier| (multiplier, dec!(1.0)), - ); + .unwrap_or(NonZeroU32::new(1).unwrap()); CoverageReward { - points: cl.coverage_points() * oracle_multiplier * rank, + points: CoverageRewardPoints { + boost_multiplier, + coverage_points: cl.coverage_points(), + urbanized: cl.urbanized, + footfall: cl.footfall, + rank: Some(rank), + }, hotspot: cl.hotspot, radio_key: cl.radio_key, boosted_hex_info: BoostedHex { @@ -790,20 +856,18 @@ fn into_indoor_rewards( .into_iter() .take(MAX_INDOOR_RADIOS_PER_RES12_HEX) .map(move |cl| { - let (boost_multiplier, oracle_multiplier) = boosted_hexes + let boost_multiplier = boosted_hexes .get_current_multiplier(hex.into(), epoch_start) - .map_or_else( - || { - ( - NonZeroU32::new(1).unwrap(), - urbanization_multiplier(cl.urbanized), - ) - }, - |multiplier| (multiplier, dec!(1.0)), - ); + .unwrap_or(NonZeroU32::new(1).unwrap()); CoverageReward { - points: cl.coverage_points() * oracle_multiplier, + points: CoverageRewardPoints { + boost_multiplier, + coverage_points: cl.coverage_points(), + urbanized: cl.urbanized, + footfall: cl.footfall, + rank: None, + }, hotspot: cl.hotspot, radio_key: cl.radio_key, boosted_hex_info: BoostedHex { @@ -1010,7 +1074,13 @@ mod test { vec![CoverageReward { radio_key: OwnedKeyType::Cbrs("3".to_string()), hotspot: owner, - points: dec!(400), + points: CoverageRewardPoints { + coverage_points: dec!(400), + boost_multiplier: NonZeroU32::new(1).unwrap(), + urbanized: Assignment::A, + footfall: Assignment::A, + rank: None + }, boosted_hex_info: BoostedHex { location: 0x8a1fb46622dffff_u64, multiplier: NonZeroU32::new(1).unwrap(), @@ -1043,6 +1113,7 @@ mod test { coverage_claim_time, inserted_at: DateTime::::MIN_UTC, urbanized: Assignment::A, + footfall: Assignment::A, } } @@ -1121,7 +1192,13 @@ mod test { vec![CoverageReward { radio_key: OwnedKeyType::Cbrs("10".to_string()), hotspot: owner.clone(), - points: dec!(400), + points: CoverageRewardPoints { + coverage_points: dec!(400), + boost_multiplier: NonZeroU32::new(1).unwrap(), + urbanized: Assignment::A, + footfall: Assignment::A, + rank: None + }, boosted_hex_info: BoostedHex { location: 0x8a1fb46622dffff_u64, multiplier: NonZeroU32::new(1).unwrap(), @@ -1159,7 +1236,13 @@ mod test { CoverageReward { radio_key: OwnedKeyType::Cbrs("5".to_string()), hotspot: owner.clone(), - points: dec!(16), + points: CoverageRewardPoints { + coverage_points: dec!(16), + rank: Some(dec!(1.0)), + boost_multiplier: NonZeroU32::new(1).unwrap(), + urbanized: Assignment::A, + footfall: Assignment::A + }, boosted_hex_info: BoostedHex { location: 0x8a1fb46622dffff_u64, multiplier: NonZeroU32::new(1).unwrap(), @@ -1168,7 +1251,13 @@ mod test { CoverageReward { radio_key: OwnedKeyType::Cbrs("4".to_string()), hotspot: owner.clone(), - points: dec!(8), + points: CoverageRewardPoints { + coverage_points: dec!(16), + rank: Some(dec!(0.50)), + boost_multiplier: NonZeroU32::new(1).unwrap(), + urbanized: Assignment::A, + footfall: Assignment::A + }, boosted_hex_info: BoostedHex { location: 0x8a1fb46622dffff_u64, multiplier: NonZeroU32::new(1).unwrap(), @@ -1177,7 +1266,13 @@ mod test { CoverageReward { radio_key: OwnedKeyType::Cbrs("3".to_string()), hotspot: owner, - points: dec!(4), + points: CoverageRewardPoints { + coverage_points: dec!(16), + rank: Some(dec!(0.25)), + boost_multiplier: NonZeroU32::new(1).unwrap(), + urbanized: Assignment::A, + footfall: Assignment::A + }, boosted_hex_info: BoostedHex { location: 0x8a1fb46622dffff_u64, multiplier: NonZeroU32::new(1).unwrap(), @@ -1325,6 +1420,7 @@ mod test { .find(|r| r.radio_key == OwnedKeyType::Cbrs("oco1-3".to_string())) .unwrap() .points + .points() ); assert_eq!( @@ -1334,6 +1430,7 @@ mod test { .find(|r| r.radio_key == OwnedKeyType::Cbrs("oco1-4".to_string())) .unwrap() .points + .points() ); assert_eq!( @@ -1343,6 +1440,7 @@ mod test { .find(|r| r.radio_key == OwnedKeyType::Cbrs("oco1-1".to_string())) .unwrap() .points + .points() ); assert_eq!( @@ -1360,6 +1458,7 @@ mod test { .find(|r| r.radio_key == OwnedKeyType::Cbrs("ico1-1".to_string())) .unwrap() .points + .points() ); assert_eq!( @@ -1377,6 +1476,7 @@ mod test { .find(|r| r.radio_key == OwnedKeyType::Wifi(outdoor_wifi_owner3.clone())) .unwrap() .points + .points() ); assert_eq!( @@ -1386,6 +1486,7 @@ mod test { .find(|r| r.radio_key == OwnedKeyType::Wifi(outdoor_wifi_owner4.clone())) .unwrap() .points + .points() ); assert_eq!( @@ -1395,6 +1496,7 @@ mod test { .find(|r| r.radio_key == OwnedKeyType::Wifi(outdoor_wifi_owner1.clone())) .unwrap() .points + .points() ); assert_eq!( @@ -1412,6 +1514,7 @@ mod test { .find(|r| r.radio_key == OwnedKeyType::Wifi(indoor_wifi_owner1.clone())) .unwrap() .points + .points() ); assert_eq!( @@ -1440,6 +1543,7 @@ mod test { coverage_claim_time: coverage_claim_time.unwrap_or(DateTime::::MIN_UTC), inserted_at: DateTime::::MIN_UTC, urbanized: Assignment::A, + footfall: Assignment::A, } } @@ -1458,6 +1562,7 @@ mod test { coverage_claim_time, inserted_at: DateTime::::MIN_UTC, urbanized: Assignment::A, + footfall: Assignment::A, } } @@ -1476,6 +1581,7 @@ mod test { coverage_claim_time, inserted_at: DateTime::::MIN_UTC, urbanized: Assignment::A, + footfall: Assignment::A, } } @@ -1494,6 +1600,7 @@ mod test { coverage_claim_time, inserted_at: DateTime::::MIN_UTC, urbanized: Assignment::A, + footfall: Assignment::A, } } } diff --git a/mobile_verifier/src/geofence.rs b/mobile_verifier/src/geofence.rs index 1d878518d..7b6559e12 100644 --- a/mobile_verifier/src/geofence.rs +++ b/mobile_verifier/src/geofence.rs @@ -9,8 +9,8 @@ pub trait GeofenceValidator: Clone + Send + Sync + 'static { fn in_valid_region(&self, t: &T) -> bool; } -impl GeofenceValidator for HashSet { - fn in_valid_region(&self, cell: &u64) -> bool { +impl GeofenceValidator for HashSet { + fn in_valid_region(&self, cell: &hextree::Cell) -> bool { self.contains(cell) } } @@ -42,12 +42,9 @@ impl GeofenceValidator for Geofence { } } -impl GeofenceValidator for Geofence { - fn in_valid_region(&self, cell: &u64) -> bool { - let Ok(cell) = Cell::try_from(*cell) else { - return false; - }; - self.regions.contains(cell) +impl GeofenceValidator for Geofence { + fn in_valid_region(&self, cell: &hextree::Cell) -> bool { + self.regions.contains(*cell) } } diff --git a/mobile_verifier/src/reward_shares.rs b/mobile_verifier/src/reward_shares.rs index eebf697d0..b152682f1 100644 --- a/mobile_verifier/src/reward_shares.rs +++ b/mobile_verifier/src/reward_shares.rs @@ -1,5 +1,5 @@ use crate::{ - coverage::{CoverageReward, CoveredHexStream, CoveredHexes}, + coverage::{CoverageReward, CoverageRewardPoints, CoveredHexStream, CoveredHexes}, data_session::{HotspotMap, ServiceProviderDataSession}, heartbeats::{HeartbeatReward, OwnedKeyType}, radio_threshold::VerifiedRadioThresholds, @@ -375,15 +375,20 @@ 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, - points: Decimal, - // list of all hexes that have been boosted for this hotspot along with the multiplier for each hex + // Boosted hexes are included in CoverageRewardPointsWithMultiplier, // this gets included in the radio reward share proto - boosted_hexes: Vec, + reward_points: Vec, } impl RadioPoints { @@ -396,13 +401,24 @@ impl RadioPoints { location_trust_score_multiplier, seniority, coverage_object, - points: Decimal::ZERO, - boosted_hexes: vec![], + reward_points: vec![], } } - fn points(&self) -> Decimal { - (self.location_trust_score_multiplier * self.points).max(Decimal::ZERO) + 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() } } @@ -421,7 +437,7 @@ impl HotspotPoints { &mut self, radio_key: OwnedKeyType, hotspot: PublicKeyBinary, - points: Decimal, + points: CoverageRewardPoints, boosted_hex_info: BoostedHex, verified_radio_thresholds: &VerifiedRadioThresholds, ) { @@ -441,8 +457,11 @@ impl HotspotPoints { } else { boosted_hex_info }; - rp.points += points * Decimal::from(final_boost_info.multiplier.get()); - rp.boosted_hexes.push(final_boost_info); + + rp.reward_points.push(CoverageRewardPointsWithMultiplier { + coverage_points: points, + boosted_hex: final_boost_info, + }); } } @@ -461,7 +480,7 @@ impl HotspotPoints { * self .radio_points .values() - .fold(Decimal::ZERO, |sum, radio| sum + radio.points()) + .fold(Decimal::ZERO, |sum, radio| sum + radio.coverage_points()) } } @@ -609,10 +628,7 @@ fn new_radio_reward( speedtest_multiplier: Decimal, radio_points: RadioPoints, ) -> (u64, proto::MobileRewardShare) { - let poc_reward = poc_rewards_per_share - * speedtest_multiplier - * radio_points.location_trust_score_multiplier - * radio_points.points; + 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 @@ -620,12 +636,12 @@ fn new_radio_reward( .to_u64() .unwrap_or(0); let boosted_hexes = radio_points - .boosted_hexes + .reward_points .iter() - .filter(|boosted_hex| boosted_hex.multiplier > NonZeroU32::new(1).unwrap()) - .map(|boosted_hex| proto::BoostedHex { - location: boosted_hex.location, - multiplier: boosted_hex.multiplier.get(), + .filter(|radio_points| radio_points.boosted_hex.multiplier > NonZeroU32::new(1).unwrap()) + .map(|radio_points| proto::BoostedHex { + location: radio_points.boosted_hex.location, + multiplier: radio_points.boosted_hex.multiplier.get(), }) .collect(); ( @@ -638,7 +654,7 @@ fn new_radio_reward( hotspot_key, cbsd_id, poc_reward, - coverage_points: radio_points.points.to_u64().unwrap_or(0), + 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 @@ -989,6 +1005,7 @@ mod test { coverage_claim_time: DateTime::::MIN_UTC, inserted_at: DateTime::::MIN_UTC, urbanized: Assignment::A, + footfall: Assignment::C, }] } @@ -1869,8 +1886,19 @@ mod test { location_trust_score_multiplier: dec!(1.0), seniority: DateTime::default(), coverage_object: Uuid::new_v4(), - points: dec!(10.0), - boosted_hexes: vec![], + reward_points: vec![CoverageRewardPointsWithMultiplier { + coverage_points: CoverageRewardPoints { + boost_multiplier: NonZeroU32::new(1).unwrap(), + coverage_points: dec!(10.0), + urbanized: Assignment::A, + footfall: Assignment::A, + rank: None, + }, + boosted_hex: BoostedHex { + location: 0, + multiplier: NonZeroU32::new(1).unwrap(), + }, + }], }, )] .into_iter() @@ -1888,18 +1916,16 @@ mod test { location_trust_score_multiplier: dec!(1.0), seniority: DateTime::default(), coverage_object: Uuid::new_v4(), - points: dec!(-1.0), - boosted_hexes: vec![], + reward_points: vec![], }, ), ( Some(c3), RadioPoints { location_trust_score_multiplier: dec!(1.0), - points: dec!(0.0), + reward_points: vec![], seniority: DateTime::default(), coverage_object: Uuid::new_v4(), - boosted_hexes: vec![], }, ), ] diff --git a/mobile_verifier/src/settings.rs b/mobile_verifier/src/settings.rs index b645836b2..7f0698fef 100644 --- a/mobile_verifier/src/settings.rs +++ b/mobile_verifier/src/settings.rs @@ -43,6 +43,7 @@ pub struct Settings { #[serde(default = "default_fencing_resolution")] pub usa_fencing_resolution: u8, pub urbanization_data_set: PathBuf, + pub footfall_data_set: PathBuf, } fn default_fencing_resolution() -> u8 { diff --git a/mobile_verifier/tests/boosting_oracles.rs b/mobile_verifier/tests/boosting_oracles.rs index 3b1abd365..5687b53cb 100644 --- a/mobile_verifier/tests/boosting_oracles.rs +++ b/mobile_verifier/tests/boosting_oracles.rs @@ -6,10 +6,12 @@ use file_store::{ }; use futures::stream::{self, StreamExt}; use helium_crypto::PublicKeyBinary; -use helium_proto::services::poc_mobile::{CoverageObjectValidity, SignalLevel}; +use helium_proto::services::poc_mobile::{ + CoverageObjectValidity, OracleBoostingHexAssignment, SignalLevel, +}; use mobile_config::boosted_hex_info::BoostedHexes; use mobile_verifier::{ - boosting_oracles::Urbanization, + boosting_oracles::{Assignment, HexBoostData, UrbanizationData}, coverage::{ set_oracle_boosting_assignments, CoverageClaimTimeCache, CoverageObject, CoverageObjectCache, Seniority, UnassignedHex, @@ -115,14 +117,200 @@ fn signal_level(hex: &str, signal_level: SignalLevel) -> anyhow::Result hextree::Cell { + hextree::Cell::from_raw(u64::from_str_radix(loc, 16).unwrap()).unwrap() +} + #[sqlx::test] -async fn test_urbanization(pool: PgPool) -> anyhow::Result<()> { +async fn test_footfall_and_urbanization_report(pool: PgPool) -> anyhow::Result<()> { + let uuid = Uuid::new_v4(); + let cbsd_id = "P27-SCE4255W120200039521XGB0102".to_string(); + + let hex1 = OracleBoostingHexAssignment { + location: "8c2681a3064d9ff".to_string(), + assignment_multiplier: 1000, + urbanized: Assignment::A.into(), + footfall: Assignment::A.into(), + }; + let hex2 = OracleBoostingHexAssignment { + location: "8c2681a3064d1ff".to_string(), + assignment_multiplier: 1000, + urbanized: Assignment::B.into(), + footfall: Assignment::A.into(), + }; + let hex3 = OracleBoostingHexAssignment { + location: "8c450e64dc899ff".to_string(), + assignment_multiplier: 0, + urbanized: Assignment::C.into(), + footfall: Assignment::A.into(), + }; + let hex4 = OracleBoostingHexAssignment { + location: "8c2681a3064dbff".to_string(), + assignment_multiplier: 750, + urbanized: Assignment::A.into(), + footfall: Assignment::B.into(), + }; + let hex5 = OracleBoostingHexAssignment { + location: "8c2681a339365ff".to_string(), + assignment_multiplier: 500, + urbanized: Assignment::B.into(), + footfall: Assignment::B.into(), + }; + let hex6 = OracleBoostingHexAssignment { + location: "8c450e64dc89dff".to_string(), + assignment_multiplier: 0, + urbanized: Assignment::C.into(), + footfall: Assignment::B.into(), + }; + let hex7 = OracleBoostingHexAssignment { + location: "8c2681a3066b3ff".to_string(), + assignment_multiplier: 400, + urbanized: Assignment::A.into(), + footfall: Assignment::C.into(), + }; + let hex8 = OracleBoostingHexAssignment { + location: "8c2681a3066b7ff".to_string(), + assignment_multiplier: 100, + urbanized: Assignment::B.into(), + footfall: Assignment::C.into(), + }; + let hex9 = OracleBoostingHexAssignment { + location: "8c450e64dc883ff".to_string(), + assignment_multiplier: 0, + urbanized: Assignment::C.into(), + footfall: Assignment::C.into(), + }; + + let coverage_object = file_store::coverage::CoverageObject { + pub_key: PublicKeyBinary::from(vec![1]), + uuid, + key_type: file_store::coverage::KeyType::CbsdId(cbsd_id.clone()), + coverage_claim_time: "2022-01-01 00:00:00.000000000 UTC".parse()?, + indoor: true, + signature: Vec::new(), + coverage: vec![ + signal_level(&hex1.location, SignalLevel::High)?, + signal_level(&hex2.location, SignalLevel::High)?, + signal_level(&hex3.location, SignalLevel::High)?, + signal_level(&hex4.location, SignalLevel::High)?, + signal_level(&hex5.location, SignalLevel::High)?, + signal_level(&hex6.location, SignalLevel::High)?, + signal_level(&hex7.location, SignalLevel::High)?, + signal_level(&hex8.location, SignalLevel::High)?, + signal_level(&hex9.location, SignalLevel::High)?, + ], + trust_score: 1000, + }; + + let mut footfall = HashMap::new(); + footfall.insert(hex_cell(&hex1.location), true); + footfall.insert(hex_cell(&hex2.location), true); + footfall.insert(hex_cell(&hex3.location), true); + footfall.insert(hex_cell(&hex4.location), false); + footfall.insert(hex_cell(&hex5.location), false); + footfall.insert(hex_cell(&hex6.location), false); + + let mut urbanized = HashSet::new(); + urbanized.insert(hex_cell(&hex1.location)); + urbanized.insert(hex_cell(&hex4.location)); + urbanized.insert(hex_cell(&hex7.location)); + + let mut geofence = HashSet::new(); + geofence.insert(hex_cell(&hex1.location)); + geofence.insert(hex_cell(&hex2.location)); + geofence.insert(hex_cell(&hex4.location)); + geofence.insert(hex_cell(&hex5.location)); + geofence.insert(hex_cell(&hex7.location)); + geofence.insert(hex_cell(&hex8.location)); + + let mut transaction = pool.begin().await?; + CoverageObject { + coverage_object, + validity: CoverageObjectValidity::Valid, + } + .save(&mut transaction) + .await?; + transaction.commit().await?; + + let unassigned_hexes = UnassignedHex::fetch(&pool); + let urbanization = UrbanizationData::new(urbanized, geofence); + let hex_boost_data = HexBoostData::new(urbanization, footfall); + let oba = set_oracle_boosting_assignments(unassigned_hexes, &hex_boost_data, &pool) + .await? + .collect::>(); + + assert_eq!(oba.len(), 1); + assert_eq!( + oba[0].assignments, + vec![hex1, hex2, hex3, hex4, hex5, hex6, hex7, hex8, hex9] + ); + + Ok(()) +} + +#[sqlx::test] +async fn test_footfall_and_urbanization(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()?; let uuid = Uuid::new_v4(); let cbsd_id = "P27-SCE4255W120200039521XGB0102".to_string(); let owner: PublicKeyBinary = "11xtYwQYnvkFYnJ9iZ8kmnetYKwhdi87Mcr36e1pVLrhBMPLjV9".parse()?; + let hex1 = OracleBoostingHexAssignment { + location: "8c2681a3064d9ff".to_string(), + assignment_multiplier: 1000, + urbanized: Assignment::A.into(), + footfall: Assignment::A.into(), + }; + let hex2 = OracleBoostingHexAssignment { + location: "8c2681a3064d1ff".to_string(), + assignment_multiplier: 1000, + urbanized: Assignment::B.into(), + footfall: Assignment::A.into(), + }; + let hex3 = OracleBoostingHexAssignment { + location: "8c450e64dc899ff".to_string(), + assignment_multiplier: 0, + urbanized: Assignment::C.into(), + footfall: Assignment::A.into(), + }; + let hex4 = OracleBoostingHexAssignment { + location: "8c2681a3064dbff".to_string(), + assignment_multiplier: 750, + urbanized: Assignment::A.into(), + footfall: Assignment::B.into(), + }; + let hex5 = OracleBoostingHexAssignment { + location: "8c2681a339365ff".to_string(), + assignment_multiplier: 500, + urbanized: Assignment::B.into(), + footfall: Assignment::B.into(), + }; + let hex6 = OracleBoostingHexAssignment { + location: "8c450e64dc89dff".to_string(), + assignment_multiplier: 0, + urbanized: Assignment::C.into(), + footfall: Assignment::B.into(), + }; + let hex7 = OracleBoostingHexAssignment { + location: "8c2681a3066b3ff".to_string(), + assignment_multiplier: 400, + urbanized: Assignment::A.into(), + footfall: Assignment::C.into(), + }; + let hex8 = OracleBoostingHexAssignment { + location: "8c2681a3066b7ff".to_string(), + assignment_multiplier: 100, + urbanized: Assignment::B.into(), + footfall: Assignment::C.into(), + }; + let hex9 = OracleBoostingHexAssignment { + location: "8c450e64dc883ff".to_string(), + assignment_multiplier: 0, + urbanized: Assignment::C.into(), + footfall: Assignment::C.into(), + }; + let coverage_object = file_store::coverage::CoverageObject { pub_key: PublicKeyBinary::from(vec![1]), uuid, @@ -131,28 +319,39 @@ async fn test_urbanization(pool: PgPool) -> anyhow::Result<()> { indoor: true, signature: Vec::new(), coverage: vec![ - // Urbanized, not boosted - signal_level("8c2681a3064d9ff", SignalLevel::High)?, - // Not urbanized, not boosted - signal_level("8c2681a339a4bff", SignalLevel::High)?, - // Outside the US, not boosted - signal_level("8c2681a3066e7ff", SignalLevel::High)?, + signal_level(&hex1.location, SignalLevel::High)?, + signal_level(&hex2.location, SignalLevel::High)?, + signal_level(&hex3.location, SignalLevel::High)?, + signal_level(&hex4.location, SignalLevel::High)?, + signal_level(&hex5.location, SignalLevel::High)?, + signal_level(&hex6.location, SignalLevel::High)?, + signal_level(&hex7.location, SignalLevel::High)?, + signal_level(&hex8.location, SignalLevel::High)?, + signal_level(&hex9.location, SignalLevel::High)?, ], trust_score: 1000, }; - let epoch = start..end; - // Only the first is urbanized - let mut urbanized = HashMap::>::new(); - urbanized.insert( - hextree::Cell::from_raw(u64::from_str_radix("8c2681a3064d9ff", 16)?)?, - vec![], - ); + let mut footfall = HashMap::new(); + footfall.insert(hex_cell(&hex1.location), true); + footfall.insert(hex_cell(&hex2.location), true); + footfall.insert(hex_cell(&hex3.location), true); + footfall.insert(hex_cell(&hex4.location), false); + footfall.insert(hex_cell(&hex5.location), false); + footfall.insert(hex_cell(&hex6.location), false); + + let mut urbanized = HashSet::new(); + urbanized.insert(hex_cell(&hex1.location)); + urbanized.insert(hex_cell(&hex4.location)); + urbanized.insert(hex_cell(&hex7.location)); - // The last hex is outside the US let mut geofence = HashSet::new(); - geofence.insert(u64::from_str_radix("8c2681a3064d9ff", 16)?); - geofence.insert(u64::from_str_radix("8c2681a339a4bff", 16)?); + geofence.insert(hex_cell(&hex1.location)); + geofence.insert(hex_cell(&hex2.location)); + geofence.insert(hex_cell(&hex4.location)); + geofence.insert(hex_cell(&hex5.location)); + geofence.insert(hex_cell(&hex7.location)); + geofence.insert(hex_cell(&hex8.location)); let mut transaction = pool.begin().await?; CoverageObject { @@ -163,15 +362,17 @@ async fn test_urbanization(pool: PgPool) -> anyhow::Result<()> { .await?; transaction.commit().await?; - let urbanization = Urbanization::new(urbanized, geofence); let unassigned_hexes = UnassignedHex::fetch(&pool); - let _ = set_oracle_boosting_assignments(unassigned_hexes, &urbanization, &pool).await?; + let urbanization = UrbanizationData::new(urbanized, geofence); + let hex_boost_data = HexBoostData::new(urbanization, footfall); + let _ = set_oracle_boosting_assignments(unassigned_hexes, &hex_boost_data, &pool).await?; let heartbeats = heartbeats(12, start, &owner, &cbsd_id, 0.0, 0.0, uuid); let coverage_objects = CoverageObjectCache::new(&pool); let coverage_claim_time_cache = CoverageClaimTimeCache::new(); + let epoch = start..end; let mut heartbeats = pin!(ValidatedHeartbeat::validate_heartbeats( &AllOwnersValid, stream::iter(heartbeats.map(Heartbeat::from)), @@ -223,15 +424,21 @@ async fn test_urbanization(pool: PgPool) -> anyhow::Result<()> { ) .await?; - // Hex | Points Equation | Sum - // ----------------------------- - // hex1 | 400 | 400 - // hex3 | 400 * 0.25 | 100 - // hex5 | 400 * 0.00 | 0 - // ----------------------------- - // = 500 + // Hex | Assignment | Points Equation | Sum + // -------------------------------------------- + // hex1 | A, A | 400 * 1 | 400 + // hex2 | A, B | 400 * 1 | 400 + // hex3 | B, A | 400 * 0.75 | 300 + // hex4 | B, B | 400 * 0.50 | 200 + // hex5 | C, A | 400 * 0.40 | 160 + // hex6 | C, B | 400 * 0.10 | 40 + // hex7 | A, C | 400 * 0.00 | 0 + // hex8 | B, C | 400 * 0.00 | 0 + // hex9 | C, C | 400 * 0.00 | 0 + // ------------------------------------------- + // = 1,500 - assert_eq!(coverage_points.hotspot_points(&owner), dec!(500)); + assert_eq!(coverage_points.hotspot_points(&owner), dec!(1500)); Ok(()) } diff --git a/mobile_verifier/tests/common/mod.rs b/mobile_verifier/tests/common/mod.rs index a6e92b421..c3ae9b8f6 100644 --- a/mobile_verifier/tests/common/mod.rs +++ b/mobile_verifier/tests/common/mod.rs @@ -7,6 +7,7 @@ use helium_proto::{ Message, }; use mobile_config::boosted_hex_info::BoostedHexInfo; +use mobile_verifier::boosting_oracles::{Assignment, HexAssignment, HexBoostData}; use std::collections::HashMap; use tokio::{sync::mpsc::error::TryRecvError, time::timeout}; @@ -153,6 +154,7 @@ impl MockFileSinkReceiver { } } +#[allow(dead_code)] pub fn create_file_sink() -> (FileSinkClient, MockFileSinkReceiver) { let (tx, rx) = tokio::sync::mpsc::channel(20); ( @@ -167,3 +169,15 @@ pub fn create_file_sink() -> (FileSinkClient, MockFileSinkReceiver) { pub fn seconds(s: u64) -> std::time::Duration { std::time::Duration::from_secs(s) } + +pub struct MockHexAssignments; + +#[allow(dead_code)] +impl MockHexAssignments { + pub fn best() -> HexBoostData { + HexBoostData { + urbanization: Assignment::A, + footfall: Assignment::A, + } + } +} diff --git a/mobile_verifier/tests/hex_boosting.rs b/mobile_verifier/tests/hex_boosting.rs index b0fa5b517..e57708ef6 100644 --- a/mobile_verifier/tests/hex_boosting.rs +++ b/mobile_verifier/tests/hex_boosting.rs @@ -18,10 +18,8 @@ use mobile_config::{ client::{hex_boosting_client::HexBoostingInfoResolver, ClientError}, }; use mobile_verifier::{ - boosting_oracles::{MockDiskTree, Urbanization}, cell_type::CellType, coverage::{set_oracle_boosting_assignments, CoverageObject, UnassignedHex}, - geofence::GeofenceValidator, heartbeats::{HbType, Heartbeat, ValidatedHeartbeat}, radio_threshold, reward_shares, rewarder, speedtests, }; @@ -62,19 +60,14 @@ impl HexBoostingInfoResolver for MockHexBoostingClient { } } -#[derive(Clone)] -struct MockGeofence; - -impl GeofenceValidator for MockGeofence { - fn in_valid_region(&self, _cell: &u64) -> bool { - true - } -} - async fn update_assignments(pool: &PgPool) -> anyhow::Result<()> { - let urbanization = Urbanization::new(MockDiskTree, MockGeofence); let unassigned_hexes = UnassignedHex::fetch(pool); - let _ = set_oracle_boosting_assignments(unassigned_hexes, &urbanization, pool).await?; + let _ = set_oracle_boosting_assignments( + unassigned_hexes, + &common::MockHexAssignments::best(), + pool, + ) + .await?; Ok(()) } diff --git a/mobile_verifier/tests/modeled_coverage.rs b/mobile_verifier/tests/modeled_coverage.rs index eef3ef7b5..84cbe6563 100644 --- a/mobile_verifier/tests/modeled_coverage.rs +++ b/mobile_verifier/tests/modeled_coverage.rs @@ -1,3 +1,4 @@ +mod common; use chrono::{DateTime, Duration, Utc}; use file_store::{ coverage::{CoverageObjectIngestReport, RadioHexSignalLevel}, @@ -14,7 +15,6 @@ use helium_proto::services::{ use mobile_config::boosted_hex_info::{BoostedHexInfo, BoostedHexes}; use mobile_verifier::{ - boosting_oracles::{MockDiskTree, Urbanization}, coverage::{ set_oracle_boosting_assignments, CoverageClaimTimeCache, CoverageObject, CoverageObjectCache, Seniority, UnassignedHex, @@ -42,8 +42,8 @@ impl GeofenceValidator for MockGeofence { } } -impl GeofenceValidator for MockGeofence { - fn in_valid_region(&self, _cell: &u64) -> bool { +impl GeofenceValidator for MockGeofence { + fn in_valid_region(&self, _cell: &hextree::Cell) -> bool { true } } @@ -409,9 +409,13 @@ async fn process_input( } transaction.commit().await?; - let urbanization = Urbanization::new(MockDiskTree, MockGeofence); let unassigned_hexes = UnassignedHex::fetch(pool); - let _ = set_oracle_boosting_assignments(unassigned_hexes, &urbanization, pool).await?; + let _ = set_oracle_boosting_assignments( + unassigned_hexes, + &common::MockHexAssignments::best(), + pool, + ) + .await?; let mut transaction = pool.begin().await?; let mut heartbeats = pin!(ValidatedHeartbeat::validate_heartbeats( diff --git a/mobile_verifier/tests/rewarder_poc_dc.rs b/mobile_verifier/tests/rewarder_poc_dc.rs index c6b15ac32..25092cc82 100644 --- a/mobile_verifier/tests/rewarder_poc_dc.rs +++ b/mobile_verifier/tests/rewarder_poc_dc.rs @@ -17,7 +17,6 @@ use mobile_config::{ client::{hex_boosting_client::HexBoostingInfoResolver, ClientError}, }; use mobile_verifier::{ - boosting_oracles::{MockDiskTree, Urbanization}, cell_type::CellType, coverage::{set_oracle_boosting_assignments, CoverageObject, UnassignedHex}, data_session, @@ -60,8 +59,8 @@ impl HexBoostingInfoResolver for MockHexBoostingClient { #[derive(Clone)] struct MockGeofence; -impl GeofenceValidator for MockGeofence { - fn in_valid_region(&self, _cell: &u64) -> bool { +impl GeofenceValidator for MockGeofence { + fn in_valid_region(&self, _cell: &hextree::Cell) -> bool { true } } @@ -305,9 +304,13 @@ async fn seed_heartbeats( } async fn update_assignments(pool: &PgPool) -> anyhow::Result<()> { - let urbanization = Urbanization::new(MockDiskTree, MockGeofence); let unassigned_hexes = UnassignedHex::fetch(pool); - let _ = set_oracle_boosting_assignments(unassigned_hexes, &urbanization, pool).await?; + let _ = set_oracle_boosting_assignments( + unassigned_hexes, + &common::MockHexAssignments::best(), + pool, + ) + .await?; Ok(()) }