diff --git a/Cargo.lock b/Cargo.lock index a06e8f3ae..2500d18e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1617,17 +1617,17 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "beacon" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#376765fe006051d6dcccf709def58e7ed291b845" +source = "git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type#dcf951fe0c8d9d58aef29dc128f9176e9276dfb3" dependencies = [ "base64 0.21.7", "byteorder", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "prost", "rand 0.8.5", "rand_chacha 0.3.0", "rust_decimal", "serde", - "sha2 0.10.8", + "sha2 0.9.9", "thiserror", ] @@ -1776,7 +1776,7 @@ dependencies = [ "futures", "futures-util", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "http 0.2.11", "http-serde", "humantime-serde", @@ -2618,7 +2618,7 @@ dependencies = [ "axum 0.7.4", "bs58 0.4.0", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "http 0.2.11", "notify", "serde", @@ -3200,7 +3200,7 @@ dependencies = [ "futures-util", "h3o", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "hex-literal", "http 0.2.11", "lazy_static", @@ -3776,7 +3776,7 @@ dependencies = [ "h3o", "helium-anchor-gen", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=master)", "hex", "itertools", "jsonrpc_client", @@ -3802,6 +3802,18 @@ dependencies = [ name = "helium-proto" version = "0.1.0" source = "git+https://github.com/helium/proto?branch=master#376765fe006051d6dcccf709def58e7ed291b845" +dependencies = [ + "bytes", + "prost", + "prost-build", + "serde", + "serde_json", +] + +[[package]] +name = "helium-proto" +version = "0.1.0" +source = "git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type#dcf951fe0c8d9d58aef29dc128f9176e9276dfb3" dependencies = [ "bytes", "prost", @@ -3853,7 +3865,7 @@ dependencies = [ "async-trait", "chrono", "derive_builder", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "hextree", "rust_decimal", "rust_decimal_macros", @@ -4269,7 +4281,7 @@ dependencies = [ "futures", "futures-util", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "http 0.2.11", "humantime-serde", "metrics", @@ -4338,7 +4350,7 @@ dependencies = [ "futures", "futures-util", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "hextree", "http 0.2.11", "http-serde", @@ -4380,7 +4392,7 @@ dependencies = [ "futures", "futures-util", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "http 0.2.11", "http-serde", "humantime-serde", @@ -4422,7 +4434,7 @@ dependencies = [ "futures-util", "h3o", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "http-serde", "humantime-serde", "iot-config", @@ -5010,7 +5022,7 @@ dependencies = [ "futures", "futures-util", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "hextree", "http 0.2.11", "http-serde", @@ -5050,7 +5062,7 @@ dependencies = [ "futures", "h3o", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "mobile-config", "prost", "rand 0.8.5", @@ -5086,7 +5098,7 @@ dependencies = [ "futures", "futures-util", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "http 0.2.11", "http-serde", "humantime-serde", @@ -5130,7 +5142,7 @@ dependencies = [ "futures-util", "h3o", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "hex-assignments", "hextree", "http-serde", @@ -5813,7 +5825,7 @@ dependencies = [ "futures", "futures-util", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "http 0.2.11", "hyper 0.14.28", "jsonrpsee", @@ -5896,7 +5908,7 @@ dependencies = [ "futures-util", "helium-anchor-gen", "helium-lib", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "humantime-serde", "metrics", "metrics-exporter-prometheus", @@ -6503,7 +6515,7 @@ dependencies = [ "futures", "futures-util", "helium-crypto", - "helium-proto", + "helium-proto 0.1.0 (git+https://github.com/helium/proto?branch=mj/hip-109-boost-by-device-type)", "humantime-serde", "lazy_static", "metrics", @@ -9851,7 +9863,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.9.9", "thiserror", "twox-hash", "xorf", diff --git a/Cargo.toml b/Cargo.toml index a45355a99..475d5bc6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,10 +70,10 @@ helium-lib = { git = "https://github.com/helium/helium-wallet-rs.git", branch = hextree = { git = "https://github.com/jaykickliter/HexTree", branch = "main", features = [ "disktree", ] } -helium-proto = { git = "https://github.com/helium/proto", branch = "master", features = [ +helium-proto = { git = "https://github.com/helium/proto", branch = "mj/hip-109-boost-by-device-type", features = [ "services", ] } -beacon = { git = "https://github.com/helium/proto", branch = "master" } +beacon = { git = "https://github.com/helium/proto", branch = "mj/hip-109-boost-by-device-type" } solana-client = "1.18" solana-sdk = "1.18" solana-program = "1.18" @@ -126,10 +126,10 @@ sqlx = { git = "https://github.com/helium/sqlx.git", rev = "92a2268f02e0cac6fccb # When attempting to test proto changes without needing to push a branch you can # patch the github url to point to your local proto repo. -# +# # Patching for beacon must point directly to the crate, it will not look in the # repo for sibling crates. -# +# # [patch.'https://github.com/helium/proto'] # helium-proto = { path = "../proto" } # beacon = { path = "../proto/beacon" } diff --git a/boost_manager/src/activator.rs b/boost_manager/src/activator.rs index 796a772d4..9d7759fee 100644 --- a/boost_manager/src/activator.rs +++ b/boost_manager/src/activator.rs @@ -95,8 +95,7 @@ where txn: &mut Transaction<'_, Postgres>, manifest: RewardManifest, ) -> Result<()> { - // get latest boosted hexes info from mobile config - let boosted_hexes = BoostedHexes::get_all(&self.hex_boosting_client).await?; + let boosted_hexes = BoostedHexes::get_active(&self.hex_boosting_client).await?; // get the rewards file from the manifest let manifest_time = manifest.end_timestamp; @@ -114,9 +113,9 @@ where while let Some(msg) = reward_shares.try_next().await? { let share = MobileRewardShare::decode(msg)?; if let Some(MobileReward::RadioReward(r)) = share.reward { - for hex in r.boosted_hexes.into_iter() { - process_boosted_hex(txn, manifest_time, &boosted_hexes, &hex.try_into()?) - .await? + for hex_proto in r.boosted_hexes.into_iter() { + let boosted_hex = hex_proto.try_into()?; + process_boosted_hex(txn, manifest_time, &boosted_hexes, &boosted_hex).await? } } } @@ -130,17 +129,19 @@ pub async fn process_boosted_hex( boosted_hexes: &BoostedHexes, hex: &BoostedHex, ) -> Result<()> { - match boosted_hexes.hexes.get(&hex.location) { - Some(info) => { - if info.start_ts.is_none() { - db::insert_activated_hex( - txn, - hex.location.into_raw(), - &info.boosted_hex_pubkey.to_string(), - &info.boost_config_pubkey.to_string(), - manifest_time, - ) - .await?; + match boosted_hexes.get(&hex.location) { + Some(hexes) => { + for info in hexes { + if info.start_ts.is_none() { + db::insert_activated_hex( + txn, + hex.location.into_raw(), + &info.boosted_hex_pubkey.to_string(), + &info.boost_config_pubkey.to_string(), + manifest_time, + ) + .await?; + } } } None => { diff --git a/boost_manager/src/watcher.rs b/boost_manager/src/watcher.rs index c669cb1f2..a54321f0a 100644 --- a/boost_manager/src/watcher.rs +++ b/boost_manager/src/watcher.rs @@ -87,9 +87,9 @@ where tracing::info!( "modified hexes count since {}: {} ", last_processed_ts, - boosted_hexes.hexes.len() + boosted_hexes.count() ); - for info in boosted_hexes.hexes.values() { + for info in boosted_hexes.iter_hexes() { let proto: BoostedHexUpdateProto = BoostedHexUpdateProto { timestamp: now.encode_timestamp(), update: Some(info.clone().try_into()?), diff --git a/boost_manager/tests/integrations/activator_tests.rs b/boost_manager/tests/integrations/activator_tests.rs index 394df1b64..b59ff4e4d 100644 --- a/boost_manager/tests/integrations/activator_tests.rs +++ b/boost_manager/tests/integrations/activator_tests.rs @@ -1,9 +1,11 @@ use boost_manager::{activator, db, OnChainStatus}; use chrono::{DateTime, Duration as ChronoDuration, Duration, Timelike, Utc}; -use mobile_config::boosted_hex_info::{BoostedHex, BoostedHexInfo, BoostedHexes}; +use mobile_config::boosted_hex_info::{ + BoostedHex, BoostedHexDeviceType, BoostedHexInfo, BoostedHexes, +}; use solana_sdk::pubkey::Pubkey; use sqlx::PgPool; -use std::{collections::HashMap, num::NonZeroU32, str::FromStr}; +use std::{num::NonZeroU32, str::FromStr}; const BOOST_HEX_PUBKEY: &str = "J9JiLTpjaShxL8eMvUs8txVw6TZ36E38SiJ89NxnMbLU"; const BOOST_CONFIG_PUBKEY: &str = "BZM1QTud72B2cpTW7PhEnFmRX7ZWzvY7DpPpNJJuDrWG"; @@ -50,6 +52,7 @@ impl TestContext { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { location: 0x8a1fb49642dffff_u64.try_into().expect("valid h3 cell"), @@ -60,6 +63,7 @@ impl TestContext { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 3's location @@ -71,6 +75,7 @@ impl TestContext { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; Ok(Self { @@ -82,14 +87,7 @@ impl TestContext { async fn test_activated_hex_insert(pool: PgPool) -> anyhow::Result<()> { let now = Utc::now(); let ctx = TestContext::setup(now)?; - let boosted_hexes_map = ctx - .boosted_hexes - .iter() - .map(|info| (info.location, info.clone())) - .collect::>(); - let boosted_hexes = BoostedHexes { - hexes: boosted_hexes_map, - }; + let boosted_hexes = BoostedHexes::test_new_active(ctx.boosted_hexes)?; // test a boosted hex derived from radio rewards // with a non set start date, will result in a row being @@ -119,14 +117,7 @@ async fn test_activated_hex_insert(pool: PgPool) -> anyhow::Result<()> { async fn test_activated_hex_no_insert(pool: PgPool) -> anyhow::Result<()> { let now = Utc::now(); let ctx = TestContext::setup(now)?; - let boosted_hexes_map = ctx - .boosted_hexes - .iter() - .map(|info| (info.location, info.clone())) - .collect::>(); - let boosted_hexes = BoostedHexes { - hexes: boosted_hexes_map, - }; + let boosted_hexes = BoostedHexes::test_new_active(ctx.boosted_hexes)?; // test a boosted hex derived from radio rewards // with an active start date, will result in no row being @@ -152,14 +143,7 @@ async fn test_activated_hex_no_insert(pool: PgPool) -> anyhow::Result<()> { async fn test_activated_dup_hex_insert(pool: PgPool) -> anyhow::Result<()> { let now = Utc::now().with_second(0).unwrap(); let ctx = TestContext::setup(now)?; - let boosted_hexes_map = ctx - .boosted_hexes - .iter() - .map(|info| (info.location, info.clone())) - .collect::>(); - let boosted_hexes = BoostedHexes { - hexes: boosted_hexes_map, - }; + let boosted_hexes = BoostedHexes::test_new_active(ctx.boosted_hexes)?; // test with DUPLICATE boosted hexes derived from radio rewards // with a non set start date, will result in a single row being diff --git a/boost_manager/tests/integrations/watcher_tests.rs b/boost_manager/tests/integrations/watcher_tests.rs index 64f42c0a4..a403efab7 100644 --- a/boost_manager/tests/integrations/watcher_tests.rs +++ b/boost_manager/tests/integrations/watcher_tests.rs @@ -2,7 +2,7 @@ use crate::common::{self, MockFileSinkReceiver, MockHexBoostingClient}; use boost_manager::watcher::{self, Watcher}; use chrono::{Duration as ChronoDuration, Duration, Utc}; use helium_proto::BoostedHexInfoV1 as BoostedHexInfoProto; -use mobile_config::boosted_hex_info::BoostedHexInfo; +use mobile_config::boosted_hex_info::{BoostedHexDeviceType, BoostedHexInfo}; use solana_sdk::pubkey::Pubkey; use sqlx::PgPool; use std::{num::NonZeroU32, str::FromStr}; @@ -45,6 +45,7 @@ async fn test_boosted_hex_updates_to_filestore(pool: PgPool) -> anyhow::Result<( boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { location: 0x8a1fb49642dffff_u64.try_into().expect("valid h3 cell"), @@ -55,6 +56,7 @@ async fn test_boosted_hex_updates_to_filestore(pool: PgPool) -> anyhow::Result<( boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; diff --git a/coverage_map/src/indoor.rs b/coverage_map/src/indoor.rs index 5cbd0ed56..2ba047070 100644 --- a/coverage_map/src/indoor.rs +++ b/coverage_map/src/indoor.rs @@ -7,7 +7,9 @@ use chrono::{DateTime, Utc}; use hex_assignments::assignment::HexAssignments; use hextree::Cell; -use crate::{BoostedHexMap, CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage}; +use crate::{ + BoostedHexMap, CoverageObject, DeviceType, RankedCoverage, SignalLevel, UnrankedCoverage, +}; pub type IndoorCellTree = HashMap>>; @@ -88,10 +90,11 @@ pub fn clone_indoor_coverage_into_submap( pub fn into_indoor_coverage_map( indoor: IndoorCellTree, boosted_hexes: &impl BoostedHexMap, + device_type: DeviceType, epoch_start: DateTime, ) -> impl Iterator + '_ { indoor.into_iter().flat_map(move |(hex, radios)| { - let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); + let boosted = boosted_hexes.get_current_multiplier(hex, device_type, epoch_start); radios .into_values() .flat_map(move |radios| radios.into_sorted_vec().into_iter()) @@ -130,10 +133,14 @@ mod test { { insert_indoor_coverage_object(&mut indoor_coverage, cov_obj); } - let ranked: HashMap<_, _> = - into_indoor_coverage_map(indoor_coverage, &NoBoostedHexes, Utc::now()) - .map(|x| (x.cbsd_id.clone().unwrap(), x)) - .collect(); + let ranked: HashMap<_, _> = into_indoor_coverage_map( + indoor_coverage, + &NoBoostedHexes, + DeviceType::CbrsIndoor, + Utc::now(), + ) + .map(|x| (x.cbsd_id.clone().unwrap(), x)) + .collect(); assert_eq!(ranked.get("3").unwrap().rank, 1); assert!({ let rank = ranked.get("2").unwrap().rank; @@ -172,10 +179,14 @@ mod test { { insert_indoor_coverage_object(&mut indoor_coverage, cov_obj); } - let ranked: HashMap<_, _> = - into_indoor_coverage_map(indoor_coverage, &NoBoostedHexes, Utc::now()) - .map(|x| (x.cbsd_id.clone().unwrap(), x)) - .collect(); + let ranked: HashMap<_, _> = into_indoor_coverage_map( + indoor_coverage, + &NoBoostedHexes, + DeviceType::CbrsIndoor, + Utc::now(), + ) + .map(|x| (x.cbsd_id.clone().unwrap(), x)) + .collect(); assert_eq!(ranked.get("1").unwrap().rank, 9); assert_eq!(ranked.get("2").unwrap().rank, 5); assert_eq!(ranked.get("3").unwrap().rank, 10); @@ -225,8 +236,13 @@ mod test { ), ); - let coverage = into_indoor_coverage_map(indoor_coverage, &NoBoostedHexes, Utc::now()) - .collect::>(); + let coverage = into_indoor_coverage_map( + indoor_coverage, + &NoBoostedHexes, + DeviceType::CbrsIndoor, + Utc::now(), + ) + .collect::>(); // Both coverages should be ranked 1 assert_eq!(coverage[0].rank, 1); assert_eq!(coverage[1].rank, 1); diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index fc4d13618..4b83a1526 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -10,6 +10,14 @@ mod outdoor; use indoor::*; use outdoor::*; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceType { + WifiIndoor, + WifiOutdoor, + CbrsIndoor, + CbrsOutdoor, +} + /// Data structure for keeping track of the ranking the coverage in each hex cell for indoor /// and outdoor CBRS and WiFi radios. #[derive(Clone, Default, Debug)] @@ -80,23 +88,31 @@ impl CoverageMapBuilder { ) -> CoverageMap { let mut wifi_hotspots = HashMap::<_, Vec>::new(); let mut cbrs_radios = HashMap::<_, Vec>::new(); - for coverage in into_indoor_coverage_map(self.indoor_cbrs, boosted_hexes, epoch_start) - .chain(into_indoor_coverage_map( - self.indoor_wifi, - boosted_hexes, - epoch_start, - )) - .chain(into_outdoor_coverage_map( - self.outdoor_cbrs, - boosted_hexes, - epoch_start, - )) - .chain(into_outdoor_coverage_map( - self.outdoor_wifi, - boosted_hexes, - epoch_start, - )) - { + + for coverage in into_indoor_coverage_map( + self.indoor_cbrs, + boosted_hexes, + DeviceType::CbrsIndoor, + epoch_start, + ) + .chain(into_indoor_coverage_map( + self.indoor_wifi, + boosted_hexes, + DeviceType::WifiIndoor, + epoch_start, + )) + .chain(into_outdoor_coverage_map( + self.outdoor_cbrs, + boosted_hexes, + DeviceType::CbrsOutdoor, + epoch_start, + )) + .chain(into_outdoor_coverage_map( + self.outdoor_wifi, + boosted_hexes, + DeviceType::WifiOutdoor, + epoch_start, + )) { if let Some(ref cbsd_id) = coverage.cbsd_id { cbrs_radios .entry(cbsd_id.clone()) @@ -183,7 +199,12 @@ pub enum SignalLevel { } pub trait BoostedHexMap { - fn get_current_multiplier(&self, cell: Cell, ts: DateTime) -> Option; + fn get_current_multiplier( + &self, + cell: Cell, + device_type: DeviceType, + ts: DateTime, + ) -> Option; } #[cfg(test)] @@ -191,7 +212,12 @@ pub(crate) struct NoBoostedHexes; #[cfg(test)] impl BoostedHexMap for NoBoostedHexes { - fn get_current_multiplier(&self, _cell: Cell, _ts: DateTime) -> Option { + fn get_current_multiplier( + &self, + _cell: Cell, + _device_type: DeviceType, + _ts: DateTime, + ) -> Option { None } } diff --git a/coverage_map/src/outdoor.rs b/coverage_map/src/outdoor.rs index 28c3a7e1a..b73b19644 100644 --- a/coverage_map/src/outdoor.rs +++ b/coverage_map/src/outdoor.rs @@ -7,7 +7,9 @@ use chrono::{DateTime, Utc}; use hex_assignments::assignment::HexAssignments; use hextree::Cell; -use crate::{BoostedHexMap, CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage}; +use crate::{ + BoostedHexMap, CoverageObject, DeviceType, RankedCoverage, SignalLevel, UnrankedCoverage, +}; /// Data structure for storing outdoor radios ranked by their coverage level pub type OutdoorCellTree = HashMap>; @@ -96,10 +98,11 @@ pub fn clone_outdoor_coverage_into_submap( pub fn into_outdoor_coverage_map( outdoor: OutdoorCellTree, boosted_hexes: &impl BoostedHexMap, + device_type: DeviceType, epoch_start: DateTime, ) -> impl Iterator + '_ { outdoor.into_iter().flat_map(move |(hex, radios)| { - let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); + let boosted = boosted_hexes.get_current_multiplier(hex, device_type, epoch_start); radios .into_sorted_vec() .into_iter() @@ -138,10 +141,14 @@ mod test { { insert_outdoor_coverage_object(&mut outdoor_coverage, cov_obj); } - let ranked: HashMap<_, _> = - into_outdoor_coverage_map(outdoor_coverage, &NoBoostedHexes, Utc::now()) - .map(|x| (x.cbsd_id.clone().unwrap(), x)) - .collect(); + let ranked: HashMap<_, _> = into_outdoor_coverage_map( + outdoor_coverage, + &NoBoostedHexes, + DeviceType::CbrsOutdoor, + Utc::now(), + ) + .map(|x| (x.cbsd_id.clone().unwrap(), x)) + .collect(); assert_eq!(ranked.get("5").unwrap().rank, 1); assert_eq!(ranked.get("4").unwrap().rank, 2); assert_eq!(ranked.get("3").unwrap().rank, 3); diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index b7027ac92..646e9f97c 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -402,6 +402,7 @@ impl BoostedHexMap for NoBoostedHexes { fn get_current_multiplier( &self, _cell: hextree::Cell, + _device_type: coverage_map::DeviceType, _ts: chrono::DateTime, ) -> Option { None diff --git a/mobile_config/src/boosted_hex_info.rs b/mobile_config/src/boosted_hex_info.rs index cedb34fd9..77076b727 100644 --- a/mobile_config/src/boosted_hex_info.rs +++ b/mobile_config/src/boosted_hex_info.rs @@ -8,6 +8,7 @@ use hextree::Cell; use solana_sdk::pubkey::Pubkey; use std::{collections::HashMap, convert::TryFrom, num::NonZeroU32}; +pub use helium_proto::BoostedHexDeviceTypeV1 as BoostedHexDeviceType; pub type BoostedHexInfoStream = BoxStream<'static, BoostedHexInfo>; lazy_static::lazy_static! { @@ -24,19 +25,22 @@ pub struct BoostedHexInfo { pub boosted_hex_pubkey: Pubkey, pub boost_config_pubkey: Pubkey, pub version: u32, + pub device_type: BoostedHexDeviceType, } impl TryFrom for BoostedHexInfo { type Error = anyhow::Error; + fn try_from(v: BoostedHexInfoProto) -> anyhow::Result { let period_length = Duration::seconds(v.period_length as i64); + let device_type = v.device_type(); let multipliers = v .multipliers .into_iter() .map(NonZeroU32::new) .collect::>>() .ok_or_else(|| anyhow::anyhow!("multipliers cannot contain values of 0"))?; - let start_ts = to_start_ts(v.start_ts); + let start_ts = to_timestamp(v.start_ts); let end_ts = to_end_ts(start_ts, period_length, multipliers.len()); let boosted_hex_pubkey: Pubkey = Pubkey::try_from(v.boosted_hex_pubkey.as_slice())?; let boost_config_pubkey: Pubkey = Pubkey::try_from(v.boost_config_pubkey.as_slice())?; @@ -49,6 +53,7 @@ impl TryFrom for BoostedHexInfo { boosted_hex_pubkey, boost_config_pubkey, version: v.version, + device_type, }) } } @@ -73,16 +78,17 @@ impl TryFrom for BoostedHexInfoProto { boosted_hex_pubkey: v.boosted_hex_pubkey.to_bytes().into(), boost_config_pubkey: v.boost_config_pubkey.to_bytes().into(), version: v.version, + device_type: v.device_type.into(), }) } } impl BoostedHexInfo { - pub fn current_multiplier(&self, ts: DateTime) -> anyhow::Result> { + pub fn current_multiplier(&self, ts: DateTime) -> Option { if self.end_ts.is_some() && ts >= self.end_ts.unwrap() { // end time has been set and the current time is after the end time, so return None // to indicate that the hex is no longer boosted - return Ok(None); + return None; }; if self.start_ts.is_some() { // start time has previously been set, so we can calculate the current multiplier @@ -93,18 +99,27 @@ impl BoostedHexInfo { .num_seconds() .checked_div(self.period_length.num_seconds()) .unwrap_or(0) as usize; - Ok(Some(self.multipliers[index])) + Some(self.multipliers[index]) } else { // start time has not been previously set, assume this is the first time rewarding this hex // and use the first multiplier - Ok(Some(self.multipliers[0])) + Some(self.multipliers[0]) } } + + fn matches_device_type(&self, device_type: &BoostedHexDeviceType) -> bool { + self.device_type == *device_type || self.device_type == BoostedHexDeviceType::All + } } -#[derive(Debug, Clone, Default)] -pub struct BoostedHexes { - pub hexes: HashMap, +fn device_type_from_str(s: &str) -> anyhow::Result { + match s { + "cbrsIndoor" => Ok(BoostedHexDeviceType::CbrsIndoor), + "cbrsOutdoor" => Ok(BoostedHexDeviceType::CbrsOutdoor), + "wifiIndoor" => Ok(BoostedHexDeviceType::WifiIndoor), + "wifiOutdoor" => Ok(BoostedHexDeviceType::WifiOutdoor), + unknown => anyhow::bail!("unknown device type value: {unknown}"), + } } #[derive(PartialEq, Debug, Clone)] @@ -128,27 +143,39 @@ impl TryFrom for BoostedHex { } } +#[derive(Debug, Clone, Default)] +pub struct BoostedHexes { + hexes: HashMap>, +} + impl BoostedHexes { - pub fn new(hexes: Vec) -> Self { - let hexes = hexes - .into_iter() - .map(|info| (info.location, info)) - .collect(); - Self { hexes } + pub fn test_new_active(hexes: Vec) -> anyhow::Result { + let mut me = Self::default(); + for hex in hexes { + if hex.end_ts.is_some_and(|end| end < Utc::now()) { + // mobile-config does not deliver expired boosts from the database. + // Tests using this struct to mimic mobile-config should uphold the + // same contract. + panic!("Active BoostedHexes should not contain expired boosts"); + } + me.insert(hex); + } + Ok(me) } - pub async fn get_all( + pub async fn get_active( hex_service_client: &impl HexBoostingInfoResolver, ) -> anyhow::Result { - let mut map = HashMap::new(); let mut stream = hex_service_client .clone() .stream_boosted_hexes_info() .await?; + + let mut me = Self::default(); while let Some(info) = stream.next().await { - map.insert(info.location, info); + me.insert(info); } - Ok(Self { hexes: map }) + Ok(me) } pub fn is_boosted(&self, location: &Cell) -> bool { @@ -159,72 +186,148 @@ impl BoostedHexes { hex_service_client: &impl HexBoostingInfoResolver, timestamp: DateTime, ) -> anyhow::Result { - let mut map = HashMap::new(); let mut stream = hex_service_client .clone() .stream_modified_boosted_hexes_info(timestamp) .await?; + + let mut me = Self::default(); while let Some(info) = stream.next().await { - map.insert(info.location, info); + me.insert(info); } - Ok(Self { hexes: map }) + Ok(me) + } + + pub fn get_current_multiplier( + &self, + location: Cell, + device_type: BoostedHexDeviceType, + ts: DateTime, + ) -> Option { + let current_multiplier = self + .hexes + .get(&location)? + .iter() + .filter(|info| info.matches_device_type(&device_type)) + .flat_map(|info| info.current_multiplier(ts)) + .map(|non_zero| non_zero.get()) + .sum::(); + + NonZeroU32::new(current_multiplier) + } + + pub fn count(&self) -> usize { + self.hexes.len() + } + + pub fn iter_hexes(&self) -> impl Iterator { + self.hexes.values().flatten() + } + + pub fn get(&self, location: &Cell) -> Option<&Vec> { + self.hexes.get(location) } - pub fn get_current_multiplier(&self, location: Cell, ts: DateTime) -> Option { - self.hexes - .get(&location) - .and_then(|info| info.current_multiplier(ts).ok()?) + fn insert(&mut self, info: BoostedHexInfo) { + self.hexes.entry(info.location).or_default().push(info); } } impl coverage_map::BoostedHexMap for BoostedHexes { - fn get_current_multiplier(&self, location: Cell, ts: DateTime) -> Option { - self.get_current_multiplier(location, ts) + fn get_current_multiplier( + &self, + location: Cell, + device_type: coverage_map::DeviceType, + ts: DateTime, + ) -> Option { + let boosted_device_type = match device_type { + coverage_map::DeviceType::WifiIndoor => BoostedHexDeviceType::WifiIndoor, + coverage_map::DeviceType::WifiOutdoor => BoostedHexDeviceType::WifiOutdoor, + coverage_map::DeviceType::CbrsIndoor => BoostedHexDeviceType::CbrsIndoor, + coverage_map::DeviceType::CbrsOutdoor => BoostedHexDeviceType::CbrsOutdoor, + }; + self.get_current_multiplier(location, boosted_device_type, ts) } } pub(crate) mod db { - use super::{to_end_ts, to_start_ts, BoostedHexInfo}; + + use super::{to_timestamp, BoostedHexInfo}; use chrono::{DateTime, Duration, Utc}; use futures::stream::{Stream, StreamExt}; use hextree::Cell; use solana_sdk::pubkey::Pubkey; use sqlx::{PgExecutor, Row}; use std::num::NonZeroU32; + use std::ops::Deref; use std::str::FromStr; const GET_BOOSTED_HEX_INFO_SQL: &str = r#" - select - CAST(hexes.location as bigint), - CAST(hexes.start_ts as bigint), + WITH boosted_hexes_replacement AS ( + SELECT + h.*, + CASE + WHEN start_ts = 0 THEN 0 + ELSE h.start_ts + (c.period_length * length(h.boosts_by_period)) + END AS end_ts + FROM boost_configs c + INNER JOIN boosted_hexes h ON c.address = h.boost_config + ) + SELECT + CAST(hexes.location as bigint), + CAST(hexes.start_ts as bigint), + CAST(hexes.end_ts as bigint), config.period_length, hexes.boosts_by_period as multipliers, - hexes.address as boosted_hex_pubkey, + hexes.address as boosted_hex_pubkey, config.address as boost_config_pubkey, - hexes.version - from boosted_hexes hexes + hexes.version, + hexes.device_type + from boosted_hexes_replacement hexes join boost_configs config on hexes.boost_config = config.address + WHERE + hexes.start_ts = 0 + OR hexes.end_ts > date_part('epoch', $1) "#; - // TODO: reuse with string above + // NOTE(mj): modified hexes should be returned regardless of expiration status const GET_MODIFIED_BOOSTED_HEX_INFO_SQL: &str = r#" - select - CAST(hexes.location as bigint), - CAST(hexes.start_ts as bigint), + WITH boosted_hexes_replacement AS ( + SELECT + h.*, + CASE + WHEN start_ts = 0 THEN 0 + ELSE h.start_ts + (c.period_length * length(h.boosts_by_period)) + END AS end_ts + FROM boost_configs c + INNER JOIN boosted_hexes h ON c.address = h.boost_config + ) + SELECT + CAST(hexes.location AS bigint), + CAST(hexes.start_ts AS bigint), config.period_length, - hexes.boosts_by_period as multipliers, - hexes.address as boosted_hex_pubkey, - config.address as boost_config_pubkey, - hexes.version - from boosted_hexes hexes - join boost_configs config on hexes.boost_config = config.address - where hexes.refreshed_at > $1 + hexes.boosts_by_period AS multipliers, + hexes.address AS boosted_hex_pubkey, + config.address AS boost_config_pubkey, + hexes.version, + hexes.device_type + FROM boosted_hexes_replacement hexes + JOIN boost_configs config ON hexes.boost_config = config.address + WHERE hexes.refreshed_at > $1 "#; - pub fn all_info_stream<'a>( + pub fn all_info_stream_with_time_now<'a>( db: impl PgExecutor<'a> + 'a, + ) -> impl Stream + 'a { + all_info_stream(db, Utc::now()) + } + + fn all_info_stream<'a>( + db: impl PgExecutor<'a> + 'a, + now: DateTime, ) -> impl Stream + 'a { sqlx::query_as::<_, BoostedHexInfo>(GET_BOOSTED_HEX_INFO_SQL) + .bind(now) .fetch(db) .filter_map(|info| async move { info.ok() }) .boxed() @@ -244,7 +347,7 @@ pub(crate) mod db { impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for BoostedHexInfo { fn from_row(row: &sqlx::postgres::PgRow) -> sqlx::Result { let period_length = Duration::seconds(row.get::("period_length") as i64); - let start_ts = to_start_ts(row.get::("start_ts") as u64); + let start_ts = to_timestamp(row.get::("start_ts") as u64); let multipliers = row .get::, &str>("multipliers") .into_iter() @@ -253,7 +356,7 @@ pub(crate) mod db { .ok_or_else(|| { sqlx::Error::Decode(Box::from("multipliers cannot contain values of 0")) })?; - let end_ts = to_end_ts(start_ts, period_length, multipliers.len()); + let end_ts = to_timestamp(row.get::("end_ts") as u64); let boost_config_pubkey = Pubkey::from_str(row.get::<&str, &str>("boost_config_pubkey")) .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; @@ -264,6 +367,14 @@ pub(crate) mod db { let location = Cell::try_from(row.get::("location")) .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; + type MaybeJsonb = Option>; + let device_type_jsonb = row.get::("device_type"); + let device_type = match device_type_jsonb { + None => super::BoostedHexDeviceType::All, + Some(val) => super::device_type_from_str(val.deref()) + .map_err(|e| sqlx::Error::Decode(e.into()))?, + }; + Ok(Self { location, start_ts, @@ -273,12 +384,13 @@ pub(crate) mod db { boosted_hex_pubkey, boost_config_pubkey, version, + device_type, }) } } } -fn to_start_ts(timestamp: u64) -> Option> { +fn to_timestamp(timestamp: u64) -> Option> { if timestamp == 0 { None } else { @@ -299,11 +411,60 @@ mod tests { use super::*; use chrono::NaiveDateTime; use hextree::Cell; + use sqlx::PgPool; use std::str::FromStr; const BOOST_HEX_PUBKEY: &str = "J9JiLTpjaShxL8eMvUs8txVw6TZ36E38SiJ89NxnMbLU"; const BOOST_HEX_CONFIG_PUBKEY: &str = "BZM1QTud72B2cpTW7PhEnFmRX7ZWzvY7DpPpNJJuDrWG"; + #[test] + fn boosted_hexes_accumulate_multipliers() -> anyhow::Result<()> { + let cell = Cell::from_raw(631252734740306943)?; + let now = Utc::now(); + + let hexes = vec![ + BoostedHexInfo { + location: cell, + start_ts: None, + end_ts: None, + period_length: Duration::seconds(2592000), + multipliers: vec![NonZeroU32::new(2).unwrap()], + boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY)?, + boost_config_pubkey: Pubkey::from_str(BOOST_HEX_CONFIG_PUBKEY)?, + version: 0, + device_type: BoostedHexDeviceType::All, + }, + BoostedHexInfo { + location: cell, + start_ts: None, + end_ts: None, + period_length: Duration::seconds(2592000), + multipliers: vec![NonZeroU32::new(3).unwrap()], + boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY)?, + boost_config_pubkey: Pubkey::from_str(BOOST_HEX_CONFIG_PUBKEY)?, + version: 0, + device_type: BoostedHexDeviceType::CbrsIndoor, + }, + ]; + + let boosted_hexes = BoostedHexes::test_new_active(hexes)?; + let boosts = boosted_hexes.get(&cell).expect("boosts for test cell"); + assert_eq!(boosts.len(), 2, "a hex can be boosted multiple times"); + + assert_eq!( + boosted_hexes.get_current_multiplier(cell, BoostedHexDeviceType::CbrsIndoor, now), + NonZeroU32::new(5), + "Specific boosts stack with ::ALL" + ); + assert_eq!( + boosted_hexes.get_current_multiplier(cell, BoostedHexDeviceType::WifiIndoor, now), + NonZeroU32::new(2), + "Missing boosts still return ::ALL" + ); + + Ok(()) + } + #[test] fn boosted_hex_from_proto_valid_not_started() -> anyhow::Result<()> { let proto = BoostedHexInfoProto { @@ -321,6 +482,7 @@ mod tests { .to_bytes() .to_vec(), version: 1, + device_type: BoostedHexDeviceType::All.into(), }; let msg = BoostedHexInfo::try_from(proto)?; @@ -362,6 +524,7 @@ mod tests { .to_bytes() .to_vec(), version: 1, + device_type: BoostedHexDeviceType::All.into(), }; let msg = BoostedHexInfo::try_from(proto)?; @@ -397,6 +560,7 @@ mod tests { boosted_hex_pubkey: BOOST_HEX_PUBKEY.as_bytes().to_vec(), boost_config_pubkey: BOOST_HEX_CONFIG_PUBKEY.as_bytes().to_vec(), version: 1, + device_type: BoostedHexDeviceType::All.into(), }; assert_eq!( "multipliers cannot contain values of 0", @@ -410,4 +574,222 @@ mod tests { .expect("unable_to_parse") .and_utc() } + + mod metadata_db { + /// Table creation was taken from a dump of the metadata db. + /// `device_type` was added to boosted_hexes as a jsonb field because the + /// mobile_hotspot_infos table has a device_type column that maps to an + /// enum, and it is a nullable jsonb column. + /// + /// When the `boosted_hexes` or `boost_configs` tables from the metadata_db + /// are updated, these tests will not break until the new table formats are + /// put into `create_tables()`. + use super::*; + + #[sqlx::test] + async fn parse_boosted_hex_info_from_database(pool: PgPool) -> anyhow::Result<()> { + let boost_config_address = Pubkey::new_unique(); + let now = Utc::now(); + + create_tables(&pool).await?; + insert_boost_config(&pool, &boost_config_address, now).await?; + + let device_types = vec![ + None, // legacy boosted hex with NULL device_type + Some(serde_json::json!("cbrsIndoor")), + Some(serde_json::json!("cbrsOutdoor")), + Some(serde_json::json!("wifiIndoor")), + Some(serde_json::json!("wifiOutdoor")), + // Some(serde_json::json!(null)) // this is different from None, and will/should break parsing as it's not part of the enum + ]; + for device_type in device_types { + insert_boosted_hex(&pool, &boost_config_address, now, device_type, Some(now)) + .await?; + } + + assert_eq!( + 5, + boosted_hexes_count(&pool).await?, + "there should be 1 of each type of boosted hex" + ); + assert_eq!( + 5, + streamed_hexes_count(&pool).await?, + "not all rows were able to parse" + ); + + Ok(()) + } + + #[sqlx::test] + async fn filter_expired_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { + let boost_config_address = Pubkey::new_unique(); + let now = Utc::now(); + + create_tables(&pool).await?; + insert_boost_config(&pool, &boost_config_address, now).await?; + + let times = vec![ + None, // unstarted + Some(now), // still boosting + Some(now - Duration::days(400)), // expired + ]; + + for time in times { + insert_boosted_hex( + &pool, + &boost_config_address, + now, + Some(serde_json::json!("cbrsIndoor")), + time, + ) + .await?; + } + + assert_eq!(3, boosted_hexes_count(&pool).await?); + assert_eq!(2, streamed_hexes_count(&pool).await?); + + Ok(()) + } + + async fn create_tables(pool: &PgPool) -> anyhow::Result<()> { + const CREATE_BOOSTED_HEXES_TABLE: &str = r#" + CREATE TABLE + boosted_hexes ( + address character varying(255) NOT NULL PRIMARY KEY, + boost_config character varying(255) NULL, + location numeric NULL, + start_ts numeric NULL, + reserved numeric[] NULL, + bump_seed integer NULL, + boosts_by_period bytea NULL, + version integer NULL, + refreshed_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + device_type jsonb + ) + "#; + + const CREATE_BOOST_CONFIG_TABLE: &str = r#" + CREATE TABLE + boost_configs ( + address character varying(255) NOT NULL PRIMARY KEY, + price_oracle character varying(255) NULL, + payment_mint character varying(255) NULL, + sub_dao character varying(255) NULL, + rent_reclaim_authority character varying(255) NULL, + boost_price numeric NULL, + period_length integer NULL, + minimum_periods integer NULL, + bump_seed integer NULL, + start_authority character varying(255) NULL, + refreshed_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL + ) + "#; + + sqlx::query(CREATE_BOOSTED_HEXES_TABLE) + .execute(pool) + .await?; + sqlx::query(CREATE_BOOST_CONFIG_TABLE).execute(pool).await?; + Ok(()) + } + + async fn insert_boost_config( + pool: &PgPool, + boost_config_address: &Pubkey, + created_at: DateTime, + ) -> anyhow::Result<()> { + const INSERT_BOOST_CONFIG: &str = r#" + INSERT INTO boost_configs ( + "boost_price", "bump_seed", "minimum_periods", "period_length", "refreshed_at", + + -- pubkeys + "price_oracle", + "payment_mint", + "rent_reclaim_authority", + "start_authority", + "sub_dao", + + -- our values + "address", + "created_at" + ) + VALUES ( + '5000', 250, 6, 2592000, '2024-03-12 21:13:52.692+00', + + $1, $2, $3, $4, $5, $6, $7 + ) + "#; + + sqlx::query(INSERT_BOOST_CONFIG) + .bind(Pubkey::new_unique().to_string()) // price_oracle + .bind(Pubkey::new_unique().to_string()) // payment_mint + .bind(Pubkey::new_unique().to_string()) // rent_reclaim_authority + .bind(Pubkey::new_unique().to_string()) // start_authority + .bind(Pubkey::new_unique().to_string()) // sub_dao + // -- + .bind(boost_config_address.to_string()) // address + .bind(created_at) // created_at + .execute(pool) + .await?; + + Ok(()) + } + + async fn insert_boosted_hex( + pool: &PgPool, + boost_config_address: &Pubkey, + created_at: DateTime, + device_type: Option, + start_ts: Option>, + ) -> anyhow::Result<()> { + const INSERT_BOOSTED_HEX: &str = r#" + INSERT INTO boosted_hexes ( + "boosts_by_period", "bump_seed", "location", "refreshed_at", "reserved", "version", + + -- our values + "address", + "boost_config", + "created_at", + "device_type", + "start_ts" + ) + VALUES ( + 'ZGRkZGRk', 1, '631798453297853439', '2024-03-12 21:13:53.773+00', '{0,0,0,0,0,0,0,0}', 1, + + $1, $2, $3, $4, $5 + ) + "#; + + sqlx::query(INSERT_BOOSTED_HEX) + .bind(Pubkey::new_unique().to_string()) // address + .bind(boost_config_address.to_string()) // boost_config + .bind(created_at) // created_at + .bind(device_type) // device_type + .bind(start_ts.map(|t| t.timestamp()).unwrap_or_default()) // start_ts + .execute(pool) + .await?; + + Ok(()) + } + + async fn boosted_hexes_count(pool: &PgPool) -> anyhow::Result { + let count: i64 = sqlx::query_scalar("select count(*) from boosted_hexes") + .fetch_one(pool) + .await?; + Ok(count) + } + + async fn streamed_hexes_count(pool: &PgPool) -> anyhow::Result { + // If a row cannot be parsed, it is dropped from the stream with no erros. + let mut infos = super::db::all_info_stream_with_time_now(pool); + let mut count = 0; + while let Some(_info) = infos.next().await { + // println!("info: {_info:?}"); + count += 1; + } + Ok(count) + } + } } diff --git a/mobile_config/src/hex_boosting_service.rs b/mobile_config/src/hex_boosting_service.rs index 08ed50f1c..82b6fc22d 100644 --- a/mobile_config/src/hex_boosting_service.rs +++ b/mobile_config/src/hex_boosting_service.rs @@ -71,7 +71,7 @@ impl mobile_config::HexBoosting for HexBoostingService { let (tx, rx) = tokio::sync::mpsc::channel(100); tokio::spawn(async move { - let stream = boosted_hex_info::db::all_info_stream(&pool); + let stream = boosted_hex_info::db::all_info_stream_with_time_now(&pool); stream_multi_info(stream, tx.clone(), signing_key.clone(), batch_size).await }); diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index 76cb12313..4fcd61e93 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -424,7 +424,7 @@ async fn reward_poc( speedtest_averages.write_all(speedtest_avg_sink).await?; - let boosted_hexes = BoostedHexes::get_all(hex_service_client).await?; + let boosted_hexes = BoostedHexes::get_active(hex_service_client).await?; let boosted_hex_eligibility = BoostedHexEligibility::new( radio_threshold::verified_radio_thresholds(pool, reward_period).await?, diff --git a/mobile_verifier/tests/integrations/hex_boosting.rs b/mobile_verifier/tests/integrations/hex_boosting.rs index ae11f4363..a72fb18db 100644 --- a/mobile_verifier/tests/integrations/hex_boosting.rs +++ b/mobile_verifier/tests/integrations/hex_boosting.rs @@ -14,7 +14,7 @@ use helium_proto::services::{ }, }; use hextree::Cell; -use mobile_config::boosted_hex_info::BoostedHexInfo; +use mobile_config::boosted_hex_info::{BoostedHexDeviceType, BoostedHexInfo}; use mobile_verifier::{ cell_type::CellType, coverage::CoverageObject, @@ -25,7 +25,7 @@ use rust_decimal::prelude::*; use rust_decimal_macros::dec; use solana_sdk::pubkey::Pubkey; use sqlx::{PgPool, Postgres, Transaction}; -use std::{num::NonZeroU32, str::FromStr}; +use std::{collections::HashMap, num::NonZeroU32, str::FromStr}; use uuid::Uuid; const HOTSPOT_1: &str = "112E7TxoNHV46M6tiPA8N1MkeMeQxc9ztb4JQLXBVAAUfq1kJLoF"; @@ -106,6 +106,7 @@ async fn test_poc_with_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 2's location @@ -117,6 +118,7 @@ async fn test_poc_with_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 3's location @@ -128,6 +130,7 @@ async fn test_poc_with_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; @@ -294,6 +297,7 @@ async fn test_poc_boosted_hexes_thresholds_not_met(pool: PgPool) -> anyhow::Resu boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 2's location @@ -305,6 +309,7 @@ async fn test_poc_boosted_hexes_thresholds_not_met(pool: PgPool) -> anyhow::Resu boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 3's location @@ -316,6 +321,7 @@ async fn test_poc_boosted_hexes_thresholds_not_met(pool: PgPool) -> anyhow::Resu boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; @@ -442,6 +448,7 @@ async fn test_poc_with_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Res boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 1's second covered location @@ -453,6 +460,7 @@ async fn test_poc_with_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Res boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 2's location @@ -464,6 +472,7 @@ async fn test_poc_with_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Res boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 3's location @@ -475,6 +484,7 @@ async fn test_poc_with_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Res boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; @@ -643,6 +653,7 @@ async fn test_expired_boosted_hex(pool: PgPool) -> anyhow::Result<()> { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { location: Cell::from_raw(0x8a1fb49642dffff_u64)?, @@ -653,6 +664,7 @@ async fn test_expired_boosted_hex(pool: PgPool) -> anyhow::Result<()> { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; @@ -771,6 +783,7 @@ async fn test_reduced_location_score_with_boosted_hexes(pool: PgPool) -> anyhow: boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 3's location @@ -782,6 +795,7 @@ async fn test_reduced_location_score_with_boosted_hexes(pool: PgPool) -> anyhow: boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; @@ -951,6 +965,7 @@ async fn test_distance_from_asserted_removes_boosting_but_not_location_trust( boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 3's location @@ -962,6 +977,7 @@ async fn test_distance_from_asserted_removes_boosting_but_not_location_trust( boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; @@ -1134,6 +1150,7 @@ async fn test_poc_with_cbrs_and_multi_coverage_boosted_hexes(pool: PgPool) -> an boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 1's second covered location @@ -1145,6 +1162,7 @@ async fn test_poc_with_cbrs_and_multi_coverage_boosted_hexes(pool: PgPool) -> an boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 2's location @@ -1156,6 +1174,7 @@ async fn test_poc_with_cbrs_and_multi_coverage_boosted_hexes(pool: PgPool) -> an boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, BoostedHexInfo { // hotspot 3's location @@ -1167,6 +1186,7 @@ async fn test_poc_with_cbrs_and_multi_coverage_boosted_hexes(pool: PgPool) -> an boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, ]; @@ -1291,6 +1311,256 @@ fn rounded(num: Decimal) -> u64 { num.to_u64().unwrap_or_default() } +#[sqlx::test] +async fn test_poc_boosted_hex_stack_multiplier(pool: PgPool) -> anyhow::Result<()> { + // This test seeds the database with 2 identically performing radios on + // neighboring hexes. Both are eligible for boosted rewards. + // + // In the first pass, no hexes are boosted, to ensure the radios are equal. + // In the second pass, 1 radio is boosted with 2 boosts. + // - legacy boost, BoostType::All + // - specific boost, BoostType::CbrsIndoor + // Rewards are checked to ensure the 2 boosts accumulate correctly. + + let (mobile_rewards_client, mut mobile_rewards) = common::create_file_sink(); + let (speedtest_avg_client, _speedtest_avg_server) = common::create_file_sink(); + + let now = Utc::now(); + let epoch = (now - ChronoDuration::hours(24))..now; + + let boosted_pubkey = PublicKeyBinary::from_str(HOTSPOT_1)?; + let unboosted_pubkey = PublicKeyBinary::from_str(HOTSPOT_2)?; + + let boosted_location = Cell::from_raw(0x8a1fb466d2dffff)?; + let unboosted_location = Cell::from_raw(0x8a1fb46622d7fff)?; + + let boostable_radios = vec![ + HexBoostableRadio::cbrs_indoor(boosted_pubkey, boosted_location), + HexBoostableRadio::cbrs_indoor(unboosted_pubkey, unboosted_location), + ]; + + let mut txn = pool.begin().await?; + for radio in boostable_radios { + radio.seed(epoch.start, &mut txn).await?; + } + txn.commit().await?; + update_assignments(&pool).await?; + + // First pass with no boosted hexes, radios perform the same + { + let hex_boosting_client = MockHexBoostingClient::new(vec![]); + let (_, poc_rewards) = tokio::join!( + rewarder::reward_poc_and_dc( + &pool, + &hex_boosting_client, + &mobile_rewards_client, + &speedtest_avg_client, + &epoch, + dec!(0.0001) + ), + async { + let one = mobile_rewards.receive_radio_reward().await; + let two = mobile_rewards.receive_radio_reward().await; + + mobile_rewards.assert_no_messages(); + + [one, two] + }, + ); + + let [radio_one, radio_two] = &poc_rewards; + + assert_eq!( + radio_one.total_poc_reward(), + radio_two.total_poc_reward(), + "radios earn equally unboosted" + ); + + // Make sure there were no unallocated rewards + let total = poc_rewards + .iter() + .map(|r| r.total_poc_reward()) + .sum::(); + assert_eq!( + reward_shares::get_scheduled_tokens_for_poc(epoch.end - epoch.start) + .to_u64() + .unwrap(), + total, + "allocated equals scheduled output" + ); + } + + // Second pass with boosted hex, first radio will be setup to receive 10x boost + { + let base = BoostedHexInfo { + location: boosted_location, + start_ts: None, + end_ts: None, + period_length: Duration::days(30), + multipliers: vec![], + boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY)?, + boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY)?, + version: 0, + device_type: BoostedHexDeviceType::All, + }; + + let boosted_hexes = vec![ + BoostedHexInfo { + multipliers: vec![NonZeroU32::new(3).unwrap()], + device_type: BoostedHexDeviceType::All, + ..base.clone() + }, + BoostedHexInfo { + multipliers: vec![NonZeroU32::new(7).unwrap()], + device_type: BoostedHexDeviceType::CbrsIndoor, + ..base.clone() + }, + ]; + + let hex_boosting_client = MockHexBoostingClient::new(boosted_hexes); + let (_, mut poc_rewards) = tokio::join!( + rewarder::reward_poc_and_dc( + &pool, + &hex_boosting_client, + &mobile_rewards_client, + &speedtest_avg_client, + &epoch, + dec!(0.0001) + ), + async move { + let one = mobile_rewards.receive_radio_reward().await; + let two = mobile_rewards.receive_radio_reward().await; + + mobile_rewards.assert_no_messages(); + + [one, two] + }, + ); + + // sort by rewards ascending + poc_rewards.sort_by_key(|r| r.total_poc_reward()); + let [unboosted, boosted] = &poc_rewards; + + let boosted_hexes = boosted.boosted_hexes(); + let boosted_hex = boosted_hexes.first().expect("boosted hex"); + assert_eq!(10, boosted_hex.boosted_multiplier); + + assert_eq!( + 10, + boosted.total_coverage_points() / unboosted.total_coverage_points(), + "boosted radio should have 10x coverage_points" + ); + + // We expect a single unallocated + let total = poc_rewards + .iter() + .map(|r| r.total_poc_reward()) + .sum::(); + let allocated = reward_shares::get_scheduled_tokens_for_poc(epoch.end - epoch.start) + .to_u64() + .unwrap(); + assert_eq!(allocated - 1, total, "allocated equals scheduled output"); + } + + Ok(()) +} + +#[sqlx::test] +async fn test_poc_boosted_hex_only_applies_to_device_type(pool: PgPool) -> anyhow::Result<()> { + // This tests ensures that device specific device types don't affect other device types. + // There are 2 hexes: + // - #1 BoostType::CbrsOutdoor at 5x + // - #2 No Boosts + // + // There are 4 radios that all qualify for hex boosting: + // - CbrsOutdoor in boosted hex + // - Cbrsoutdoor in unboosted hex + // - WifiOutdoor in boosted hex + // - WifiOutdoor in unboosted hex + // + // The boosted cbrs radio should have 5x the rewards of the unboosted cbrs radio. + // The wifi radios should have the same rewards. + // + + let (mobile_rewards_client, mut mobile_rewards) = common::create_file_sink(); + let (speedtest_avg_client, _speedtest_avg_server) = common::create_file_sink(); + + let now = Utc::now(); + let epoch = (now - ChronoDuration::hours(24))..now; + + let boosted_cbrs_pubkey = PublicKeyBinary::from_str(HOTSPOT_1)?; + let unboosted_cbrs_pubkey = PublicKeyBinary::from_str(HOTSPOT_2)?; + let wifi_pubkey1 = PublicKeyBinary::from_str(HOTSPOT_3)?; + let wifi_pubkey2 = PublicKeyBinary::from_str(HOTSPOT_4)?; + + let boosted_location = Cell::from_raw(0x8a1fb466d2dffff)?; + let unboosted_location = Cell::from_raw(0x8a1fb46622d7fff)?; + + let boostable_radios = vec![ + HexBoostableRadio::cbrs_outdoor(boosted_cbrs_pubkey.clone(), boosted_location), + HexBoostableRadio::cbrs_outdoor(unboosted_cbrs_pubkey.clone(), unboosted_location), + HexBoostableRadio::wifi_outdoor(wifi_pubkey1.clone(), boosted_location), + HexBoostableRadio::wifi_outdoor(wifi_pubkey2.clone(), unboosted_location), + ]; + + let mut txn = pool.begin().await?; + for radio in boostable_radios { + radio.seed(epoch.start, &mut txn).await?; + } + txn.commit().await?; + update_assignments(&pool).await?; + + let boosted_hex = BoostedHexInfo { + location: boosted_location, + start_ts: None, + end_ts: None, + period_length: Duration::days(30), + multipliers: vec![NonZeroU32::new(5).unwrap()], + boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY)?, + boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY)?, + version: 0, + device_type: BoostedHexDeviceType::CbrsOutdoor, + }; + + let hex_boosting_client = MockHexBoostingClient::new(vec![boosted_hex]); + let (_, poc_rewards_map) = tokio::join!( + rewarder::reward_poc_and_dc( + &pool, + &hex_boosting_client, + &mobile_rewards_client, + &speedtest_avg_client, + &epoch, + dec!(0.0001) + ), + async move { + let one = mobile_rewards.receive_radio_reward().await; + let two = mobile_rewards.receive_radio_reward().await; + let three = mobile_rewards.receive_radio_reward().await; + let four = mobile_rewards.receive_radio_reward().await; + + mobile_rewards.assert_no_messages(); + + vec![one, two, three, four] + .into_iter() + .map(|r| (r.hotspot_key.clone(), r)) + .collect::>() + } + ); + + let boosted_cbrs = poc_rewards_map.get(boosted_cbrs_pubkey.as_ref()).unwrap(); + let unboosted_cbrs = poc_rewards_map.get(unboosted_cbrs_pubkey.as_ref()).unwrap(); + let wifi1 = poc_rewards_map.get(wifi_pubkey1.as_ref()).unwrap(); + let wifi2 = poc_rewards_map.get(wifi_pubkey2.as_ref()).unwrap(); + + assert_eq!( + 5, + boosted_cbrs.total_coverage_points() / unboosted_cbrs.total_coverage_points(), + ); + assert_eq!(wifi1.total_coverage_points(), wifi2.total_coverage_points()); + + Ok(()) +} + async fn receive_expected_rewards( mobile_rewards: &mut MockFileSinkReceiver, ) -> anyhow::Result<(Vec, UnallocatedReward)> { @@ -1903,3 +2173,121 @@ fn get_poc_allocation_buckets(epoch_duration: Duration) -> (Decimal, Decimal) { (regular_poc, boosted_poc) } + +/// Hex Boostable Radio Checklist: +/// - Coverage Object: 1 +/// - Seniority Update: 1 +/// - Heartbeats: 12 +/// - Speedtests: 2 +/// - Radio Threshold: 1 +struct HexBoostableRadio { + pubkey: PublicKeyBinary, + cbsd_id: Option, + hb_type: HbType, + is_indoor: bool, + location: Cell, + heartbeat_count: i64, +} + +impl HexBoostableRadio { + fn cbrs_indoor(pubkey: PublicKeyBinary, location: Cell) -> Self { + Self::new(pubkey, BoostedHexDeviceType::CbrsIndoor, location) + } + + fn cbrs_outdoor(pubkey: PublicKeyBinary, location: Cell) -> Self { + Self::new(pubkey, BoostedHexDeviceType::CbrsOutdoor, location) + } + + fn wifi_outdoor(pubkey: PublicKeyBinary, location: Cell) -> Self { + Self::new(pubkey, BoostedHexDeviceType::WifiOutdoor, location) + } + + fn new(pubkey: PublicKeyBinary, boost_type: BoostedHexDeviceType, location: Cell) -> Self { + let (hb_type, is_indoor, cbsd_id) = match boost_type { + BoostedHexDeviceType::All => panic!("a radio cannot be all types at once"), + BoostedHexDeviceType::CbrsIndoor => { + (HbType::Cbrs, true, Some(format!("cbsd-indoor-{pubkey}"))) + } + BoostedHexDeviceType::CbrsOutdoor => { + (HbType::Cbrs, false, Some(format!("cbsd-outdoor-{pubkey}"))) + } + BoostedHexDeviceType::WifiIndoor => (HbType::Wifi, true, None), + BoostedHexDeviceType::WifiOutdoor => (HbType::Wifi, false, None), + }; + Self { + pubkey, + cbsd_id, + hb_type, + is_indoor, + location, + heartbeat_count: 12, + } + } + + async fn seed( + self, + timestamp: DateTime, + txn: &mut Transaction<'_, Postgres>, + ) -> anyhow::Result<()> { + let cov_obj = create_coverage_object( + timestamp, + self.cbsd_id.clone(), + self.pubkey.clone(), + self.location.into_raw(), + self.is_indoor, + ); + for n in 0..=self.heartbeat_count { + let time_ahead = timestamp + ChronoDuration::hours(n); + let time_behind = timestamp - ChronoDuration::hours(24); + + let wifi_heartbeat = ValidatedHeartbeat { + heartbeat: Heartbeat { + hb_type: self.hb_type, + hotspot_key: self.pubkey.clone(), + cbsd_id: self.cbsd_id.clone(), + operation_mode: true, + lat: 0.0, + lon: 0.0, + coverage_object: Some(cov_obj.coverage_object.uuid), + location_validation_timestamp: Some(time_behind), + timestamp: time_ahead, + location_source: LocationSource::Skyhook, + }, + cell_type: CellType::NovaGenericWifiIndoor, + distance_to_asserted: Some(10), + coverage_meta: None, + location_trust_score_multiplier: dec!(1.0), + validity: HeartbeatValidity::Valid, + }; + + let speedtest = CellSpeedtest { + pubkey: self.pubkey.clone(), + serial: format!("serial-{}", self.pubkey), + timestamp: timestamp - ChronoDuration::hours(n * 4), + upload_speed: 100_000_000, + download_speed: 100_000_000, + latency: 49, + }; + + save_seniority_object(time_ahead, &wifi_heartbeat, txn).await?; + wifi_heartbeat.save(txn).await?; + speedtests::save_speedtest(&speedtest, txn).await?; + } + cov_obj.save(txn).await?; + + let report = RadioThresholdIngestReport { + received_timestamp: Default::default(), + report: RadioThresholdReportReq { + hotspot_pubkey: self.pubkey.clone(), + cbsd_id: self.cbsd_id.clone(), + bytes_threshold: 1_000_000, + subscriber_threshold: 3, + threshold_timestamp: timestamp, + carrier_pub_key: CARRIER_HOTSPOT_KEY.parse().unwrap(), + }, + }; + radio_threshold::save(&report, txn).await?; + + Ok(()) + } +} diff --git a/mobile_verifier/tests/integrations/modeled_coverage.rs b/mobile_verifier/tests/integrations/modeled_coverage.rs index bb9c406f9..825523282 100644 --- a/mobile_verifier/tests/integrations/modeled_coverage.rs +++ b/mobile_verifier/tests/integrations/modeled_coverage.rs @@ -13,7 +13,7 @@ use helium_proto::services::{ poc_mobile::{CoverageObjectValidity, LocationSource, SignalLevel}, }; use hextree::Cell; -use mobile_config::boosted_hex_info::{BoostedHexInfo, BoostedHexes}; +use mobile_config::boosted_hex_info::{BoostedHexDeviceType, BoostedHexInfo, BoostedHexes}; use mobile_verifier::{ coverage::{CoverageClaimTimeCache, CoverageObject, CoverageObjectCache}, @@ -840,9 +840,7 @@ async fn scenario_three(pool: PgPool) -> anyhow::Result<()> { averages.insert(owner_6.clone(), SpeedtestAverage::from(speedtests_6)); let speedtest_avgs = SpeedtestAverages { averages }; - let mut boosted_hexes = BoostedHexes::default(); - boosted_hexes.hexes.insert( - Cell::from_raw(0x8a1fb466d2dffff)?, + let boosted_hexes = BoostedHexes::test_new_active(vec![ BoostedHexInfo { location: Cell::from_raw(0x8a1fb466d2dffff)?, start_ts: None, @@ -852,10 +850,8 @@ async fn scenario_three(pool: PgPool) -> anyhow::Result<()> { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, - ); - boosted_hexes.hexes.insert( - Cell::from_raw(0x8a1fb49642dffff)?, BoostedHexInfo { location: Cell::from_raw(0x8a1fb49642dffff)?, start_ts: None, @@ -865,10 +861,8 @@ async fn scenario_three(pool: PgPool) -> anyhow::Result<()> { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, - ); - boosted_hexes.hexes.insert( - Cell::from_raw(0x8c2681a306607ff)?, BoostedHexInfo { // hotspot 1's location location: Cell::from_raw(0x8c2681a306607ff)?, @@ -879,8 +873,9 @@ async fn scenario_three(pool: PgPool) -> anyhow::Result<()> { boosted_hex_pubkey: Pubkey::from_str(BOOST_HEX_PUBKEY).unwrap(), boost_config_pubkey: Pubkey::from_str(BOOST_CONFIG_PUBKEY).unwrap(), version: 0, + device_type: BoostedHexDeviceType::All, }, - ); + ])?; let reward_period = start..end; let heartbeats = HeartbeatReward::validated(&pool, &reward_period);