From 4ffe342c70f61e3b54e62fd8452491fd62d4f748 Mon Sep 17 00:00:00 2001 From: Connor McKenzie Date: Thu, 18 Dec 2025 17:45:47 +0000 Subject: [PATCH 1/6] Unallocated rewards written for service provider & poc combined --- Cargo.lock | 6 ++-- Cargo.toml | 5 +-- mobile_verifier/src/rewarder.rs | 35 +++++++++---------- .../tests/integrations/hex_boosting.rs | 35 +++++++++++-------- .../tests/integrations/rewarder_poc_dc.rs | 29 +++++++++------ 5 files changed, 61 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98ccf6395..47b4568b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3503,7 +3503,7 @@ dependencies = [ [[package]] name = "helium-proto" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#3dbaedf3a39ab79cabd3b80cf7c648bf23d3c3f8" +source = "git+https://www.github.com/helium/proto.git?branch=connor%2Fmissing-bones-sp-rewards#af7461fcc7ddc5f302fa6b08c25c5380a623c895" dependencies = [ "bytes", "msg-signature", @@ -5101,7 +5101,7 @@ dependencies = [ [[package]] name = "msg-signature" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#3dbaedf3a39ab79cabd3b80cf7c648bf23d3c3f8" +source = "git+https://www.github.com/helium/proto.git?branch=connor%2Fmissing-bones-sp-rewards#af7461fcc7ddc5f302fa6b08c25c5380a623c895" dependencies = [ "msg-signature-macro", ] @@ -5109,7 +5109,7 @@ dependencies = [ [[package]] name = "msg-signature-macro" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#3dbaedf3a39ab79cabd3b80cf7c648bf23d3c3f8" +source = "git+https://www.github.com/helium/proto.git?branch=connor%2Fmissing-bones-sp-rewards#af7461fcc7ddc5f302fa6b08c25c5380a623c895" dependencies = [ "quote", "syn 2.0.106", diff --git a/Cargo.toml b/Cargo.toml index 10734f8ac..ac41e0918 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,5 +170,6 @@ anchor-lang = { git = "https://github.com/madninja/anchor.git", branch = "madnin # helium-proto = { path = "../proto" } # beacon = { path = "../proto/beacon" } -# [patch.'https://github.com/helium/proto'] -# helium-proto = { git = "https://www.github.com/helium/proto.git", branch = "dcyoung/radio-usage-stats-req-v2" } + [patch.'https://github.com/helium/proto'] + helium-proto = { git = "https://www.github.com/helium/proto.git", branch = "connor/missing-bones-sp-rewards" } + msg-signature = {git = "https://www.github.com/helium/proto.git", branch = "connor/missing-bones-sp-rewards"} diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index 83fa45ab5..6b074fcea 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -272,7 +272,7 @@ where ); // process rewards for poc and data transfer - let poc_dc_shares = reward_poc_and_dc( + let (poc_dc_shares, poc_unallocated_amount) = reward_poc_and_dc( &self.pool, &self.hex_service_client, self.mobile_rewards.clone(), @@ -282,7 +282,16 @@ where .await?; // process rewards for service providers - reward_service_providers(self.mobile_rewards.clone(), &reward_info).await?; + let sp_unallocated_amount = reward_service_providers(self.mobile_rewards.clone(), &reward_info).await?; + + // write combined poc and sp unallocated reward + let total_unallocated_amount = (poc_unallocated_amount + sp_unallocated_amount).to_u64().unwrap_or(0); + write_unallocated_reward( + &self.mobile_rewards.clone(), + UnallocatedRewardType::PocAndServiceProvider, + total_unallocated_amount, + &reward_info + ).await?; self.speedtest_averages.commit().await?; let written_files = self.mobile_rewards.commit().await?.await??; @@ -355,7 +364,7 @@ pub async fn reward_poc_and_dc( mobile_rewards: FileSinkClient, reward_info: &EpochRewardInfo, price_info: PriceInfo, -) -> anyhow::Result { +) -> anyhow::Result<(CalculatedPocRewardShares, Decimal)> { let mut reward_shares = DataTransferAndPocAllocatedRewardBuckets::new(reward_info.epoch_emissions); @@ -394,20 +403,7 @@ pub async fn reward_poc_and_dc( ) .await?; - let poc_unallocated_amount = poc_unallocated_amount - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0); - - write_unallocated_reward( - &mobile_rewards, - UnallocatedRewardType::Poc, - poc_unallocated_amount, - reward_info, - ) - .await?; - - Ok(calculated_poc_reward_shares) + Ok((calculated_poc_reward_shares, poc_unallocated_amount)) } pub async fn reward_poc( @@ -503,7 +499,7 @@ pub async fn reward_dc( pub async fn reward_service_providers( mobile_rewards: FileSinkClient, reward_info: &EpochRewardInfo, -) -> anyhow::Result<()> { +) -> anyhow::Result { let total_sp_rewards = get_scheduled_tokens_for_service_providers(reward_info.epoch_emissions); let sp_reward_amount = total_sp_rewards .round_dp_with_strategy(0, RoundingStrategy::ToZero) @@ -512,6 +508,7 @@ pub async fn reward_service_providers( let subscriber_reward = std::cmp::min(sp_reward_amount, HELIUM_MOBILE_SERVICE_REWARD_BONES); let network_reward = sp_reward_amount.saturating_sub(subscriber_reward); + let unallocated_reward = total_sp_rewards - Decimal::from(subscriber_reward + network_reward); // Write a ServiceProviderReward for HeliumMobile Subscriber Wallet for 450 HNT write_service_provider_reward( @@ -533,7 +530,7 @@ pub async fn reward_service_providers( ) .await?; - Ok(()) + Ok(unallocated_reward) } async fn write_unallocated_reward( diff --git a/mobile_verifier/tests/integrations/hex_boosting.rs b/mobile_verifier/tests/integrations/hex_boosting.rs index 0823c336f..dc056bfe4 100644 --- a/mobile_verifier/tests/integrations/hex_boosting.rs +++ b/mobile_verifier/tests/integrations/hex_boosting.rs @@ -134,7 +134,7 @@ async fn test_poc_with_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { let price_info = default_price_info(); - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_amount) = rewarder::reward_poc_and_dc( &pool, &hex_boosting_client, mobile_rewards_client, @@ -144,6 +144,7 @@ async fn test_poc_with_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { .await?; let rewards = mobile_rewards.finish().await?; + let poc_unallocated_amount = poc_unallocated_amount.to_u64().unwrap_or(0); let poc_rewards = rewards.radio_reward_v2s.as_keyed_map(); let hotspot_1 = poc_rewards.get(HOTSPOT_1).expect("hotspot 1"); @@ -193,7 +194,7 @@ async fn test_poc_with_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default(); + let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -288,7 +289,7 @@ async fn test_poc_boosted_hexes_unique_connections_not_seeded(pool: PgPool) -> a let price_info = default_price_info(); // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_amount) = rewarder::reward_poc_and_dc( &pool, &hex_boosting_client, mobile_rewards_client, @@ -298,6 +299,7 @@ async fn test_poc_boosted_hexes_unique_connections_not_seeded(pool: PgPool) -> a .await?; let rewards = mobile_rewards.finish().await?; + let poc_unallocated_amount = poc_unallocated_amount.to_u64().unwrap_or(0); let poc_rewards = rewards.radio_reward_v2s.as_keyed_map(); let hotspot_1 = poc_rewards.get(HOTSPOT_1).expect("hotspot 1"); @@ -315,7 +317,7 @@ async fn test_poc_boosted_hexes_unique_connections_not_seeded(pool: PgPool) -> a // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default(); + let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -417,7 +419,7 @@ async fn test_poc_with_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Res ]; // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_amount) = rewarder::reward_poc_and_dc( &pool, &MockHexBoostingClient::new(boosted_hexes), mobile_rewards_client, @@ -427,6 +429,7 @@ async fn test_poc_with_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Res .await?; let rewards = mobile_rewards.finish().await?; + let poc_unallocated_amount = poc_unallocated_amount.to_u64().unwrap_or(0); let poc_rewards = rewards.radio_reward_v2s.as_keyed_map(); let hotspot_1 = poc_rewards.get(HOTSPOT_1).expect("hotspot 1"); @@ -487,7 +490,7 @@ async fn test_poc_with_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Res // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default(); + let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -552,7 +555,7 @@ async fn test_expired_boosted_hex(pool: PgPool) -> anyhow::Result<()> { ]; // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_amount) = rewarder::reward_poc_and_dc( &pool, &MockHexBoostingClient::new(boosted_hexes), mobile_rewards_client, @@ -562,6 +565,7 @@ async fn test_expired_boosted_hex(pool: PgPool) -> anyhow::Result<()> { .await?; let rewards = mobile_rewards.finish().await?; + let poc_unallocated_amount = poc_unallocated_amount.to_u64().unwrap_or(0); let poc_rewards = rewards.radio_reward_v2s.as_keyed_map(); let hotspot_1 = poc_rewards.get(HOTSPOT_1).expect("hotspot 1"); @@ -583,7 +587,7 @@ async fn test_expired_boosted_hex(pool: PgPool) -> anyhow::Result<()> { // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default(); + let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -654,7 +658,7 @@ async fn test_reduced_location_score_with_boosted_hexes(pool: PgPool) -> anyhow: ]; // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_amount) = rewarder::reward_poc_and_dc( &pool, &MockHexBoostingClient::new(boosted_hexes), mobile_rewards_client, @@ -664,6 +668,7 @@ async fn test_reduced_location_score_with_boosted_hexes(pool: PgPool) -> anyhow: .await?; let rewards = mobile_rewards.finish().await?; + let poc_unallocated_amount = poc_unallocated_amount.to_u64().unwrap_or(0); let poc_rewards = rewards.radio_reward_v2s.as_keyed_map(); let hotspot_1 = poc_rewards.get(HOTSPOT_1).expect("hotspot 1"); // full location trust 1 boost @@ -715,7 +720,7 @@ async fn test_reduced_location_score_with_boosted_hexes(pool: PgPool) -> anyhow: // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default(); + let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -791,7 +796,7 @@ async fn test_distance_from_asserted_removes_boosting_but_not_location_trust( ]; // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_amount) = rewarder::reward_poc_and_dc( &pool, &MockHexBoostingClient::new(boosted_hexes), mobile_rewards_client, @@ -801,6 +806,7 @@ async fn test_distance_from_asserted_removes_boosting_but_not_location_trust( .await?; let rewards = mobile_rewards.finish().await?; + let poc_unallocated_amount = poc_unallocated_amount.to_u64().unwrap_or(0); let poc_rewards = rewards.radio_reward_v2s.as_keyed_map(); let hotspot_1 = poc_rewards.get(HOTSPOT_1).expect("hotspot 1"); // full location trust 1 boost @@ -855,7 +861,7 @@ async fn test_distance_from_asserted_removes_boosting_but_not_location_trust( // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default(); + let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -954,7 +960,7 @@ async fn test_poc_with_wifi_and_multi_coverage_boosted_hexes(pool: PgPool) -> an ]; // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_reward) = rewarder::reward_poc_and_dc( &pool, &MockHexBoostingClient::new(boosted_hexes), mobile_rewards_client, @@ -964,6 +970,7 @@ async fn test_poc_with_wifi_and_multi_coverage_boosted_hexes(pool: PgPool) -> an .await?; let rewards = mobile_rewards.finish().await?; + let poc_unallocated_reward = poc_unallocated_reward.to_u64().unwrap_or(0); let poc_rewards = rewards.radio_reward_v2s.as_keyed_map(); let hotspot_1 = poc_rewards.get(HOTSPOT_1).expect("hotspot 1"); // 2 boosts at 10x @@ -1023,7 +1030,7 @@ async fn test_poc_with_wifi_and_multi_coverage_boosted_hexes(pool: PgPool) -> an // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default(); + let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_reward; assert_total_matches_emissions(total, &reward_info); Ok(()) diff --git a/mobile_verifier/tests/integrations/rewarder_poc_dc.rs b/mobile_verifier/tests/integrations/rewarder_poc_dc.rs index 9dc12f47d..5f8f6246c 100644 --- a/mobile_verifier/tests/integrations/rewarder_poc_dc.rs +++ b/mobile_verifier/tests/integrations/rewarder_poc_dc.rs @@ -11,6 +11,7 @@ use file_store_oracles::{ unique_connections::{UniqueConnectionReq, UniqueConnectionsIngestReport}, }; use helium_crypto::PublicKeyBinary; +use helium_proto::services::poc_mobile::UnallocatedRewardType; use mobile_verifier::{ banning, cell_type::CellType, @@ -60,7 +61,7 @@ async fn test_poc_and_dc_rewards(pool: PgPool) -> anyhow::Result<()> { let price_info = default_price_info(); // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_amount) = rewarder::reward_poc_and_dc( &pool, &hex_boosting_client, mobile_rewards_client, @@ -72,7 +73,6 @@ async fn test_poc_and_dc_rewards(pool: PgPool) -> anyhow::Result<()> { let rewards = mobile_rewards.finish().await?; let poc_rewards = rewards.radio_reward_v2s; let dc_rewards = rewards.gateway_rewards; - let unallocated_reward = rewards.unallocated.first(); let poc_sum: u64 = poc_rewards.iter().map(|r| r.total_poc_reward()).sum(); @@ -80,9 +80,15 @@ async fn test_poc_and_dc_rewards(pool: PgPool) -> anyhow::Result<()> { assert_eq!(poc_sum / 3, poc_rewards[1].total_poc_reward()); assert_eq!(poc_sum / 3, poc_rewards[2].total_poc_reward()); - // assert the unallocated reward - let unallocated_reward = unallocated_reward.unwrap(); - assert_eq!(unallocated_reward.amount, 1); + // assert no unallocated reward writes + assert_eq!( + rewards + .unallocated + .iter() + .filter(|r| r.reward_type == UnallocatedRewardType::Poc as i32) + .count(), + 0 + ); // assert the boosted hexes in the radio rewards // boosted hexes will contain the used multiplier for each boosted hex @@ -98,7 +104,7 @@ async fn test_poc_and_dc_rewards(pool: PgPool) -> anyhow::Result<()> { // confirm the total rewards allocated matches expectations let dc_sum: u64 = dc_rewards.iter().map(|r| r.dc_transfer_reward).sum(); - let total = poc_sum + dc_sum + unallocated_reward.amount; + let total = poc_sum + dc_sum + poc_unallocated_amount.to_u64().unwrap_or(0); let expected_sum = reward_shares::get_scheduled_tokens_for_poc(reward_info.epoch_emissions) .to_u64() @@ -275,7 +281,7 @@ async fn test_all_banned_radio(pool: PgPool) -> anyhow::Result<()> { txn.commit().await?; // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_reward) = rewarder::reward_poc_and_dc( &pool, &hex_boosting_client, mobile_rewards_client, @@ -286,13 +292,13 @@ async fn test_all_banned_radio(pool: PgPool) -> anyhow::Result<()> { let rewards = mobile_rewards.finish().await?; let poc_rewards = rewards.radio_reward_v2s; - let dc_rewards = rewards.gateway_rewards; + let poc_unallocated_reward = poc_unallocated_reward.to_u64().unwrap_or(0); // 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(), 1); + assert!(poc_unallocated_reward >= 1); Ok(()) } @@ -328,7 +334,7 @@ async fn test_data_banned_radio_still_receives_poc(pool: PgPool) -> anyhow::Resu txn.commit().await?; // run rewards for poc and dc - rewarder::reward_poc_and_dc( + let (_, poc_unallocated_reward) = rewarder::reward_poc_and_dc( &pool, &hex_boosting_client, mobile_rewards_client, @@ -340,10 +346,11 @@ async fn test_data_banned_radio_still_receives_poc(pool: PgPool) -> anyhow::Resu let rewards = mobile_rewards.finish().await?; let poc_rewards = rewards.radio_reward_v2s; let dc_rewards = rewards.gateway_rewards; + let poc_unallocated_reward = poc_unallocated_reward.to_u64().unwrap_or(0); assert_eq!(poc_rewards.len(), 3); assert_eq!(dc_rewards.len(), 0); - assert_eq!(rewards.unallocated.len(), 1); + assert!(poc_unallocated_reward >= 1); Ok(()) } From bf11724da74520d9f2b5d30d27300b5d975fc8df Mon Sep 17 00:00:00 2001 From: Connor McKenzie Date: Fri, 19 Dec 2025 12:10:24 +0000 Subject: [PATCH 2/6] Fix fmt --- mobile_verifier/src/rewarder.rs | 12 +++++--- .../tests/integrations/hex_boosting.rs | 28 ++++++++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index 6b074fcea..8a7e34a9e 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -282,16 +282,20 @@ where .await?; // process rewards for service providers - let sp_unallocated_amount = reward_service_providers(self.mobile_rewards.clone(), &reward_info).await?; + let sp_unallocated_amount = + reward_service_providers(self.mobile_rewards.clone(), &reward_info).await?; // write combined poc and sp unallocated reward - let total_unallocated_amount = (poc_unallocated_amount + sp_unallocated_amount).to_u64().unwrap_or(0); + let total_unallocated_amount = (poc_unallocated_amount + sp_unallocated_amount) + .to_u64() + .unwrap_or(0); write_unallocated_reward( &self.mobile_rewards.clone(), UnallocatedRewardType::PocAndServiceProvider, total_unallocated_amount, - &reward_info - ).await?; + &reward_info, + ) + .await?; self.speedtest_averages.commit().await?; let written_files = self.mobile_rewards.commit().await?.await??; diff --git a/mobile_verifier/tests/integrations/hex_boosting.rs b/mobile_verifier/tests/integrations/hex_boosting.rs index dc056bfe4..007bc5a6c 100644 --- a/mobile_verifier/tests/integrations/hex_boosting.rs +++ b/mobile_verifier/tests/integrations/hex_boosting.rs @@ -194,7 +194,9 @@ async fn test_poc_with_boosted_hexes(pool: PgPool) -> anyhow::Result<()> { // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; + let total = rewards.total_poc_rewards() + + rewards.unallocated_amount_or_default() + + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -317,7 +319,9 @@ async fn test_poc_boosted_hexes_unique_connections_not_seeded(pool: PgPool) -> a // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; + let total = rewards.total_poc_rewards() + + rewards.unallocated_amount_or_default() + + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -490,7 +494,9 @@ async fn test_poc_with_multi_coverage_boosted_hexes(pool: PgPool) -> anyhow::Res // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; + let total = rewards.total_poc_rewards() + + rewards.unallocated_amount_or_default() + + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -587,7 +593,9 @@ async fn test_expired_boosted_hex(pool: PgPool) -> anyhow::Result<()> { // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; + let total = rewards.total_poc_rewards() + + rewards.unallocated_amount_or_default() + + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -720,7 +728,9 @@ async fn test_reduced_location_score_with_boosted_hexes(pool: PgPool) -> anyhow: // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; + let total = rewards.total_poc_rewards() + + rewards.unallocated_amount_or_default() + + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -861,7 +871,9 @@ async fn test_distance_from_asserted_removes_boosting_but_not_location_trust( // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_amount; + let total = rewards.total_poc_rewards() + + rewards.unallocated_amount_or_default() + + poc_unallocated_amount; assert_total_matches_emissions(total, &reward_info); Ok(()) @@ -1030,7 +1042,9 @@ async fn test_poc_with_wifi_and_multi_coverage_boosted_hexes(pool: PgPool) -> an // confirm the total rewards allocated matches emissions // and the rewarded percentage amount matches percentage - let total = rewards.total_poc_rewards() + rewards.unallocated_amount_or_default() + poc_unallocated_reward; + let total = rewards.total_poc_rewards() + + rewards.unallocated_amount_or_default() + + poc_unallocated_reward; assert_total_matches_emissions(total, &reward_info); Ok(()) From 6ba26e42b7a9bcc0acae195333e5bec825400d0c Mon Sep 17 00:00:00 2001 From: Connor McKenzie Date: Fri, 19 Dec 2025 12:14:46 +0000 Subject: [PATCH 3/6] Add rounding strategy to unallocated amount --- mobile_verifier/src/rewarder.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index 8a7e34a9e..00cd6806e 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -287,8 +287,10 @@ where // write combined poc and sp unallocated reward let total_unallocated_amount = (poc_unallocated_amount + sp_unallocated_amount) + .round_dp_with_strategy(0, RoundingStrategy::ToZero) .to_u64() .unwrap_or(0); + write_unallocated_reward( &self.mobile_rewards.clone(), UnallocatedRewardType::PocAndServiceProvider, From 42277e90ee5a6c6ae021cf0eb9473fbd94fe3b52 Mon Sep 17 00:00:00 2001 From: Connor McKenzie Date: Mon, 5 Jan 2026 16:49:12 +0000 Subject: [PATCH 4/6] Refactor reward distributions & reward distributor IT --- mobile_verifier/src/reward_shares.rs | 6 +- mobile_verifier/src/rewarder.rs | 61 ++-- .../tests/integrations/common/mod.rs | 2 + .../tests/integrations/common/seed.rs | 303 +++++++++++++++++ mobile_verifier/tests/integrations/main.rs | 1 + .../tests/integrations/reward_distributor.rs | 67 ++++ .../tests/integrations/rewarder_poc_dc.rs | 315 +----------------- 7 files changed, 427 insertions(+), 328 deletions(-) create mode 100644 mobile_verifier/tests/integrations/common/seed.rs create mode 100644 mobile_verifier/tests/integrations/reward_distributor.rs diff --git a/mobile_verifier/src/reward_shares.rs b/mobile_verifier/src/reward_shares.rs index a052abfb6..68a072d4e 100644 --- a/mobile_verifier/src/reward_shares.rs +++ b/mobile_verifier/src/reward_shares.rs @@ -30,10 +30,10 @@ 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.7); +pub 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); +pub const POC_REWARDS_PERCENT: Decimal = dec!(0.0); /// The fixed price of a mobile data credit const DC_USD_PRICE: Decimal = dec!(0.00001); @@ -42,7 +42,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.24); +pub const SERVICE_PROVIDER_PERCENT: Decimal = dec!(0.24); // Fixed price of service provider rewards to be given to Helium Mobile Service Rewards pub const HELIUM_MOBILE_SERVICE_REWARD_BONES: u64 = 45_000_000_000; diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index 00cd6806e..daca4fdb2 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -271,8 +271,7 @@ where reward_info.epoch_emissions, ); - // process rewards for poc and data transfer - let (poc_dc_shares, poc_unallocated_amount) = reward_poc_and_dc( + let poc_dc_shares = distribute_rewards( &self.pool, &self.hex_service_client, self.mobile_rewards.clone(), @@ -281,24 +280,6 @@ where ) .await?; - // process rewards for service providers - let sp_unallocated_amount = - reward_service_providers(self.mobile_rewards.clone(), &reward_info).await?; - - // write combined poc and sp unallocated reward - let total_unallocated_amount = (poc_unallocated_amount + sp_unallocated_amount) - .round_dp_with_strategy(0, RoundingStrategy::ToZero) - .to_u64() - .unwrap_or(0); - - write_unallocated_reward( - &self.mobile_rewards.clone(), - UnallocatedRewardType::PocAndServiceProvider, - total_unallocated_amount, - &reward_info, - ) - .await?; - self.speedtest_averages.commit().await?; let written_files = self.mobile_rewards.commit().await?.await??; @@ -364,6 +345,44 @@ where } } +pub async fn distribute_rewards( + pool: &Pool, + hex_service_client: &impl HexBoostingInfoResolver, + mobile_rewards: FileSinkClient, + reward_info: &EpochRewardInfo, + price_info: PriceInfo, +) -> anyhow::Result { + // process rewards for poc and data transfer + let (poc_dc_shares, poc_unallocated_amount) = reward_poc_and_dc( + pool, + hex_service_client, + mobile_rewards.clone(), + &reward_info, + price_info.clone(), + ) + .await?; + + // process rewards for service providers + let sp_unallocated_amount = + reward_service_providers(mobile_rewards.clone(), &reward_info).await?; + + // write combined poc and sp unallocated reward + let total_unallocated_amount = (poc_unallocated_amount + sp_unallocated_amount) + .round_dp_with_strategy(0, RoundingStrategy::ToZero) + .to_u64() + .unwrap_or(0); + + write_unallocated_reward( + mobile_rewards, + UnallocatedRewardType::PocAndServiceProvider, + total_unallocated_amount, + &reward_info, + ) + .await?; + + Ok(poc_dc_shares) +} + pub async fn reward_poc_and_dc( pool: &Pool, hex_service_client: &impl HexBoostingInfoResolver, @@ -540,7 +559,7 @@ pub async fn reward_service_providers( } async fn write_unallocated_reward( - mobile_rewards: &FileSinkClient, + mobile_rewards: FileSinkClient, unallocated_type: UnallocatedRewardType, unallocated_amount: u64, reward_info: &'_ EpochRewardInfo, diff --git a/mobile_verifier/tests/integrations/common/mod.rs b/mobile_verifier/tests/integrations/common/mod.rs index 8da3c5011..833de0bfb 100644 --- a/mobile_verifier/tests/integrations/common/mod.rs +++ b/mobile_verifier/tests/integrations/common/mod.rs @@ -1,3 +1,5 @@ +pub mod seed; + use chrono::{DateTime, Duration, Utc}; use file_store::{ file_sink::{FileSinkClient, Message as SinkMessage}, diff --git a/mobile_verifier/tests/integrations/common/seed.rs b/mobile_verifier/tests/integrations/common/seed.rs new file mode 100644 index 000000000..383a2c713 --- /dev/null +++ b/mobile_verifier/tests/integrations/common/seed.rs @@ -0,0 +1,303 @@ +use crate::common; +use crate::rewarder_poc_dc::proto; +use chrono::{DateTime, Duration as ChronoDuration, Utc}; +use file_store_oracles::coverage::{ + CoverageObject as FSCoverageObject, KeyType, RadioHexSignalLevel, +}; +use file_store_oracles::speedtest::CellSpeedtest; +use file_store_oracles::unique_connections::{UniqueConnectionReq, UniqueConnectionsIngestReport}; +use helium_crypto::PublicKeyBinary; +use mobile_verifier::cell_type::CellType; +use mobile_verifier::coverage::CoverageObject; +use mobile_verifier::heartbeats::{HbType, Heartbeat, ValidatedHeartbeat}; +use mobile_verifier::{data_session, speedtests, unique_connections}; +use rust_decimal_macros::dec; +use sqlx::{PgPool, Postgres, Transaction}; +use std::ops::Range; +use uuid::Uuid; + +const HOTSPOT_1: &str = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6"; +const HOTSPOT_2: &str = "11uJHS2YaEWJqgqC7yza9uvSmpv5FWoMQXiP8WbxBGgNUmifUJf"; +pub const HOTSPOT_3: &str = "112E7TxoNHV46M6tiPA8N1MkeMeQxc9ztb4JQLXBVAAUfq1kJLoF"; +const PAYER_1: &str = "11eX55faMbqZB7jzN4p67m6w7ScPMH6ubnvCjCPLh72J49PaJEL"; + +pub async fn seed_heartbeats( + ts: DateTime, + txn: &mut Transaction<'_, Postgres>, +) -> anyhow::Result<()> { + for n in 0..24 { + let hotspot_key1: PublicKeyBinary = HOTSPOT_1.to_string().parse()?; + let cov_obj_1 = create_coverage_object( + ts + ChronoDuration::hours(n), + hotspot_key1.clone(), + 0x8a1fb466d2dffff_u64, + true, + ); + let wifi_heartbeat_1 = ValidatedHeartbeat { + heartbeat: Heartbeat { + hb_type: HbType::Wifi, + hotspot_key: hotspot_key1, + operation_mode: true, + lat: 0.0, + lon: 0.0, + coverage_object: Some(cov_obj_1.coverage_object.uuid), + location_validation_timestamp: None, + timestamp: ts + ChronoDuration::hours(n), + location_source: proto::LocationSource::Gps, + }, + cell_type: CellType::NovaGenericWifiIndoor, + distance_to_asserted: Some(0), + coverage_meta: None, + location_trust_score_multiplier: dec!(1.0), + validity: proto::HeartbeatValidity::Valid, + }; + + let hotspot_key2: PublicKeyBinary = HOTSPOT_2.to_string().parse()?; + let cov_obj_2 = create_coverage_object( + ts + ChronoDuration::hours(n), + hotspot_key2.clone(), + 0x8a1fb49642dffff_u64, + true, + ); + let wifi_heartbeat_2 = ValidatedHeartbeat { + heartbeat: Heartbeat { + hb_type: HbType::Wifi, + hotspot_key: hotspot_key2, + operation_mode: true, + lat: 0.0, + lon: 0.0, + coverage_object: Some(cov_obj_2.coverage_object.uuid), + location_validation_timestamp: None, + timestamp: ts + ChronoDuration::hours(n), + location_source: proto::LocationSource::Gps, + }, + cell_type: CellType::NovaGenericWifiIndoor, + distance_to_asserted: Some(0), + coverage_meta: None, + location_trust_score_multiplier: dec!(1.0), + validity: proto::HeartbeatValidity::Valid, + }; + + let hotspot_key3: PublicKeyBinary = HOTSPOT_3.to_string().parse()?; + let cov_obj_3 = create_coverage_object( + ts + ChronoDuration::hours(n), + hotspot_key3.clone(), + 0x8c2681a306607ff_u64, + true, + ); + let wifi_heartbeat_3 = ValidatedHeartbeat { + heartbeat: Heartbeat { + hb_type: HbType::Wifi, + hotspot_key: hotspot_key3, + operation_mode: true, + lat: 0.0, + lon: 0.0, + coverage_object: Some(cov_obj_3.coverage_object.uuid), + location_validation_timestamp: Some(ts - ChronoDuration::hours(24)), + timestamp: ts + ChronoDuration::hours(n), + location_source: proto::LocationSource::Skyhook, + }, + cell_type: CellType::NovaGenericWifiIndoor, + distance_to_asserted: Some(0), + coverage_meta: None, + location_trust_score_multiplier: dec!(1.0), + validity: proto::HeartbeatValidity::Valid, + }; + + save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat_3, txn).await?; + save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat_1, txn).await?; + save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat_2, txn).await?; + + wifi_heartbeat_1.save(txn).await?; + wifi_heartbeat_2.save(txn).await?; + wifi_heartbeat_3.save(txn).await?; + + cov_obj_1.save(txn).await?; + cov_obj_2.save(txn).await?; + cov_obj_3.save(txn).await?; + } + Ok(()) +} + +pub async fn update_assignments(pool: &PgPool) -> anyhow::Result<()> { + let _ = common::set_unassigned_oracle_boosting_assignments( + pool, + &common::mock_hex_boost_data_default(), + ) + .await?; + Ok(()) +} + +pub async fn update_assignments_bad(pool: &PgPool) -> anyhow::Result<()> { + let _ = common::set_unassigned_oracle_boosting_assignments( + pool, + &common::mock_hex_boost_data_bad(), + ) + .await?; + Ok(()) +} + +pub async fn seed_speedtests( + ts: DateTime, + txn: &mut Transaction<'_, Postgres>, +) -> anyhow::Result<()> { + for n in 0..24 { + let hotspot1_speedtest = CellSpeedtest { + pubkey: HOTSPOT_1.parse()?, + serial: "serial1".to_string(), + timestamp: ts - ChronoDuration::hours(n * 4), + upload_speed: 100_000_000, + download_speed: 100_000_000, + latency: 50, + }; + + let hotspot2_speedtest = CellSpeedtest { + pubkey: HOTSPOT_2.parse()?, + serial: "serial2".to_string(), + timestamp: ts - ChronoDuration::hours(n * 4), + upload_speed: 100_000_000, + download_speed: 100_000_000, + latency: 50, + }; + + let hotspot3_speedtest = CellSpeedtest { + pubkey: HOTSPOT_3.parse()?, + serial: "serial3".to_string(), + timestamp: ts - ChronoDuration::hours(n * 4), + upload_speed: 100_000_000, + download_speed: 100_000_000, + latency: 50, + }; + + speedtests::save_speedtest(&hotspot1_speedtest, txn).await?; + speedtests::save_speedtest(&hotspot2_speedtest, txn).await?; + speedtests::save_speedtest(&hotspot3_speedtest, txn).await?; + } + Ok(()) +} + +pub async fn seed_data_sessions( + ts: DateTime, + txn: &mut Transaction<'_, Postgres>, +) -> anyhow::Result { + let rewardable_bytes_1 = 1_024 * 1_000; + let data_session_1 = data_session::HotspotDataSession { + pub_key: HOTSPOT_1.parse()?, + payer: PAYER_1.parse()?, + upload_bytes: 1_024 * 1_000, + download_bytes: 1_024 * 50_000, + // Here to test that rewardable_bytes is the one taken into account we lower it + rewardable_bytes: rewardable_bytes_1, + num_dcs: 5_000_000, + received_timestamp: ts + ChronoDuration::hours(1), + burn_timestamp: ts + ChronoDuration::hours(1), + }; + + let rewardable_bytes_2 = 1_024 * 1_000 + 1_024 * 50_000; + let data_session_2 = data_session::HotspotDataSession { + pub_key: HOTSPOT_2.parse()?, + payer: PAYER_1.parse()?, + upload_bytes: 1_024 * 1_000, + download_bytes: 1_024 * 50_000, + rewardable_bytes: rewardable_bytes_2, + num_dcs: 5_000_000, + received_timestamp: ts + ChronoDuration::hours(1), + burn_timestamp: ts + ChronoDuration::hours(1), + }; + + let rewardable_bytes_3 = 1_024 * 1_000 + 1_024 * 50_000; + let data_session_3 = data_session::HotspotDataSession { + pub_key: HOTSPOT_3.parse()?, + payer: PAYER_1.parse()?, + upload_bytes: 1_024 * 1_000, + download_bytes: 1_024 * 50_000, + rewardable_bytes: rewardable_bytes_3, + num_dcs: 5_000_000, + received_timestamp: ts + ChronoDuration::hours(1), + burn_timestamp: ts + ChronoDuration::hours(1), + }; + data_session_1.save(txn).await?; + data_session_2.save(txn).await?; + data_session_3.save(txn).await?; + + let rewardable = rewardable_bytes_1 + rewardable_bytes_2 + rewardable_bytes_3; + + Ok(rewardable as u64) +} + +pub async fn seed_unique_connections( + txn: &mut Transaction<'_, Postgres>, + things: &[(PublicKeyBinary, u64)], + epoch: &Range>, +) -> anyhow::Result<()> { + let mut reports = vec![]; + for (pubkey, unique_connections) in things { + reports.push(UniqueConnectionsIngestReport { + received_timestamp: epoch.start + chrono::Duration::hours(1), + report: UniqueConnectionReq { + pubkey: pubkey.clone(), + start_timestamp: Utc::now(), + end_timestamp: Utc::now(), + unique_connections: *unique_connections, + timestamp: Utc::now(), + carrier_key: pubkey.clone(), + signature: vec![], + }, + }); + } + unique_connections::db::save(txn, &reports).await?; + Ok(()) +} + +fn create_coverage_object( + ts: DateTime, + pub_key: PublicKeyBinary, + hex: u64, + indoor: bool, +) -> CoverageObject { + let location = h3o::CellIndex::try_from(hex).unwrap(); + let key_type = KeyType::HotspotKey(pub_key.clone()); + let report = FSCoverageObject { + pub_key, + uuid: Uuid::new_v4(), + key_type, + coverage_claim_time: ts, + coverage: vec![RadioHexSignalLevel { + location, + signal_level: proto::SignalLevel::High, + signal_power: 1000, + }], + indoor, + trust_score: 1000, + signature: Vec::new(), + }; + CoverageObject { + coverage_object: report, + validity: proto::CoverageObjectValidity::Valid, + } +} + +async fn save_seniority_object( + ts: DateTime, + hb: &ValidatedHeartbeat, + exec: &mut Transaction<'_, Postgres>, +) -> anyhow::Result<()> { + sqlx::query( + r#" + INSERT INTO seniority + (radio_key, last_heartbeat, uuid, seniority_ts, inserted_at, update_reason, radio_type) + VALUES + ($1, $2, $3, $4, $5, $6, $7) + "#, + ) + .bind(hb.heartbeat.key()) + .bind(hb.heartbeat.timestamp) + .bind(hb.heartbeat.coverage_object) + .bind(ts) + .bind(ts) + .bind(proto::SeniorityUpdateReason::NewCoverageClaimTime as i32) + .bind(hb.heartbeat.hb_type) + .execute(&mut **exec) + .await?; + Ok(()) +} diff --git a/mobile_verifier/tests/integrations/main.rs b/mobile_verifier/tests/integrations/main.rs index 5b4c1557e..d47b582a2 100644 --- a/mobile_verifier/tests/integrations/main.rs +++ b/mobile_verifier/tests/integrations/main.rs @@ -7,6 +7,7 @@ mod heartbeats; mod hex_boosting; mod last_location; mod modeled_coverage; +mod reward_distributor; mod rewarder_poc_dc; mod rewarder_sp_rewards; mod seniority; diff --git a/mobile_verifier/tests/integrations/reward_distributor.rs b/mobile_verifier/tests/integrations/reward_distributor.rs new file mode 100644 index 000000000..c447b327e --- /dev/null +++ b/mobile_verifier/tests/integrations/reward_distributor.rs @@ -0,0 +1,67 @@ +use crate::common::{ + create_file_sink, default_price_info, reward_info_24_hours, + seed::{seed_data_sessions, seed_heartbeats, seed_speedtests, update_assignments}, + MockHexBoostingClient, RadioRewardV2Ext, +}; +use mobile_verifier::reward_shares::{ + MAX_DATA_TRANSFER_REWARDS_PERCENT, POC_REWARDS_PERCENT, SERVICE_PROVIDER_PERCENT, +}; +use mobile_verifier::rewarder; +use rust_decimal::{prelude::ToPrimitive, Decimal}; +use sqlx::PgPool; + +#[sqlx::test] +async fn test_distribute_rewards(pool: PgPool) -> anyhow::Result<()> { + let (mobile_rewards_client, mobile_rewards) = create_file_sink(); + let reward_info = reward_info_24_hours(); + + // seed all the things + let mut txn = pool.clone().begin().await?; + seed_heartbeats(reward_info.epoch_period.start, &mut txn).await?; + seed_speedtests(reward_info.epoch_period.end, &mut txn).await?; + seed_data_sessions(reward_info.epoch_period.start, &mut txn).await?; + txn.commit().await?; + update_assignments(&pool).await?; + + let hex_boosting_client = MockHexBoostingClient::new(vec![]); + let price_info = default_price_info(); + + // Run rewards + rewarder::distribute_rewards( + &pool, + &hex_boosting_client, + mobile_rewards_client, + &reward_info, + price_info, + ) + .await?; + + // Retrieve distributed rewards + let rewards = mobile_rewards.finish().await?; + let poc_rewards = rewards.radio_reward_v2s; + let dc_rewards = rewards.gateway_rewards; + let sp_rewards = rewards.sp_rewards; + let unallocated_rewards = rewards.unallocated; + + let poc_sum: u64 = poc_rewards.iter().map(|r| r.total_poc_reward()).sum(); + let dc_sum: u64 = dc_rewards.iter().map(|r| r.dc_transfer_reward).sum(); + let sp_sum: u64 = sp_rewards.iter().map(|r| r.amount).sum(); + let unallocated_sum: u64 = unallocated_rewards.iter().map(|r| r.amount).sum(); + + let total: u64 = poc_sum + dc_sum + sp_sum + unallocated_sum; + + // Calculate expected rewards + let expected_total = calculate_expected_total_rewards(reward_info.epoch_emissions); + + // Assert total + assert_eq!(total, expected_total); + + Ok(()) +} + +fn calculate_expected_total_rewards(total_emission_pool: Decimal) -> u64 { + (total_emission_pool + * (SERVICE_PROVIDER_PERCENT + POC_REWARDS_PERCENT + MAX_DATA_TRANSFER_REWARDS_PERCENT)) + .to_u64() + .unwrap() +} diff --git a/mobile_verifier/tests/integrations/rewarder_poc_dc.rs b/mobile_verifier/tests/integrations/rewarder_poc_dc.rs index 5f8f6246c..c2a88ef71 100644 --- a/mobile_verifier/tests/integrations/rewarder_poc_dc.rs +++ b/mobile_verifier/tests/integrations/rewarder_poc_dc.rs @@ -1,30 +1,24 @@ use std::ops::Range; +use crate::common::seed::{ + seed_data_sessions, seed_heartbeats, seed_speedtests, seed_unique_connections, + update_assignments, update_assignments_bad, HOTSPOT_3, +}; use crate::common::{ self, default_price_info, reward_info_24_hours, MockHexBoostingClient, RadioRewardV2Ext, }; -use chrono::{DateTime, Duration as ChronoDuration, Utc}; -use file_store_oracles::{ - coverage::{CoverageObject as FSCoverageObject, KeyType, RadioHexSignalLevel}, - mobile_ban, - speedtest::CellSpeedtest, - unique_connections::{UniqueConnectionReq, UniqueConnectionsIngestReport}, -}; +use chrono::{DateTime, Utc}; +use file_store_oracles::mobile_ban; use helium_crypto::PublicKeyBinary; use helium_proto::services::poc_mobile::UnallocatedRewardType; use mobile_verifier::{ banning, - cell_type::CellType, - coverage::CoverageObject, - data_session, - heartbeats::{HbType, Heartbeat, ValidatedHeartbeat}, reward_shares::{self, DataTransferAndPocAllocatedRewardBuckets}, - rewarder, speedtests, unique_connections, + rewarder, }; use rust_decimal::prelude::*; use rust_decimal_macros::dec; use sqlx::{PgPool, Postgres, Transaction}; -use uuid::Uuid; pub mod proto { pub use helium_proto::services::poc_mobile::{ @@ -38,11 +32,6 @@ pub mod proto { }; } -const HOTSPOT_1: &str = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6"; -const HOTSPOT_2: &str = "11uJHS2YaEWJqgqC7yza9uvSmpv5FWoMQXiP8WbxBGgNUmifUJf"; -const HOTSPOT_3: &str = "112E7TxoNHV46M6tiPA8N1MkeMeQxc9ztb4JQLXBVAAUfq1kJLoF"; -const PAYER_1: &str = "11eX55faMbqZB7jzN4p67m6w7ScPMH6ubnvCjCPLh72J49PaJEL"; - #[sqlx::test] async fn test_poc_and_dc_rewards(pool: PgPool) -> anyhow::Result<()> { let (mobile_rewards_client, mobile_rewards) = common::create_file_sink(); @@ -125,7 +114,7 @@ async fn test_qualified_wifi_poc_rewards(pool: PgPool) -> anyhow::Result<()> { let reward_info = reward_info_24_hours(); - let pubkey: PublicKeyBinary = HOTSPOT_3.to_string().parse().unwrap(); // wifi hotspot + let pubkey: PublicKeyBinary = HOTSPOT_3.to_string().parse()?; // wifi hotspot // seed all the things let mut txn = pool.clone().begin().await?; @@ -196,7 +185,7 @@ async fn test_sp_banned_radio(pool: PgPool) -> anyhow::Result<()> { let reward_info = reward_info_24_hours(); - let pubkey: PublicKeyBinary = HOTSPOT_3.to_string().parse().unwrap(); // wifi hotspot + let pubkey: PublicKeyBinary = HOTSPOT_3.to_string().parse()?; // wifi hotspot // seed all the things let mut txn = pool.clone().begin().await?; @@ -255,7 +244,7 @@ async fn test_all_banned_radio(pool: PgPool) -> anyhow::Result<()> { let reward_info = reward_info_24_hours(); - let pubkey: PublicKeyBinary = HOTSPOT_3.to_string().parse().unwrap(); // wifi hotspot + let pubkey: PublicKeyBinary = HOTSPOT_3.to_string().parse()?; // wifi hotspot // seed all the things let mut txn = pool.clone().begin().await?; @@ -309,7 +298,7 @@ async fn test_data_banned_radio_still_receives_poc(pool: PgPool) -> anyhow::Resu let reward_info = reward_info_24_hours(); - let pubkey: PublicKeyBinary = HOTSPOT_3.to_string().parse().unwrap(); // wifi hotspot + let pubkey: PublicKeyBinary = HOTSPOT_3.to_string().parse()?; // wifi hotspot // seed all the things let mut txn = pool.clone().begin().await?; @@ -355,288 +344,6 @@ async fn test_data_banned_radio_still_receives_poc(pool: PgPool) -> anyhow::Resu Ok(()) } -async fn seed_heartbeats( - ts: DateTime, - txn: &mut Transaction<'_, Postgres>, -) -> anyhow::Result<()> { - for n in 0..24 { - let hotspot_key1: PublicKeyBinary = HOTSPOT_1.to_string().parse().unwrap(); - let cov_obj_1 = create_coverage_object( - ts + ChronoDuration::hours(n), - hotspot_key1.clone(), - 0x8a1fb466d2dffff_u64, - true, - ); - let wifi_heartbeat_1 = ValidatedHeartbeat { - heartbeat: Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot_key1, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(cov_obj_1.coverage_object.uuid), - location_validation_timestamp: None, - timestamp: ts + ChronoDuration::hours(n), - location_source: proto::LocationSource::Gps, - }, - cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(0), - coverage_meta: None, - location_trust_score_multiplier: dec!(1.0), - validity: proto::HeartbeatValidity::Valid, - }; - - let hotspot_key2: PublicKeyBinary = HOTSPOT_2.to_string().parse().unwrap(); - let cov_obj_2 = create_coverage_object( - ts + ChronoDuration::hours(n), - hotspot_key2.clone(), - 0x8a1fb49642dffff_u64, - true, - ); - let wifi_heartbeat_2 = ValidatedHeartbeat { - heartbeat: Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot_key2, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(cov_obj_2.coverage_object.uuid), - location_validation_timestamp: None, - timestamp: ts + ChronoDuration::hours(n), - location_source: proto::LocationSource::Gps, - }, - cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(0), - coverage_meta: None, - location_trust_score_multiplier: dec!(1.0), - validity: proto::HeartbeatValidity::Valid, - }; - - let hotspot_key3: PublicKeyBinary = HOTSPOT_3.to_string().parse().unwrap(); - let cov_obj_3 = create_coverage_object( - ts + ChronoDuration::hours(n), - hotspot_key3.clone(), - 0x8c2681a306607ff_u64, - true, - ); - let wifi_heartbeat_3 = ValidatedHeartbeat { - heartbeat: Heartbeat { - hb_type: HbType::Wifi, - hotspot_key: hotspot_key3, - operation_mode: true, - lat: 0.0, - lon: 0.0, - coverage_object: Some(cov_obj_3.coverage_object.uuid), - location_validation_timestamp: Some(ts - ChronoDuration::hours(24)), - timestamp: ts + ChronoDuration::hours(n), - location_source: proto::LocationSource::Skyhook, - }, - cell_type: CellType::NovaGenericWifiIndoor, - distance_to_asserted: Some(0), - coverage_meta: None, - location_trust_score_multiplier: dec!(1.0), - validity: proto::HeartbeatValidity::Valid, - }; - - save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat_3, txn).await?; - save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat_1, txn).await?; - save_seniority_object(ts + ChronoDuration::hours(n), &wifi_heartbeat_2, txn).await?; - - wifi_heartbeat_1.save(txn).await?; - wifi_heartbeat_2.save(txn).await?; - wifi_heartbeat_3.save(txn).await?; - - cov_obj_1.save(txn).await?; - cov_obj_2.save(txn).await?; - cov_obj_3.save(txn).await?; - } - Ok(()) -} - -async fn update_assignments(pool: &PgPool) -> anyhow::Result<()> { - let _ = common::set_unassigned_oracle_boosting_assignments( - pool, - &common::mock_hex_boost_data_default(), - ) - .await?; - Ok(()) -} - -async fn update_assignments_bad(pool: &PgPool) -> anyhow::Result<()> { - let _ = common::set_unassigned_oracle_boosting_assignments( - pool, - &common::mock_hex_boost_data_bad(), - ) - .await?; - Ok(()) -} - -async fn seed_speedtests( - ts: DateTime, - txn: &mut Transaction<'_, Postgres>, -) -> anyhow::Result<()> { - for n in 0..24 { - let hotspot1_speedtest = CellSpeedtest { - pubkey: HOTSPOT_1.parse().unwrap(), - serial: "serial1".to_string(), - timestamp: ts - ChronoDuration::hours(n * 4), - upload_speed: 100_000_000, - download_speed: 100_000_000, - latency: 50, - }; - - let hotspot2_speedtest = CellSpeedtest { - pubkey: HOTSPOT_2.parse().unwrap(), - serial: "serial2".to_string(), - timestamp: ts - ChronoDuration::hours(n * 4), - upload_speed: 100_000_000, - download_speed: 100_000_000, - latency: 50, - }; - - let hotspot3_speedtest = CellSpeedtest { - pubkey: HOTSPOT_3.parse().unwrap(), - serial: "serial3".to_string(), - timestamp: ts - ChronoDuration::hours(n * 4), - upload_speed: 100_000_000, - download_speed: 100_000_000, - latency: 50, - }; - - speedtests::save_speedtest(&hotspot1_speedtest, txn).await?; - speedtests::save_speedtest(&hotspot2_speedtest, txn).await?; - speedtests::save_speedtest(&hotspot3_speedtest, txn).await?; - } - Ok(()) -} - -async fn seed_data_sessions( - ts: DateTime, - txn: &mut Transaction<'_, Postgres>, -) -> anyhow::Result { - let rewardable_bytes_1 = 1_024 * 1_000; - let data_session_1 = data_session::HotspotDataSession { - pub_key: HOTSPOT_1.parse().unwrap(), - payer: PAYER_1.parse().unwrap(), - upload_bytes: 1_024 * 1_000, - download_bytes: 1_024 * 50_000, - // Here to test that rewardable_bytes is the one taken into account we lower it - rewardable_bytes: rewardable_bytes_1, - num_dcs: 5_000_000, - received_timestamp: ts + ChronoDuration::hours(1), - burn_timestamp: ts + ChronoDuration::hours(1), - }; - - let rewardable_bytes_2 = 1_024 * 1_000 + 1_024 * 50_000; - let data_session_2 = data_session::HotspotDataSession { - pub_key: HOTSPOT_2.parse().unwrap(), - payer: PAYER_1.parse().unwrap(), - upload_bytes: 1_024 * 1_000, - download_bytes: 1_024 * 50_000, - rewardable_bytes: rewardable_bytes_2, - num_dcs: 5_000_000, - received_timestamp: ts + ChronoDuration::hours(1), - burn_timestamp: ts + ChronoDuration::hours(1), - }; - - let rewardable_bytes_3 = 1_024 * 1_000 + 1_024 * 50_000; - let data_session_3 = data_session::HotspotDataSession { - pub_key: HOTSPOT_3.parse().unwrap(), - payer: PAYER_1.parse().unwrap(), - upload_bytes: 1_024 * 1_000, - download_bytes: 1_024 * 50_000, - rewardable_bytes: rewardable_bytes_3, - num_dcs: 5_000_000, - received_timestamp: ts + ChronoDuration::hours(1), - burn_timestamp: ts + ChronoDuration::hours(1), - }; - data_session_1.save(txn).await?; - data_session_2.save(txn).await?; - data_session_3.save(txn).await?; - - let rewardable = rewardable_bytes_1 + rewardable_bytes_2 + rewardable_bytes_3; - - Ok(rewardable as u64) -} - -async fn seed_unique_connections( - txn: &mut Transaction<'_, Postgres>, - things: &[(PublicKeyBinary, u64)], - epoch: &Range>, -) -> anyhow::Result<()> { - let mut reports = vec![]; - for (pubkey, unique_connections) in things { - reports.push(UniqueConnectionsIngestReport { - received_timestamp: epoch.start + chrono::Duration::hours(1), - report: UniqueConnectionReq { - pubkey: pubkey.clone(), - start_timestamp: Utc::now(), - end_timestamp: Utc::now(), - unique_connections: *unique_connections, - timestamp: Utc::now(), - carrier_key: pubkey.clone(), - signature: vec![], - }, - }); - } - unique_connections::db::save(txn, &reports).await?; - Ok(()) -} - -fn create_coverage_object( - ts: DateTime, - pub_key: PublicKeyBinary, - hex: u64, - indoor: bool, -) -> CoverageObject { - let location = h3o::CellIndex::try_from(hex).unwrap(); - let key_type = KeyType::HotspotKey(pub_key.clone()); - let report = FSCoverageObject { - pub_key, - uuid: Uuid::new_v4(), - key_type, - coverage_claim_time: ts, - coverage: vec![RadioHexSignalLevel { - location, - signal_level: proto::SignalLevel::High, - signal_power: 1000, - }], - indoor, - trust_score: 1000, - signature: Vec::new(), - }; - CoverageObject { - coverage_object: report, - validity: proto::CoverageObjectValidity::Valid, - } -} - -//TODO: use existing save methods instead of manual sql -async fn save_seniority_object( - ts: DateTime, - hb: &ValidatedHeartbeat, - exec: &mut Transaction<'_, Postgres>, -) -> anyhow::Result<()> { - sqlx::query( - r#" - INSERT INTO seniority - (radio_key, last_heartbeat, uuid, seniority_ts, inserted_at, update_reason, radio_type) - VALUES - ($1, $2, $3, $4, $5, $6, $7) - "#, - ) - .bind(hb.heartbeat.key()) - .bind(hb.heartbeat.timestamp) - .bind(hb.heartbeat.coverage_object) - .bind(ts) - .bind(ts) - .bind(proto::SeniorityUpdateReason::NewCoverageClaimTime as i32) - .bind(hb.heartbeat.hb_type) - .execute(&mut **exec) - .await?; - Ok(()) -} - async fn ban_wifi_radio_for_epoch( txn: &mut Transaction<'_, Postgres>, pubkey: PublicKeyBinary, From d23a7f8887fb08df5e5d26e3bc0fddd5f374653c Mon Sep 17 00:00:00 2001 From: Connor McKenzie Date: Mon, 5 Jan 2026 16:53:10 +0000 Subject: [PATCH 5/6] Remove test comments --- mobile_verifier/tests/integrations/reward_distributor.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mobile_verifier/tests/integrations/reward_distributor.rs b/mobile_verifier/tests/integrations/reward_distributor.rs index c447b327e..3892cd3c2 100644 --- a/mobile_verifier/tests/integrations/reward_distributor.rs +++ b/mobile_verifier/tests/integrations/reward_distributor.rs @@ -15,7 +15,6 @@ async fn test_distribute_rewards(pool: PgPool) -> anyhow::Result<()> { let (mobile_rewards_client, mobile_rewards) = create_file_sink(); let reward_info = reward_info_24_hours(); - // seed all the things let mut txn = pool.clone().begin().await?; seed_heartbeats(reward_info.epoch_period.start, &mut txn).await?; seed_speedtests(reward_info.epoch_period.end, &mut txn).await?; @@ -26,7 +25,6 @@ async fn test_distribute_rewards(pool: PgPool) -> anyhow::Result<()> { let hex_boosting_client = MockHexBoostingClient::new(vec![]); let price_info = default_price_info(); - // Run rewards rewarder::distribute_rewards( &pool, &hex_boosting_client, @@ -36,7 +34,6 @@ async fn test_distribute_rewards(pool: PgPool) -> anyhow::Result<()> { ) .await?; - // Retrieve distributed rewards let rewards = mobile_rewards.finish().await?; let poc_rewards = rewards.radio_reward_v2s; let dc_rewards = rewards.gateway_rewards; @@ -50,10 +47,8 @@ async fn test_distribute_rewards(pool: PgPool) -> anyhow::Result<()> { let total: u64 = poc_sum + dc_sum + sp_sum + unallocated_sum; - // Calculate expected rewards let expected_total = calculate_expected_total_rewards(reward_info.epoch_emissions); - // Assert total assert_eq!(total, expected_total); Ok(()) From a9d225481a8d14445f3f2c53bd74ef27d1b8f552 Mon Sep 17 00:00:00 2001 From: Connor McKenzie Date: Mon, 5 Jan 2026 17:09:46 +0000 Subject: [PATCH 6/6] Fix clippy --- mobile_verifier/src/rewarder.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile_verifier/src/rewarder.rs b/mobile_verifier/src/rewarder.rs index daca4fdb2..a518748a6 100644 --- a/mobile_verifier/src/rewarder.rs +++ b/mobile_verifier/src/rewarder.rs @@ -357,14 +357,14 @@ pub async fn distribute_rewards( pool, hex_service_client, mobile_rewards.clone(), - &reward_info, + reward_info, price_info.clone(), ) .await?; // process rewards for service providers let sp_unallocated_amount = - reward_service_providers(mobile_rewards.clone(), &reward_info).await?; + reward_service_providers(mobile_rewards.clone(), reward_info).await?; // write combined poc and sp unallocated reward let total_unallocated_amount = (poc_unallocated_amount + sp_unallocated_amount) @@ -376,7 +376,7 @@ pub async fn distribute_rewards( mobile_rewards, UnallocatedRewardType::PocAndServiceProvider, total_unallocated_amount, - &reward_info, + reward_info, ) .await?;