From 179fdd2f39020a69478a88044ba0c27e0f4c7ff4 Mon Sep 17 00:00:00 2001 From: Brian Balser Date: Wed, 8 Oct 2025 09:46:28 -0400 Subject: [PATCH 1/5] return 10% of mapping rewards to service provider rewards pool --- mobile_verifier/src/reward_shares.rs | 20 ++++++++-------- .../tests/integrations/rewarder_mappers.rs | 21 ++++++++-------- .../tests/integrations/rewarder_sp_rewards.rs | 24 +++++++++---------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/mobile_verifier/src/reward_shares.rs b/mobile_verifier/src/reward_shares.rs index 2319a318b..90cd4678b 100644 --- a/mobile_verifier/src/reward_shares.rs +++ b/mobile_verifier/src/reward_shares.rs @@ -44,10 +44,10 @@ const DC_USD_PRICE: Decimal = dec!(0.00001); pub const DEFAULT_PREC: u32 = 15; /// Percent of total emissions allocated for mapper rewards -const MAPPERS_REWARDS_PERCENT: Decimal = dec!(0.2); +const MAPPERS_REWARDS_PERCENT: Decimal = dec!(0.1); // Percent of total emissions allocated for service provider rewards -const SERVICE_PROVIDER_PERCENT: Decimal = dec!(0.1); +const SERVICE_PROVIDER_PERCENT: Decimal = dec!(0.2); // Percent of total emissions allocated for oracles const ORACLES_PERCENT: Decimal = dec!(0.04); @@ -769,13 +769,13 @@ mod test { #[test] fn test_mappers_scheduled_tokens() { let v = get_scheduled_tokens_for_mappers(dec!(100)); - assert_eq!(dec!(20), v, "mappers get 20%"); + assert_eq!(dec!(10), v, "mappers get 10%"); } #[test] fn test_service_provider_scheduled_tokens() { let v = get_scheduled_tokens_for_service_providers(dec!(100)); - assert_eq!(dec!(10), v, "service providers get 10%"); + assert_eq!(dec!(20), v, "service providers get 20%"); } #[test] @@ -826,7 +826,7 @@ mod test { .round_dp_with_strategy(0, RoundingStrategy::ToZero) .to_u64() .unwrap_or(0); - assert_eq!(16_438_356_164_383, total_mapper_rewards); + assert_eq!(8_219_178_082_191, total_mapper_rewards); let expected_reward_per_subscriber = total_mapper_rewards / NUM_SUBSCRIBERS; @@ -846,20 +846,20 @@ mod test { ); // These are the same because we gave `total_reward_points: 30,` for each // VerifiedMappingEventShares which is the same amount as discovery mapping - assert_eq!(821_917_808, r.discovery_location_amount); - assert_eq!(821_917_808, r.verification_mapping_amount); + assert_eq!(410_958_904, r.discovery_location_amount); + assert_eq!(410_958_904, r.verification_mapping_amount); allocated_mapper_rewards += reward_amount; } } // verify the total rewards awarded for discovery mapping - assert_eq!(16_438_356_160_000, allocated_mapper_rewards); + assert_eq!(8_219_178_080_000, allocated_mapper_rewards); // confirm the unallocated service provider reward amounts // this should not be more than the total number of subscribers ( 10 k) // as we can at max drop one bone per subscriber due to rounding let unallocated_mapper_reward_amount = total_mapper_rewards - allocated_mapper_rewards; - assert_eq!(unallocated_mapper_reward_amount, 4383); + assert_eq!(unallocated_mapper_reward_amount, 2191); assert!(unallocated_mapper_reward_amount < NUM_SUBSCRIBERS); } @@ -1692,7 +1692,7 @@ mod test { .round_dp_with_strategy(0, RoundingStrategy::ToZero) .to_u64() .unwrap_or(0); - assert_eq!(unallocated_sp_reward_amount, 342_465_752_425); + assert_eq!(unallocated_sp_reward_amount, 684_931_505_850); } #[test] diff --git a/mobile_verifier/tests/integrations/rewarder_mappers.rs b/mobile_verifier/tests/integrations/rewarder_mappers.rs index c1b4f63f4..7b461e92d 100644 --- a/mobile_verifier/tests/integrations/rewarder_mappers.rs +++ b/mobile_verifier/tests/integrations/rewarder_mappers.rs @@ -38,21 +38,22 @@ async fn test_mapper_rewards(pool: PgPool) -> anyhow::Result<()> { // subscriber 1 has two qualifying mapping criteria reports, // other two subscribers one qualifying mapping criteria reports let sub_reward_1 = subscriber_rewards.get(SUBSCRIBER_1).expect("sub 1"); - assert_eq!(5_479_452_054_794, sub_reward_1.discovery_location_amount); + assert_eq!(2_739_726_027_397, sub_reward_1.discovery_location_amount); let sub_reward_2 = subscriber_rewards.get(SUBSCRIBER_2).expect("sub 2"); - assert_eq!(5_479_452_054_794, sub_reward_2.discovery_location_amount); + assert_eq!(2_739_726_027_397, sub_reward_2.discovery_location_amount); let sub_reward_3 = subscriber_rewards.get(SUBSCRIBER_3).expect("sub 3"); - assert_eq!(5_479_452_054_794, sub_reward_3.discovery_location_amount); + assert_eq!(2_739_726_027_397, sub_reward_3.discovery_location_amount); // confirm our unallocated amount - let unallocated_reward = rewards.unallocated.first().expect("unallocated"); - assert_eq!( - UnallocatedRewardType::Mapper as i32, - unallocated_reward.reward_type - ); - assert_eq!(1, unallocated_reward.amount); + let unallocated_amount = rewards + .unallocated + .iter() + .find(|r| r.reward_type == UnallocatedRewardType::Mapper as i32) + .map(|r| r.amount) + .unwrap_or(0); + assert_eq!(0, unallocated_amount); // confirm the total rewards allocated matches expectations let expected_sum = reward_shares::get_scheduled_tokens_for_mappers(reward_info.epoch_emissions) @@ -65,7 +66,7 @@ async fn test_mapper_rewards(pool: PgPool) -> anyhow::Result<()> { // confirm the rewarded percentage amount matches expectations let percent = (Decimal::from(subscriber_sum) / reward_info.epoch_emissions) .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); - assert_eq!(percent, dec!(0.2)); + assert_eq!(percent, dec!(0.1)); Ok(()) } diff --git a/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs b/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs index 39829cf75..e140f5f05 100644 --- a/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs +++ b/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs @@ -96,14 +96,14 @@ async fn test_service_provider_rewards(pool: PgPool) -> anyhow::Result<()> { let rewards = mobile_rewards.finish().await?; let sp_reward = rewards.sp_rewards.first().expect("sp 1 reward"); - assert_eq!(5_999, sp_reward.amount); + assert_eq!(6_000, sp_reward.amount); let unallocated_reward = rewards.unallocated.first().expect("unallocated"); assert_eq!( UnallocatedRewardType::ServiceProvider as i32, unallocated_reward.reward_type ); - assert_eq!(8_219_178_076_192, unallocated_reward.amount); + assert_eq!(16_438_356_158_383, unallocated_reward.amount); // confirm the total rewards allocated matches expectations let expected_sum = @@ -115,7 +115,7 @@ async fn test_service_provider_rewards(pool: PgPool) -> anyhow::Result<()> { // confirm the rewarded percentage amount matches expectations let percent = (Decimal::from(unallocated_reward.amount) / reward_info.epoch_emissions) .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); - assert_eq!(percent, dec!(0.1)); + assert_eq!(percent, dec!(0.2)); Ok(()) } @@ -205,18 +205,18 @@ async fn test_service_provider_promotion_rewards(pool: PgPool) -> anyhow::Result // 1 share let promo_reward_1 = promo_rewards.get("one").expect("promo 1"); - assert_eq!(promo_reward_1.service_provider_amount, 1_499); - assert_eq!(promo_reward_1.matched_amount, 1_499); + assert_eq!(promo_reward_1.service_provider_amount, 1_500); + assert_eq!(promo_reward_1.matched_amount, 1_500); // 2 shares let promo_reward_2 = promo_rewards.get("two").expect("promo 2"); - assert_eq!(promo_reward_2.service_provider_amount, 2999); - assert_eq!(promo_reward_2.matched_amount, 2999); + assert_eq!(promo_reward_2.service_provider_amount, 3_000); + assert_eq!(promo_reward_2.matched_amount, 3_000); // 3 shares let promo_reward_3 = promo_rewards.get("three").expect("promo 3"); - assert_eq!(promo_reward_3.service_provider_amount, 4_499); - assert_eq!(promo_reward_3.matched_amount, 4_499); + assert_eq!(promo_reward_3.service_provider_amount, 4_500); + assert_eq!(promo_reward_3.matched_amount, 4_500); // dc_percentage * total_sp_allocation rounded down let sp_reward = rewards.sp_rewards.first().expect("sp 1 reward"); @@ -225,9 +225,9 @@ async fn test_service_provider_promotion_rewards(pool: PgPool) -> anyhow::Result let unallocated_sp_rewards = get_unallocated_sp_rewards(reward_info.epoch_emissions); let expected_unallocated = unallocated_sp_rewards - 50_999 // 85% service provider rewards rounded down - - 8_998 // 15% service provider promotions - - 8_998 // matched promotion - + 2; // rounding + - 9_000 // 15% service provider promotions + - 9_000 // matched promotion + + 0; // rounding let unallocated = rewards.unallocated.first().expect("unallocated"); assert_eq!(unallocated.amount, expected_unallocated); From 6a89e232a450e57a16e3226055328a1437e3a4f7 Mon Sep 17 00:00:00 2001 From: Brian Balser Date: Wed, 8 Oct 2025 11:10:15 -0400 Subject: [PATCH 2/5] Remove mapper rewards and move 10% to data transfer bucket --- mobile_verifier/src/cli/server.rs | 17 +- mobile_verifier/src/lib.rs | 1 - mobile_verifier/src/reward_shares.rs | 173 ++-------- mobile_verifier/src/rewarder.rs | 58 +--- .../src/subscriber_mapping_activity.rs | 322 ------------------ .../src/subscriber_mapping_activity/db.rs | 77 ----- .../tests/integrations/common/mod.rs | 11 +- .../tests/integrations/hex_boosting.rs | 16 +- mobile_verifier/tests/integrations/main.rs | 1 - .../tests/integrations/rewarder_mappers.rs | 148 -------- .../tests/integrations/rewarder_poc_dc.rs | 6 +- 11 files changed, 37 insertions(+), 793 deletions(-) delete mode 100644 mobile_verifier/src/subscriber_mapping_activity.rs delete mode 100644 mobile_verifier/src/subscriber_mapping_activity/db.rs delete mode 100644 mobile_verifier/tests/integrations/rewarder_mappers.rs diff --git a/mobile_verifier/src/cli/server.rs b/mobile_verifier/src/cli/server.rs index bfeb35e3f..f5420b0d3 100644 --- a/mobile_verifier/src/cli/server.rs +++ b/mobile_verifier/src/cli/server.rs @@ -9,7 +9,6 @@ use crate::{ heartbeats::wifi::WifiHeartbeatDaemon, rewarder::Rewarder, speedtests::SpeedtestDaemon, - subscriber_mapping_activity::SubscriberMappingActivityDaemon, telemetry, unique_connections::ingestor::UniqueConnectionsIngestor, Settings, @@ -19,8 +18,8 @@ use file_store::file_upload; use file_store_oracles::traits::{FileSinkCommitStrategy, FileSinkRollTime, FileSinkWriteExt}; use helium_proto::services::poc_mobile::{Heartbeat, SeniorityUpdate, SpeedtestAvg}; use mobile_config::client::{ - entity_client::EntityClient, hex_boosting_client::HexBoostingClient, - sub_dao_client::SubDaoClient, AuthorizationClient, CarrierServiceClient, GatewayClient, + hex_boosting_client::HexBoostingClient, sub_dao_client::SubDaoClient, AuthorizationClient, + CarrierServiceClient, GatewayClient, }; use task_manager::TaskManager; @@ -43,7 +42,6 @@ impl Cmd { // mobile config clients let gateway_client = GatewayClient::from_settings(&settings.config_client)?; let auth_client = AuthorizationClient::from_settings(&settings.config_client)?; - let entity_client = EntityClient::from_settings(&settings.config_client)?; let carrier_client = CarrierServiceClient::from_settings(&settings.config_client)?; let hex_boosting_client = HexBoostingClient::from_settings(&settings.config_client)?; let sub_dao_rewards_client = SubDaoClient::from_settings(&settings.config_client)?; @@ -120,17 +118,6 @@ impl Cmd { ) .await?, ) - .add_task( - SubscriberMappingActivityDaemon::create_managed_task( - pool.clone(), - settings, - auth_client.clone(), - entity_client.clone(), - ingest_bucket_client.clone(), - file_upload.clone(), - ) - .await?, - ) .add_task( CoverageDaemon::create_managed_task( pool.clone(), diff --git a/mobile_verifier/src/lib.rs b/mobile_verifier/src/lib.rs index bbdc58c30..4a04e2942 100644 --- a/mobile_verifier/src/lib.rs +++ b/mobile_verifier/src/lib.rs @@ -15,7 +15,6 @@ pub mod service_provider; mod settings; pub mod speedtests; pub mod speedtests_average; -pub mod subscriber_mapping_activity; pub mod telemetry; pub mod unique_connections; diff --git a/mobile_verifier/src/reward_shares.rs b/mobile_verifier/src/reward_shares.rs index 90cd4678b..e4704b823 100644 --- a/mobile_verifier/src/reward_shares.rs +++ b/mobile_verifier/src/reward_shares.rs @@ -6,7 +6,6 @@ use crate::{ rewarder::boosted_hex_eligibility::BoostedHexEligibility, seniority::Seniority, speedtests_average::SpeedtestAverages, - subscriber_mapping_activity::SubscriberMappingShares, unique_connections::{self, UniqueConnectionCounts}, PriceInfo, }; @@ -19,7 +18,7 @@ use file_store::traits::TimestampEncode; use futures::{Stream, StreamExt}; use helium_crypto::PublicKeyBinary; use helium_proto::services::{ - poc_mobile as proto, poc_mobile::mobile_reward_share::Reward as ProtoReward, + poc_mobile as proto, }; use mobile_config::{boosted_hex_info::BoostedHexes, sub_dao_epoch_reward_info::EpochRewardInfo}; use radio_reward_v2::{RadioRewardV2Ext, ToProtoDecimal}; @@ -32,7 +31,7 @@ mod radio_reward_v2; /// Maximum amount of the total emissions pool allocated for data transfer /// rewards -const MAX_DATA_TRANSFER_REWARDS_PERCENT: Decimal = dec!(0.6); +const MAX_DATA_TRANSFER_REWARDS_PERCENT: Decimal = dec!(0.7); /// Percentage of total emissions pool allocated for proof of coverage const POC_REWARDS_PERCENT: Decimal = dec!(0.0); @@ -43,9 +42,6 @@ const DC_USD_PRICE: Decimal = dec!(0.00001); /// Default precision used for rounding pub const DEFAULT_PREC: u32 = 15; -/// Percent of total emissions allocated for mapper rewards -const MAPPERS_REWARDS_PERCENT: Decimal = dec!(0.1); - // Percent of total emissions allocated for service provider rewards const SERVICE_PROVIDER_PERCENT: Decimal = dec!(0.2); @@ -174,69 +170,6 @@ impl TransferRewards { } } -#[derive(Default)] -pub struct MapperShares { - mapping_activity_shares: Vec, -} - -impl MapperShares { - pub fn new(mapping_activity_shares: Vec) -> Self { - Self { - mapping_activity_shares, - } - } - - pub fn rewards_per_share(&self, total_mappers_pool: Decimal) -> anyhow::Result { - let total_shares = self - .mapping_activity_shares - .iter() - .map(|mas| Decimal::from(mas.discovery_reward_shares + mas.verification_reward_shares)) - .sum(); - - let res = total_mappers_pool - .checked_div(total_shares) - .unwrap_or(Decimal::ZERO); - - Ok(res) - } - - pub fn into_subscriber_rewards( - self, - reward_period: &Range>, - reward_per_share: Decimal, - ) -> impl Iterator + '_ { - self.mapping_activity_shares.into_iter().map(move |mas| { - let discovery_location_amount = (Decimal::from(mas.discovery_reward_shares) - * reward_per_share) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or_default(); - - let verification_mapping_amount = (Decimal::from(mas.verification_reward_shares) - * reward_per_share) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or_default(); - - ( - discovery_location_amount + verification_mapping_amount, - proto::MobileRewardShare { - start_period: reward_period.start.encode_timestamp(), - end_period: reward_period.end.encode_timestamp(), - reward: Some(ProtoReward::SubscriberReward(proto::SubscriberReward { - subscriber_id: mas.subscriber_id, - discovery_location_amount, - verification_mapping_amount, - reward_override_entity_key: mas - .reward_override_entity_key - .unwrap_or_default(), - })), - }, - ) - }) - } -} - /// Returns the equivalent amount of Hnt bones for a specified amount of Data Credits pub fn dc_to_hnt_bones(dc_amount: Decimal, hnt_bone_price: Decimal) -> Decimal { let dc_in_usd = dc_amount * DC_USD_PRICE; @@ -616,9 +549,6 @@ pub fn get_scheduled_tokens_for_poc(total_emission_pool: Decimal) -> Decimal { total_emission_pool * poc_percent } -pub fn get_scheduled_tokens_for_mappers(total_emission_pool: Decimal) -> Decimal { - total_emission_pool * MAPPERS_REWARDS_PERCENT -} pub fn get_scheduled_tokens_for_service_providers(total_emission_pool: Decimal) -> Decimal { total_emission_pool * SERVICE_PROVIDER_PERCENT @@ -666,7 +596,6 @@ mod test { coverage::{CoveredHexStream, HexCoverage}, data_session::{self, HotspotDataSession, HotspotReward}, heartbeats::{HeartbeatReward, KeyType, OwnedKeyType}, - reward_shares, service_provider::{ self, ServiceProviderDCSessions, ServiceProviderPromotions, ServiceProviderRewardInfos, }, @@ -680,7 +609,6 @@ mod test { services::poc_mobile::mobile_reward_share::Reward as MobileReward, ServiceProvider, }; use hextree::Cell; - use prost::Message; use solana::Token; use std::collections::HashMap; use uuid::Uuid; @@ -763,14 +691,9 @@ mod test { #[test] fn test_poc_scheduled_tokens() { let v = get_scheduled_tokens_for_poc(dec!(100)); - assert_eq!(dec!(60), v, "poc gets 60%"); + assert_eq!(dec!(70), v, "poc gets 70%"); } - #[test] - fn test_mappers_scheduled_tokens() { - let v = get_scheduled_tokens_for_mappers(dec!(100)); - assert_eq!(dec!(10), v, "mappers get 10%"); - } #[test] fn test_service_provider_scheduled_tokens() { @@ -798,71 +721,6 @@ mod test { assert_eq!(hnt_dollar_price, hnt_price.price_per_token); } - #[tokio::test] - async fn subscriber_rewards() { - const NUM_SUBSCRIBERS: u64 = 10_000; - - let mut mapping_activity_shares = Vec::new(); - for n in 0..NUM_SUBSCRIBERS { - mapping_activity_shares.push(SubscriberMappingShares { - subscriber_id: n.encode_to_vec(), - discovery_reward_shares: 30, - verification_reward_shares: 30, - reward_override_entity_key: None, - }) - } - - // set our rewards info - let rewards_info = rewards_info_24_hours(); - - // translate location shares into shares - let shares = MapperShares::new(mapping_activity_shares); - let total_mappers_pool = - reward_shares::get_scheduled_tokens_for_mappers(rewards_info.epoch_emissions); - let rewards_per_share = shares.rewards_per_share(total_mappers_pool).unwrap(); - - // verify total rewards allocated to mappers the epoch - let total_mapper_rewards = get_scheduled_tokens_for_mappers(rewards_info.epoch_emissions) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0); - assert_eq!(8_219_178_082_191, total_mapper_rewards); - - let expected_reward_per_subscriber = total_mapper_rewards / NUM_SUBSCRIBERS; - - // get the summed rewards allocated to subscribers for discovery location - let mut allocated_mapper_rewards = 0_u64; - for (reward_amount, subscriber_share) in - shares.into_subscriber_rewards(&rewards_info.epoch_period, rewards_per_share) - { - if let Some(MobileReward::SubscriberReward(r)) = subscriber_share.reward { - assert_eq!( - expected_reward_per_subscriber, - r.discovery_location_amount + r.verification_mapping_amount - ); - assert_eq!( - reward_amount, - r.discovery_location_amount + r.verification_mapping_amount - ); - // These are the same because we gave `total_reward_points: 30,` for each - // VerifiedMappingEventShares which is the same amount as discovery mapping - assert_eq!(410_958_904, r.discovery_location_amount); - assert_eq!(410_958_904, r.verification_mapping_amount); - allocated_mapper_rewards += reward_amount; - } - } - - // verify the total rewards awarded for discovery mapping - assert_eq!(8_219_178_080_000, allocated_mapper_rewards); - - // confirm the unallocated service provider reward amounts - // this should not be more than the total number of subscribers ( 10 k) - // as we can at max drop one bone per subscriber due to rounding - let unallocated_mapper_reward_amount = total_mapper_rewards - allocated_mapper_rewards; - assert_eq!(unallocated_mapper_reward_amount, 2191); - assert!(unallocated_mapper_reward_amount < NUM_SUBSCRIBERS); - } - /// Test to ensure that the correct data transfer amount is rewarded. #[tokio::test] async fn ensure_data_correct_transfer_reward_amount() { @@ -899,9 +757,11 @@ mod test { // confirm our hourly rewards add up to expected 24hr amount // total_rewards will be in bones + // 70% of 82_191_780_821_917 = 57,534,246,575,342 bones + // divided by 1_000_000 = 57,534,246.575342 assert_eq!( (total_rewards / dec!(1_000_000) * dec!(24)).trunc(), - dec!(49_315_068) + dec!(57_534_246) ); let reward_shares = @@ -976,9 +836,8 @@ mod test { .await; // We have constructed the data transfer in such a way that they easily exceed the maximum - // allotted reward amount for data transfer, which is 40% of the daily tokens. We check to - // ensure that amount of tokens remaining for POC is no less than 20% of the rewards allocated - // for POC and data transfer (which is 60% of the daily total emissions). + // allotted reward amount for data transfer, which is 70% of the daily total emissions. + // We check to ensure that the data transfer rewards consume the full allocation. let available_poc_rewards = get_scheduled_tokens_for_poc(rewards_info.epoch_emissions) - data_transfer_rewards.reward_sum; assert_eq!(available_poc_rewards.trunc(), Decimal::ZERO); @@ -987,7 +846,7 @@ mod test { data_transfer_rewards.reward(&owner).trunc(), get_scheduled_tokens_for_poc(rewards_info.epoch_emissions).trunc(), ); - assert_eq!(data_transfer_rewards.reward_scale().round_dp(1), dec!(0.7)); + assert_eq!(data_transfer_rewards.reward_scale().round_dp(1), dec!(0.9)); } fn bytes_per_s(mbps: u64) -> u64 { @@ -1382,9 +1241,19 @@ mod test { .expect("Could not fetch owner3 rewards"); assert_eq!((owner_1_reward as f64 * 1.5) as u64, owner_2_reward); - assert_eq!((owner_1_reward as f64 * 0.75) as u64, owner_3_reward); + // Allow for 1 bone rounding error due to f64 precision + let expected_owner_3 = (owner_1_reward as f64 * 0.75) as u64; + assert!( + owner_3_reward == expected_owner_3 || owner_3_reward == expected_owner_3 + 1, + "owner_3_reward {} should be {} or {} (±1 bone)", + owner_3_reward, + expected_owner_3, + expected_owner_3 + 1 + ); - let expected_unallocated = 3; + // Due to rounding in reward calculations, there will be a small amount unallocated. + // With 70% allocation, the expected unallocated amount is 2 bones. + let expected_unallocated = 2; assert_eq!( allocated_poc_rewards, reward_shares.total_poc().to_u64().unwrap() - expected_unallocated diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index bf69b170f..c44b57c36 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -6,12 +6,12 @@ use crate::{ resolve_subdao_pubkey, reward_shares::{ self, CalculatedPocRewardShares, CoverageShares, DataTransferAndPocAllocatedRewardBuckets, - MapperShares, TransferRewards, + TransferRewards, }, service_provider::{self, ServiceProviderDCSessions, ServiceProviderPromotions}, speedtests, speedtests_average::SpeedtestAverages, - subscriber_mapping_activity, telemetry, unique_connections, PriceInfo, Settings, + telemetry, unique_connections, PriceInfo, Settings, }; use anyhow::bail; use chrono::{DateTime, TimeZone, Utc}; @@ -288,9 +288,6 @@ where ) .await?; - // process rewards for mappers - reward_mappers(&self.pool, self.mobile_rewards.clone(), &reward_info).await?; - // process rewards for service providers let dc_sessions = service_provider::get_dc_sessions( &self.pool, @@ -326,8 +323,6 @@ where ) .await?; coverage::clear_coverage_objects(&mut transaction, &reward_info.epoch_period.start).await?; - subscriber_mapping_activity::db::clear(&mut *transaction, reward_info.epoch_period.start) - .await?; unique_connections::db::clear(&mut transaction, &reward_info.epoch_period.start).await?; banning::clear_bans(&mut transaction, reward_info.epoch_period.start).await?; @@ -532,55 +527,6 @@ pub async fn reward_dc( Ok(unallocated_dc_reward_amount) } -pub async fn reward_mappers( - pool: &Pool, - mobile_rewards: FileSinkClient, - reward_info: &EpochRewardInfo, -) -> anyhow::Result<()> { - let rewardable_mapping_activity = subscriber_mapping_activity::db::rewardable_mapping_activity( - pool, - &reward_info.epoch_period, - ) - .await?; - - let mapping_shares = MapperShares::new(rewardable_mapping_activity); - let total_mappers_pool = - reward_shares::get_scheduled_tokens_for_mappers(reward_info.epoch_emissions); - let rewards_per_share = mapping_shares.rewards_per_share(total_mappers_pool)?; - - // translate discovery mapping shares into subscriber rewards - let mut allocated_mapping_rewards = 0_u64; - let mut count_mappers_rewarded = 0; - for (reward_amount, mapping_share) in - mapping_shares.into_subscriber_rewards(&reward_info.epoch_period, rewards_per_share) - { - allocated_mapping_rewards += reward_amount; - count_mappers_rewarded += 1; - mobile_rewards - .write(mapping_share.clone(), []) - .await? - // Await the returned one shot to ensure that we wrote the file - .await??; - } - telemetry::mappers_rewarded(count_mappers_rewarded); - - // write out any unallocated mapping rewards - let unallocated_mapping_reward_amount = total_mappers_pool - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0) - - allocated_mapping_rewards; - write_unallocated_reward( - &mobile_rewards, - UnallocatedRewardType::Mapper, - unallocated_mapping_reward_amount, - reward_info, - ) - .await?; - - Ok(()) -} - pub async fn reward_oracles( mobile_rewards: FileSinkClient, reward_info: &EpochRewardInfo, diff --git a/mobile_verifier/src/subscriber_mapping_activity.rs b/mobile_verifier/src/subscriber_mapping_activity.rs deleted file mode 100644 index b5f1a11a5..000000000 --- a/mobile_verifier/src/subscriber_mapping_activity.rs +++ /dev/null @@ -1,322 +0,0 @@ -use std::{sync::Arc, time::Instant}; - -use chrono::{DateTime, Utc}; -use file_store::{ - file_info_poller::FileInfoStream, - file_sink::FileSinkClient, - file_source, - file_upload::FileUpload, - traits::{TimestampDecode, TimestampEncode}, - BucketClient, -}; -use file_store_oracles::{ - traits::{FileSinkCommitStrategy, FileSinkRollTime, FileSinkWriteExt}, - FileType, -}; -use futures::{StreamExt, TryStreamExt}; -use helium_crypto::PublicKeyBinary; -use helium_proto::services::{ - mobile_config::NetworkKeyRole, - poc_mobile::{ - SubscriberMappingActivityIngestReportV1, SubscriberReportVerificationStatus, - VerifiedSubscriberMappingActivityReportV1, - }, -}; -use mobile_config::client::{ - authorization_client::AuthorizationVerifier, entity_client::EntityVerifier, -}; -use sqlx::{Pool, Postgres}; -use task_manager::{ManagedTask, TaskManager}; -use tokio::sync::mpsc::Receiver; - -use crate::Settings; - -pub mod db; - -pub struct SubscriberMappingActivityDaemon { - pool: Pool, - authorization_verifier: Arc, - entity_verifier: Arc, - stream_receiver: Receiver>, - verified_sink: FileSinkClient, -} - -impl SubscriberMappingActivityDaemon -where - AV: AuthorizationVerifier, - EV: EntityVerifier, -{ - pub fn new( - pool: Pool, - authorization_verifier: AV, - entity_verifier: EV, - stream_receiver: Receiver>, - verified_sink: FileSinkClient, - ) -> Self { - Self { - pool, - authorization_verifier: Arc::new(authorization_verifier), - entity_verifier: Arc::new(entity_verifier), - stream_receiver, - verified_sink, - } - } - - pub async fn create_managed_task( - pool: Pool, - settings: &Settings, - authorization_verifier: AV, - entity_verifier: EV, - bucket_client: BucketClient, - file_upload: FileUpload, - ) -> anyhow::Result { - let (stream_reciever, stream_server) = file_source::Continuous::prost_source() - .state(pool.clone()) - .bucket_client(bucket_client) - .lookback_start_after(settings.start_after) - .prefix(FileType::SubscriberMappingActivityIngestReport.to_string()) - .create() - .await?; - - let (verified_sink, verified_sink_server) = - VerifiedSubscriberMappingActivityReportV1::file_sink( - settings.store_base_path(), - file_upload.clone(), - FileSinkCommitStrategy::Manual, - FileSinkRollTime::Default, - env!("CARGO_PKG_NAME"), - ) - .await?; - - let daemon = Self::new( - pool, - authorization_verifier, - entity_verifier, - stream_reciever, - verified_sink, - ); - - Ok(TaskManager::builder() - .add_task(stream_server) - .add_task(verified_sink_server) - .add_task(daemon) - .build()) - } - - pub async fn run(mut self, mut shutdown: triggered::Listener) -> anyhow::Result<()> { - tracing::info!("starting"); - - loop { - tokio::select! { - biased; - _ = &mut shutdown => break, - Some(file) = self.stream_receiver.recv() => { - let start = Instant::now(); - self.process_file(file).await?; - metrics::histogram!("subscriber_mapping_activity_processing_time") - .record(start.elapsed()); - } - } - } - - tracing::info!("stopping"); - Ok(()) - } - - async fn process_file( - &self, - file_info_stream: FileInfoStream, - ) -> anyhow::Result<()> { - let mut transaction = self.pool.begin().await?; - let stream = file_info_stream.into_stream(&mut transaction).await?; - - let activity_stream = stream - .map(|proto| { - let activity = SubscriberMappingActivity::try_from(proto.clone())?; - Ok((activity, proto)) - }) - .and_then(|(activity, proto)| { - let av = self.authorization_verifier.clone(); - let ev = self.entity_verifier.clone(); - async move { - let status = verify_activity(av, ev, &activity).await?; - Ok((activity, proto, status)) - } - }) - .and_then(|(activity, proto, status)| { - let sink = self.verified_sink.clone(); - async move { - write_verified_report(sink, proto, status).await?; - Ok((activity, status)) - } - }) - .try_filter_map(|(activity, status)| async move { - Ok(matches!(status, SubscriberReportVerificationStatus::Valid).then_some(activity)) - }); - - db::save(&mut transaction, activity_stream).await?; - self.verified_sink.commit().await?; - transaction.commit().await?; - Ok(()) - } -} - -async fn write_verified_report( - sink: FileSinkClient, - proto: SubscriberMappingActivityIngestReportV1, - status: SubscriberReportVerificationStatus, -) -> anyhow::Result<()> { - let verified_proto = VerifiedSubscriberMappingActivityReportV1 { - report: Some(proto), - status: status as i32, - timestamp: Utc::now().encode_timestamp_millis(), - }; - - sink.write(verified_proto, &[("status", status.as_str_name())]) - .await?; - - Ok(()) -} - -async fn verify_activity( - authorization_verifier: impl AsRef, - entity_verifier: impl AsRef, - activity: &SubscriberMappingActivity, -) -> anyhow::Result -where - AV: AuthorizationVerifier, - EV: EntityVerifier, -{ - if !verify_known_carrier_key(authorization_verifier, &activity.carrier_pub_key).await? { - return Ok(SubscriberReportVerificationStatus::InvalidCarrierKey); - }; - if !verify_entity(&entity_verifier, &activity.subscriber_id).await? { - return Ok(SubscriberReportVerificationStatus::InvalidSubscriberId); - }; - if let Some(rek) = &activity.reward_override_entity_key { - // use UTF8(key_serialization) as bytea - if !verify_entity(entity_verifier, &rek.clone().into_bytes()).await? { - return Ok(SubscriberReportVerificationStatus::InvalidRewardOverrideEntityKey); - }; - } - Ok(SubscriberReportVerificationStatus::Valid) -} - -async fn verify_known_carrier_key( - authorization_verifier: impl AsRef, - public_key: &PublicKeyBinary, -) -> anyhow::Result -where - AV: AuthorizationVerifier, -{ - authorization_verifier - .as_ref() - .verify_authorized_key(public_key, NetworkKeyRole::MobileCarrier) - .await - .map_err(anyhow::Error::from) -} - -async fn verify_entity( - entity_verifier: impl AsRef, - entity_id: &[u8], -) -> anyhow::Result -where - EV: EntityVerifier, -{ - entity_verifier - .as_ref() - .verify_rewardable_entity(entity_id) - .await - .map_err(anyhow::Error::from) -} - -impl ManagedTask for SubscriberMappingActivityDaemon -where - AV: AuthorizationVerifier, - EV: EntityVerifier, -{ - fn start_task( - self: Box, - shutdown: triggered::Listener, - ) -> task_manager::TaskLocalBoxFuture { - task_manager::spawn(self.run(shutdown)) - } -} - -pub struct SubscriberMappingActivity { - pub subscriber_id: Vec, - pub discovery_reward_shares: u64, - pub verification_reward_shares: u64, - pub received_timestamp: DateTime, - pub carrier_pub_key: PublicKeyBinary, - pub reward_override_entity_key: Option, -} - -impl TryFrom for SubscriberMappingActivity { - type Error = anyhow::Error; - - fn try_from(value: SubscriberMappingActivityIngestReportV1) -> Result { - let report = value - .report - .ok_or_else(|| anyhow::anyhow!("SubscriberMappingActivityReqV1 not found"))?; - - let reward_override_entity_key = if report.reward_override_entity_key.is_empty() { - None - } else { - Some(report.reward_override_entity_key) - }; - - Ok(Self { - subscriber_id: report.subscriber_id, - discovery_reward_shares: report.discovery_reward_shares, - verification_reward_shares: report.verification_reward_shares, - received_timestamp: value.received_timestamp.to_timestamp_millis()?, - carrier_pub_key: PublicKeyBinary::from(report.carrier_pub_key), - reward_override_entity_key, - }) - } -} - -#[derive(Clone, Debug, sqlx::FromRow)] -pub struct SubscriberMappingShares { - pub subscriber_id: Vec, - #[sqlx(try_from = "i64")] - pub discovery_reward_shares: u64, - #[sqlx(try_from = "i64")] - pub verification_reward_shares: u64, - - pub reward_override_entity_key: Option, -} - -#[cfg(test)] -mod tests { - use super::SubscriberMappingActivity; - use helium_proto::services::poc_mobile::SubscriberMappingActivityIngestReportV1; - - #[test] - fn try_from_subscriber_mapping_activity_check_entity_key() { - // Make sure reward_override_entity_key empty string transforms to None - let smair = SubscriberMappingActivityIngestReportV1 { - received_timestamp: 1, - report: Some({ - helium_proto::services::poc_mobile::SubscriberMappingActivityReqV1 { - subscriber_id: vec![10], - discovery_reward_shares: 2, - verification_reward_shares: 3, - timestamp: 4, - carrier_pub_key: vec![11], - signature: vec![12], - reward_override_entity_key: "".to_string(), - } - }), - }; - let mut smair2 = smair.clone(); - smair2.report.as_mut().unwrap().reward_override_entity_key = "key".to_string(); - - let res = SubscriberMappingActivity::try_from(smair).unwrap(); - assert!(res.reward_override_entity_key.is_none()); - - let res = SubscriberMappingActivity::try_from(smair2).unwrap(); - assert_eq!(res.reward_override_entity_key, Some("key".to_string())); - } -} diff --git a/mobile_verifier/src/subscriber_mapping_activity/db.rs b/mobile_verifier/src/subscriber_mapping_activity/db.rs deleted file mode 100644 index 6eb94805f..000000000 --- a/mobile_verifier/src/subscriber_mapping_activity/db.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::ops::Range; - -use chrono::{DateTime, Utc}; -use futures::{Stream, TryStreamExt}; -use sqlx::{PgExecutor, Postgres, QueryBuilder, Transaction}; - -use crate::subscriber_mapping_activity::SubscriberMappingActivity; - -use super::SubscriberMappingShares; - -pub async fn save( - transaction: &mut Transaction<'_, Postgres>, - ingest_reports: impl Stream>, -) -> anyhow::Result<()> { - const NUM_IN_BATCH: usize = (u16::MAX / 6) as usize; - - ingest_reports - .try_chunks(NUM_IN_BATCH) - .err_into::() - .try_fold(transaction, |txn, chunk| async move { - QueryBuilder::new(r#"INSERT INTO subscriber_mapping_activity( - subscriber_id, discovery_reward_shares, verification_reward_shares, received_timestamp, inserted_at, reward_override_entity_key)"#) - .push_values(chunk, |mut b, activity| { - - b.push_bind(activity.subscriber_id) - .push_bind(activity.discovery_reward_shares as i64) - .push_bind(activity.verification_reward_shares as i64) - .push_bind(activity.received_timestamp) - .push_bind(Utc::now()) - .push_bind(activity.reward_override_entity_key); - }) - .push("ON CONFLICT (subscriber_id, received_timestamp) DO NOTHING") - .build() - .execute(&mut **txn) - .await?; - - Ok(txn) - }) - .await?; - - Ok(()) -} - -pub async fn rewardable_mapping_activity( - db: impl PgExecutor<'_>, - epoch_period: &Range>, -) -> anyhow::Result> { - sqlx::query_as( - r#" - SELECT DISTINCT ON (subscriber_id) subscriber_id, discovery_reward_shares, verification_reward_shares, reward_override_entity_key - FROM subscriber_mapping_activity - WHERE received_timestamp >= $1 - AND received_timestamp < $2 - AND (discovery_reward_shares > 0 OR verification_reward_shares > 0) - ORDER BY subscriber_id, received_timestamp DESC - "#, - ) - .bind(epoch_period.start) - .bind(epoch_period.end) - .fetch_all(db) - .await - .map_err(anyhow::Error::from) -} - -pub async fn clear(db: impl PgExecutor<'_>, timestamp: DateTime) -> anyhow::Result<()> { - sqlx::query( - " - DELETE FROM subscriber_mapping_activity - WHERE received_timestamp < $1 - ", - ) - .bind(timestamp) - .execute(db) - .await?; - - Ok(()) -} diff --git a/mobile_verifier/tests/integrations/common/mod.rs b/mobile_verifier/tests/integrations/common/mod.rs index 4e60f7eaa..45694a557 100644 --- a/mobile_verifier/tests/integrations/common/mod.rs +++ b/mobile_verifier/tests/integrations/common/mod.rs @@ -18,9 +18,7 @@ use mobile_config::{ sub_dao_epoch_reward_info::EpochRewardInfo, }; use mobile_verifier::{ - boosting_oracles::AssignedCoverageObjects, - subscriber_mapping_activity::SubscriberMappingShares, GatewayResolution, GatewayResolver, - PriceInfo, + boosting_oracles::AssignedCoverageObjects, GatewayResolution, GatewayResolver, PriceInfo, }; use rust_decimal::{prelude::ToPrimitive, Decimal}; use rust_decimal_macros::dec; @@ -420,13 +418,6 @@ impl AsStringKeyedMapKey for PromotionReward { self.entity.to_owned() } } -impl AsStringKeyedMapKey for SubscriberMappingShares { - fn key(&self) -> String { - use prost::Message; - let bytes = prost::bytes::Bytes::from_owner(self.subscriber_id.clone()); - String::decode(bytes).expect("decode subscriber id") - } -} impl AsStringKeyedMap for Vec { fn as_keyed_map(&self) -> HashMap diff --git a/mobile_verifier/tests/integrations/hex_boosting.rs b/mobile_verifier/tests/integrations/hex_boosting.rs index 7493501ad..e1595813c 100644 --- a/mobile_verifier/tests/integrations/hex_boosting.rs +++ b/mobile_verifier/tests/integrations/hex_boosting.rs @@ -304,9 +304,9 @@ async fn test_poc_boosted_hexes_unique_connections_not_seeded(pool: PgPool) -> a let hotspot_2 = poc_rewards.get(HOTSPOT_2).expect("hotspot 2"); let hotspot_3 = poc_rewards.get(HOTSPOT_3).expect("hotspot 3"); - assert_eq!(16_438_356_164_383, hotspot_1.total_poc_reward()); - assert_eq!(16_438_356_164_383, hotspot_2.total_poc_reward()); - assert_eq!(16_438_356_164_383, hotspot_3.total_poc_reward()); + assert_eq!(19_178_082_191_780, hotspot_1.total_poc_reward()); + assert_eq!(19_178_082_191_780, hotspot_2.total_poc_reward()); + assert_eq!(19_178_082_191_780, hotspot_3.total_poc_reward()); // assert the number of boosted hexes for each radio assert_eq!(0, hotspot_1.boosted_hexes_len()); @@ -571,9 +571,9 @@ async fn test_expired_boosted_hex(pool: PgPool) -> anyhow::Result<()> { assert_eq!(poc_rewards.len(), 3); // assert poc reward outputs - assert_eq!(16_438_356_164_383, hotspot_1.total_poc_reward()); - assert_eq!(16_438_356_164_383, hotspot_2.total_poc_reward()); - assert_eq!(16_438_356_164_383, hotspot_3.total_poc_reward()); + assert_eq!(19_178_082_191_780, hotspot_1.total_poc_reward()); + assert_eq!(19_178_082_191_780, hotspot_2.total_poc_reward()); + assert_eq!(19_178_082_191_780, hotspot_3.total_poc_reward()); // assert the number of boosted hexes for each radio // all will be zero as the boost period has expired for the single boosted hex @@ -1528,7 +1528,7 @@ async fn save_seniority_object( fn get_poc_allocation_buckets(total_emissions: Decimal) -> Decimal { // To not deal with percentages of percentages, let's start with the // total emissions and work from there. - let data_transfer = total_emissions * dec!(0.6); + let data_transfer = total_emissions * dec!(0.7); let regular_poc = total_emissions * dec!(0.0); // There is no data transfer in this test to be rewarded, so we know @@ -1551,5 +1551,5 @@ fn assert_total_matches_emissions(total: u64, reward_info: &EpochRewardInfo) { // confirm the rewarded percentage amount matches expectations let percent = (Decimal::from(total) / reward_info.epoch_emissions) .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); - assert_eq!(percent, dec!(0.6), "POC and DC is always 60%"); + assert_eq!(percent, dec!(0.70), "POC and DC is always 70%"); } diff --git a/mobile_verifier/tests/integrations/main.rs b/mobile_verifier/tests/integrations/main.rs index 3e3639f4e..259c0bf81 100644 --- a/mobile_verifier/tests/integrations/main.rs +++ b/mobile_verifier/tests/integrations/main.rs @@ -7,7 +7,6 @@ mod heartbeats; mod hex_boosting; mod last_location; mod modeled_coverage; -mod rewarder_mappers; mod rewarder_oracles; mod rewarder_poc_dc; mod rewarder_sp_rewards; diff --git a/mobile_verifier/tests/integrations/rewarder_mappers.rs b/mobile_verifier/tests/integrations/rewarder_mappers.rs deleted file mode 100644 index 7b461e92d..000000000 --- a/mobile_verifier/tests/integrations/rewarder_mappers.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::common::{self, reward_info_24_hours, AsStringKeyedMap}; -use chrono::{DateTime, Duration as ChronoDuration, Utc}; -use futures::{stream, StreamExt}; -use helium_crypto::PublicKeyBinary; -use helium_proto::{services::poc_mobile::UnallocatedRewardType, Message}; -use mobile_verifier::{ - reward_shares, rewarder, - subscriber_mapping_activity::{self, SubscriberMappingActivity}, -}; -use rust_decimal::prelude::*; -use rust_decimal_macros::dec; -use sqlx::{PgPool, Postgres, Transaction}; -use std::{str::FromStr, string::ToString}; - -const SUBSCRIBER_1: &str = "subscriber1"; -const SUBSCRIBER_2: &str = "subscriber2"; -const SUBSCRIBER_3: &str = "subscriber3"; -const HOTSPOT_1: &str = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6"; - -#[sqlx::test] -async fn test_mapper_rewards(pool: PgPool) -> anyhow::Result<()> { - let (mobile_rewards_client, mobile_rewards) = common::create_file_sink(); - let reward_info = reward_info_24_hours(); - - // seed db - let mut txn = pool.clone().begin().await?; - seed_mapping_data(reward_info.epoch_period.end, &mut txn).await?; - txn.commit().await.expect("db txn failed"); - - rewarder::reward_mappers(&pool, mobile_rewards_client, &reward_info).await?; - - let rewards = mobile_rewards.finish().await?; - let subscriber_rewards = rewards.subscriber_rewards.as_keyed_map(); - - // assert the mapper rewards - // all 3 subscribers will have an equal share, - // requirement is 1 qualifying mapping criteria report per epoch - // subscriber 1 has two qualifying mapping criteria reports, - // other two subscribers one qualifying mapping criteria reports - let sub_reward_1 = subscriber_rewards.get(SUBSCRIBER_1).expect("sub 1"); - assert_eq!(2_739_726_027_397, sub_reward_1.discovery_location_amount); - - let sub_reward_2 = subscriber_rewards.get(SUBSCRIBER_2).expect("sub 2"); - assert_eq!(2_739_726_027_397, sub_reward_2.discovery_location_amount); - - let sub_reward_3 = subscriber_rewards.get(SUBSCRIBER_3).expect("sub 3"); - assert_eq!(2_739_726_027_397, sub_reward_3.discovery_location_amount); - - // confirm our unallocated amount - let unallocated_amount = rewards - .unallocated - .iter() - .find(|r| r.reward_type == UnallocatedRewardType::Mapper as i32) - .map(|r| r.amount) - .unwrap_or(0); - assert_eq!(0, unallocated_amount); - - // confirm the total rewards allocated matches expectations - let expected_sum = reward_shares::get_scheduled_tokens_for_mappers(reward_info.epoch_emissions) - .to_u64() - .unwrap(); - let subscriber_sum = - rewards.total_sub_discovery_amount() + rewards.unallocated_amount_or_default(); - assert_eq!(expected_sum, subscriber_sum); - - // confirm the rewarded percentage amount matches expectations - let percent = (Decimal::from(subscriber_sum) / reward_info.epoch_emissions) - .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); - assert_eq!(percent, dec!(0.1)); - - Ok(()) -} - -#[sqlx::test] -async fn reward_mapper_check_entity_key_db(pool: PgPool) { - let reward_info = reward_info_24_hours(); - // seed db - let mut txn = pool.clone().begin().await.unwrap(); - seed_mapping_data(reward_info.epoch_period.end, &mut txn) - .await - .unwrap(); - txn.commit().await.expect("db txn failed"); - - let rewardable_mapping_activity = subscriber_mapping_activity::db::rewardable_mapping_activity( - &pool, - &reward_info.epoch_period, - ) - .await - .unwrap(); - - let sub_map = rewardable_mapping_activity.as_keyed_map(); - let sub_1 = sub_map.get(SUBSCRIBER_1).expect("sub 1"); - let sub_3 = sub_map.get(SUBSCRIBER_3).expect("sub 3"); - - assert!(sub_1.reward_override_entity_key.is_none()); - assert_eq!( - sub_3.reward_override_entity_key, - Some("entity key".to_string()) - ); -} - -async fn seed_mapping_data( - ts: DateTime, - txn: &mut Transaction<'_, Postgres>, -) -> anyhow::Result<()> { - // subscriber 1 has two qualifying mapping criteria reports - // subscribers 2 and 3 have a single qualifying mapping criteria report - - let reports = vec![ - SubscriberMappingActivity { - received_timestamp: ts - ChronoDuration::hours(1), - subscriber_id: SUBSCRIBER_1.to_string().encode_to_vec(), - discovery_reward_shares: 30, - verification_reward_shares: 0, - carrier_pub_key: PublicKeyBinary::from_str(HOTSPOT_1).unwrap(), - reward_override_entity_key: None, - }, - SubscriberMappingActivity { - received_timestamp: ts - ChronoDuration::hours(2), - subscriber_id: SUBSCRIBER_1.to_string().encode_to_vec(), - discovery_reward_shares: 30, - verification_reward_shares: 0, - carrier_pub_key: PublicKeyBinary::from_str(HOTSPOT_1).unwrap(), - reward_override_entity_key: None, - }, - SubscriberMappingActivity { - received_timestamp: ts - ChronoDuration::hours(1), - subscriber_id: SUBSCRIBER_2.to_string().encode_to_vec(), - discovery_reward_shares: 30, - verification_reward_shares: 0, - carrier_pub_key: PublicKeyBinary::from_str(HOTSPOT_1).unwrap(), - reward_override_entity_key: None, - }, - SubscriberMappingActivity { - received_timestamp: ts - ChronoDuration::hours(1), - subscriber_id: SUBSCRIBER_3.to_string().encode_to_vec(), - discovery_reward_shares: 30, - verification_reward_shares: 0, - carrier_pub_key: PublicKeyBinary::from_str(HOTSPOT_1).unwrap(), - reward_override_entity_key: Some("entity key".to_string()), - }, - ]; - - subscriber_mapping_activity::db::save(txn, stream::iter(reports.into_iter()).map(anyhow::Ok)) - .await?; - - Ok(()) -} diff --git a/mobile_verifier/tests/integrations/rewarder_poc_dc.rs b/mobile_verifier/tests/integrations/rewarder_poc_dc.rs index 12dc9c11f..9dc12f47d 100644 --- a/mobile_verifier/tests/integrations/rewarder_poc_dc.rs +++ b/mobile_verifier/tests/integrations/rewarder_poc_dc.rs @@ -108,7 +108,7 @@ async fn test_poc_and_dc_rewards(pool: PgPool) -> anyhow::Result<()> { // confirm the rewarded percentage amount matches expectations let percent = (Decimal::from(total) / reward_info.epoch_emissions) .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); - assert_eq!(percent, dec!(0.6)); + assert_eq!(percent, dec!(0.70)); Ok(()) } @@ -289,10 +289,10 @@ async fn test_all_banned_radio(pool: PgPool) -> anyhow::Result<()> { let dc_rewards = rewards.gateway_rewards; - // expecting single radio with poc rewards, no unallocated + // expecting single radio with poc rewards, minimal unallocated due to rounding assert_eq!(poc_rewards.len(), 2); assert_eq!(dc_rewards.len(), 3); - assert_eq!(rewards.unallocated.len(), 0); + assert_eq!(rewards.unallocated.len(), 1); Ok(()) } From 8602f5278e12fa9611498948a235673c405936a8 Mon Sep 17 00:00:00 2001 From: Brian Balser Date: Wed, 8 Oct 2025 11:30:21 -0400 Subject: [PATCH 3/5] Remove oracles reward bucket and move to service provider bucket --- mobile_verifier/src/reward_shares.rs | 17 ++------ mobile_verifier/src/rewarder.rs | 28 +------------ .../tests/integrations/common/mod.rs | 7 ---- mobile_verifier/tests/integrations/main.rs | 1 - .../tests/integrations/rewarder_oracles.rs | 39 ------------------- .../tests/integrations/rewarder_sp_rewards.rs | 6 +-- 6 files changed, 7 insertions(+), 91 deletions(-) delete mode 100644 mobile_verifier/tests/integrations/rewarder_oracles.rs diff --git a/mobile_verifier/src/reward_shares.rs b/mobile_verifier/src/reward_shares.rs index e4704b823..6002d943c 100644 --- a/mobile_verifier/src/reward_shares.rs +++ b/mobile_verifier/src/reward_shares.rs @@ -43,10 +43,7 @@ const DC_USD_PRICE: Decimal = dec!(0.00001); pub const DEFAULT_PREC: u32 = 15; // Percent of total emissions allocated for service provider rewards -const SERVICE_PROVIDER_PERCENT: Decimal = dec!(0.2); - -// Percent of total emissions allocated for oracles -const ORACLES_PERCENT: Decimal = dec!(0.04); +const SERVICE_PROVIDER_PERCENT: Decimal = dec!(0.24); #[derive(Debug)] pub struct TransferRewards { @@ -554,9 +551,6 @@ pub fn get_scheduled_tokens_for_service_providers(total_emission_pool: Decimal) total_emission_pool * SERVICE_PROVIDER_PERCENT } -pub fn get_scheduled_tokens_for_oracles(total_emission_pool: Decimal) -> Decimal { - total_emission_pool * ORACLES_PERCENT -} fn eligible_for_coverage_map( oracle_boosting_status: OracleBoostingStatus, @@ -698,14 +692,9 @@ mod test { #[test] fn test_service_provider_scheduled_tokens() { let v = get_scheduled_tokens_for_service_providers(dec!(100)); - assert_eq!(dec!(20), v, "service providers get 20%"); + assert_eq!(dec!(24), v, "service providers get 24%"); } - #[test] - fn test_oracles_scheduled_tokens() { - let v = get_scheduled_tokens_for_oracles(dec!(100)); - assert_eq!(dec!(4), v, "oracles get 4%"); - } #[test] fn test_price_conversion() { @@ -1561,7 +1550,7 @@ mod test { .round_dp_with_strategy(0, RoundingStrategy::ToZero) .to_u64() .unwrap_or(0); - assert_eq!(unallocated_sp_reward_amount, 684_931_505_850); + assert_eq!(unallocated_sp_reward_amount, 821_917_807_220); } #[test] diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index c44b57c36..d9ee27287 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -5,7 +5,7 @@ use crate::{ heartbeats::{self, HeartbeatReward}, resolve_subdao_pubkey, reward_shares::{ - self, CalculatedPocRewardShares, CoverageShares, DataTransferAndPocAllocatedRewardBuckets, + CalculatedPocRewardShares, CoverageShares, DataTransferAndPocAllocatedRewardBuckets, TransferRewards, }, service_provider::{self, ServiceProviderDCSessions, ServiceProviderPromotions}, @@ -307,9 +307,6 @@ where ) .await?; - // process rewards for oracles - reward_oracles(self.mobile_rewards.clone(), &reward_info).await?; - self.speedtest_averages.commit().await?; let written_files = self.mobile_rewards.commit().await?.await??; @@ -527,29 +524,6 @@ pub async fn reward_dc( Ok(unallocated_dc_reward_amount) } -pub async fn reward_oracles( - mobile_rewards: FileSinkClient, - reward_info: &EpochRewardInfo, -) -> anyhow::Result<()> { - // atm 100% of oracle rewards are assigned to 'unallocated' - let total_oracle_rewards = - reward_shares::get_scheduled_tokens_for_oracles(reward_info.epoch_emissions); - let allocated_oracle_rewards = 0_u64; - let unallocated_oracle_reward_amount = total_oracle_rewards - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0) - - allocated_oracle_rewards; - write_unallocated_reward( - &mobile_rewards, - UnallocatedRewardType::Oracle, - unallocated_oracle_reward_amount, - reward_info, - ) - .await?; - Ok(()) -} - pub async fn reward_service_providers( dc_sessions: ServiceProviderDCSessions, sp_promotions: ServiceProviderPromotions, diff --git a/mobile_verifier/tests/integrations/common/mod.rs b/mobile_verifier/tests/integrations/common/mod.rs index 45694a557..88ad3478f 100644 --- a/mobile_verifier/tests/integrations/common/mod.rs +++ b/mobile_verifier/tests/integrations/common/mod.rs @@ -296,13 +296,6 @@ impl MobileRewardShareMessages { .map(|reward| reward.total_poc_reward()) .sum() } - - pub fn total_sub_discovery_amount(&self) -> u64 { - self.subscriber_rewards - .iter() - .map(|reward| reward.discovery_location_amount) - .sum() - } } trait TestTimeoutExt diff --git a/mobile_verifier/tests/integrations/main.rs b/mobile_verifier/tests/integrations/main.rs index 259c0bf81..5b4c1557e 100644 --- a/mobile_verifier/tests/integrations/main.rs +++ b/mobile_verifier/tests/integrations/main.rs @@ -7,7 +7,6 @@ mod heartbeats; mod hex_boosting; mod last_location; mod modeled_coverage; -mod rewarder_oracles; mod rewarder_poc_dc; mod rewarder_sp_rewards; mod seniority; diff --git a/mobile_verifier/tests/integrations/rewarder_oracles.rs b/mobile_verifier/tests/integrations/rewarder_oracles.rs deleted file mode 100644 index 4d97e7217..000000000 --- a/mobile_verifier/tests/integrations/rewarder_oracles.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::common::{self, reward_info_24_hours}; -use helium_proto::services::poc_mobile::UnallocatedRewardType; -use mobile_verifier::{reward_shares, rewarder}; -use rust_decimal::prelude::*; -use rust_decimal_macros::dec; -use sqlx::PgPool; - -#[sqlx::test] -async fn test_oracle_rewards(_pool: PgPool) -> anyhow::Result<()> { - let (mobile_rewards_client, mobile_rewards) = common::create_file_sink(); - - let reward_info = reward_info_24_hours(); - - // run rewards for oracles - rewarder::reward_oracles(mobile_rewards_client, &reward_info).await?; - - let rewards = mobile_rewards.finish().await?; - let unallocated_reward = rewards.unallocated.first().expect("Unallocated"); - - assert_eq!( - UnallocatedRewardType::Oracle as i32, - unallocated_reward.reward_type - ); - // confirm our unallocated amount - assert_eq!(3_287_671_232_876, unallocated_reward.amount); - - // confirm the total rewards allocated matches expectations - let expected_sum = reward_shares::get_scheduled_tokens_for_oracles(reward_info.epoch_emissions) - .to_u64() - .unwrap(); - assert_eq!(expected_sum, unallocated_reward.amount); - - // confirm the rewarded percentage amount matches expectations - let percent = (Decimal::from(unallocated_reward.amount) / reward_info.epoch_emissions) - .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); - assert_eq!(percent, dec!(0.04)); - - Ok(()) -} diff --git a/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs b/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs index e140f5f05..6047863e8 100644 --- a/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs +++ b/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs @@ -96,14 +96,14 @@ async fn test_service_provider_rewards(pool: PgPool) -> anyhow::Result<()> { let rewards = mobile_rewards.finish().await?; let sp_reward = rewards.sp_rewards.first().expect("sp 1 reward"); - assert_eq!(6_000, sp_reward.amount); + assert_eq!(5_999, sp_reward.amount); let unallocated_reward = rewards.unallocated.first().expect("unallocated"); assert_eq!( UnallocatedRewardType::ServiceProvider as i32, unallocated_reward.reward_type ); - assert_eq!(16_438_356_158_383, unallocated_reward.amount); + assert_eq!(19_726_027_391_261, unallocated_reward.amount); // confirm the total rewards allocated matches expectations let expected_sum = @@ -115,7 +115,7 @@ async fn test_service_provider_rewards(pool: PgPool) -> anyhow::Result<()> { // confirm the rewarded percentage amount matches expectations let percent = (Decimal::from(unallocated_reward.amount) / reward_info.epoch_emissions) .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); - assert_eq!(percent, dec!(0.2)); + assert_eq!(percent, dec!(0.24)); Ok(()) } From 5ab958f98aee3f18bf715263041dd2a8199c7bf8 Mon Sep 17 00:00:00 2001 From: Brian Balser Date: Wed, 8 Oct 2025 13:07:44 -0400 Subject: [PATCH 4/5] Change service provider rewarder to be flat 24% and remove promotions support --- mobile_verifier/src/cli/mod.rs | 1 - mobile_verifier/src/cli/server.rs | 4 +- .../src/cli/service_provider_promotions.rs | 39 - mobile_verifier/src/main.rs | 5 +- mobile_verifier/src/reward_shares.rs | 194 +--- mobile_verifier/src/rewarder.rs | 82 +- .../src/service_provider/dc_sessions.rs | 180 ---- mobile_verifier/src/service_provider/mod.rs | 12 - .../src/service_provider/promotions.rs | 53 -- .../src/service_provider/reward.rs | 866 ------------------ .../tests/integrations/rewarder_sp_rewards.rs | 296 +----- 11 files changed, 45 insertions(+), 1687 deletions(-) delete mode 100644 mobile_verifier/src/cli/service_provider_promotions.rs delete mode 100644 mobile_verifier/src/service_provider/dc_sessions.rs delete mode 100644 mobile_verifier/src/service_provider/promotions.rs delete mode 100644 mobile_verifier/src/service_provider/reward.rs diff --git a/mobile_verifier/src/cli/mod.rs b/mobile_verifier/src/cli/mod.rs index c6ee2d024..611db001b 100644 --- a/mobile_verifier/src/cli/mod.rs +++ b/mobile_verifier/src/cli/mod.rs @@ -1,4 +1,3 @@ pub mod reward_from_db; pub mod server; -pub mod service_provider_promotions; pub mod verify_disktree; diff --git a/mobile_verifier/src/cli/server.rs b/mobile_verifier/src/cli/server.rs index f5420b0d3..64d3dd14a 100644 --- a/mobile_verifier/src/cli/server.rs +++ b/mobile_verifier/src/cli/server.rs @@ -19,7 +19,7 @@ use file_store_oracles::traits::{FileSinkCommitStrategy, FileSinkRollTime, FileS use helium_proto::services::poc_mobile::{Heartbeat, SeniorityUpdate, SpeedtestAvg}; use mobile_config::client::{ hex_boosting_client::HexBoostingClient, sub_dao_client::SubDaoClient, AuthorizationClient, - CarrierServiceClient, GatewayClient, + GatewayClient, }; use task_manager::TaskManager; @@ -42,7 +42,6 @@ impl Cmd { // mobile config clients let gateway_client = GatewayClient::from_settings(&settings.config_client)?; let auth_client = AuthorizationClient::from_settings(&settings.config_client)?; - let carrier_client = CarrierServiceClient::from_settings(&settings.config_client)?; let hex_boosting_client = HexBoostingClient::from_settings(&settings.config_client)?; let sub_dao_rewards_client = SubDaoClient::from_settings(&settings.config_client)?; @@ -173,7 +172,6 @@ impl Cmd { pool, settings, file_upload, - carrier_client, hex_boosting_client, sub_dao_rewards_client, speedtests_avg, diff --git a/mobile_verifier/src/cli/service_provider_promotions.rs b/mobile_verifier/src/cli/service_provider_promotions.rs deleted file mode 100644 index 6c00042c8..000000000 --- a/mobile_verifier/src/cli/service_provider_promotions.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::{service_provider, Settings}; -use anyhow::Result; -use chrono::{DateTime, Utc}; -use mobile_config::client::CarrierServiceClient; - -#[derive(Debug, clap::Args)] -pub struct Cmd { - #[clap(long)] - start: Option>, -} - -impl Cmd { - pub async fn run(self, settings: &Settings) -> Result<()> { - let epoch_start = match self.start { - Some(dt) => dt, - None => Utc::now(), - }; - - let carrier_client = CarrierServiceClient::from_settings(&settings.config_client)?; - let promos = service_provider::get_promotions(&carrier_client, &epoch_start).await?; - - println!("Promotions as of {epoch_start}"); - for sp in promos.into_proto() { - println!("Service Provider: {:?}", sp.service_provider()); - println!(" incentive_escrow_bps: {:?}", sp.incentive_escrow_fund_bps); - println!(" Promotions: ({})", sp.promotions.len()); - for promo in sp.promotions { - let start = DateTime::from_timestamp(promo.start_ts as i64, 0).unwrap(); - let end = DateTime::from_timestamp(promo.end_ts as i64, 0).unwrap(); - let duration = humantime::format_duration((end - start).to_std()?); - println!(" name: {}", promo.entity); - println!(" duration: {duration} ({start:?} -> {end:?})",); - println!(" shares: {}", promo.shares); - } - } - - Ok(()) - } -} diff --git a/mobile_verifier/src/main.rs b/mobile_verifier/src/main.rs index 1c8eb5fef..aa7de1105 100644 --- a/mobile_verifier/src/main.rs +++ b/mobile_verifier/src/main.rs @@ -1,7 +1,7 @@ use anyhow::Result; use clap::Parser; use mobile_verifier::{ - cli::{reward_from_db, server, service_provider_promotions, verify_disktree}, + cli::{reward_from_db, server, verify_disktree}, Settings, }; use std::path; @@ -38,8 +38,6 @@ pub enum Cmd { /// Go through every cell and ensure it's value can be turned into an Assignment. /// NOTE: This can take a very long time. Run with a --release binary. VerifyDisktree(verify_disktree::Cmd), - /// Print active Service Provider Promotions - ServiceProviderPromotions(service_provider_promotions::Cmd), } impl Cmd { @@ -48,7 +46,6 @@ impl Cmd { Self::Server(cmd) => cmd.run(&settings).await, Self::RewardFromDb(cmd) => cmd.run(&settings).await, Self::VerifyDisktree(cmd) => cmd.run(&settings).await, - Self::ServiceProviderPromotions(cmd) => cmd.run(&settings).await, } } } diff --git a/mobile_verifier/src/reward_shares.rs b/mobile_verifier/src/reward_shares.rs index 6002d943c..8345daadd 100644 --- a/mobile_verifier/src/reward_shares.rs +++ b/mobile_verifier/src/reward_shares.rs @@ -17,9 +17,7 @@ use coverage_point_calculator::{ use file_store::traits::TimestampEncode; use futures::{Stream, StreamExt}; use helium_crypto::PublicKeyBinary; -use helium_proto::services::{ - poc_mobile as proto, -}; +use helium_proto::services::poc_mobile as proto; use mobile_config::{boosted_hex_info::BoostedHexes, sub_dao_epoch_reward_info::EpochRewardInfo}; use radio_reward_v2::{RadioRewardV2Ext, ToProtoDecimal}; use rust_decimal::prelude::*; @@ -546,12 +544,10 @@ pub fn get_scheduled_tokens_for_poc(total_emission_pool: Decimal) -> Decimal { total_emission_pool * poc_percent } - pub fn get_scheduled_tokens_for_service_providers(total_emission_pool: Decimal) -> Decimal { total_emission_pool * SERVICE_PROVIDER_PERCENT } - fn eligible_for_coverage_map( oracle_boosting_status: OracleBoostingStatus, speedtests: &[Speedtest], @@ -590,18 +586,13 @@ mod test { coverage::{CoveredHexStream, HexCoverage}, data_session::{self, HotspotDataSession, HotspotReward}, heartbeats::{HeartbeatReward, KeyType, OwnedKeyType}, - service_provider::{ - self, ServiceProviderDCSessions, ServiceProviderPromotions, ServiceProviderRewardInfos, - }, speedtests::Speedtest, speedtests_average::SpeedtestAverage, }; use chrono::{Duration, Utc}; use file_store_oracles::speedtest::CellSpeedtest; use futures::stream::{self, BoxStream}; - use helium_proto::{ - services::poc_mobile::mobile_reward_share::Reward as MobileReward, ServiceProvider, - }; + use helium_proto::services::poc_mobile::mobile_reward_share::Reward as MobileReward; use hextree::Cell; use solana::Token; use std::collections::HashMap; @@ -647,11 +638,6 @@ mod test { assert_eq!(dc_to_hnt_bones(Decimal::from(2), dec!(1.0)), dec!(0.00002)); } - fn hnt_bones_to_dc(hnt_bones_amount: Decimal, hnt_bones_price: Decimal) -> Decimal { - let hnt_value = hnt_bones_amount * hnt_bones_price; - (hnt_value / DC_USD_PRICE).round_dp_with_strategy(0, RoundingStrategy::ToNegativeInfinity) - } - fn rewards_info_1_hour() -> EpochRewardInfo { let now = Utc::now(); let epoch_duration = Duration::hours(1); @@ -688,14 +674,12 @@ mod test { assert_eq!(dec!(70), v, "poc gets 70%"); } - #[test] fn test_service_provider_scheduled_tokens() { let v = get_scheduled_tokens_for_service_providers(dec!(100)); assert_eq!(dec!(24), v, "service providers get 24%"); } - #[test] fn test_price_conversion() { let token = Token::Hnt; @@ -1511,178 +1495,4 @@ mod test { .into_rewards(reward_shares, &rewards_info.epoch_period) .is_none()); } - - #[test] - fn service_provider_reward_amounts() { - let hnt_bone_price = dec!(0.00001); - - let sp1 = ServiceProvider::HeliumMobile; - - let rewards_info = rewards_info_1_hour(); - - let total_sp_rewards = service_provider::get_scheduled_tokens(rewards_info.epoch_emissions); - let sp_reward_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(sp1, dec!(1000))]), - ServiceProviderPromotions::default(), - total_sp_rewards, - hnt_bone_price, - rewards_info.clone(), - ); - - let mut sp_rewards = HashMap::::new(); - let mut allocated_sp_rewards = 0_u64; - - for (reward_amount, reward) in sp_reward_infos.iter_rewards() { - if let Some(MobileReward::ServiceProviderReward(r)) = reward.reward { - sp_rewards.insert(r.service_provider_id, r.amount); - assert_eq!(reward_amount, r.amount); - allocated_sp_rewards += reward_amount; - } - } - - let sp1_reward_amount = *sp_rewards - .get(&(sp1 as i32)) - .expect("Could not fetch sp1 shares"); - assert_eq!(sp1_reward_amount, 999); - - // confirm the unallocated service provider reward amounts - let unallocated_sp_reward_amount = (total_sp_rewards - Decimal::from(allocated_sp_rewards)) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0); - assert_eq!(unallocated_sp_reward_amount, 821_917_807_220); - } - - #[test] - fn service_provider_reward_amounts_capped() { - let hnt_bone_price = dec!(1.0); - let sp1 = ServiceProvider::HeliumMobile; - - let rewards_info = rewards_info_1_hour(); - - let total_sp_rewards_in_bones = dec!(1_0000_0000); - let total_rewards_value_in_dc = hnt_bones_to_dc(total_sp_rewards_in_bones, hnt_bone_price); - - let sp_reward_infos = ServiceProviderRewardInfos::new( - // force the service provider to have spend more DC than total rewardable - ServiceProviderDCSessions::from([(sp1, total_rewards_value_in_dc * dec!(2.0))]), - ServiceProviderPromotions::default(), - total_sp_rewards_in_bones, - hnt_bone_price, - rewards_info.clone(), - ); - - let mut sp_rewards = HashMap::new(); - let mut allocated_sp_rewards = 0_u64; - - for (reward_amount, reward) in sp_reward_infos.iter_rewards() { - if let Some(MobileReward::ServiceProviderReward(r)) = reward.reward { - sp_rewards.insert(r.service_provider_id, r.amount); - assert_eq!(reward_amount, r.amount); - allocated_sp_rewards += reward_amount; - } - } - - let sp1_reward_amount = *sp_rewards - .get(&(sp1 as i32)) - .expect("Could not fetch sp1 shares"); - - assert_eq!(Decimal::from(sp1_reward_amount), total_sp_rewards_in_bones); - assert_eq!(sp1_reward_amount, 1_0000_0000); - - // confirm the unallocated service provider reward amounts - let unallocated_sp_reward_amount = (total_sp_rewards_in_bones - - Decimal::from(allocated_sp_rewards)) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0); - assert_eq!(unallocated_sp_reward_amount, 0); - } - - #[test] - fn service_provider_reward_hip87_ex1() { - // price from hip example and converted to bones - let hnt_bone_price = dec!(0.0001) / dec!(1_0000_0000); - let sp1 = ServiceProvider::HeliumMobile; - - let rewards_info = rewards_info_1_hour(); - - let total_sp_rewards_in_bones = dec!(500_000_000) * dec!(1_0000_0000); - - let sp_reward_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(sp1, dec!(1_0000_0000))]), - ServiceProviderPromotions::default(), - total_sp_rewards_in_bones, - hnt_bone_price, - rewards_info.clone(), - ); - - let mut sp_rewards = HashMap::new(); - let mut allocated_sp_rewards = 0_u64; - - for (reward_amount, reward) in sp_reward_infos.iter_rewards() { - if let Some(MobileReward::ServiceProviderReward(r)) = reward.reward { - sp_rewards.insert(r.service_provider_id, r.amount); - assert_eq!(reward_amount, r.amount); - allocated_sp_rewards += reward_amount; - } - } - - let sp1_reward_amount_in_bones = *sp_rewards - .get(&(sp1 as i32)) - .expect("Could not fetch sp1 shares"); - // assert expected value in bones - assert_eq!(sp1_reward_amount_in_bones, 10_000_000 * 1_0000_0000); - - // confirm the unallocated service provider reward amounts - let unallocated_sp_reward_amount = (total_sp_rewards_in_bones - - Decimal::from(allocated_sp_rewards)) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0); - assert_eq!(unallocated_sp_reward_amount, 49_000_000_000_000_000); - } - - #[test] - fn service_provider_reward_hip87_ex2() { - // price from hip example and converted to bones - let hnt_bone_price = dec!(0.0001) / dec!(1_0000_0000); - let sp1 = ServiceProvider::HeliumMobile; - - let rewards_info = rewards_info_1_hour(); - let total_sp_rewards_in_bones = dec!(500_000_000) * dec!(1_0000_0000); - - let sp_reward_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(sp1, dec!(100_000_000_000))]), - ServiceProviderPromotions::default(), - total_sp_rewards_in_bones, - hnt_bone_price, - rewards_info.clone(), - ); - - let mut sp_rewards = HashMap::new(); - let mut allocated_sp_rewards = 0_u64; - - for (reward_amount, reward) in sp_reward_infos.iter_rewards() { - if let Some(MobileReward::ServiceProviderReward(r)) = reward.reward { - sp_rewards.insert(r.service_provider_id, r.amount); - assert_eq!(reward_amount, r.amount); - allocated_sp_rewards += reward_amount; - } - } - - let sp1_reward_amount_in_bones = *sp_rewards - .get(&(sp1 as i32)) - .expect("Could not fetch sp1 shares"); - // assert expected value in bones - assert_eq!(sp1_reward_amount_in_bones, 500_000_000 * 1_0000_0000); - - // confirm the unallocated service provider reward amounts - let unallocated_sp_reward_amount = (total_sp_rewards_in_bones - - Decimal::from(allocated_sp_rewards)) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0); - assert_eq!(unallocated_sp_reward_amount, 0); - } } diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index d9ee27287..1c22698fc 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -8,8 +8,7 @@ use crate::{ CalculatedPocRewardShares, CoverageShares, DataTransferAndPocAllocatedRewardBuckets, TransferRewards, }, - service_provider::{self, ServiceProviderDCSessions, ServiceProviderPromotions}, - speedtests, + service_provider, speedtests, speedtests_average::SpeedtestAverages, telemetry, unique_connections, PriceInfo, Settings, }; @@ -27,13 +26,12 @@ use helium_proto::{ UnallocatedReward, UnallocatedRewardType, }, MobileRewardData as ManifestMobileRewardData, MobileRewardToken, RewardManifest, + ServiceProvider, }; use mobile_config::{ boosted_hex_info::BoostedHexes, client::{ - carrier_service_client::CarrierServiceVerifier, - hex_boosting_client::HexBoostingInfoResolver, - sub_dao_client::SubDaoEpochRewardInfoResolver, + hex_boosting_client::HexBoostingInfoResolver, sub_dao_client::SubDaoEpochRewardInfoResolver, }, sub_dao_epoch_reward_info::EpochRewardInfo, EpochInfo, @@ -52,10 +50,9 @@ mod db; const REWARDS_NOT_CURRENT_DELAY_PERIOD: i64 = 5; -pub struct Rewarder { +pub struct Rewarder { sub_dao: SolPubkey, pool: Pool, - carrier_client: A, hex_service_client: B, sub_dao_epoch_reward_client: C, reward_period_duration: Duration, @@ -66,9 +63,8 @@ pub struct Rewarder { speedtest_averages: FileSinkClient, } -impl Rewarder +impl Rewarder where - A: CarrierServiceVerifier + 'static, B: HexBoostingInfoResolver, C: SubDaoEpochRewardInfoResolver, { @@ -77,7 +73,6 @@ where pool: Pool, settings: &Settings, file_upload: FileUpload, - carrier_service_verifier: A, hex_boosting_info_resolver: B, sub_dao_epoch_reward_info_resolver: C, speedtests_avg: FileSinkClient, @@ -104,7 +99,6 @@ where let rewarder = Rewarder::new( pool.clone(), - carrier_service_verifier, hex_boosting_info_resolver, sub_dao_epoch_reward_info_resolver, settings.reward_period, @@ -126,7 +120,6 @@ where #[allow(clippy::too_many_arguments)] pub fn new( pool: Pool, - carrier_client: A, hex_service_client: B, sub_dao_epoch_reward_client: C, reward_period_duration: Duration, @@ -143,7 +136,6 @@ where Ok(Self { sub_dao, pool, - carrier_client, hex_service_client, sub_dao_epoch_reward_client, reward_period_duration, @@ -289,23 +281,7 @@ where .await?; // process rewards for service providers - let dc_sessions = service_provider::get_dc_sessions( - &self.pool, - &self.carrier_client, - &reward_info.epoch_period, - ) - .await?; - let sp_promotions = - service_provider::get_promotions(&self.carrier_client, &reward_info.epoch_period.start) - .await?; - reward_service_providers( - dc_sessions, - sp_promotions.clone(), - self.mobile_rewards.clone(), - &reward_info, - price_info.price_per_bone, - ) - .await?; + reward_service_providers(self.mobile_rewards.clone(), &reward_info).await?; self.speedtest_averages.commit().await?; let written_files = self.mobile_rewards.commit().await?.await??; @@ -335,7 +311,7 @@ where boosted_poc_bones_per_reward_share: Some(helium_proto::Decimal { value: poc_dc_shares.boost.to_string(), }), - service_provider_promotions: sp_promotions.into_proto(), + service_provider_promotions: vec![], token: MobileRewardToken::Hnt as i32, }; self.reward_manifests @@ -359,9 +335,8 @@ where } } -impl ManagedTask for Rewarder +impl ManagedTask for Rewarder where - A: CarrierServiceVerifier, B: HexBoostingInfoResolver, C: SubDaoEpochRewardInfoResolver, { @@ -525,42 +500,27 @@ pub async fn reward_dc( } pub async fn reward_service_providers( - dc_sessions: ServiceProviderDCSessions, - sp_promotions: ServiceProviderPromotions, mobile_rewards: FileSinkClient, reward_info: &EpochRewardInfo, - hnt_bone_price: Decimal, ) -> anyhow::Result<()> { - use service_provider::ServiceProviderRewardInfos; - let total_sp_rewards = service_provider::get_scheduled_tokens(reward_info.epoch_emissions); - - let sps = ServiceProviderRewardInfos::new( - dc_sessions, - sp_promotions, - total_sp_rewards, - hnt_bone_price, - reward_info.clone(), - ); - - let mut unallocated_sp_rewards = total_sp_rewards + let sp_reward_amount = total_sp_rewards .round_dp_with_strategy(0, RoundingStrategy::ToZero) .to_u64() .unwrap_or(0); - for (amount, reward) in sps.iter_rewards() { - unallocated_sp_rewards -= amount; - mobile_rewards.write(reward, []).await?.await??; - } - - // write out any unallocated service provider reward - write_unallocated_reward( - &mobile_rewards, - UnallocatedRewardType::ServiceProvider, - unallocated_sp_rewards, - reward_info, - ) - .await?; + // Write a single ServiceProviderReward for HeliumMobile with the full 24% allocation + let sp_reward = proto::MobileRewardShare { + start_period: reward_info.epoch_period.start.encode_timestamp(), + end_period: reward_info.epoch_period.end.encode_timestamp(), + reward: Some(ProtoReward::ServiceProviderReward( + proto::ServiceProviderReward { + service_provider_id: ServiceProvider::HeliumMobile as i32, + amount: sp_reward_amount, + }, + )), + }; + mobile_rewards.write(sp_reward, []).await?.await??; Ok(()) } diff --git a/mobile_verifier/src/service_provider/dc_sessions.rs b/mobile_verifier/src/service_provider/dc_sessions.rs deleted file mode 100644 index 86bda0ba8..000000000 --- a/mobile_verifier/src/service_provider/dc_sessions.rs +++ /dev/null @@ -1,180 +0,0 @@ -use std::{collections::HashMap, ops::Range}; - -use chrono::{DateTime, Utc}; -use mobile_config::client::carrier_service_client::CarrierServiceVerifier; -use rust_decimal::{Decimal, RoundingStrategy}; -use sqlx::PgPool; - -use crate::{ - data_session, - reward_shares::{dc_to_hnt_bones, DEFAULT_PREC}, -}; - -use super::ServiceProviderId; - -pub async fn get_dc_sessions( - pool: &PgPool, - carrier_client: &impl CarrierServiceVerifier, - reward_period: &Range>, -) -> anyhow::Result { - let payer_dc_sessions = - data_session::sum_data_sessions_to_dc_by_payer(pool, reward_period).await?; - - let mut dc_sessions = ServiceProviderDCSessions::default(); - for (payer_key, dc_amount) in payer_dc_sessions { - let service_provider = carrier_client - .payer_key_to_service_provider(&payer_key) - .await?; - dc_sessions.insert( - service_provider as ServiceProviderId, - Decimal::from(dc_amount), - ); - } - - Ok(dc_sessions) -} - -#[derive(Debug, Default)] -pub struct ServiceProviderDCSessions(pub(crate) HashMap); - -impl ServiceProviderDCSessions { - pub fn insert(&mut self, service_provider: ServiceProviderId, dc: Decimal) { - *self.0.entry(service_provider).or_insert(Decimal::ZERO) += dc; - } - - pub fn all_transfer(&self) -> Decimal { - self.0.values().sum() - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().map(|(k, v)| (*k, *v)) - } - - pub fn rewards_per_share( - &self, - total_sp_rewards: Decimal, - mobile_bone_price: Decimal, - ) -> anyhow::Result { - // the total amount of DC spent across all service providers - let total_sp_dc = self.all_transfer(); - // the total amount of service provider rewards in bones based on the spent DC - let total_sp_rewards_used = dc_to_hnt_bones(total_sp_dc, mobile_bone_price); - // cap the service provider rewards if used > pool total - let capped_sp_rewards_used = - Self::maybe_cap_service_provider_rewards(total_sp_rewards_used, total_sp_rewards); - Ok(Self::calc_rewards_per_share( - capped_sp_rewards_used, - total_sp_dc, - )) - } - - fn maybe_cap_service_provider_rewards( - total_sp_rewards_used: Decimal, - total_sp_rewards: Decimal, - ) -> Decimal { - match total_sp_rewards_used <= total_sp_rewards { - true => total_sp_rewards_used, - false => total_sp_rewards, - } - } - - fn calc_rewards_per_share(total_rewards: Decimal, total_shares: Decimal) -> Decimal { - if total_shares > Decimal::ZERO { - (total_rewards / total_shares) - .round_dp_with_strategy(DEFAULT_PREC, RoundingStrategy::MidpointNearestEven) - } else { - Decimal::ZERO - } - } -} - -impl From for ServiceProviderDCSessions -where - F: IntoIterator, - I: Into, -{ - fn from(iter: F) -> Self { - let mut me = Self::default(); - for (k, v) in iter { - me.insert(k.into(), v); - } - me - } -} - -#[cfg(test)] -pub mod tests { - - use chrono::Duration; - use helium_proto::{ServiceProvider, ServiceProviderPromotions}; - use mobile_config::client::ClientError; - - use crate::data_session::HotspotDataSession; - - use super::*; - - impl ServiceProviderDCSessions { - fn len(&self) -> usize { - self.0.len() - } - } - - #[sqlx::test] - fn multiple_payer_keys_accumulate_to_service_provider(pool: PgPool) -> anyhow::Result<()> { - // Client always resolves to single service provider no matter payer key - struct MockClient; - - #[async_trait::async_trait] - impl CarrierServiceVerifier for MockClient { - async fn payer_key_to_service_provider( - &self, - _pubkey: &str, - ) -> Result { - Ok(ServiceProvider::HeliumMobile) - } - - async fn list_incentive_promotions( - &self, - _epoch_start: &DateTime, - ) -> Result, ClientError> { - Ok(vec![]) - } - } - - // Save multiple data sessions with different payers - let one = HotspotDataSession { - pub_key: vec![0].into(), - payer: vec![0].into(), - upload_bytes: 1_000, - download_bytes: 1_000, - rewardable_bytes: 3_000, - num_dcs: 2_000, - received_timestamp: Utc::now(), - burn_timestamp: Utc::now(), - }; - let two = HotspotDataSession { - pub_key: vec![1].into(), - payer: vec![1].into(), - upload_bytes: 1_000, - download_bytes: 1_000, - rewardable_bytes: 3_000, - num_dcs: 2_000, - received_timestamp: Utc::now(), - burn_timestamp: Utc::now(), - }; - let mut txn = pool.begin().await?; - one.save(&mut txn).await?; - two.save(&mut txn).await?; - txn.commit().await?; - - let now = Utc::now(); - let epoch = now - Duration::hours(24)..now; - - // dc sessions should represent single payer, and all dc is combined - let map = get_dc_sessions(&pool, &MockClient, &epoch).await?; - assert_eq!(map.len(), 1); - assert_eq!(map.all_transfer(), Decimal::from(4_000)); - - Ok(()) - } -} diff --git a/mobile_verifier/src/service_provider/mod.rs b/mobile_verifier/src/service_provider/mod.rs index ac0670aaa..bfc9c8132 100644 --- a/mobile_verifier/src/service_provider/mod.rs +++ b/mobile_verifier/src/service_provider/mod.rs @@ -1,17 +1,5 @@ -pub use dc_sessions::{get_dc_sessions, ServiceProviderDCSessions}; -pub use promotions::{get_promotions, ServiceProviderPromotions}; -pub use reward::ServiceProviderRewardInfos; use rust_decimal::Decimal; -mod dc_sessions; -mod promotions; -mod reward; - -// This type is used in lieu of the helium_proto::ServiceProvider enum so we can -// handle more than a single value without adding a hard deploy dependency to -// mobile-verifier when a new carrier is added.. -pub type ServiceProviderId = i32; - pub fn get_scheduled_tokens(total_emission_pool: Decimal) -> Decimal { crate::reward_shares::get_scheduled_tokens_for_service_providers(total_emission_pool) } diff --git a/mobile_verifier/src/service_provider/promotions.rs b/mobile_verifier/src/service_provider/promotions.rs deleted file mode 100644 index bf332dc1b..000000000 --- a/mobile_verifier/src/service_provider/promotions.rs +++ /dev/null @@ -1,53 +0,0 @@ -use chrono::{DateTime, Utc}; -use mobile_config::client::carrier_service_client::CarrierServiceVerifier; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; - -use crate::service_provider::ServiceProviderId; - -mod proto { - pub use helium_proto::{service_provider_promotions::Promotion, ServiceProviderPromotions}; -} - -pub async fn get_promotions( - client: &impl CarrierServiceVerifier, - epoch_start: &DateTime, -) -> anyhow::Result { - let promos = client.list_incentive_promotions(epoch_start).await?; - Ok(ServiceProviderPromotions(promos)) -} - -#[derive(Debug, Default, Clone)] -pub struct ServiceProviderPromotions(Vec); - -impl ServiceProviderPromotions { - pub fn into_proto(self) -> Vec { - self.0 - } - - pub(crate) fn get_fund_percent(&self, sp_id: ServiceProviderId) -> Decimal { - for promo in &self.0 { - if promo.service_provider == sp_id { - return Decimal::from(promo.incentive_escrow_fund_bps) / dec!(10_000); - } - } - - dec!(0) - } - - pub(crate) fn get_active_promotions(&self, sp_id: ServiceProviderId) -> Vec { - for promo in &self.0 { - if promo.service_provider == sp_id { - return promo.promotions.clone(); - } - } - - vec![] - } -} - -impl From> for ServiceProviderPromotions { - fn from(value: Vec) -> Self { - Self(value) - } -} diff --git a/mobile_verifier/src/service_provider/reward.rs b/mobile_verifier/src/service_provider/reward.rs deleted file mode 100644 index b4e9d9955..000000000 --- a/mobile_verifier/src/service_provider/reward.rs +++ /dev/null @@ -1,866 +0,0 @@ -use crate::reward_shares::dc_to_hnt_bones; -use file_store::traits::TimestampEncode; -use mobile_config::sub_dao_epoch_reward_info::EpochRewardInfo; -use rust_decimal::{Decimal, RoundingStrategy}; -use rust_decimal_macros::dec; - -use super::{dc_sessions::ServiceProviderDCSessions, promotions::ServiceProviderPromotions}; - -mod proto { - pub use helium_proto::{ - service_provider_promotions::Promotion, - services::poc_mobile::{ - mobile_reward_share::Reward, MobileRewardShare, PromotionReward, ServiceProviderReward, - }, - }; -} - -/// Container for all Service Provider rewarding -#[derive(Debug)] -pub struct ServiceProviderRewardInfos { - coll: Vec, - total_sp_allocation: Decimal, - reward_info: EpochRewardInfo, -} - -// Represents a single Service Providers information for rewarding, -// only used internally. -#[derive(Debug, Clone, PartialEq)] -struct RewardInfo { - // proto::ServiceProvider enum repr - sp_id: i32, - - // Total DC transferred for reward epoch in Bones - bones: Decimal, - // % of total allocated rewards for data transfer - data_perc: Decimal, - // % allocated from DC to promo rewards (found in db from file from on chain) - allocated_promo_perc: Decimal, - - // % of total allocated rewards going towards promotions - realized_promo_perc: Decimal, - // % of total allocated rewards awarded for data transfer - realized_data_perc: Decimal, - // % matched promotions from unallocated, can never exceed realized_promo_perc - matched_promo_perc: Decimal, - - // Active promotions for the epoch - promotions: Vec, -} - -impl ServiceProviderRewardInfos { - pub fn new( - dc_sessions: ServiceProviderDCSessions, - promotions: ServiceProviderPromotions, - total_sp_allocation: Decimal, // Bones - hnt_bone_price: Decimal, // Price in Bones - reward_info: EpochRewardInfo, - ) -> Self { - let all_transfer = dc_sessions.all_transfer(); // DC - - let mut me = Self { - coll: vec![], - total_sp_allocation, - reward_info, - }; - - // After this point, we enter percentage land. This number is the basis - // for all percentages, our 100%. If the DC transferred in Bones is - // greater than the amount of Bones allocated for rewarding, we use the - // greater number; to not exceed 100% allocation. - // - // When rewards are output, the percentages are taken from the allocated - // Bones for service providers. Which has the effect of scaling the rewards. - let used_allocation = - total_sp_allocation.max(dc_to_hnt_bones(all_transfer, hnt_bone_price)); - - for (service_provider, dc_transfer) in dc_sessions.iter() { - let promo_fund_perc = promotions.get_fund_percent(service_provider); - let promos = promotions.get_active_promotions(service_provider); - - me.coll.push(RewardInfo::new( - service_provider, - dc_to_hnt_bones(dc_transfer, hnt_bone_price), - promo_fund_perc, - used_allocation, - promos, - )); - } - - me.coll.sort_by_key(|x| x.sp_id); - - distribute_unallocated(&mut me.coll); - - me - } - - pub fn iter_rewards(&self) -> Vec<(u64, proto::MobileRewardShare)> { - self.coll - .iter() - .flat_map(|sp| sp.iter_rewards(self.total_sp_allocation, &self.reward_info)) - .filter(|(amount, _r)| *amount > 0) - .collect::>() - } -} - -impl RewardInfo { - fn new( - sp_id: i32, - bones_transfer: Decimal, // Bones - promo_fund_perc: Decimal, - total_sp_allocation: Decimal, // Bones - promotions: Vec, - ) -> Self { - let data_perc = bones_transfer / total_sp_allocation; - let realized_promo_perc = if promotions.is_empty() { - dec!(0) - } else { - data_perc * promo_fund_perc - }; - let realized_dc_perc = data_perc - realized_promo_perc; - - Self { - sp_id, - bones: bones_transfer, - allocated_promo_perc: promo_fund_perc, - - data_perc, - realized_promo_perc, - realized_data_perc: realized_dc_perc, - matched_promo_perc: dec!(0), - - promotions, - } - } - - pub fn iter_rewards( - &self, - total_allocation: Decimal, - reward_info: &EpochRewardInfo, - ) -> Vec<(u64, proto::MobileRewardShare)> { - let mut rewards = self.promo_rewards(total_allocation, reward_info); - rewards.push(self.carrier_reward(total_allocation, reward_info)); - rewards - } - - pub fn carrier_reward( - &self, - total_allocation: Decimal, - reward_info: &EpochRewardInfo, - ) -> (u64, proto::MobileRewardShare) { - let amount = (total_allocation * self.realized_data_perc).to_u64_floored(); // Rewarded BONES - - ( - amount, - proto::MobileRewardShare { - start_period: reward_info.epoch_period.start.encode_timestamp(), - end_period: reward_info.epoch_period.end.encode_timestamp(), - reward: Some(proto::Reward::ServiceProviderReward( - proto::ServiceProviderReward { - service_provider_id: self.sp_id, - amount, - }, - )), - }, - ) - } - - pub fn promo_rewards( - &self, - total_allocation: Decimal, - reward_info: &EpochRewardInfo, - ) -> Vec<(u64, proto::MobileRewardShare)> { - if self.promotions.is_empty() { - return vec![]; - } - - let mut rewards = vec![]; - - let sp_amount = total_allocation * self.realized_promo_perc; - let matched_amount = total_allocation * self.matched_promo_perc; - - let total_shares = self - .promotions - .iter() - .map(|x| Decimal::from(x.shares)) - .sum::(); - let sp_amount_per_share = sp_amount / total_shares; - let matched_amount_per_share = matched_amount / total_shares; - - for r in self.promotions.iter() { - let shares = Decimal::from(r.shares); - - let service_provider_amount = (sp_amount_per_share * shares).to_u64_floored(); - let matched_amount = (matched_amount_per_share * shares).to_u64_floored(); - - let total_amount = service_provider_amount + matched_amount; - - rewards.push(( - total_amount, - proto::MobileRewardShare { - start_period: reward_info.epoch_period.start.encode_timestamp(), - end_period: reward_info.epoch_period.end.encode_timestamp(), - reward: Some(proto::Reward::PromotionReward(proto::PromotionReward { - service_provider_amount, - matched_amount, - entity: r.entity.to_owned(), - })), - }, - )) - } - - rewards - } -} - -fn distribute_unallocated(coll: &mut [RewardInfo]) { - let allocated_perc = coll.iter().map(|x| x.data_perc).sum::(); - let unallocated_perc = dec!(1) - allocated_perc; - - let maybe_matching_perc = coll - .iter() - .filter(|x| !x.promotions.is_empty()) - .map(|x| x.realized_promo_perc) - .sum::(); - - if maybe_matching_perc > unallocated_perc { - distribute_unalloc_over_limit(coll, unallocated_perc); - } else { - distribute_unalloc_under_limit(coll); - } -} - -fn distribute_unalloc_over_limit(coll: &mut [RewardInfo], unallocated_perc: Decimal) { - // NOTE: This can also allocate based off the dc_perc of each carrier. - let total = coll.iter().map(|x| x.realized_promo_perc).sum::() * dec!(100); - - for sp in coll.iter_mut() { - if sp.promotions.is_empty() { - continue; - } - let shares = sp.realized_promo_perc * dec!(100); - sp.matched_promo_perc = (shares / total) * unallocated_perc; - } -} - -fn distribute_unalloc_under_limit(coll: &mut [RewardInfo]) { - for sp in coll.iter_mut() { - if sp.promotions.is_empty() { - continue; - } - sp.matched_promo_perc = sp.realized_promo_perc - } -} - -trait DecimalRoundingExt { - fn to_u64_floored(&self) -> u64; -} - -impl DecimalRoundingExt for Decimal { - fn to_u64_floored(&self) -> u64 { - use rust_decimal::prelude::ToPrimitive; - - self.round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0) - } -} - -#[cfg(test)] -mod tests { - use chrono::{Duration, Utc}; - use helium_proto::services::poc_mobile::{MobileRewardShare, PromotionReward}; - - use crate::service_provider; - - use super::*; - - pub const EPOCH_ADDRESS: &str = "112E7TxoNHV46M6tiPA8N1MkeMeQxc9ztb4JQLXBVAAUfq1kJLoF"; - pub const SUB_DAO_ADDRESS: &str = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6"; - - pub fn rewards_info_24_hours() -> EpochRewardInfo { - let now = Utc::now(); - let epoch_duration = Duration::hours(24); - EpochRewardInfo { - epoch_day: 1, - epoch_address: EPOCH_ADDRESS.into(), - sub_dao_address: SUB_DAO_ADDRESS.into(), - epoch_period: (now - epoch_duration)..now, - epoch_emissions: dec!(82_191_780_821_917), - rewards_issued_at: now, - } - } - - #[test] - fn no_promotions() { - let reward_info = rewards_info_24_hours(); - - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, dec!(12)), (1, dec!(6))]), - ServiceProviderPromotions::default(), - dec!(100), - dec!(0.00001), - reward_info, - ); - - let mut iter = sp_infos.iter_rewards().into_iter(); - - let sp_1 = iter.next().unwrap().1.sp_reward(); - let sp_2 = iter.next().unwrap().1.sp_reward(); - - assert_eq!(sp_1.amount, 12); - assert_eq!(sp_2.amount, 6); - - assert_eq!(None, iter.next()); - } - - #[test] - fn unallocated_reward_scaling_1() { - let reward_info = rewards_info_24_hours(); - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, dec!(12)), (1, dec!(6))]), - ServiceProviderPromotions::from(vec![ - make_test_promotion(0, "promo-0", 5000, 1), - make_test_promotion(1, "promo-1", 5000, 1), - ]), - dec!(100), - dec!(0.00001), - reward_info, - ); - - let (promo_1, sp_1) = sp_infos.single_sp_rewards(0); - assert_eq!(promo_1.service_provider_amount, 6); - assert_eq!(promo_1.matched_amount, 6); - assert_eq!(sp_1.amount, 6); - - let (promo_2, sp_2) = sp_infos.single_sp_rewards(1); - assert_eq!(promo_2.service_provider_amount, 3); - assert_eq!(promo_2.matched_amount, 3); - assert_eq!(sp_2.amount, 3); - } - - #[test] - fn unallocated_reward_scaling_2() { - let reward_info = rewards_info_24_hours(); - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, dec!(12)), (1, dec!(6))]), - ServiceProviderPromotions::from(vec![ - helium_proto::ServiceProviderPromotions { - service_provider: 0, - incentive_escrow_fund_bps: 5000, - promotions: vec![helium_proto::service_provider_promotions::Promotion { - entity: "promo-0".to_string(), - shares: 1, - ..Default::default() - }], - }, - helium_proto::ServiceProviderPromotions { - service_provider: 1, - incentive_escrow_fund_bps: 10000, - promotions: vec![helium_proto::service_provider_promotions::Promotion { - entity: "promo-1".to_string(), - shares: 1, - ..Default::default() - }], - }, - ]), - dec!(100), - dec!(0.00001), - reward_info, - ); - - let (promo_1, sp_1) = sp_infos.single_sp_rewards(0); - assert_eq!(promo_1.service_provider_amount, 6); - assert_eq!(promo_1.matched_amount, 6); - assert_eq!(sp_1.amount, 6); - - let (promo_2, sp_2) = sp_infos.single_sp_rewards(1); - assert_eq!(promo_2.service_provider_amount, 6); - assert_eq!(promo_2.matched_amount, 6); - assert_eq!(sp_2.amount, 0); - } - - #[test] - fn unallocated_reward_scaling_3() { - let reward_info = rewards_info_24_hours(); - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, dec!(10)), (1, dec!(1000))]), - ServiceProviderPromotions::from(vec![ - make_test_promotion(0, "promo-0", 10000, 1), - make_test_promotion(1, "promo-1", 200, 1), - ]), - dec!(2000), - dec!(0.00001), - reward_info, - ); - - let (promo_1, sp_1) = sp_infos.single_sp_rewards(0); - assert_eq!(promo_1.service_provider_amount, 10); - assert_eq!(promo_1.matched_amount, 10); - assert_eq!(sp_1.amount, 0); - - let (promo_2, sp_2) = sp_infos.single_sp_rewards(1); - assert_eq!(promo_2.service_provider_amount, 20); - assert_eq!(promo_2.matched_amount, 20); - assert_eq!(sp_2.amount, 980); - } - - #[test] - fn no_rewards_if_none_allocated() { - let reward_info = rewards_info_24_hours(); - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, dec!(100))]), - ServiceProviderPromotions::from(vec![make_test_promotion(0, "promo-0", 5000, 1)]), - dec!(0), - dec!(0.0001), - reward_info, - ); - - assert!(sp_infos.iter_rewards().is_empty()); - } - - #[test] - fn no_matched_rewards_if_no_unallocated() { - let reward_info = rewards_info_24_hours(); - let total_rewards = dec!(1000); - - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, dec!(5000))]), - ServiceProviderPromotions::from(vec![make_test_promotion(0, "promo-0", 5000, 1)]), - total_rewards, - dec!(0.00001), - reward_info, - ); - - let promo_rewards = sp_infos.iter_rewards().only_promotion_rewards(); - - assert!(!promo_rewards.is_empty()); - for reward in promo_rewards { - assert_eq!(reward.matched_amount, 0); - } - } - - #[test] - fn single_sp_unallocated_less_than_matched_distributed_by_shares() { - let reward_info = rewards_info_24_hours(); - // 100 unallocated - let total_rewards = dec!(1100); - let sp_session = dec!(1000); - - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, sp_session)]), - ServiceProviderPromotions::from(vec![helium_proto::ServiceProviderPromotions { - service_provider: 0, - incentive_escrow_fund_bps: 10000, - promotions: vec![ - helium_proto::service_provider_promotions::Promotion { - entity: "promo-0".to_string(), - shares: 1, - ..Default::default() - }, - helium_proto::service_provider_promotions::Promotion { - entity: "promo-1".to_string(), - shares: 2, - ..Default::default() - }, - ], - }]), - total_rewards, - dec!(0.00001), - reward_info, - ); - - let promo_rewards = sp_infos.iter_rewards().only_promotion_rewards(); - assert_eq!(2, promo_rewards.len()); - - assert_eq!(promo_rewards[0].service_provider_amount, 333); - assert_eq!(promo_rewards[0].matched_amount, 33); - // - assert_eq!(promo_rewards[1].service_provider_amount, 666); - assert_eq!(promo_rewards[1].matched_amount, 66); - } - - #[test] - fn single_sp_unallocated_more_than_matched_promotion() { - let reward_info = rewards_info_24_hours(); - // 1,000 unallocated - let total_rewards = dec!(11_000); - let sp_session = dec!(1000); - - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, sp_session)]), - ServiceProviderPromotions::from(vec![helium_proto::ServiceProviderPromotions { - service_provider: 0, - incentive_escrow_fund_bps: 10000, - promotions: vec![ - helium_proto::service_provider_promotions::Promotion { - entity: "promo-0".to_string(), - shares: 1, - ..Default::default() - }, - helium_proto::service_provider_promotions::Promotion { - entity: "promo-1".to_string(), - shares: 2, - ..Default::default() - }, - ], - }]), - total_rewards, - dec!(0.00001), - reward_info, - ); - - let promo_rewards = sp_infos.iter_rewards().only_promotion_rewards(); - assert_eq!(2, promo_rewards.len()); - - assert_eq!(promo_rewards[0].service_provider_amount, 333); - assert_eq!(promo_rewards[0].matched_amount, 333); - // - assert_eq!(promo_rewards[1].service_provider_amount, 666); - assert_eq!(promo_rewards[1].matched_amount, 666); - } - - #[test] - fn unallocated_matching_does_not_exceed_promotion() { - let reward_info = rewards_info_24_hours(); - // 100 unallocated - let total_rewards = dec!(1100); - let sp_session = dec!(1000); - - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, sp_session)]), - ServiceProviderPromotions::from(vec![helium_proto::ServiceProviderPromotions { - service_provider: 0, - incentive_escrow_fund_bps: 100, // severely limit promotions - promotions: vec![ - helium_proto::service_provider_promotions::Promotion { - entity: "promo-0".to_string(), - shares: 1, - ..Default::default() - }, - helium_proto::service_provider_promotions::Promotion { - entity: "promo-1".to_string(), - shares: 2, - ..Default::default() - }, - ], - }]), - total_rewards, - dec!(0.00001), - reward_info, - ); - - let promo_rewards = sp_infos.iter_rewards().only_promotion_rewards(); - assert_eq!(2, promo_rewards.len()); - - assert_eq!(promo_rewards[0].service_provider_amount, 3); - assert_eq!(promo_rewards[0].matched_amount, 3); - // - assert_eq!(promo_rewards[1].service_provider_amount, 6); - assert_eq!(promo_rewards[1].matched_amount, 6); - } - - #[test] - fn no_matched_promotions_full_bucket_allocation() { - let reward_info = rewards_info_24_hours(); - // The service providers DC session represents _more_ than the - // available amount of sp_rewards for an epoch. - // No matching on promotions should occur. - let total_rewards = dec!(8_219_178_082_191); - let sp_session = dec!(553_949_301); - - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, sp_session)]), - ServiceProviderPromotions::from(vec![helium_proto::ServiceProviderPromotions { - service_provider: 0, - incentive_escrow_fund_bps: 396, // severely limit promotions - promotions: vec![helium_proto::service_provider_promotions::Promotion { - entity: "promo-1".to_string(), - shares: 100, - ..Default::default() - }], - }]), - total_rewards, - dec!(629) / dec!(1_000_000) / dec!(1_000_000), - reward_info, - ); - - let (promo_1, sp_1) = sp_infos.single_sp_rewards(0); - assert_eq!(promo_1.service_provider_amount, 325_479_452_054); - assert_eq!(promo_1.matched_amount, 0); - assert_eq!(sp_1.amount, 7_893_698_630_136); - - let mut unallocated = total_rewards.to_u64_floored(); - for (amount, _reward) in sp_infos.iter_rewards() { - unallocated -= amount; - } - - assert_eq!(unallocated, 1); - } - - #[test] - fn no_matched_promotions_multiple_sp_full_bucket_allocation() { - let reward_info = rewards_info_24_hours(); - // The Service Providers DC sessions far surpass the - // available amount of sp_rewards for an epoch. - // No matching on promotions should occur. - let total_rewards = dec!(8_219_178_082_191); - let sp_session = dec!(553_949_301); - - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, sp_session), (1, sp_session)]), - ServiceProviderPromotions::from(vec![ - helium_proto::ServiceProviderPromotions { - service_provider: 0, - incentive_escrow_fund_bps: 396, - promotions: vec![helium_proto::service_provider_promotions::Promotion { - entity: "promo-1".to_string(), - shares: 100, - ..Default::default() - }], - }, - helium_proto::ServiceProviderPromotions { - service_provider: 1, - incentive_escrow_fund_bps: 400, - promotions: vec![helium_proto::service_provider_promotions::Promotion { - entity: "promo-2".to_string(), - shares: 100, - ..Default::default() - }], - }, - ]), - total_rewards, - dec!(629) / dec!(1_000_000) / dec!(1_000_000), - reward_info, - ); - - let sp_base_reward = dec!(4_109_589_041_095.50); - let sp_1_promotion_bones = dec!(162_739_726_027.38); // 3.96% - let sp_2_promotion_bones = dec!(164_383_561_643.82); // 4.00% - - let (promo_1, sp_1) = sp_infos.single_sp_rewards(0); - assert_eq!( - sp_1.amount, - (sp_base_reward - sp_1_promotion_bones).to_u64_floored() - ); - assert_eq!( - promo_1.service_provider_amount, - sp_1_promotion_bones.to_u64_floored() - ); - assert_eq!(promo_1.matched_amount, 0); - - let (promo_2, sp_2) = sp_infos.single_sp_rewards(1); - assert_eq!( - sp_2.amount, - (sp_base_reward - sp_2_promotion_bones).to_u64_floored() - ); - assert_eq!( - promo_2.service_provider_amount, - sp_2_promotion_bones.to_u64_floored() - ); - assert_eq!(promo_2.matched_amount, 0); - - let mut unallocated = total_rewards.to_u64_floored(); - for (amount, _reward) in sp_infos.iter_rewards() { - unallocated -= amount; - } - assert_eq!(unallocated, 2); - } - - use proptest::prelude::*; - - prop_compose! { - fn arb_promotion()(entity: String, shares in 1..=100u32) -> helium_proto::service_provider_promotions::Promotion { - proto::Promotion { entity, shares, ..Default::default() } - } - } - - prop_compose! { - fn arb_sp_promotion()( - sp_id in 0..10_i32, - bps in arb_bps(), - promotions in prop::collection::vec(arb_promotion(), 0..10) - ) -> helium_proto::ServiceProviderPromotions { - helium_proto::ServiceProviderPromotions { - service_provider: sp_id, - incentive_escrow_fund_bps: bps, - promotions - } - } - } - - prop_compose! { - fn arb_bps()(bps in 0..=10_000u32) -> u32 { bps } - } - - prop_compose! { - fn arb_dc_session()( - sp_id in 0..10_i32, - // below 1 trillion - dc_session in (0..=1_000_000_000_000_u64).prop_map(Decimal::from) - ) -> (i32, Decimal) { - (sp_id, dc_session) - } - } - - proptest! { - // #![proptest_config(ProptestConfig::with_cases(100_000))] - - #[test] - fn single_provider_does_not_overallocate( - dc_session in any::().prop_map(Decimal::from), - promotions in prop::collection::vec(arb_sp_promotion(), 0..10), - total_allocation in any::().prop_map(Decimal::from) - ) { - - let reward_info = rewards_info_24_hours(); - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from([(0, dc_session)]), - ServiceProviderPromotions::from(promotions), - total_allocation, - dec!(0.00001), - reward_info, - ); - - let total_perc= sp_infos.total_percent(); - assert!(total_perc <= dec!(1)); - - let mut allocated = dec!(0); - for (amount, _) in sp_infos.iter_rewards() { - allocated += Decimal::from(amount); - } - assert!(allocated <= total_allocation); - } - - - #[test] - fn multiple_provider_does_not_overallocate( - dc_sessions in prop::collection::vec(arb_dc_session(), 0..10), - promotions in prop::collection::vec(arb_sp_promotion(), 0..10), - mobile_bone_price in 1..5000 - ) { - let reward_info = rewards_info_24_hours(); - let total_allocation = service_provider::get_scheduled_tokens(reward_info.epoch_emissions); - - let sp_infos = ServiceProviderRewardInfos::new( - ServiceProviderDCSessions::from(dc_sessions), - ServiceProviderPromotions::from(promotions), - total_allocation, - Decimal::from(mobile_bone_price) / dec!(1_000_000) / dec!(1_000_000), - reward_info, - ); - - // NOTE: This can be a sanity check when debugging. There are cases - // generated where the total percentage is - // 1.0000000000000000000000000001%, but as long as we don't - // allocated more than what is available, this is okay. - - // let total_perc = sp_infos.total_percent(); - // println!("total_perc: {}", total_perc); - // prop_assert!(total_perc <= dec!(1)); - - let mut allocated = dec!(0); - for (amount, _) in sp_infos.iter_rewards() { - allocated += Decimal::from(amount); - } - prop_assert!(allocated <= total_allocation); - } - - } - - trait RewardExt { - fn promotion_reward(self) -> proto::PromotionReward; - fn sp_reward(self) -> proto::ServiceProviderReward; - } - - impl RewardExt for proto::MobileRewardShare { - fn promotion_reward(self) -> proto::PromotionReward { - match self.reward { - Some(proto::Reward::PromotionReward(promo)) => promo.clone(), - other => panic!("expected promotion reward, got {other:?}"), - } - } - - fn sp_reward(self) -> proto::ServiceProviderReward { - match self.reward { - Some(proto::Reward::ServiceProviderReward(promo)) => promo, - other => panic!("expected sp reward, got {other:?}"), - } - } - } - - trait PromoRewardFiltersExt { - fn only_promotion_rewards(&self) -> Vec; - } - - impl PromoRewardFiltersExt for Vec<(u64, MobileRewardShare)> { - fn only_promotion_rewards(&self) -> Vec { - self.clone() - .into_iter() - .filter_map(|(_, r)| { - if let Some(proto::Reward::PromotionReward(reward)) = r.reward { - Some(reward) - } else { - None - } - }) - .collect() - } - } - - impl RewardInfo { - fn total_percent(&self) -> Decimal { - self.realized_data_perc + self.realized_promo_perc + self.matched_promo_perc - } - } - - impl ServiceProviderRewardInfos { - fn total_percent(&self) -> Decimal { - self.coll.iter().map(|x| x.total_percent()).sum() - } - - fn iter_sp_rewards(&self, sp_id: i32) -> Vec { - for info in self.coll.iter() { - if info.sp_id == sp_id { - return info - .iter_rewards(self.total_sp_allocation, &self.reward_info) - .into_iter() - .map(|(_, x)| x) - .collect(); - } - } - vec![] - } - - fn single_sp_rewards( - &self, - sp_id: i32, - ) -> (proto::PromotionReward, proto::ServiceProviderReward) { - let binding = self.iter_sp_rewards(sp_id); - let mut rewards = binding.iter(); - - let promo = rewards.next().cloned().unwrap().promotion_reward(); - let sp = rewards.next().cloned().unwrap().sp_reward(); - - (promo, sp) - } - } - - fn make_test_promotion( - sp_id: i32, - entity: &str, - incentive_escrow_fund_bps: u32, - shares: u32, - ) -> helium_proto::ServiceProviderPromotions { - helium_proto::ServiceProviderPromotions { - service_provider: sp_id, - incentive_escrow_fund_bps, - promotions: vec![helium_proto::service_provider_promotions::Promotion { - entity: entity.to_string(), - start_ts: Utc::now().encode_timestamp_millis(), - end_ts: Utc::now().encode_timestamp_millis(), - shares, - }], - } - } -} diff --git a/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs b/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs index 6047863e8..0ec15b1b5 100644 --- a/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs +++ b/mobile_verifier/tests/integrations/rewarder_sp_rewards.rs @@ -1,306 +1,50 @@ -use std::collections::HashMap; -use std::string::ToString; - -use async_trait::async_trait; -use chrono::{DateTime, Duration, Utc}; -use helium_proto::{ - service_provider_promotions::Promotion, services::poc_mobile::UnallocatedRewardType, - ServiceProvider, ServiceProviderPromotions, -}; +use helium_proto::{services::poc_mobile::UnallocatedRewardType, ServiceProvider}; use rust_decimal::prelude::*; use rust_decimal_macros::dec; -use sqlx::{PgPool, Postgres, Transaction}; - -use crate::common::{self, reward_info_24_hours, AsStringKeyedMap}; -use mobile_config::client::{carrier_service_client::CarrierServiceVerifier, ClientError}; -use mobile_verifier::{data_session, reward_shares, rewarder, service_provider}; - -const HOTSPOT_1: &str = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6"; -const HOTSPOT_2: &str = "11eX55faMbqZB7jzN4p67m6w7ScPMH6ubnvCjCPLh72J49PaJEL"; -const PAYER_1: &str = "11uJHS2YaEWJqgqC7yza9uvSmpv5FWoMQXiP8WbxBGgNUmifUJf"; -const PAYER_2: &str = "11sctWiP9r5wDJVuDe1Th4XSL2vaawaLLSQF8f8iokAoMAJHxqp"; -const SP_1: &str = "Helium Mobile"; - -pub type ValidSpMap = HashMap; - -#[derive(Debug, Clone)] -pub struct MockCarrierServiceClient { - pub valid_sps: ValidSpMap, - pub promotions: Vec, -} - -impl MockCarrierServiceClient { - fn new(valid_sps: ValidSpMap) -> Self { - Self { - valid_sps, - promotions: vec![], - } - } +use sqlx::PgPool; - fn with_promotions(self, promotions: Vec) -> Self { - Self { promotions, ..self } - } -} - -#[async_trait] -impl CarrierServiceVerifier for MockCarrierServiceClient { - async fn payer_key_to_service_provider( - &self, - pubkey: &str, - ) -> Result { - match self.valid_sps.get(pubkey) { - Some(v) => Ok(ServiceProvider::from_str(v) - .map_err(|_| ClientError::UnknownServiceProvider(pubkey.to_string()))?), - None => Err(ClientError::UnknownServiceProvider(pubkey.to_string())), - } - } - - async fn list_incentive_promotions( - &self, - _epoch_start: &DateTime, - ) -> Result, ClientError> { - Ok(self.promotions.clone()) - } -} +use crate::common::{self, reward_info_24_hours}; +use mobile_verifier::{reward_shares, rewarder}; #[sqlx::test] -async fn test_service_provider_rewards(pool: PgPool) -> anyhow::Result<()> { - let mut valid_sps = HashMap::::new(); - valid_sps.insert(PAYER_1.to_string(), SP_1.to_string()); - let carrier_client = MockCarrierServiceClient::new(valid_sps); +async fn test_service_provider_rewards(_pool: PgPool) -> anyhow::Result<()> { let (mobile_rewards_client, mobile_rewards) = common::create_file_sink(); let reward_info = reward_info_24_hours(); - // seed db with test specific data - let mut txn = pool.clone().begin().await?; - seed_hotspot_data(reward_info.epoch_period.end, &mut txn).await?; - txn.commit().await?; - - let dc_sessions = - service_provider::get_dc_sessions(&pool, &carrier_client, &reward_info.epoch_period) - .await?; - let sp_promotions = carrier_client - .list_incentive_promotions(&reward_info.epoch_period.start) - .await?; - - rewarder::reward_service_providers( - dc_sessions, - sp_promotions.into(), - mobile_rewards_client, - &reward_info, - dec!(0.0001), - ) - .await?; + rewarder::reward_service_providers(mobile_rewards_client, &reward_info).await?; let rewards = mobile_rewards.finish().await?; - let sp_reward = rewards.sp_rewards.first().expect("sp 1 reward"); - assert_eq!(5_999, sp_reward.amount); - - let unallocated_reward = rewards.unallocated.first().expect("unallocated"); + // Verify single ServiceProviderReward with full 24% allocation + assert_eq!(rewards.sp_rewards.len(), 1); + let sp_reward = rewards.sp_rewards.first().expect("sp reward"); assert_eq!( - UnallocatedRewardType::ServiceProvider as i32, - unallocated_reward.reward_type + sp_reward.service_provider_id, + ServiceProvider::HeliumMobile as i32 ); - assert_eq!(19_726_027_391_261, unallocated_reward.amount); // confirm the total rewards allocated matches expectations let expected_sum = reward_shares::get_scheduled_tokens_for_service_providers(reward_info.epoch_emissions) .to_u64() .unwrap(); - assert_eq!(expected_sum, sp_reward.amount + unallocated_reward.amount); + assert_eq!(expected_sum, sp_reward.amount); // confirm the rewarded percentage amount matches expectations - let percent = (Decimal::from(unallocated_reward.amount) / reward_info.epoch_emissions) + let percent = (Decimal::from(sp_reward.amount) / reward_info.epoch_emissions) .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven); assert_eq!(percent, dec!(0.24)); - Ok(()) -} - -#[sqlx::test] -async fn test_service_provider_rewards_halt_on_invalid_sp(pool: PgPool) -> anyhow::Result<()> { - // only payer 1 has a corresponding SP key - // data sessions from payer 2 will result in an error, halting rewards - let mut valid_sps = HashMap::::new(); - valid_sps.insert(PAYER_1.to_string(), SP_1.to_string()); - let carrier_client = MockCarrierServiceClient::new(valid_sps); - - let reward_info = reward_info_24_hours(); - - let mut txn = pool.clone().begin().await?; - seed_hotspot_data_invalid_sp(reward_info.epoch_period.end, &mut txn).await?; - txn.commit().await.expect("db txn failed"); - - let dc_sessions = - service_provider::get_dc_sessions(&pool, &carrier_client, &reward_info.epoch_period).await; + // Verify no unallocated service provider rewards assert_eq!( - dc_sessions.unwrap_err().to_string(), - format!("unknown service provider {PAYER_2}") + rewards + .unallocated + .iter() + .filter(|r| r.reward_type == UnallocatedRewardType::ServiceProvider as i32) + .count(), + 0 ); - // This is where rewarding would happen if we could properly fetch dc_sessions - - Ok(()) -} - -#[sqlx::test] -async fn test_service_provider_promotion_rewards(pool: PgPool) -> anyhow::Result<()> { - // Single SP has allocated shares for a few of their subscribers. - // Rewards are matched by the unallocated SP rewards for the subscribers - - let valid_sps = HashMap::from_iter([(PAYER_1.to_string(), SP_1.to_string())]); - // promotions allocated 15.00% - let carrier_client = - MockCarrierServiceClient::new(valid_sps).with_promotions(vec![ServiceProviderPromotions { - service_provider: 0, - incentive_escrow_fund_bps: 1500, - promotions: vec![ - Promotion { - entity: "one".to_string(), - shares: 1, - ..Default::default() - }, - Promotion { - entity: "two".to_string(), - shares: 2, - ..Default::default() - }, - Promotion { - entity: "three".to_string(), - shares: 3, - ..Default::default() - }, - ], - }]); - let reward_info = reward_info_24_hours(); - - let (mobile_rewards_client, mobile_rewards) = common::create_file_sink(); - - let mut txn = pool.begin().await?; - seed_hotspot_data(reward_info.epoch_period.end, &mut txn).await?; // DC transferred == 6,000 reward amount - - txn.commit().await?; - - let dc_sessions = - service_provider::get_dc_sessions(&pool, &carrier_client, &reward_info.epoch_period) - .await?; - let sp_promotions = carrier_client - .list_incentive_promotions(&reward_info.epoch_period.start) - .await?; - - rewarder::reward_service_providers( - dc_sessions, - sp_promotions.into(), - mobile_rewards_client, - &reward_info, - dec!(0.00001), - ) - .await?; - - let rewards = mobile_rewards.finish().await?; - let promo_rewards = rewards.promotion_rewards.as_keyed_map(); - - // 1 share - let promo_reward_1 = promo_rewards.get("one").expect("promo 1"); - assert_eq!(promo_reward_1.service_provider_amount, 1_500); - assert_eq!(promo_reward_1.matched_amount, 1_500); - - // 2 shares - let promo_reward_2 = promo_rewards.get("two").expect("promo 2"); - assert_eq!(promo_reward_2.service_provider_amount, 3_000); - assert_eq!(promo_reward_2.matched_amount, 3_000); - - // 3 shares - let promo_reward_3 = promo_rewards.get("three").expect("promo 3"); - assert_eq!(promo_reward_3.service_provider_amount, 4_500); - assert_eq!(promo_reward_3.matched_amount, 4_500); - - // dc_percentage * total_sp_allocation rounded down - let sp_reward = rewards.sp_rewards.first().expect("sp 1 reward"); - assert_eq!(sp_reward.amount, 50_999); - - let unallocated_sp_rewards = get_unallocated_sp_rewards(reward_info.epoch_emissions); - let expected_unallocated = unallocated_sp_rewards - - 50_999 // 85% service provider rewards rounded down - - 9_000 // 15% service provider promotions - - 9_000 // matched promotion - + 0; // rounding - - let unallocated = rewards.unallocated.first().expect("unallocated"); - assert_eq!(unallocated.amount, expected_unallocated); - - Ok(()) -} - -async fn seed_hotspot_data( - ts: DateTime, - txn: &mut Transaction<'_, Postgres>, -) -> anyhow::Result<()> { - let data_session_1 = data_session::HotspotDataSession { - pub_key: HOTSPOT_1.parse().unwrap(), - payer: PAYER_1.parse().unwrap(), - upload_bytes: 1024 * 1000, - download_bytes: 1024 * 10000, - rewardable_bytes: 1024 * 1000 + 1024 * 10000, - num_dcs: 10_000, - received_timestamp: ts - Duration::hours(1), - burn_timestamp: ts - Duration::hours(1), - }; - - let data_session_2 = data_session::HotspotDataSession { - pub_key: HOTSPOT_1.parse().unwrap(), - payer: PAYER_1.parse().unwrap(), - upload_bytes: 1024 * 1000, - download_bytes: 1024 * 50000, - rewardable_bytes: 1024 * 1000 + 1024 * 50000, - num_dcs: 50_000, - received_timestamp: ts - Duration::hours(2), - burn_timestamp: ts - Duration::hours(2), - }; - - data_session_1.save(txn).await?; - data_session_2.save(txn).await?; Ok(()) } - -async fn seed_hotspot_data_invalid_sp( - ts: DateTime, - txn: &mut Transaction<'_, Postgres>, -) -> anyhow::Result<()> { - let data_session_1 = data_session::HotspotDataSession { - pub_key: HOTSPOT_1.parse().unwrap(), - payer: PAYER_1.parse().unwrap(), - upload_bytes: 1024 * 1000, - download_bytes: 1024 * 10000, - rewardable_bytes: 1024 * 1000 + 1024 * 10000, - num_dcs: 10_000, - received_timestamp: ts - Duration::hours(2), - burn_timestamp: ts - Duration::hours(2), - }; - - let data_session_2 = data_session::HotspotDataSession { - pub_key: HOTSPOT_2.parse().unwrap(), - payer: PAYER_2.parse().unwrap(), - upload_bytes: 1024 * 1000, - download_bytes: 1024 * 50000, - rewardable_bytes: 1024 * 1000 + 1024 * 50000, - num_dcs: 50_000, - received_timestamp: ts - Duration::hours(2), - burn_timestamp: ts - Duration::hours(2), - }; - - data_session_1.save(txn).await?; - data_session_2.save(txn).await?; - Ok(()) -} - -// Helper for turning Decimal -> u64 to compare against output rewards -fn get_unallocated_sp_rewards(total_emissions: Decimal) -> u64 { - reward_shares::get_scheduled_tokens_for_service_providers(total_emissions) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0) -} From 44e39135253d5f6f7da5622ee985b1f9b832b82d Mon Sep 17 00:00:00 2001 From: Brian Balser Date: Wed, 5 Nov 2025 09:00:42 -0500 Subject: [PATCH 5/5] Change as to .into --- mobile_verifier/src/rewarder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index 1c22698fc..751ec28f7 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -515,7 +515,7 @@ pub async fn reward_service_providers( end_period: reward_info.epoch_period.end.encode_timestamp(), reward: Some(ProtoReward::ServiceProviderReward( proto::ServiceProviderReward { - service_provider_id: ServiceProvider::HeliumMobile as i32, + service_provider_id: ServiceProvider::HeliumMobile.into(), amount: sp_reward_amount, }, )),