diff --git a/pallets/ddc-payouts/src/lib.rs b/pallets/ddc-payouts/src/lib.rs index 42ed6080f..4b309dd0d 100644 --- a/pallets/ddc-payouts/src/lib.rs +++ b/pallets/ddc-payouts/src/lib.rs @@ -190,6 +190,19 @@ pub mod pallet { node_provider_id: T::AccountId, amount: u128, }, + NotDistributedReward { + cluster_id: ClusterId, + era: DdcEra, + node_provider_id: T::AccountId, + expected_reward: u128, + distributed_reward: BalanceOf, + }, + NotDistributedOverallReward { + cluster_id: ClusterId, + era: DdcEra, + expected_reward: u128, + total_distributed_reward: u128, + }, RewardingFinished { cluster_id: ClusterId, era: DdcEra, @@ -212,7 +225,6 @@ pub mod pallet { BatchIndexAlreadyProcessed, BatchIndexIsOutOfRange, BatchesMissed, - NotDistributedBalance, BatchIndexOverflow, BoundedVecOverflow, ArithmeticOverflow, @@ -682,17 +694,22 @@ pub mod pallet { let mut reward: BalanceOf = amount_to_reward.saturated_into::>(); - let balance = ::Currency::free_balance( + let vault_balance = ::Currency::free_balance( &updated_billing_report.vault, ) - ::Currency::minimum_balance(); - if reward > balance { - ensure!( - reward - balance <= MaxDust::get().into(), - Error::::NotDistributedBalance - ); - - reward = balance; + if reward > vault_balance { + if reward - vault_balance > MaxDust::get().into() { + Self::deposit_event(Event::::NotDistributedReward { + cluster_id, + era, + node_provider_id: node_provider_id.clone(), + expected_reward: amount_to_reward, + distributed_reward: vault_balance, + }); + } + + reward = vault_balance; } ::Currency::transfer( @@ -758,11 +775,16 @@ pub mod pallet { })() .ok_or(Error::::ArithmeticOverflow)?; - ensure!( - expected_amount_to_reward - billing_report.total_distributed_reward <= - MaxDust::get().into(), - Error::::NotDistributedBalance - ); + if expected_amount_to_reward - billing_report.total_distributed_reward > + MaxDust::get().into() + { + Self::deposit_event(Event::::NotDistributedOverallReward { + cluster_id, + era, + expected_reward: expected_amount_to_reward, + total_distributed_reward: billing_report.total_distributed_reward, + }); + } billing_report.state = State::ProvidersRewarded; ActiveBillingReports::::insert(cluster_id, era, billing_report); @@ -854,14 +876,17 @@ pub mod pallet { let mut node_reward = NodeReward::default(); let mut ratio = Perquintill::from_rational( - node_usage.transferred_bytes, - total_nodes_usage.transferred_bytes, + node_usage.transferred_bytes as u128, + total_nodes_usage.transferred_bytes as u128, ); // ratio multiplied by X will be > 0, < X no overflow node_reward.transfer = ratio * total_customer_charge.transfer; - ratio = Perquintill::from_rational(node_usage.stored_bytes, total_nodes_usage.stored_bytes); + ratio = Perquintill::from_rational( + node_usage.stored_bytes as u128, + total_nodes_usage.stored_bytes as u128, + ); node_reward.storage = ratio * total_customer_charge.storage; ratio = diff --git a/pallets/ddc-payouts/src/mock.rs b/pallets/ddc-payouts/src/mock.rs index 00eba7232..23fd03310 100644 --- a/pallets/ddc-payouts/src/mock.rs +++ b/pallets/ddc-payouts/src/mock.rs @@ -53,7 +53,7 @@ construct_runtime!( } ); -pub static MAX_DUST: u16 = 20000; +pub static MAX_DUST: u16 = 100; parameter_types! { pub static ExistentialDeposit: Balance = 1; diff --git a/pallets/ddc-payouts/src/tests.rs b/pallets/ddc-payouts/src/tests.rs index f4d1732af..4a1a9047e 100644 --- a/pallets/ddc-payouts/src/tests.rs +++ b/pallets/ddc-payouts/src/tests.rs @@ -1449,7 +1449,7 @@ fn send_rewarding_providers_batch_works() { } #[test] -fn send_rewarding_providers_batch_works100nodes() { +fn send_rewarding_providers_batch_100nodes_small_usage_works() { ExtBuilder.build_and_execute(|| { System::set_block_number(1); @@ -1700,6 +1700,271 @@ fn send_rewarding_providers_batch_works100nodes() { }) } +#[test] +fn send_rewarding_providers_batch_100nodes_large_usage_works() { + ExtBuilder.build_and_execute(|| { + System::set_block_number(1); + + let num_nodes = 100; + let num_users = 5; + let dac_account = 123u128; + let bank = 1u128; + let cluster_id = ONE_CLUSTER_ID; + let era = 100; + let user_batch_size = 10; + let node_batch_size = 10; + let mut batch_user_index = 0; + let mut batch_node_index = 0; + let usage1 = CustomerUsage { + transferred_bytes: 1024, + stored_bytes: 1024, + number_of_puts: 1, + number_of_gets: 1, + }; + + let node_usage1 = NodeUsage { + // CDN + transferred_bytes: Perquintill::from_float(0.75) * usage1.transferred_bytes, + stored_bytes: 0, + number_of_puts: Perquintill::from_float(0.75) * usage1.number_of_puts, + number_of_gets: Perquintill::from_float(0.75) * usage1.number_of_gets, + }; + + let node_usage2 = NodeUsage { + // Storage + transferred_bytes: 0, + stored_bytes: usage1.stored_bytes * 2, + number_of_puts: 0, + number_of_gets: 0, + }; + + let node_usage3 = NodeUsage { + // CDN + Storage + transferred_bytes: usage1.transferred_bytes * 2, + stored_bytes: usage1.stored_bytes * 3, + number_of_puts: usage1.number_of_puts * 2, + number_of_gets: usage1.number_of_gets * 2, + }; + + let mut payees: Vec> = Vec::new(); + let mut node_batch: Vec<(u128, NodeUsage)> = Vec::new(); + let mut total_nodes_usage = NodeUsage::default(); + for i in 10..10 + num_nodes { + let ratio = match i % 5 { + 0 => Perquintill::from_float(1_000_000.0), + 1 => Perquintill::from_float(10_000_000.0), + 2 => Perquintill::from_float(100_000_000.0), + 3 => Perquintill::from_float(1_000_000_000.0), + 4 => Perquintill::from_float(10_000_000_000.0), + _ => unreachable!(), + }; + let mut node_usage = match i % 3 { + 0 => node_usage1.clone(), + 1 => node_usage2.clone(), + 2 => node_usage3.clone(), + _ => unreachable!(), + }; + node_usage.transferred_bytes = ratio * node_usage.transferred_bytes; + node_usage.stored_bytes = ratio * node_usage.stored_bytes; + node_usage.number_of_puts = ratio * node_usage.number_of_puts; + node_usage.number_of_gets = ratio * node_usage.number_of_gets; + + total_nodes_usage.transferred_bytes += node_usage.transferred_bytes; + total_nodes_usage.stored_bytes += node_usage.stored_bytes; + total_nodes_usage.number_of_puts += node_usage.number_of_puts; + total_nodes_usage.number_of_gets += node_usage.number_of_gets; + + node_batch.push((i, node_usage)); + if node_batch.len() == node_batch_size { + payees.push(node_batch.clone()); + node_batch.clear(); + } + } + if !node_batch.is_empty() { + payees.push(node_batch.clone()); + } + + let mut total_charge = 0u128; + let mut payers: Vec> = Vec::new(); + let mut user_batch: Vec<(u128, CustomerUsage)> = Vec::new(); + for user_id in 1000..1000 + num_users { + let ratio = match user_id % 5 { + 0 => Perquintill::from_float(1_000_000.0), + 1 => Perquintill::from_float(10_000_000.0), + 2 => Perquintill::from_float(100_000_000.0), + 3 => Perquintill::from_float(1_000_000_000.0), + 4 => Perquintill::from_float(10_000_000_000.0), + _ => unreachable!(), + }; + + let mut user_usage = usage1.clone(); + user_usage.transferred_bytes = ratio * user_usage.transferred_bytes; + user_usage.stored_bytes = ratio * user_usage.stored_bytes; + user_usage.number_of_puts = ratio * user_usage.number_of_puts; + user_usage.number_of_gets = ratio * user_usage.number_of_gets; + + let expected_charge = calculate_charge(cluster_id, user_usage.clone()); + Balances::transfer( + RuntimeOrigin::signed(bank), + user_id, + (expected_charge * 2).max(Balances::minimum_balance()), + ) + .unwrap(); + total_charge += expected_charge; + + user_batch.push((user_id, user_usage)); + if user_batch.len() == user_batch_size { + payers.push(user_batch.clone()); + user_batch.clear(); + } + } + if !user_batch.is_empty() { + payers.push(user_batch.clone()); + } + + assert_ok!(DdcPayouts::set_authorised_caller(RuntimeOrigin::root(), dac_account)); + assert_ok!(DdcPayouts::begin_billing_report( + RuntimeOrigin::signed(dac_account), + cluster_id, + era, + )); + assert_ok!(DdcPayouts::begin_charging_customers( + RuntimeOrigin::signed(dac_account), + cluster_id, + era, + (payers.len() - 1) as u16, + )); + + for batch in payers.iter() { + assert_ok!(DdcPayouts::send_charging_customers_batch( + RuntimeOrigin::signed(dac_account), + cluster_id, + era, + batch_user_index, + batch.to_vec(), + )); + + for (customer_id, usage) in batch.iter() { + let charge = calculate_charge(cluster_id, usage.clone()); + + System::assert_has_event( + Event::Charged { + cluster_id, + era, + customer_id: *customer_id, + batch_index: batch_user_index, + amount: charge, + } + .into(), + ); + } + batch_user_index += 1; + } + + let report_before = DdcPayouts::active_billing_reports(cluster_id, era).unwrap(); + let balance1 = Balances::free_balance(report_before.vault); + let balance2 = Balances::free_balance(DdcPayouts::account_id()); + assert_eq!(balance1, balance2); + assert_eq!(report_before.vault, DdcPayouts::account_id()); + assert_eq!(balance1 - Balances::minimum_balance(), total_charge); + + assert_ok!(DdcPayouts::end_charging_customers( + RuntimeOrigin::signed(dac_account), + cluster_id, + era, + )); + + let report_after = DdcPayouts::active_billing_reports(cluster_id, era).unwrap(); + let total_left_from_one = (get_fees(&cluster_id).treasury_share + + get_fees(&cluster_id).validators_share + + get_fees(&cluster_id).cluster_reserve_share) + .left_from_one(); + + let total_charge = report_after.total_customer_charge.transfer + + report_before.total_customer_charge.storage + + report_before.total_customer_charge.puts + + report_before.total_customer_charge.gets; + let balance_after = Balances::free_balance(DdcPayouts::account_id()); + assert_eq!(total_charge, balance_after - Balances::minimum_balance()); + + assert_eq!( + report_after.total_customer_charge.transfer, + total_left_from_one * report_before.total_customer_charge.transfer + ); + assert_eq!( + report_after.total_customer_charge.storage, + total_left_from_one * report_before.total_customer_charge.storage + ); + assert_eq!( + report_after.total_customer_charge.puts, + total_left_from_one * report_before.total_customer_charge.puts + ); + assert_eq!( + report_after.total_customer_charge.gets, + total_left_from_one * report_before.total_customer_charge.gets + ); + + assert_ok!(DdcPayouts::begin_rewarding_providers( + RuntimeOrigin::signed(dac_account), + cluster_id, + era, + (payees.len() - 1) as u16, + total_nodes_usage.clone(), + )); + + for batch in payees.iter() { + let before_batch = Balances::free_balance(DdcPayouts::account_id()); + assert_ok!(DdcPayouts::send_rewarding_providers_batch( + RuntimeOrigin::signed(dac_account), + cluster_id, + era, + batch_node_index, + batch.to_vec(), + )); + + let mut batch_charge = 0; + for (node1, node_usage1) in batch.iter() { + let ratio1_transfer = Perquintill::from_rational( + node_usage1.transferred_bytes, + total_nodes_usage.transferred_bytes, + ); + let transfer_charge = ratio1_transfer * report_after.total_customer_charge.transfer; + + let ratio1_storage = Perquintill::from_rational( + node_usage1.stored_bytes, + total_nodes_usage.stored_bytes, + ); + let storage_charge = ratio1_storage * report_after.total_customer_charge.storage; + + let ratio1_puts = Perquintill::from_rational( + node_usage1.number_of_puts, + total_nodes_usage.number_of_puts, + ); + let puts_charge = ratio1_puts * report_after.total_customer_charge.puts; + + let ratio1_gets = Perquintill::from_rational( + node_usage1.number_of_gets, + total_nodes_usage.number_of_gets, + ); + let gets_charge = ratio1_gets * report_after.total_customer_charge.gets; + + let balance_node1 = Balances::free_balance(node1); + assert!( + (transfer_charge + storage_charge + puts_charge + gets_charge) - balance_node1 < + MAX_DUST.into() + ); + + batch_charge += transfer_charge + storage_charge + puts_charge + gets_charge; + } + let after_batch = Balances::free_balance(DdcPayouts::account_id()); + assert!(batch_charge + after_batch - before_batch < MAX_DUST.into()); + + batch_node_index += 1; + } + assert!(Balances::free_balance(DdcPayouts::account_id()) < MAX_DUST.into()); + }) +} + #[test] fn end_rewarding_providers_fails_uninitialised() { ExtBuilder.build_and_execute(|| {