Skip to content

Commit

Permalink
refactor: Billing refactor (#992)
Browse files Browse the repository at this point in the history
* Refactor billing logic for smart contracts to improve payment tracking, overdraft handling, and fund reservations.

* Redesign off-chain worker logic to enhance billing effectiveness and prevent failures.

* Improve management of contract payments, focusing on overdue payments and grace periods.

*Enhance logging and error management to reduce the risk of unstable contract states.

* Introduce new events for better visibility into billing processes.

* Adjust utilization revenue distribution.

* Address bugs to improve billing reliability.

* Update benchmarks and weights for all pallets, and adjust relevant tests.

---------
  • Loading branch information
sameh-farouk authored Aug 27, 2024
1 parent 2f06d6f commit 541e5b6
Show file tree
Hide file tree
Showing 27 changed files with 2,652 additions and 1,304 deletions.
30 changes: 30 additions & 0 deletions clients/tfchain-client-go/contract_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,33 @@ type NodeExtraFeeSet struct {
ExtraFee types.U64 `json:"extra_fee"`
Topics []types.Hash
}

type RentWaived struct {
Phase types.Phase
ContractID types.U64 `json:"contract_id"`
Topics []types.Hash
}

type ContractGracePeriodElapsed struct {
Phase types.Phase
ContractID types.U64 `json:"contract_id"`
GracePeriod types.U64 `json:"grace_period"`
Topics []types.Hash
}

type ContractPaymentOverdrawn struct {
Phase types.Phase
ContractID types.U64 `json:"contract_id"`
Timestamp types.U64 `json:"timestamp"`
PartiallyBilledAmount types.U128 `json:"partially_billed_amount"`
Overdraft types.U128 `json:"overdraft"`
Topics []types.Hash
}

type RewardDistributed struct {
Phase types.Phase
ContractID types.U64 `json:"contract_id"`
StandardRewards types.U128 `json:"standard_rewards"`
AdditionalRewards types.U128 `json:"additional_rewards"`
Topics []types.Hash
}
4 changes: 4 additions & 0 deletions clients/tfchain-client-go/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ type EventRecords struct {
SmartContractModule_ServiceContractBilled []ServiceContractBilled //nolint:stylecheck,golint
SmartContractModule_BillingFrequencyChanged []BillingFrequencyChanged //nolint:stylecheck,golint
SmartContractModule_NodeExtraFeeSet []NodeExtraFeeSet //nolint:stylecheck,golint
SmartContractModule_RentWaived []RentWaived //nolint:stylecheck,golint
SmartContractModule_ContractGracePeriodElapsed []ContractGracePeriodElapsed //nolint:stylecheck,golint
SmartContractModule_ContractPaymentOverdrawn []ContractPaymentOverdrawn //nolint:stylecheck,golint
SmartContractModule_RewardDistributed []RewardDistributed //nolint:stylecheck,golint

// farm events
TfgridModule_FarmStored []FarmStored //nolint:stylecheck,golint
Expand Down
2 changes: 2 additions & 0 deletions clients/tfchain-client-go/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ var smartContractModuleErrors = []string{
"WrongAuthority",
"UnauthorizedToChangeSolutionProviderId",
"UnauthorizedToSetExtraFee",
"RewardDistributionError",
"ContractPaymentStateNotExists",
}

// https://github.com/threefoldtech/tfchain/blob/development/substrate-node/pallets/pallet-tfgrid/src/lib.rs#L442
Expand Down
48 changes: 48 additions & 0 deletions docs/architecture/0023-billing-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 23. Billing Refactor in pallet-smart-contract Module

Date: 2024-08-18

## Status

Accepted

## Context

The billing logic within the pallet-smart-contract module faced several issues, including critical bugs, inefficiencies in handling validators, and inadequate fund reservation mechanisms.
The goal of this refactor was to address these issues while enhancing the overall reliability and robustness of the billing process.

## Decision

The following architectural decisions were made to improve the billing logic:

### Refactoring Billing Logic

- Enhanced Tracking:
Improved tracking mechanisms were introduced for contract payments, especially during grace periods and when handling overdue payments.
The new ContractPaymentState was introduced to accurately manage contract payment states and resolve liquidity issues.
- Overdraft Handling:
Partial payments are now allowed, where users can cover part of their billing due if they lack sufficient funds for the full amount. Overdraft are tracked separately.
- Fund Reservation:
The use of balance locks/freezes was replaced with a more reliable reservation system, reducing issues related to fund availability for reward distribution.

### Improved Off-Chain Worker Logic

- Runtime Verification:
The is_next_block_author function was removed, with verification now occurring at runtime.
This change ensures transaction fees are reliably waived for validators and still prevents duplicate transactions.

### New Events Introduced

- ContractGracePeriodElapsed: Indicates that a contract's grace period has elapsed.
- ContractPaymentOverdrawn: Indicates that a contract's payment is overdrawn.
- RewardDistributed: Indicates that rewards have been distributed.

### Certified vs DIY Capacity

The system now correctly charges certified capacity at a higher rate (25% more) than DIY capacity.

## Consequences

- Increased Robustness: The billing process is now more robust and less prone to errors, particularly in scenarios involving partial payments and fund reservations.
- Better Visibility: The introduction of new events and improved logging provides better visibility into the billing and payment processes, aiding in debugging and monitoring.
- Backward Compatibility: While significant refactoring has been done, efforts were made to ensure backward compatibility where possible, especially concerning contract migration.
4 changes: 3 additions & 1 deletion substrate-node/node/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use sp_core::{Encode, Pair};
use sp_inherents::{InherentData, InherentDataProvider};
use sp_keyring::Sr25519Keyring;
use sp_runtime::{OpaqueExtrinsic, SaturatedConversion};
use tfchain_runtime as runtime;
use tfchain_runtime::{self as runtime, pallet_smart_contract};

use std::{sync::Arc, time::Duration};

Expand Down Expand Up @@ -132,6 +132,7 @@ pub fn create_benchmark_extrinsic(
frame_system::CheckNonce::<runtime::Runtime>::from(nonce),
frame_system::CheckWeight::<runtime::Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<runtime::Runtime>::from(0),
pallet_smart_contract::types::ContractIdProvides::<runtime::Runtime>::new(),
);

let raw_payload = runtime::SignedPayload::from_raw(
Expand All @@ -146,6 +147,7 @@ pub fn create_benchmark_extrinsic(
(),
(),
(),
(),
),
);
let signature = raw_payload.using_encoded(|e| sender.sign(e));
Expand Down
12 changes: 6 additions & 6 deletions substrate-node/pallets/pallet-burning/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//! Autogenerated weights for pallet_burning
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2024-06-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! DATE: 2024-08-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `4b80713dc969`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
//! HOSTNAME: `66e77d0da08f`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
Expand Down Expand Up @@ -45,8 +45,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof Size summary in bytes:
// Measured: `109`
// Estimated: `1594`
// Minimum execution time: 26_841_000 picoseconds.
Weight::from_parts(27_372_000, 1594)
// Minimum execution time: 26_780_000 picoseconds.
Weight::from_parts(27_291_000, 1594)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
Expand All @@ -60,8 +60,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `109`
// Estimated: `1594`
// Minimum execution time: 26_841_000 picoseconds.
Weight::from_parts(27_372_000, 1594)
// Minimum execution time: 26_780_000 picoseconds.
Weight::from_parts(27_291_000, 1594)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
Expand Down
36 changes: 18 additions & 18 deletions substrate-node/pallets/pallet-dao/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//! Autogenerated weights for pallet_dao
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2024-06-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! DATE: 2024-08-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `4b80713dc969`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
//! HOSTNAME: `66e77d0da08f`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
Expand Down Expand Up @@ -58,8 +58,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof Size summary in bytes:
// Measured: `208`
// Estimated: `4687`
// Minimum execution time: 19_196_000 picoseconds.
Weight::from_parts(19_487_000, 4687)
// Minimum execution time: 19_277_000 picoseconds.
Weight::from_parts(20_188_000, 4687)
.saturating_add(T::DbWeight::get().reads(4_u64))
.saturating_add(T::DbWeight::get().writes(5_u64))
}
Expand All @@ -77,8 +77,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof Size summary in bytes:
// Measured: `979`
// Estimated: `4444`
// Minimum execution time: 26_179_000 picoseconds.
Weight::from_parts(26_751_000, 4444)
// Minimum execution time: 26_390_000 picoseconds.
Weight::from_parts(26_840_000, 4444)
.saturating_add(T::DbWeight::get().reads(5_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
Expand All @@ -92,8 +92,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof Size summary in bytes:
// Measured: `487`
// Estimated: `4687`
// Minimum execution time: 18_154_000 picoseconds.
Weight::from_parts(18_755_000, 4687)
// Minimum execution time: 18_616_000 picoseconds.
Weight::from_parts(18_985_000, 4687)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
Expand All @@ -111,8 +111,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof Size summary in bytes:
// Measured: `521`
// Estimated: `4687`
// Minimum execution time: 24_757_000 picoseconds.
Weight::from_parts(25_268_000, 4687)
// Minimum execution time: 25_117_000 picoseconds.
Weight::from_parts(25_498_000, 4687)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(4_u64))
}
Expand All @@ -136,8 +136,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `208`
// Estimated: `4687`
// Minimum execution time: 19_196_000 picoseconds.
Weight::from_parts(19_487_000, 4687)
// Minimum execution time: 19_277_000 picoseconds.
Weight::from_parts(20_188_000, 4687)
.saturating_add(RocksDbWeight::get().reads(4_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
}
Expand All @@ -155,8 +155,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `979`
// Estimated: `4444`
// Minimum execution time: 26_179_000 picoseconds.
Weight::from_parts(26_751_000, 4444)
// Minimum execution time: 26_390_000 picoseconds.
Weight::from_parts(26_840_000, 4444)
.saturating_add(RocksDbWeight::get().reads(5_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
Expand All @@ -170,8 +170,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `487`
// Estimated: `4687`
// Minimum execution time: 18_154_000 picoseconds.
Weight::from_parts(18_755_000, 4687)
// Minimum execution time: 18_616_000 picoseconds.
Weight::from_parts(18_985_000, 4687)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
Expand All @@ -189,8 +189,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `521`
// Estimated: `4687`
// Minimum execution time: 24_757_000 picoseconds.
Weight::from_parts(25_268_000, 4687)
// Minimum execution time: 25_117_000 picoseconds.
Weight::from_parts(25_498_000, 4687)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(4_u64))
}
Expand Down
20 changes: 10 additions & 10 deletions substrate-node/pallets/pallet-kvstore/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//! Autogenerated weights for pallet_kvstore
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
//! DATE: 2024-06-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! DATE: 2024-08-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `4b80713dc969`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
//! HOSTNAME: `66e77d0da08f`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
// Executed Command:
Expand Down Expand Up @@ -46,8 +46,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 7_293_000 picoseconds.
Weight::from_parts(7_574_000, 0)
// Minimum execution time: 11_372_000 picoseconds.
Weight::from_parts(12_032_000, 0)
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: `TFKVStore::TFKVStore` (r:1 w:1)
Expand All @@ -56,8 +56,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof Size summary in bytes:
// Measured: `146`
// Estimated: `3611`
// Minimum execution time: 12_784_000 picoseconds.
Weight::from_parts(13_205_000, 3611)
// Minimum execution time: 12_414_000 picoseconds.
Weight::from_parts(12_854_000, 3611)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
Expand All @@ -71,8 +71,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `0`
// Estimated: `0`
// Minimum execution time: 7_293_000 picoseconds.
Weight::from_parts(7_574_000, 0)
// Minimum execution time: 11_372_000 picoseconds.
Weight::from_parts(12_032_000, 0)
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: `TFKVStore::TFKVStore` (r:1 w:1)
Expand All @@ -81,8 +81,8 @@ impl WeightInfo for () {
// Proof Size summary in bytes:
// Measured: `146`
// Estimated: `3611`
// Minimum execution time: 12_784_000 picoseconds.
Weight::from_parts(13_205_000, 3611)
// Minimum execution time: 12_414_000 picoseconds.
Weight::from_parts(12_854_000, 3611)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,11 @@ benchmarks! {

let contract = SmartContractModule::<T>::contracts(contract_id).unwrap();
// Get contract cost before billing to take into account nu
let (cost, discount_level) = contract.calculate_contract_cost_tft(balance_init_amount, elapsed_seconds).unwrap();
let (cost, discount_level) = contract.calculate_contract_cost_tft(balance_init_amount, elapsed_seconds, None).unwrap();
}: _(RawOrigin::Signed(farmer), contract_id)
verify {
let lock = SmartContractModule::<T>::contract_number_of_cylces_billed(contract_id);
assert_eq!(lock.amount_locked, cost);
let contract_payment_state = SmartContractModule::<T>::contract_payment_state(contract_id).unwrap();
assert_eq!(contract_payment_state.standard_reserve, cost);
let contract_bill = types::ContractBill {
contract_id,
timestamp: SmartContractModule::<T>::get_current_timestamp_in_secs(),
Expand Down
Loading

0 comments on commit 541e5b6

Please sign in to comment.